first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
Question import/export formats
==============================
This directory contains plug-ins to supprt importing and exporting questions in
a variety of formats.
Each sub-module must contain at least a format.php file containing a class that
contains functions for reading, writing, importing and exporting questions.
For correct operation the class name must be based on the name of the plugin.
For example:
plugin: webct
class: class qformat_webct extends qformat_default {
Most of them are based on the class found in question/format.php.
See the comments therein for more information.
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for qformat_aiken.
*
* @package qformat_aiken
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qformat_aiken\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qformat_aiken implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @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';
}
}
+206
View File
@@ -0,0 +1,206 @@
<?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/>.
/**
* Aiken format question importer.
*
* @package qformat_aiken
* @copyright 2003 Tom Robb <tom@robb.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Aiken format - a simple format for creating multiple choice questions (with
* only one correct choice, and no feedback).
*
* The format looks like this:
*
* Question text
* A) Choice #1
* B) Choice #2
* C) Choice #3
* D) Choice #4
* ANSWER: B
*
* That is,
* + question text all one one line.
* + then a number of choices, one to a line. Each line must comprise a letter,
* then ')' or '.', then a space, then the choice text.
* + Then a line of the form 'ANSWER: X' to indicate the correct answer.
*
* Be sure to word "All of the above" type choices like "All of these" in
* case choices are being shuffled.
*
* @copyright 2003 Tom Robb <tom@robb.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qformat_aiken extends qformat_default {
public function provide_import() {
return true;
}
public function provide_export() {
return true;
}
public function validate_file(stored_file $file): string {
return $this->validate_is_utf8_file($file);
}
public function readquestions($lines) {
$questions = array();
$question = null;
$endchar = chr(13);
$linenumber = 0;
foreach ($lines as $line) {
$stp = strpos($line, $endchar, 0);
$newlines = explode($endchar, $line);
$linescount = count($newlines);
for ($i=0; $i < $linescount; $i++) {
$linenumber++;
$nowline = trim($newlines[$i]);
// Go through the array and build an object called $question
// When done, add $question to $questions.
if (strlen($nowline) < 2) {
continue;
}
if (preg_match('/^[A-Z][).][ \t]?/', $nowline)) {
if (is_null($question)) {
// We have a response line, but we aren't currently in a question.
$this->error(get_string('questionnotstarted', 'qformat_aiken', $linenumber));
continue;
}
// A choice. Trim off the label and space, then save.
$question->answer[] = $this->text_field(substr($nowline, 2));
$question->fraction[] = 0;
$question->feedback[] = $this->text_field('');
} else if (preg_match('/^ANSWER:/', $nowline)) {
if (is_null($question)) {
// We have an answer line, but we aren't currently in a question.
$this->error(get_string('questionnotstarted', 'qformat_aiken', $linenumber));
continue;
}
// The line that indicates the correct answer. This question is finised.
$ans = trim(substr($nowline, strpos($nowline, ':') + 1));
$ans = substr($ans, 0, 1);
// We want to map A to 0, B to 1, etc.
$rightans = ord($ans) - ord('A');
if (count($question->answer) < 2) {
// The multichoice question requires at least 2 answers, or there will be a failure later.
$this->error(get_string('questionmissinganswers', 'qformat_aiken', $linenumber), '', $question->name);
$question = null;
continue;
}
$question->fraction[$rightans] = 1;
$questions[] = $question;
// Clear variable for next question set.
$question = null;
continue;
} else {
// Must be the first line of a new question, since no recognised prefix.
if (!is_null($question)) {
// In this case, there was already an open question that we didn't complete. It is being discarded.
$this->error(get_string('questionnotcomplete', 'qformat_aiken', $linenumber), '', $question->name);
}
$question = $this->defaultquestion();
$question->qtype = 'multichoice';
$question->name = $this->create_default_question_name($nowline, get_string('questionname', 'question'));
$question->questiontext = htmlspecialchars(trim($nowline), ENT_NOQUOTES);
$question->questiontextformat = FORMAT_HTML;
$question->generalfeedback = '';
$question->generalfeedbackformat = FORMAT_HTML;
$question->single = 1;
$question->answer = array();
$question->fraction = array();
$question->feedback = array();
$question->correctfeedback = $this->text_field('');
$question->partiallycorrectfeedback = $this->text_field('');
$question->incorrectfeedback = $this->text_field('');
}
}
}
return $questions;
}
protected function text_field($text) {
return array(
'text' => htmlspecialchars(trim($text), ENT_NOQUOTES),
'format' => FORMAT_HTML,
'files' => array(),
);
}
public function readquestion($lines) {
// This is no longer needed but might still be called by default.php.
return;
}
public function exportpreprocess() {
// This format is not able to export categories.
$this->setCattofile(false);
return true;
}
public function writequestion($question) {
$endchar = "\n";
// Only export multichoice questions.
if ($question->qtype != 'multichoice') {
return null;
}
// Do not export multichoice multi questions.
if (!$question->options->single) {
return null;
}
// Aiken format is not able to handle question with more than 26 answers.
if (count($question->options->answers) > 26) {
return null;
}
// Export the question displaying message.
$expout = str_replace("\n", '', question_utils::to_plain_text($question->questiontext,
$question->questiontextformat, array('para' => false, 'newlines' => false))) . $endchar;
$num = 0;
foreach ($question->options->answers as $answer) {
$number = chr(ord('A') + $num);
$expout .= $number . ') ' . str_replace("\n", '', question_utils::to_plain_text($answer->answer,
$answer->answerformat, array('para' => false, 'newlines' => false))) . $endchar;
if ($answer->fraction > .99) {
$correctanswer = $number;
}
$num++;
}
// Add the correct answer.
$expout .= 'ANSWER: ' . $correctanswer;
return $expout;
}
}
@@ -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/>.
/**
* Strings for component 'qformat_aiken', language 'en', branch 'MOODLE_20_STABLE'
*
* @package qformat_aiken
* @copyright 2010 Helen Foster
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Aiken format';
$string['pluginname_help'] = 'This is a simple format for importing multiple choice questions from a text file.';
$string['pluginname_link'] = 'qformat/aiken';
$string['privacy:metadata'] = 'The Aiken question format plugin does not store any personal data.';
$string['questionmissinganswers'] = 'Question must have at least 2 answers on line {$a}';
$string['questionnotcomplete'] = 'Question not completed before next question start on line {$a}';
$string['questionnotstarted'] = 'Question not started on line {$a}';
@@ -0,0 +1,96 @@
<?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/>.
/**
* Unit tests for the Moodle Aiken format.
*
* @package qformat_aiken
* @copyright 2018 Eric Merrill (eric.a.merrill@gmail.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->dirroot . '/question/format.php');
require_once($CFG->dirroot . '/question/format/aiken/format.php');
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
/**
* Unit tests for the matching question definition class.
*
* @copyright 2018 Eric Merrill (eric.a.merrill@gmail.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class aikenformat_test extends question_testcase {
public function test_readquestions(): void {
global $CFG;
$lines = file($CFG->dirroot.'/question/format/aiken/tests/fixtures/aiken_errors.txt');
$importer = new qformat_aiken($lines);
// The importer echos some errors, so we need to capture and check that.
ob_start();
$questions = $importer->readquestions($lines);
$output = ob_get_contents();
ob_end_clean();
// Check that there were some expected errors.
$this->assertStringContainsString('Error importing question A question with too few answers', $output);
$this->assertStringContainsString('Question must have at least 2 answers on line 3', $output);
$this->assertStringContainsString('Question not started on line 5', $output);
$this->assertStringContainsString('Question not started on line 7', $output);
$this->assertStringContainsString('Error importing question A question started but not finished', $output);
$this->assertStringContainsString('Question not completed before next question start on line 18', $output);
// There are three expected questions.
$this->assertCount(3, $questions);
$q1 = null;
$q2 = null;
$q3 = null;
foreach ($questions as $question) {
if ($question->name === 'A good question') {
$q1 = $question;
} else if ($question->name === 'A second good question') {
$q2 = $question;
} else if ($question->name === 'A third good question with HTML chars such as > < &') {
$q3 = $question;
}
}
// Check the first good question.
$this->assertCount(2, $q1->answer);
$this->assertEquals(1, $q1->fraction[0]);
$this->assertEquals('Correct', $q1->answer[0]['text']);
$this->assertEquals('Incorrect', $q1->answer[1]['text']);
// Check the second good question.
$this->assertCount(2, $q2->answer);
$this->assertEquals(1, $q2->fraction[1]);
$this->assertEquals('Incorrect (No space)', $q2->answer[0]['text']);
$this->assertEquals('Correct (No space)', $q2->answer[1]['text']);
// Check the third good question that has anwsers with special HTML chars such as <, >, and &.
$this->assertCount(2, $q3->answer);
$this->assertEquals(1, $q3->fraction[0]);
$this->assertEquals('Correct (&lt; &gt; &amp;)', $q3->answer[0]['text']);
$this->assertEquals('Incorrect (&lt; &gt; &amp;)', $q3->answer[1]['text']);
}
}
@@ -0,0 +1,36 @@
@qformat @qformat_aiken
Feature: Test exporting questions using Aiken format.
In order to reuse questions
As an teacher
I need to be able to export them in Aiken format.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | template |
| Test questions | multichoice | Multi-choice-001 | two_of_four |
| Test questions | multichoice | Multi-choice-002 | one_of_four |
Scenario: Aiken export
When I am on the "Course 1" "core_question > course question export" page logged in as "teacher1"
And I set the field "id_format_aiken" to "1"
When I press "Export questions to file"
Then following "click here" should download a file that:
| Has mimetype | text/plain |
| Contains text | Which is the oddest number? |
# If the download step is the last in the scenario then we can sometimes run
# into the situation where the download page causes a http redirect but behat
# has already conducted its reset (generating an error). By putting a logout
# step we avoid behat doing the reset until we are off that page.
And I log out
@@ -0,0 +1,28 @@
@qformat @qformat_aiken
Feature: Test importing questions from Aiken format.
In order to reuse questions
As an teacher
I need to be able to import them in Aiken format.
Background:
Given the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "users" exist:
| username | firstname |
| teacher | Teacher |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
@javascript @_file_upload
Scenario: import some Aiken questions
Given I am on the "Course 1" "core_question > course question import" page logged in as "teacher"
And I set the field "id_format_aiken" to "1"
And I upload "question/format/aiken/tests/fixtures/questions.aiken.txt" file to "Import" filemanager
When I press "id_submitbutton"
Then I should see "Parsing questions from import file."
And I should see "Importing 2 questions from file"
And I should see "The Moodle project was started by:"
And I press "Continue"
And I should see "The Moodle project was started by:"
+26
View File
@@ -0,0 +1,26 @@
A question with too few answers
A) Only answer
ANSWER: A
A) Question not started
ANSWER: Question not started
A good question
A) Correct
B) Incorrect
ANSWER: A
A question started but not finished
A) Correct-ish
B) Incorrect-ish
A second good question
A)Incorrect (No space)
B)Correct (No space)
ANSWER: B
A third good question with HTML chars such as > < &
A) Correct (< > &)
B) Incorrect (< > &)
ANSWER: A
@@ -0,0 +1,11 @@
The Moodle project was started by:
A) Petr Skoda
B) Martin Dougiamas
C) Eloy Lafuente
D) Tim Hunt
ANSWER: B
Moodle's abilities include handling user input that includes <html class="cool"> & images:
A) True
B) False
ANSWER: A
@@ -0,0 +1,159 @@
<?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/>.
/**
* Unit tests for export/import description (info) for question category in the Moodle XML format.
*
* @package qformat_aiken
* @copyright 2018 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core_question\local\bank\question_version_status;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->dirroot . '/question/format.php');
require_once($CFG->dirroot . '/question/format/aiken/format.php');
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
require_once($CFG->dirroot . '/question/editlib.php');
/**
* Unit tests for the Aiken question format export.
*
* @copyright 2018 Jean-Michel vedrine)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \qformat_aiken
*/
class qformat_aiken_export_test extends advanced_testcase {
/**
* Assert that 2 strings are the same, ignoring ends of line.
* We need to override this function because we don't want any output
* @param string $expectedtext The expected string.
* @param string $text The actual string.
*/
public function assert_same_aiken($expectedtext, $text) {
$this->assertEquals(
phpunit_util::normalise_line_endings($expectedtext),
phpunit_util::normalise_line_endings($text)
);
}
public function test_export_questions(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a new course category and and a new course in that.
$category = $this->getDataGenerator()->create_category();
$course = $this->getDataGenerator()->create_course(array('category' => $category->id));
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$context = context_coursecat::instance($category->id);
$cat = $generator->create_question_category(array('contextid' => $context->id));
$question1 = $generator->create_question('shortanswer', null,
array('category' => $cat->id));
$question2 = $generator->create_question('essay', null,
array('category' => $cat->id));
$question3 = $generator->create_question('numerical', null,
array('category' => $cat->id));
$question4 = $generator->create_question('multichoice', 'one_of_four',
array('category' => $cat->id));
$question4 = $generator->create_question('multichoice', 'two_of_four',
array('category' => $cat->id));
$exporter = new qformat_aiken();
$exporter->category = $cat;
$exporter->setCourse($course);
$expectedoutput = <<<EOT
Which is the oddest number?
A) One
B) Two
C) Three
D) Four
ANSWER: A
EOT;
$this->assert_same_aiken($expectedoutput, $exporter->exportprocess());
}
public function test_export_multiline_question(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a new course category and and a new course in that.
$category = $this->getDataGenerator()->create_category();
$course = $this->getDataGenerator()->create_course(array('category' => $category->id));
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$context = context_coursecat::instance($category->id);
$cat = $generator->create_question_category(array('contextid' => $context->id));
$question = $generator->create_question('multichoice', 'one_of_four',
array('category' => $cat->id));
$question->questiontext = <<<EOT
<p>Which is the</p>
<p>oddest number?</p>
EOT;
$exporter = new qformat_aiken();
$exporter->category = $cat;
$exporter->setCourse($course);
$expectedoutput = <<<EOT
Which is the oddest number?
A) One
B) Two
C) Three
D) Four
ANSWER: A
EOT;
$this->assert_same_aiken($expectedoutput, $exporter->exportprocess());
}
public function test_hidden_question_not_exported(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a new course category and a new course in that.
$category = $this->getDataGenerator()->create_category();
$course = $this->getDataGenerator()->create_course(['category' => $category->id]);
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$context = context_coursecat::instance($category->id);
$cat = $generator->create_question_category(['contextid' => $context->id]);
// Create a visible and a hidden question.
$generator->create_question('multichoice', 'one_of_four', [
'category' => $cat->id,
'questiontext' => ['text' => 'This question is the visible one.', 'format' => FORMAT_HTML],
]);
$generator->create_question('multichoice', 'one_of_four', [
'category' => $cat->id,
'questiontext' => ['text' => 'This question is the hidden one.', 'format' => FORMAT_HTML],
'status' => question_version_status::QUESTION_STATUS_HIDDEN,
]);
// Prepared the expected result.
$expectedoutput = <<<EOT
This question is the visible one.
A) One
B) Two
C) Three
D) Four
ANSWER: A
EOT;
// Do the export and verify.
$exporter = new qformat_aiken();
$exporter->category = $cat;
$exporter->setCourse($course);
$this->assert_same_aiken($expectedoutput, $exporter->exportprocess());
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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 the calculated question type.
*
* @package qformat_aiken
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qformat_aiken';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for qformat_blackboard_six.
*
* @package qformat_blackboard_six
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qformat_blackboard_six\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qformat_blackboard_six implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @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';
}
}
+223
View File
@@ -0,0 +1,223 @@
<?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/>.
/**
* Blackboard V5 and V6 question importer.
*
* @package qformat_blackboard_six
* @copyright 2005 Michael Penney
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/xmlize.php');
require_once($CFG->dirroot . '/question/format/blackboard_six/formatbase.php');
require_once($CFG->dirroot . '/question/format/blackboard_six/formatqti.php');
require_once($CFG->dirroot . '/question/format/blackboard_six/formatpool.php');
/**
* Class to represent a Blackboard file.
*
* @package qformat_blackboard_six
* @copyright 2005 Michael Penney
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qformat_blackboard_six_file {
/** @var int type of file being imported, one of the constants FILETYPE_QTI or FILETYPE_POOL. */
public $filetype;
/** @var string the xml text */
public $text;
/** @var string path to path to root of image tree in unzipped archive. */
public $filebase = '';
}
/**
* Blackboard Six QTI file importer class.
*
* @package qformat_blackboard_six
* @copyright 2005 Michael Penney
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qformat_blackboard_six extends qformat_blackboard_six_base {
/** @var int Blackboard assessment qti files were always imported by the blackboard_six plugin. */
const FILETYPE_QTI = 1;
/** @var int Blackboard question pool files were previously handled by the blackboard plugin. */
const FILETYPE_POOL = 2;
/** @var string temporary directory/folder. */
public $temp_dir;
/**
* Return the content of a file given by its path in the tempdir directory.
*
* @param string $path path to the file inside tempdir
* @return mixed contents array or false on failure
*/
public function get_filecontent($path) {
$fullpath = $this->tempdir . '/' . clean_param($path, PARAM_PATH);
if (is_file($fullpath) && is_readable($fullpath)) {
return file_get_contents($fullpath);
}
return false;
}
/**
* Return content of all files containing questions,
* as an array one element for each file found,
* For each file, the corresponding element is an array of lines.
*
* @param string $filename name of file
* @return mixed contents array or false on failure
*/
public function readdata($filename) {
global $CFG;
// Find if we are importing a .dat file.
if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) == 'dat') {
if (!is_readable($filename)) {
$this->error(get_string('filenotreadable', 'error'));
return false;
}
$fileobj = new qformat_blackboard_six_file();
// As we are not importing a .zip file,
// there is no imsmanifest, and it is not possible
// to parse it to find the file type.
// So we need to guess the file type by looking at the content.
// For now we will do that searching for a required tag.
// This is certainly not bullet-proof but works for all usual files.
$fileobj->text = file_get_contents($filename);
if (strpos($fileobj->text, '<questestinterop>')) {
$fileobj->filetype = self::FILETYPE_QTI;
}
if (strpos($fileobj->text, '<POOL>')) {
$fileobj->filetype = self::FILETYPE_POOL;
}
// In all other cases we are not able to handle this question file.
// Readquestions is now expecting an array of strings.
return array($fileobj);
}
// We are importing a zip file.
// Create name for temporary directory.
$uniquecode = time();
$this->tempdir = make_temp_directory('bbquiz_import/' . $uniquecode);
if (is_readable($filename)) {
if (!copy($filename, $this->tempdir . '/bboard.zip')) {
$this->error(get_string('cannotcopybackup', 'question'));
fulldelete($this->tempdir);
return false;
}
$packer = get_file_packer('application/zip');
if ($packer->extract_to_pathname($this->tempdir . '/bboard.zip', $this->tempdir)) {
$dom = new DomDocument();
if (!$dom->load($this->tempdir . '/imsmanifest.xml')) {
$this->error(get_string('errormanifest', 'qformat_blackboard_six'));
fulldelete($this->tempdir);
return false;
}
$xpath = new DOMXPath($dom);
// We starts from the root element.
$query = '//resources/resource';
$qfile = array();
$examfiles = $xpath->query($query);
foreach ($examfiles as $examfile) {
$fileobj = new qformat_blackboard_six_file();
if ($examfile->getAttribute('type') == 'assessment/x-bb-qti-test'
|| $examfile->getAttribute('type') == 'assessment/x-bb-qti-pool') {
if ($content = $this->get_filecontent($examfile->getAttribute('bb:file'))) {
$fileobj->filetype = self::FILETYPE_QTI;
$fileobj->filebase = $this->tempdir;
$fileobj->text = $content;
$qfile[] = $fileobj;
}
}
if ($examfile->getAttribute('type') == 'assessment/x-bb-pool') {
if ($examfile->getAttribute('baseurl')) {
$fileobj->filebase = $this->tempdir. '/' . clean_param($examfile->getAttribute('baseurl'), PARAM_PATH);
}
if ($content = $this->get_filecontent($examfile->getAttribute('file'))) {
$fileobj->filetype = self::FILETYPE_POOL;
$fileobj->text = $content;
$qfile[] = $fileobj;
}
}
}
if ($qfile) {
return $qfile;
} else {
$this->error(get_string('cannotfindquestionfile', 'question'));
fulldelete($this->tempdir);
}
} else {
$this->error(get_string('cannotunzip', 'question'));
fulldelete($this->temp_dir);
}
} else {
$this->error(get_string('cannotreaduploadfile', 'error'));
fulldelete($this->tempdir);
}
return false;
}
/**
* Parse the array of objects into an array of questions.
* Each object is the content of a .dat questions file.
* This *could* burn memory - but it won't happen that much
* so fingers crossed!
*
* @param array $lines array of qformat_blackboard_six_file objects for each input file.
* @return array (of objects) question objects.
*/
public function readquestions($lines) {
// Set up array to hold all our questions.
$questions = array();
// Each element of $lines is a qformat_blackboard_six_file object.
foreach ($lines as $fileobj) {
if ($fileobj->filetype == self::FILETYPE_QTI) {
$importer = new qformat_blackboard_six_qti();
} else if ($fileobj->filetype == self::FILETYPE_POOL) {
$importer = new qformat_blackboard_six_pool();
} else {
// In all other cases we are not able to import the file.
debugging('fileobj type not recognised', DEBUG_DEVELOPER);
continue;
}
$importer->set_filebase($fileobj->filebase);
$questions = array_merge($questions, $importer->readquestions($fileobj->text));
}
// Give any unnamed categories generated names.
$unnamedcount = 0;
foreach ($questions as $question) {
if ($question->qtype == 'category' && $question->category == '') {
$question->category = get_string('importedcategory', 'qformat_blackboard_six', ++$unnamedcount);
}
}
return $questions;
}
}
@@ -0,0 +1,155 @@
<?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/>.
/**
* Blackboard V5 and V6 question importer.
*
* @package qformat_blackboard_six
* @copyright 2012 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Base class question import format for zip files with images
*
* @package qformat_blackboard_six
* @copyright 2012 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qformat_blackboard_six_base extends qformat_based_on_xml {
/** @var string path to path to root of image tree in unzipped archive. */
public $filebase = '';
/** @var string path to the temporary directory. */
public $tempdir = '';
/**
* This plugin provide import
* @return bool true
*/
public function provide_import() {
return true;
}
/**
* Check if the given file is capable of being imported by this plugin.
* As {@link file_storage::mimetype()} may use finfo PHP extension if available,
* the value returned by $file->get_mimetype for a .dat file is not the same on all servers.
* So we must made 2 checks to verify if the plugin can import the file.
* @param stored_file $file the file to check
* @return bool whether this plugin can import the file
*/
public function can_import_file($file) {
$mimetypes = array(
mimeinfo('type', '.dat'),
mimeinfo('type', '.zip')
);
return in_array($file->get_mimetype(), $mimetypes) || in_array(mimeinfo('type', $file->get_filename()), $mimetypes);
}
public function mime_type() {
return mimeinfo('type', '.zip');
}
/**
* Does any post-processing that may be desired
* Clean the temporary directory if a zip file was imported
* @return bool success
*/
public function importpostprocess() {
if ($this->tempdir != '') {
fulldelete($this->tempdir);
}
return true;
}
/**
* Set the path to the root of images tree
* @param string $path path to images root
*/
public function set_filebase($path) {
$this->filebase = $path;
}
/**
* Store an image file in a draft filearea
* @param array $text, if itemid element don't exists it will be created
* @param string $tempdir path to root of image tree
* @param string $filepathinsidetempdir path to image in the tree
* @param string $filename image's name
* @return string new name of the image as it was stored
*/
protected function store_file_for_text_field(&$text, $tempdir, $filepathinsidetempdir, $filename) {
global $USER;
$fs = get_file_storage();
if (empty($text['itemid'])) {
$text['itemid'] = file_get_unused_draft_itemid();
}
// As question file areas don't support subdirs,
// convert path to filename.
// So that images with same name can be imported.
$newfilename = clean_param(str_replace('/', '__', $filepathinsidetempdir . '__' . $filename), PARAM_FILE);
$filerecord = array(
'contextid' => context_user::instance($USER->id)->id,
'component' => 'user',
'filearea' => 'draft',
'itemid' => $text['itemid'],
'filepath' => '/',
'filename' => $newfilename,
);
$fs->create_file_from_pathname($filerecord, $tempdir . '/' . $filepathinsidetempdir . '/' . $filename);
return $newfilename;
}
/**
* Given an HTML text with references to images files,
* store all images in a draft filearea,
* and return an array with all urls in text recoded,
* format set to FORMAT_HTML, and itemid set to filearea itemid
* @param string $text text to parse and recode
* @return array with keys text, format, itemid.
*/
public function text_field($text) {
$data = array();
// Step one, find all file refs then add to array.
preg_match_all('|<img[^>]+src="([^"]*)"|i', $text, $out); // Find all src refs.
$filepaths = array();
foreach ($out[1] as $path) {
$fullpath = $this->filebase . '/' . $path;
if (is_readable($fullpath) && !in_array($path, $filepaths)) {
$dirpath = dirname($path);
$filename = basename($path);
$newfilename = $this->store_file_for_text_field($data, $this->filebase, $dirpath, $filename);
$text = preg_replace("|{$path}|", "@@PLUGINFILE@@/" . $newfilename, $text);
$filepaths[] = $path;
}
}
$data['text'] = $text;
$data['format'] = FORMAT_HTML;
return $data;
}
/**
* Same as text_field but text is cleaned.
* @param string $text text to parse and recode
* @return array with keys text, format, itemid.
*/
public function cleaned_text_field($text) {
return $this->text_field($this->cleaninput($text));
}
}
@@ -0,0 +1,492 @@
<?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/>.
/**
* Blackboard V5 and V6 question importer.
*
* @package qformat_blackboard_six
* @copyright 2003 Scott Elliott
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/xmlize.php');
/**
* Blackboard pool question importer class.
*
* @package qformat_blackboard_six
* @copyright 2003 Scott Elliott
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qformat_blackboard_six_pool extends qformat_blackboard_six_base {
/**
* @var bool Is the current question's question text escaped HTML
* (true for most if not all Blackboard files).
*/
public $ishtml = true;
/**
* Parse the xml document into an array of questions
*
* This *could* burn memory - but it won't happen that much
* so fingers crossed!
*
* @param array $text array of lines from the input file.
* @return array (of objects) questions objects.
*/
protected function readquestions($text) {
// This converts xml to big nasty data structure,
// the 0 means keep white space as it is.
try {
$xml = xmlize($text, 0, 'UTF-8', true);
} catch (xml_format_exception $e) {
$this->error($e->getMessage(), '');
return false;
}
$questions = array();
$this->process_category($xml, $questions);
$this->process_tf($xml, $questions);
$this->process_mc($xml, $questions);
$this->process_ma($xml, $questions);
$this->process_fib($xml, $questions);
$this->process_matching($xml, $questions);
$this->process_essay($xml, $questions);
return $questions;
}
/**
* Do question import processing common to every qtype.
*
* @param array $questiondata the xml tree related to the current question
* @return object initialized question object.
*/
public function process_common($questiondata) {
// This routine initialises the question object.
$question = $this->defaultquestion();
// Determine if the question is already escaped html.
$this->ishtml = $this->getpath($questiondata,
array('#', 'BODY', 0, '#', 'FLAGS', 0, '#', 'ISHTML', 0, '@', 'value'),
false, false);
// Put questiontext in question object.
$text = $this->getpath($questiondata,
array('#', 'BODY', 0, '#', 'TEXT', 0, '#'),
'', true, get_string('importnotext', 'qformat_blackboard_six'));
$questiontext = $this->cleaned_text_field($text);
$question->questiontext = $questiontext['text'];
$question->questiontextformat = $questiontext['format']; // Needed because add_blank_combined_feedback uses it.
if (isset($questiontext['itemid'])) {
$question->questiontextitemid = $questiontext['itemid'];
}
// Put name in question object. We must ensure it is not empty and it is less than 250 chars.
$id = $this->getpath($questiondata, array('@', 'id'), '', true);
$question->name = $this->create_default_question_name($question->questiontext,
get_string('defaultname', 'qformat_blackboard_six' , $id));
$question->generalfeedback = '';
$question->generalfeedbackformat = FORMAT_HTML;
$question->generalfeedbackfiles = array();
// TODO : read the mark from the POOL TITLE QUESTIONLIST section.
$question->defaultmark = 1;
return $question;
}
/**
* Add a category question entry based on the pool file title
* @param array $xml the xml tree
* @param array $questions the questions already parsed
*/
public function process_category($xml, &$questions) {
$title = $this->getpath($xml, array('POOL', '#', 'TITLE', 0, '@', 'value'), '', true);
$dummyquestion = new stdClass();
$dummyquestion->qtype = 'category';
$dummyquestion->category = $this->cleaninput($this->clean_question_name($title));
$questions[] = $dummyquestion;
}
/**
* Process Essay Questions
* @param array $xml the xml tree
* @param array $questions the questions already parsed
*/
public function process_essay($xml, &$questions) {
if ($this->getpath($xml, array('POOL', '#', 'QUESTION_ESSAY'), false, false)) {
$essayquestions = $this->getpath($xml,
array('POOL', '#', 'QUESTION_ESSAY'), false, false);
} else {
return;
}
foreach ($essayquestions as $thisquestion) {
$question = $this->process_common($thisquestion);
$question->qtype = 'essay';
$question->answer = '';
$answer = $this->getpath($thisquestion,
array('#', 'ANSWER', 0, '#', 'TEXT', 0, '#'), '', true);
$question->graderinfo = $this->cleaned_text_field($answer);
$question->responsetemplate = $this->text_field('');
$question->feedback = '';
$question->responseformat = 'editor';
$question->responserequired = 1;
$question->responsefieldlines = 15;
$question->attachments = 0;
$question->attachmentsrequired = 0;
$question->fraction = 0;
$questions[] = $question;
}
}
/**
* Process True / False Questions
* @param array $xml the xml tree
* @param array $questions the questions already parsed
*/
public function process_tf($xml, &$questions) {
if ($this->getpath($xml, array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false)) {
$tfquestions = $this->getpath($xml,
array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false);
} else {
return;
}
foreach ($tfquestions as $thisquestion) {
$question = $this->process_common($thisquestion);
$question->qtype = 'truefalse';
$question->single = 1; // Only one answer is allowed.
$choices = $this->getpath($thisquestion, array('#', 'ANSWER'), array(), false);
$correctanswer = $this->getpath($thisquestion,
array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'),
'', true);
// First choice is true, second is false.
$id = $this->getpath($choices[0], array('@', 'id'), '', true);
$correctfeedback = $this->getpath($thisquestion,
array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
'', true);
$incorrectfeedback = $this->getpath($thisquestion,
array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
'', true);
if (strcmp($id, $correctanswer) == 0) { // True is correct.
$question->answer = 1;
$question->feedbacktrue = $this->cleaned_text_field($correctfeedback);
$question->feedbackfalse = $this->cleaned_text_field($incorrectfeedback);
} else { // False is correct.
$question->answer = 0;
$question->feedbacktrue = $this->cleaned_text_field($incorrectfeedback);
$question->feedbackfalse = $this->cleaned_text_field($correctfeedback);
}
$question->correctanswer = $question->answer;
$questions[] = $question;
}
}
/**
* Process Multiple Choice Questions with single answer
* @param array $xml the xml tree
* @param array $questions the questions already parsed
*/
public function process_mc($xml, &$questions) {
if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false)) {
$mcquestions = $this->getpath($xml,
array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false);
} else {
return;
}
foreach ($mcquestions as $thisquestion) {
$question = $this->process_common($thisquestion);
$correctfeedback = $this->getpath($thisquestion,
array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
'', true);
$incorrectfeedback = $this->getpath($thisquestion,
array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
'', true);
$question->correctfeedback = $this->cleaned_text_field($correctfeedback);
$question->partiallycorrectfeedback = $this->text_field('');
$question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback);
$question->qtype = 'multichoice';
$question->single = 1; // Only one answer is allowed.
$choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
$correctanswerid = $this->getpath($thisquestion,
array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'),
'', true);
foreach ($choices as $choice) {
$choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true);
// Put this choice in the question object.
$question->answer[] = $this->cleaned_text_field($choicetext);
$choiceid = $this->getpath($choice, array('@', 'id'), '', true);
// If choice is the right answer, give 100% mark, otherwise give 0%.
if (strcmp ($choiceid, $correctanswerid) == 0) {
$question->fraction[] = 1;
} else {
$question->fraction[] = 0;
}
// There is never feedback specific to each choice.
$question->feedback[] = $this->text_field('');
}
$questions[] = $question;
}
}
/**
* Process Multiple Choice Questions With Multiple Answers
* @param array $xml the xml tree
* @param array $questions the questions already parsed
*/
public function process_ma($xml, &$questions) {
if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false)) {
$maquestions = $this->getpath($xml,
array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false);
} else {
return;
}
foreach ($maquestions as $thisquestion) {
$question = $this->process_common($thisquestion);
$correctfeedback = $this->getpath($thisquestion,
array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
'', true);
$incorrectfeedback = $this->getpath($thisquestion,
array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
'', true);
$question->correctfeedback = $this->cleaned_text_field($correctfeedback);
// As there is no partially correct feedback we use incorrect one.
$question->partiallycorrectfeedback = $this->cleaned_text_field($incorrectfeedback);
$question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback);
$question->qtype = 'multichoice';
$question->defaultmark = 1;
$question->single = 0; // More than one answers allowed.
$choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
$correctanswerids = array();
foreach ($this->getpath($thisquestion,
array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false) as $correctanswer) {
if ($correctanswer) {
$correctanswerids[] = $this->getpath($correctanswer,
array('@', 'answer_id'),
'', true);
}
}
$fraction = 1 / count($correctanswerids);
foreach ($choices as $choice) {
$choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true);
// Put this choice in the question object.
$question->answer[] = $this->cleaned_text_field($choicetext);
$choiceid = $this->getpath($choice, array('@', 'id'), '', true);
$iscorrect = in_array($choiceid, $correctanswerids);
if ($iscorrect) {
$question->fraction[] = $fraction;
} else {
$question->fraction[] = 0;
}
// There is never feedback specific to each choice.
$question->feedback[] = $this->text_field('');
}
$questions[] = $question;
}
}
/**
* Process Fill in the Blank Questions
* @param array $xml the xml tree
* @param array $questions the questions already parsed
*/
public function process_fib($xml, &$questions) {
if ($this->getpath($xml, array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false)) {
$fibquestions = $this->getpath($xml,
array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false);
} else {
return;
}
foreach ($fibquestions as $thisquestion) {
$question = $this->process_common($thisquestion);
$question->qtype = 'shortanswer';
$question->usecase = 0; // Ignore case.
$correctfeedback = $this->getpath($thisquestion,
array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
'', true);
$incorrectfeedback = $this->getpath($thisquestion,
array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
'', true);
$answers = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
foreach ($answers as $answer) {
$question->answer[] = $this->getpath($answer,
array('#', 'TEXT', 0, '#'), '', true);
$question->fraction[] = 1;
$question->feedback[] = $this->cleaned_text_field($correctfeedback);
}
$question->answer[] = '*';
$question->fraction[] = 0;
$question->feedback[] = $this->cleaned_text_field($incorrectfeedback);
$questions[] = $question;
}
}
/**
* Process Matching Questions
* @param array $xml the xml tree
* @param array $questions the questions already parsed
*/
public function process_matching($xml, &$questions) {
if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MATCH'), false, false)) {
$matchquestions = $this->getpath($xml,
array('POOL', '#', 'QUESTION_MATCH'), false, false);
} else {
return;
}
// Blackboard questions can't be imported in core Moodle without a loss in data,
// as core match question don't allow HTML in subanswers. The contributed ddmatch
// question type support HTML in subanswers.
// The ddmatch question type is not part of core, so we need to check if it is defined.
$ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch');
foreach ($matchquestions as $thisquestion) {
$question = $this->process_common($thisquestion);
if ($ddmatchisinstalled) {
$question->qtype = 'ddmatch';
} else {
$question->qtype = 'match';
}
$correctfeedback = $this->getpath($thisquestion,
array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
'', true);
$incorrectfeedback = $this->getpath($thisquestion,
array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
'', true);
$question->correctfeedback = $this->cleaned_text_field($correctfeedback);
// As there is no partially correct feedback we use incorrect one.
$question->partiallycorrectfeedback = $this->cleaned_text_field($incorrectfeedback);
$question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback);
$choices = $this->getpath($thisquestion,
array('#', 'CHOICE'), false, false); // Blackboard "choices" are Moodle subanswers.
$answers = $this->getpath($thisquestion,
array('#', 'ANSWER'), false, false); // Blackboard "answers" are Moodle subquestions.
$correctanswers = $this->getpath($thisquestion,
array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false); // Mapping between choices and answers.
$mappings = array();
foreach ($correctanswers as $correctanswer) {
if ($correctanswer) {
$correctchoiceid = $this->getpath($correctanswer,
array('@', 'choice_id'), '', true);
$correctanswerid = $this->getpath($correctanswer,
array('@', 'answer_id'),
'', true);
$mappings[$correctanswerid] = $correctchoiceid;
}
}
foreach ($choices as $choice) {
if ($ddmatchisinstalled) {
$choicetext = $this->cleaned_text_field($this->getpath($choice,
array('#', 'TEXT', 0, '#'), '', true));
} else {
$choicetext = trim(strip_tags($this->getpath($choice,
array('#', 'TEXT', 0, '#'), '', true)));
}
if ($choicetext != '') { // Only import non empty subanswers.
$subquestion = '';
$choiceid = $this->getpath($choice,
array('@', 'id'), '', true);
$fiber = array_search($choiceid, $mappings);
$fiber = moodle_array_keys_filter($mappings, $choiceid);
foreach ($fiber as $correctanswerid) {
// We have found a correspondance for this choice so we need to take the associated answer.
foreach ($answers as $answer) {
$currentanswerid = $this->getpath($answer,
array('@', 'id'), '', true);
if (strcmp ($currentanswerid, $correctanswerid) == 0) {
$subquestion = $this->getpath($answer,
array('#', 'TEXT', 0, '#'), '', true);
break;
}
}
$question->subquestions[] = $this->cleaned_text_field($subquestion);
$question->subanswers[] = $choicetext;
}
if ($subquestion == '') { // Then in this case, $choice is a distractor.
$question->subquestions[] = $this->text_field('');
$question->subanswers[] = $choicetext;
}
}
}
// Verify that this matching question has enough subquestions and subanswers.
$subquestioncount = 0;
$subanswercount = 0;
$subanswers = $question->subanswers;
foreach ($question->subquestions as $key => $subquestion) {
$subquestion = $subquestion['text'];
$subanswer = $subanswers[$key];
if ($subquestion != '') {
$subquestioncount++;
}
$subanswercount++;
}
if ($subquestioncount < 2 || $subanswercount < 3) {
$this->error(get_string('notenoughtsubans', 'qformat_blackboard_six', $question->questiontext));
} else {
$questions[] = $question;
}
}
}
}
@@ -0,0 +1,914 @@
<?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/>.
/**
* Blackboard V5 and V6 question importer.
*
* @package qformat_blackboard_six
* @copyright 2005 Michael Penney
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/xmlize.php');
/**
* Blackboard 6.0 question importer.
*
* @copyright 2005 Michael Penney
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qformat_blackboard_six_qti extends qformat_blackboard_six_base {
/**
* Parse the xml document into an array of questions
* this *could* burn memory - but it won't happen that much
* so fingers crossed!
* @param array $text array of lines from the input file.
* @return array (of objects) questions objects.
*/
protected function readquestions($text) {
// This converts xml to big nasty data structure,
// the 0 means keep white space as it is.
try {
$xml = xmlize($text, 0, 'UTF-8', true);
} catch (xml_format_exception $e) {
$this->error($e->getMessage(), '');
return false;
}
$questions = array();
// Treat the assessment title as a category title.
$this->process_category($xml, $questions);
// First step : we are only interested in the <item> tags.
$rawquestions = $this->getpath($xml,
array('questestinterop', '#', 'assessment', 0, '#', 'section', 0, '#', 'item'),
array(), false);
// Each <item> tag contains data related to a single question.
foreach ($rawquestions as $quest) {
// Second step : parse each question data into the intermediate
// rawquestion structure array.
// Warning : rawquestions are not Moodle questions.
$question = $this->create_raw_question($quest);
// Third step : convert a rawquestion into a Moodle question.
switch($question->qtype) {
case "Matching":
$this->process_matching($question, $questions);
break;
case "Multiple Choice":
$this->process_mc($question, $questions);
break;
case "Essay":
$this->process_essay($question, $questions);
break;
case "Multiple Answer":
$this->process_ma($question, $questions);
break;
case "True/False":
$this->process_tf($question, $questions);
break;
case 'Fill in the Blank':
$this->process_fblank($question, $questions);
break;
case 'Short Response':
$this->process_essay($question, $questions);
break;
default:
$this->error(get_string('unknownorunhandledtype', 'question', $question->qtype));
break;
}
}
return $questions;
}
/**
* Creates a cleaner object to deal with for processing into Moodle.
* The object returned is NOT a moodle question object.
* @param array $quest XML <item> question data
* @return object rawquestion
*/
public function create_raw_question($quest) {
$rawquestion = new stdClass();
$rawquestion->qtype = $this->getpath($quest,
array('#', 'itemmetadata', 0, '#', 'bbmd_questiontype', 0, '#'),
'', true);
$rawquestion->id = $this->getpath($quest,
array('#', 'itemmetadata', 0, '#', 'bbmd_asi_object_id', 0, '#'),
'', true);
$presentation = new stdClass();
$presentation->blocks = $this->getpath($quest,
array('#', 'presentation', 0, '#', 'flow', 0, '#', 'flow'),
array(), false);
foreach ($presentation->blocks as $pblock) {
$block = new stdClass();
$block->type = $this->getpath($pblock,
array('@', 'class'),
'', true);
switch($block->type) {
case 'QUESTION_BLOCK':
$subblocks = $this->getpath($pblock,
array('#', 'flow'),
array(), false);
foreach ($subblocks as $sblock) {
$this->process_block($sblock, $block);
}
break;
case 'RESPONSE_BLOCK':
$choices = null;
switch($rawquestion->qtype) {
case 'Matching':
$bbsubquestions = $this->getpath($pblock,
array('#', 'flow'),
array(), false);
foreach ($bbsubquestions as $bbsubquestion) {
$subquestion = new stdClass();
$subquestion->ident = $this->getpath($bbsubquestion,
array('#', 'response_lid', 0, '@', 'ident'),
'', true);
$this->process_block($this->getpath($bbsubquestion,
array('#', 'flow', 0),
false, false), $subquestion);
$bbchoices = $this->getpath($bbsubquestion,
array('#', 'response_lid', 0, '#', 'render_choice', 0,
'#', 'flow_label', 0, '#', 'response_label'),
array(), false);
$choices = array();
$this->process_choices($bbchoices, $choices);
$subquestion->choices = $choices;
if (!isset($block->subquestions)) {
$block->subquestions = array();
}
$block->subquestions[] = $subquestion;
}
break;
case 'Multiple Answer':
$bbchoices = $this->getpath($pblock,
array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'),
array(), false);
$choices = array();
$this->process_choices($bbchoices, $choices);
$block->choices = $choices;
break;
case 'Essay':
// Doesn't apply since the user responds with text input.
break;
case 'Multiple Choice':
$mcchoices = $this->getpath($pblock,
array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'),
array(), false);
foreach ($mcchoices as $mcchoice) {
$choices = new stdClass();
$choices = $this->process_block($mcchoice, $choices);
$block->choices[] = $choices;
}
break;
case 'Short Response':
// Do nothing?
break;
case 'Fill in the Blank':
// Do nothing?
break;
default:
$bbchoices = $this->getpath($pblock,
array('#', 'response_lid', 0, '#', 'render_choice', 0, '#',
'flow_label', 0, '#', 'response_label'),
array(), false);
$choices = array();
$this->process_choices($bbchoices, $choices);
$block->choices = $choices;
}
break;
case 'RIGHT_MATCH_BLOCK':
$matchinganswerset = $this->getpath($pblock,
array('#', 'flow'),
false, false);
$answerset = array();
foreach ($matchinganswerset as $answer) {
$bbanswer = new stdClass;
$bbanswer->text = $this->getpath($answer,
array('#', 'flow', 0, '#', 'material', 0, '#', 'mat_extension',
0, '#', 'mat_formattedtext', 0, '#'),
false, false);
$answerset[] = $bbanswer;
}
$block->matchinganswerset = $answerset;
break;
default:
$this->error(get_string('unhandledpresblock', 'qformat_blackboard_six'));
break;
}
$rawquestion->{$block->type} = $block;
}
// Determine response processing.
// There is a section called 'outcomes' that I don't know what to do with.
$resprocessing = $this->getpath($quest,
array('#', 'resprocessing'),
array(), false);
$respconditions = $this->getpath($resprocessing[0],
array('#', 'respcondition'),
array(), false);
$responses = array();
if ($rawquestion->qtype == 'Matching') {
$this->process_matching_responses($respconditions, $responses);
} else {
$this->process_responses($respconditions, $responses);
}
$rawquestion->responses = $responses;
$feedbackset = $this->getpath($quest,
array('#', 'itemfeedback'),
array(), false);
$feedbacks = array();
$this->process_feedback($feedbackset, $feedbacks);
$rawquestion->feedback = $feedbacks;
return $rawquestion;
}
/**
* Helper function to process an XML block into an object.
* Can call himself recursively if necessary to parse this branch of the XML tree.
* @param array $curblock XML block to parse
* @param object $block block already parsed so far
* @return object $block parsed
*/
public function process_block($curblock, $block) {
$curtype = $this->getpath($curblock,
array('@', 'class'),
'', true);
switch($curtype) {
case 'FORMATTED_TEXT_BLOCK':
$text = $this->getpath($curblock,
array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'),
'', true);
$block->text = $this->strip_applet_tags_get_mathml($text);
break;
case 'FILE_BLOCK':
$block->filename = $this->getpath($curblock,
array('#', 'material', 0, '#'),
'', true);
if ($block->filename != '') {
// TODO : determine what to do with the file's content.
$this->error(get_string('filenothandled', 'qformat_blackboard_six', $block->filename));
}
break;
case 'Block':
if ($this->getpath($curblock,
array('#', 'material', 0, '#', 'mattext'),
false, false)) {
$block->text = $this->getpath($curblock,
array('#', 'material', 0, '#', 'mattext', 0, '#'),
'', true);
} else if ($this->getpath($curblock,
array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext'),
false, false)) {
$block->text = $this->getpath($curblock,
array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'),
'', true);
} else if ($this->getpath($curblock,
array('#', 'response_label'),
false, false)) {
// This is a response label block.
$subblocks = $this->getpath($curblock,
array('#', 'response_label', 0),
array(), false);
if (!isset($block->ident)) {
if ($this->getpath($subblocks,
array('@', 'ident'), '', true)) {
$block->ident = $this->getpath($subblocks,
array('@', 'ident'), '', true);
}
}
foreach ($this->getpath($subblocks,
array('#', 'flow_mat'), array(), false) as $subblock) {
$this->process_block($subblock, $block);
}
} else {
if ($this->getpath($curblock,
array('#', 'flow_mat'), false, false)
|| $this->getpath($curblock,
array('#', 'flow'), false, false)) {
if ($this->getpath($curblock,
array('#', 'flow_mat'), false, false)) {
$subblocks = $this->getpath($curblock,
array('#', 'flow_mat'), array(), false);
} else if ($this->getpath($curblock,
array('#', 'flow'), false, false)) {
$subblocks = $this->getpath($curblock,
array('#', 'flow'), array(), false);
}
foreach ($subblocks as $sblock) {
// This will recursively grab the sub blocks which should be of one of the other types.
$this->process_block($sblock, $block);
}
}
}
break;
case 'LINK_BLOCK':
// Not sure how this should be included?
$link = $this->getpath($curblock,
array('#', 'material', 0, '#', 'mattext', 0, '@', 'uri'), '', true);
if (!empty($link)) {
$block->link = $link;
} else {
$block->link = '';
}
break;
}
return $block;
}
/**
* Preprocess XML blocks containing data for questions' choices.
* Called by {@link create_raw_question()}
* for matching, multichoice and fill in the blank questions.
* @param array $bbchoices XML block to parse
* @param array $choices array of choices suitable for a rawquestion.
*/
protected function process_choices($bbchoices, &$choices) {
foreach ($bbchoices as $choice) {
if ($this->getpath($choice,
array('@', 'ident'), '', true)) {
$curchoice = $this->getpath($choice,
array('@', 'ident'), '', true);
} else { // For multiple answers.
$curchoice = $this->getpath($choice,
array('#', 'response_label', 0), array(), false);
}
if ($this->getpath($choice,
array('#', 'flow_mat', 0), false, false)) { // For multiple answers.
$curblock = $this->getpath($choice,
array('#', 'flow_mat', 0), false, false);
// Reset $curchoice to new stdClass because process_block is expecting an object
// for the second argument and not a string,
// which is what is was set as originally - CT 8/7/06.
$curchoice = new stdClass();
$this->process_block($curblock, $curchoice);
} else if ($this->getpath($choice,
array('#', 'response_label'), false, false)) {
// Reset $curchoice to new stdClass because process_block is expecting an object
// for the second argument and not a string,
// which is what is was set as originally - CT 8/7/06.
$curchoice = new stdClass();
$this->process_block($choice, $curchoice);
}
$choices[] = $curchoice;
}
}
/**
* Preprocess XML blocks containing data for subanswers
* Called by {@link create_raw_question()}
* for matching questions only.
* @param array $bbresponses XML block to parse
* @param array $responses array of responses suitable for a matching rawquestion.
*/
protected function process_matching_responses($bbresponses, &$responses) {
foreach ($bbresponses as $bbresponse) {
$response = new stdClass;
if ($this->getpath($bbresponse,
array('#', 'conditionvar', 0, '#', 'varequal'), false, false)) {
$response->correct = $this->getpath($bbresponse,
array('#', 'conditionvar', 0, '#', 'varequal', 0, '#'), '', true);
$response->ident = $this->getpath($bbresponse,
array('#', 'conditionvar', 0, '#', 'varequal', 0, '@', 'respident'), '', true);
}
// Suppressed an else block because if the above if condition is false,
// the question is not necessary a broken one, most of the time it's an <other> tag.
$response->feedback = $this->getpath($bbresponse,
array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true);
$responses[] = $response;
}
}
/**
* Preprocess XML blocks containing data for responses processing.
* Called by {@link create_raw_question()}
* for all questions types.
* @param array $bbresponses XML block to parse
* @param array $responses array of responses suitable for a rawquestion.
*/
protected function process_responses($bbresponses, &$responses) {
foreach ($bbresponses as $bbresponse) {
$response = new stdClass();
if ($this->getpath($bbresponse,
array('@', 'title'), '', true)) {
$response->title = $this->getpath($bbresponse,
array('@', 'title'), '', true);
} else {
$response->title = $this->getpath($bbresponse,
array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true);
}
$response->ident = array();
if ($this->getpath($bbresponse,
array('#', 'conditionvar', 0, '#'), false, false)) {
$response->ident[0] = $this->getpath($bbresponse,
array('#', 'conditionvar', 0, '#'), array(), false);
} else if ($this->getpath($bbresponse,
array('#', 'conditionvar', 0, '#', 'other', 0, '#'), false, false)) {
$response->ident[0] = $this->getpath($bbresponse,
array('#', 'conditionvar', 0, '#', 'other', 0, '#'), array(), false);
}
if ($this->getpath($bbresponse,
array('#', 'conditionvar', 0, '#', 'and'), false, false)) {
$responseset = $this->getpath($bbresponse,
array('#', 'conditionvar', 0, '#', 'and'), array(), false);
foreach ($responseset as $rs) {
$response->ident[] = $this->getpath($rs, array('#'), array(), false);
if (!isset($response->feedback) and $this->getpath($rs, array('@'), false, false)) {
$response->feedback = $this->getpath($rs,
array('@', 'respident'), '', true);
}
}
} else {
$response->feedback = $this->getpath($bbresponse,
array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true);
}
// Determine what fraction to give response.
if ($this->getpath($bbresponse,
array('#', 'setvar'), false, false)) {
switch ($this->getpath($bbresponse,
array('#', 'setvar', 0, '#'), false, false)) {
case "SCORE.max":
$response->fraction = 1;
break;
default:
// I have only seen this being 0 or unset.
// There are probably fractional values of SCORE.max, but I'm not sure what they look like.
$response->fraction = 0;
break;
}
} else {
// Just going to assume this is the case this is probably not correct.
$response->fraction = 0;
}
$responses[] = $response;
}
}
/**
* Preprocess XML blocks containing data for responses feedbacks.
* Called by {@link create_raw_question()}
* for all questions types.
* @param array $feedbackset XML block to parse
* @param array $feedbacks array of feedbacks suitable for a rawquestion.
*/
public function process_feedback($feedbackset, &$feedbacks) {
foreach ($feedbackset as $bbfeedback) {
$feedback = new stdClass();
$feedback->ident = $this->getpath($bbfeedback,
array('@', 'ident'), '', true);
$feedback->text = '';
if ($this->getpath($bbfeedback,
array('#', 'flow_mat', 0), false, false)) {
$this->process_block($this->getpath($bbfeedback,
array('#', 'flow_mat', 0), false, false), $feedback);
} else if ($this->getpath($bbfeedback,
array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false)) {
$this->process_block($this->getpath($bbfeedback,
array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false), $feedback);
}
$feedbacks[$feedback->ident] = $feedback;
}
}
/**
* Create common parts of question
* @param object $quest rawquestion
* @return object Moodle question.
*/
public function process_common($quest) {
$question = $this->defaultquestion();
$text = $quest->QUESTION_BLOCK->text;
$questiontext = $this->cleaned_text_field($text);
$question->questiontext = $questiontext['text'];
$question->questiontextformat = $questiontext['format']; // Needed because add_blank_combined_feedback uses it.
if (isset($questiontext['itemid'])) {
$question->questiontextitemid = $questiontext['itemid'];
}
$question->name = $this->create_default_question_name($question->questiontext,
get_string('defaultname', 'qformat_blackboard_six' , $quest->id));
$question->generalfeedback = '';
$question->generalfeedbackformat = FORMAT_HTML;
$question->generalfeedbackfiles = array();
return $question;
}
/**
* Process True / False Questions
* Parse a truefalse rawquestion and add the result
* to the array of questions already parsed.
* @param object $quest rawquestion
* @param array $questions array of Moodle questions already done
*/
protected function process_tf($quest, &$questions) {
$question = $this->process_common($quest);
$question->qtype = 'truefalse';
$question->single = 1; // Only one answer is allowed.
$question->penalty = 1; // Penalty = 1 for truefalse questions.
// 0th [response] is the correct answer.
$responses = $quest->responses;
$correctresponse = $this->getpath($responses[0]->ident[0],
array('varequal', 0, '#'), '', true);
if ($correctresponse != 'false') {
$correct = true;
} else {
$correct = false;
}
$fback = new stdClass();
foreach ($quest->feedback as $fb) {
$fback->{$fb->ident} = $fb->text;
}
if ($correct) { // True is correct.
$question->answer = 1;
$question->feedbacktrue = $this->cleaned_text_field($fback->correct);
$question->feedbackfalse = $this->cleaned_text_field($fback->incorrect);
} else { // False is correct.
$question->answer = 0;
$question->feedbacktrue = $this->cleaned_text_field($fback->incorrect);
$question->feedbackfalse = $this->cleaned_text_field($fback->correct);
}
$question->correctanswer = $question->answer;
$questions[] = $question;
}
/**
* Process Fill in the Blank Questions
* Parse a fillintheblank rawquestion and add the result
* to the array of questions already parsed.
* @param object $quest rawquestion
* @param array $questions array of Moodle questions already done.
*/
protected function process_fblank($quest, &$questions) {
$question = $this->process_common($quest);
$question->qtype = 'shortanswer';
$question->usecase = 0; // Ignore case.
$answers = array();
$fractions = array();
$feedbacks = array();
// Extract the feedback.
$feedback = array();
foreach ($quest->feedback as $fback) {
if (isset($fback->ident)) {
if ($fback->ident == 'correct' || $fback->ident == 'incorrect') {
$feedback[$fback->ident] = $fback->text;
}
}
}
foreach ($quest->responses as $response) {
if (isset($response->title)) {
if ($this->getpath($response->ident[0],
array('varequal', 0, '#'), false, false)) {
// For BB Fill in the Blank, only interested in correct answers.
if ($response->feedback = 'correct') {
$answers[] = $this->getpath($response->ident[0],
array('varequal', 0, '#'), '', true);
$fractions[] = 1;
if (isset($feedback['correct'])) {
$feedbacks[] = $this->cleaned_text_field($feedback['correct']);
} else {
$feedbacks[] = $this->text_field('');
}
}
}
}
}
// Adding catchall to so that students can see feedback for incorrect answers when they enter something,
// the instructor did not enter.
$answers[] = '*';
$fractions[] = 0;
if (isset($feedback['incorrect'])) {
$feedbacks[] = $this->cleaned_text_field($feedback['incorrect']);
} else {
$feedbacks[] = $this->text_field('');
}
$question->answer = $answers;
$question->fraction = $fractions;
$question->feedback = $feedbacks; // Changed to assign $feedbacks to $question->feedback instead of.
if (!empty($question)) {
$questions[] = $question;
}
}
/**
* Process Multichoice Questions
* Parse a multichoice single answer rawquestion and add the result
* to the array of questions already parsed.
* @param object $quest rawquestion
* @param array $questions array of Moodle questions already done.
*/
protected function process_mc($quest, &$questions) {
$question = $this->process_common($quest);
$question->qtype = 'multichoice';
$question = $this->add_blank_combined_feedback($question);
$question->single = 1;
$feedback = array();
foreach ($quest->feedback as $fback) {
$feedback[$fback->ident] = $fback->text;
}
foreach ($quest->responses as $response) {
if (isset($response->title)) {
if ($response->title == 'correct') {
// Only one answer possible for this qtype so first index is correct answer.
$correct = $this->getpath($response->ident[0],
array('varequal', 0, '#'), '', true);
}
} else {
// Fallback method for when the title is not set.
if ($response->feedback == 'correct') {
// Only one answer possible for this qtype so first index is correct answer.
$correct = $this->getpath($response->ident[0],
array('varequal', 0, '#'), '', true);
}
}
}
$i = 0;
foreach ($quest->RESPONSE_BLOCK->choices as $response) {
$question->answer[$i] = $this->cleaned_text_field($response->text);
if ($correct == $response->ident) {
$question->fraction[$i] = 1;
// This is a bit of a hack to catch the feedback... first we see if a 'specific'
// feedback for this response exists, then if a 'correct' feedback exists.
if (!empty($feedback[$response->ident]) ) {
$question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]);
} else if (!empty($feedback['correct'])) {
$question->feedback[$i] = $this->cleaned_text_field($feedback['correct']);
} else if (!empty($feedback[$i])) {
$question->feedback[$i] = $this->cleaned_text_field($feedback[$i]);
} else {
$question->feedback[$i] = $this->cleaned_text_field(get_string('correct', 'question'));
}
} else {
$question->fraction[$i] = 0;
if (!empty($feedback[$response->ident]) ) {
$question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]);
} else if (!empty($feedback['incorrect'])) {
$question->feedback[$i] = $this->cleaned_text_field($feedback['incorrect']);
} else if (!empty($feedback[$i])) {
$question->feedback[$i] = $this->cleaned_text_field($feedback[$i]);
} else {
$question->feedback[$i] = $this->cleaned_text_field(get_string('incorrect', 'question'));
}
}
$i++;
}
if (!empty($question)) {
$questions[] = $question;
}
}
/**
* Process Multiple Choice Questions With Multiple Answers.
* Parse a multichoice multianswer rawquestion and add the result
* to the array of questions already parsed.
* @param object $quest rawquestion
* @param array $questions array of Moodle questions already done.
*/
public function process_ma($quest, &$questions) {
$question = $this->process_common($quest);
$question->qtype = 'multichoice';
$question = $this->add_blank_combined_feedback($question);
$question->single = 0; // More than one answer allowed.
$answers = $quest->responses;
$correctanswers = array();
foreach ($answers as $answer) {
if ($answer->title == 'correct') {
$answerset = $this->getpath($answer->ident[0],
array('and', 0, '#', 'varequal'), array(), false);
foreach ($answerset as $ans) {
$correctanswers[] = $ans['#'];
}
}
}
$feedback = new stdClass();
foreach ($quest->feedback as $fb) {
$feedback->{$fb->ident} = trim($fb->text);
}
$correctanswercount = count($correctanswers);
$fraction = 1 / $correctanswercount;
$choiceset = $quest->RESPONSE_BLOCK->choices;
$i = 0;
foreach ($choiceset as $choice) {
$question->answer[$i] = $this->cleaned_text_field(trim($choice->text));
if (in_array($choice->ident, $correctanswers)) {
// Correct answer.
$question->fraction[$i] = $fraction;
$question->feedback[$i] = $this->cleaned_text_field($feedback->correct);
} else {
// Wrong answer.
$question->fraction[$i] = 0;
$question->feedback[$i] = $this->cleaned_text_field($feedback->incorrect);
}
$i++;
}
$questions[] = $question;
}
/**
* Process Essay Questions
* Parse an essay rawquestion and add the result
* to the array of questions already parsed.
* @param object $quest rawquestion
* @param array $questions array of Moodle questions already done.
*/
public function process_essay($quest, &$questions) {
$question = $this->process_common($quest);
$question->qtype = 'essay';
$question->feedback = array();
// Not sure where to get the correct answer from?
foreach ($quest->feedback as $feedback) {
// Added this code to put the possible solution that the
// instructor gives as the Moodle answer for an essay question.
if ($feedback->ident == 'solution') {
$question->graderinfo = $this->cleaned_text_field($feedback->text);
}
}
// Added because essay/questiontype.php:save_question_option is expecting a
// fraction property - CT 8/10/06.
$question->fraction[] = 1;
$question->defaultmark = 1;
$question->responseformat = 'editor';
$question->responserequired = 1;
$question->responsefieldlines = 15;
$question->attachments = 0;
$question->attachmentsrequired = 0;
$question->responsetemplate = $this->text_field('');
$questions[] = $question;
}
/**
* Process Matching Questions
* Parse a matching rawquestion and add the result
* to the array of questions already parsed.
* @param object $quest rawquestion
* @param array $questions array of Moodle questions already done.
*/
public function process_matching($quest, &$questions) {
// Blackboard matching questions can't be imported in core Moodle without a loss in data,
// as core match question don't allow HTML in subanswers. The contributed ddmatch
// question type support HTML in subanswers.
// The ddmatch question type is not part of core, so we need to check if it is defined.
$ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch');
$question = $this->process_common($quest);
$question = $this->add_blank_combined_feedback($question);
$question->valid = true;
if ($ddmatchisinstalled) {
$question->qtype = 'ddmatch';
} else {
$question->qtype = 'match';
}
// Construction of the array holding mappings between subanswers and subquestions.
foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) {
foreach ($quest->responses as $rid => $resp) {
if (isset($resp->ident) && $resp->ident == $subq->ident) {
$correct = $resp->correct;
}
}
foreach ($subq->choices as $cid => $choice) {
if ($choice == $correct) {
$mappings[$subq->ident] = $cid;
}
}
}
foreach ($subq->choices as $choiceid => $choice) {
$subanswertext = $quest->RIGHT_MATCH_BLOCK->matchinganswerset[$choiceid]->text;
if ($ddmatchisinstalled) {
$subanswer = $this->cleaned_text_field($subanswertext);
} else {
$subanswertext = html_to_text($this->cleaninput($subanswertext), 0);
$subanswer = $subanswertext;
}
if ($subanswertext != '') { // Only import non empty subanswers.
$subquestion = '';
$fiber = moodle_array_keys_filter($mappings, $choiceid);
foreach ($fiber as $correctanswerid) {
// We have found a correspondance for this subanswer so we need to take the associated subquestion.
foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) {
$currentsubqid = $subq->ident;
if (strcmp ($currentsubqid, $correctanswerid) == 0) {
$subquestion = $subq->text;
break;
}
}
$question->subquestions[] = $this->cleaned_text_field($subquestion);
$question->subanswers[] = $subanswer;
}
if ($subquestion == '') { // Then in this case, $choice is a distractor.
$question->subquestions[] = $this->text_field('');
$question->subanswers[] = $subanswer;
}
}
}
// Verify that this matching question has enough subquestions and subanswers.
$subquestioncount = 0;
$subanswercount = 0;
$subanswers = $question->subanswers;
foreach ($question->subquestions as $key => $subquestion) {
$subquestion = $subquestion['text'];
$subanswer = $subanswers[$key];
if ($subquestion != '') {
$subquestioncount++;
}
$subanswercount++;
}
if ($subquestioncount < 2 || $subanswercount < 3) {
$this->error(get_string('notenoughtsubans', 'qformat_blackboard_six', $question->questiontext));
} else {
$questions[] = $question;
}
}
/**
* Add a category question entry based on the assessment title
* @param array $xml the xml tree
* @param array $questions the questions already parsed
*/
public function process_category($xml, &$questions) {
$title = $this->getpath($xml, array('questestinterop', '#', 'assessment', 0, '@', 'title'), '', true);
$dummyquestion = new stdClass();
$dummyquestion->qtype = 'category';
$dummyquestion->category = $this->cleaninput($this->clean_question_name($title));
$questions[] = $dummyquestion;
}
/**
* Strip the applet tag used by Blackboard to render mathml formulas,
* keeping the mathml tag.
* @param string $string
* @return string
*/
public function strip_applet_tags_get_mathml($string) {
if (stristr($string, '</APPLET>') === false) {
return $string;
} else {
// Strip all applet tags keeping stuff before/after and inbetween (if mathml) them.
while (stristr($string, '</APPLET>') !== false) {
preg_match("/(.*)\<applet.*value=\"(\<math\>.*\<\/math\>)\".*\<\/applet\>(.*)/i", $string, $mathmls);
$string = $mathmls[1].$mathmls[2].$mathmls[3];
}
return $string;
}
}
}
@@ -0,0 +1,35 @@
<?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 'qformat_blackboard_six', language 'en', branch 'MOODLE_20_STABLE'
*
* @package qformat_blackboard_six
* @copyright 2010 Helen Foster
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['defaultname'] = 'Imported question {$a}';
$string['errormanifest'] = 'Error while parsing the IMS manifest document';
$string['importnotext'] = 'Missing question text in XML file';
$string['filenothandled'] = 'This archive contains reference to a file material {$a} which is not currently handled by import';
$string['imagenotfound'] = 'Image file with path {$a} was not found in the import.';
$string['importedcategory'] = 'Imported category {$a}';
$string['notenoughtsubans'] = 'Unable to import matching question \'{$a}\' because a matching question must comprise at least two questions and three answers.';
$string['pluginname'] = 'Blackboard';
$string['pluginname_help'] = 'Blackboard format enables questions saved in all Blackboard export formats to be imported via a dat or zip file. For zip files, images import is supported.';
$string['privacy:metadata'] = 'The Blackbard question format plugin does not store any personal data.';
$string['unhandledpresblock'] = 'Unhandled presentation block';
@@ -0,0 +1,334 @@
<?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 qformat_blackboard_six;
use qformat_blackboard_six;
use qformat_blackboard_six_file;
use question_bank;
use question_check_specified_fields_expectation;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->dirroot . '/question/format.php');
require_once($CFG->dirroot . '/question/format/blackboard_six/format.php');
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
/**
* Unit tests for the blackboard v6+ question import format.
*
* @package qformat_blackboard_six
* @copyright 2012 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class blackboardformatpool_test extends \question_testcase {
public function make_test_xml() {
$xmlfile = new qformat_blackboard_six_file();
$xmlfile->filetype = 2;
$xmlfile->text = file_get_contents(__DIR__ . '/fixtures/sample_blackboard_pool.dat');
return array(0 => $xmlfile);
}
public function test_import_match(): void {
$xml = $this->make_test_xml();
$importer = new qformat_blackboard_six();
$questions = $importer->readquestions($xml);
$q = $questions[5];
// If qtype_ddmatch is installed, the formatter produces ddmatch
// qtypes, not match ones.
$ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch');
$expectedq = new \stdClass();
$expectedq->qtype = $ddmatchisinstalled ? 'ddmatch' : 'match';
$expectedq->name = 'Classify the animals.';
$expectedq->questiontext = '<i>Classify the animals.</i>';
$expectedq->questiontextformat = FORMAT_HTML;
$expectedq->correctfeedback = array('text' => '',
'format' => FORMAT_HTML);
$expectedq->partiallycorrectfeedback = array('text' => '',
'format' => FORMAT_HTML);
$expectedq->incorrectfeedback = array('text' => '',
'format' => FORMAT_HTML);
$expectedq->generalfeedback = '';
$expectedq->generalfeedbackformat = FORMAT_HTML;
$expectedq->defaultmark = 1;
$expectedq->length = 1;
$expectedq->penalty = 0.3333333;
$expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers');
$expectedq->subquestions = array(
array('text' => 'cat', 'format' => FORMAT_HTML),
array('text' => '', 'format' => FORMAT_HTML),
array('text' => 'frog', 'format' => FORMAT_HTML),
array('text' => 'newt', 'format' => FORMAT_HTML));
if ($ddmatchisinstalled) {
$expectedq->subanswers = array(
array('text' => 'mammal', 'format' => FORMAT_HTML),
array('text' => 'insect', 'format' => FORMAT_HTML),
array('text' => 'amphibian', 'format' => FORMAT_HTML),
array('text' => 'amphibian', 'format' => FORMAT_HTML),
);
} else {
$expectedq->subanswers = array('mammal', 'insect', 'amphibian', 'amphibian');
}
$this->assert(new question_check_specified_fields_expectation($expectedq), $q);
}
public function test_import_multichoice_single(): void {
$xml = $this->make_test_xml();
$importer = new qformat_blackboard_six();
$questions = $importer->readquestions($xml);
$q = $questions[2];
$expectedq = new \stdClass();
$expectedq->qtype = 'multichoice';
$expectedq->single = 1;
$expectedq->name = 'What\'s between orange and green in the spectrum?';
$expectedq->questiontext = '<span style="font-size:12pt">What\'s between orange and green in the spectrum?</span>';
$expectedq->questiontextformat = FORMAT_HTML;
$expectedq->correctfeedback = array('text' => 'You gave the right answer.',
'format' => FORMAT_HTML);
$expectedq->partiallycorrectfeedback = array('text' => '',
'format' => FORMAT_HTML);
$expectedq->incorrectfeedback = array('text' => 'Only yellow is between orange and green in the spectrum.',
'format' => FORMAT_HTML);
$expectedq->generalfeedback = '';
$expectedq->generalfeedbackformat = FORMAT_HTML;
$expectedq->defaultmark = 1;
$expectedq->length = 1;
$expectedq->penalty = 0.3333333;
$expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers');
$expectedq->answer = array(
0 => array(
'text' => '<span style="font-size:12pt">red</span>',
'format' => FORMAT_HTML,
),
1 => array(
'text' => '<span style="font-size:12pt">yellow</span>',
'format' => FORMAT_HTML,
),
2 => array(
'text' => '<span style="font-size:12pt">blue</span>',
'format' => FORMAT_HTML,
)
);
$expectedq->fraction = array(0, 1, 0);
$expectedq->feedback = array(
0 => array(
'text' => '',
'format' => FORMAT_HTML,
),
1 => array(
'text' => '',
'format' => FORMAT_HTML,
),
2 => array(
'text' => '',
'format' => FORMAT_HTML,
)
);
$this->assert(new question_check_specified_fields_expectation($expectedq), $q);
}
public function test_import_multichoice_multi(): void {
$xml = $this->make_test_xml();
$importer = new qformat_blackboard_six();
$questions = $importer->readquestions($xml);
$q = $questions[3];
$expectedq = new \stdClass();
$expectedq->qtype = 'multichoice';
$expectedq->single = 0;
$expectedq->name = 'What\'s between orange and green in the spectrum?';
$expectedq->questiontext = '<span style="font-size:12pt">What\'s between orange and green in the spectrum?</span>';
$expectedq->questiontextformat = FORMAT_HTML;
$expectedq->correctfeedback = array(
'text' => 'You gave the right answer.',
'format' => FORMAT_HTML,
);
$expectedq->partiallycorrectfeedback = array(
'text' => 'Only yellow and off-beige are between orange and green in the spectrum.',
'format' => FORMAT_HTML,
);
$expectedq->incorrectfeedback = array(
'text' => 'Only yellow and off-beige are between orange and green in the spectrum.',
'format' => FORMAT_HTML,
);
$expectedq->generalfeedback = '';
$expectedq->generalfeedbackformat = FORMAT_HTML;
$expectedq->defaultmark = 1;
$expectedq->length = 1;
$expectedq->penalty = 0.3333333;
$expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers');
$expectedq->answer = array(
0 => array(
'text' => '<span style="font-size:12pt">yellow</span>',
'format' => FORMAT_HTML,
),
1 => array(
'text' => '<span style="font-size:12pt">red</span>',
'format' => FORMAT_HTML,
),
2 => array(
'text' => '<span style="font-size:12pt">off-beige</span>',
'format' => FORMAT_HTML,
),
3 => array(
'text' => '<span style="font-size:12pt">blue</span>',
'format' => FORMAT_HTML,
)
);
$expectedq->fraction = array(0.5, 0, 0.5, 0);
$expectedq->feedback = array(
0 => array(
'text' => '',
'format' => FORMAT_HTML,
),
1 => array(
'text' => '',
'format' => FORMAT_HTML,
),
2 => array(
'text' => '',
'format' => FORMAT_HTML,
),
3 => array(
'text' => '',
'format' => FORMAT_HTML,
)
);
$this->assert(new question_check_specified_fields_expectation($expectedq), $q);
}
public function test_import_truefalse(): void {
$xml = $this->make_test_xml();
$importer = new qformat_blackboard_six();
$questions = $importer->readquestions($xml);
$q = $questions[1];
$expectedq = new \stdClass();
$expectedq->qtype = 'truefalse';
$expectedq->name = '42 is the Absolute Answer to everything.';
$expectedq->questiontext = '<span style="font-size:12pt">42 is the Absolute Answer to everything.</span>';
$expectedq->questiontextformat = FORMAT_HTML;
$expectedq->generalfeedback = '';
$expectedq->generalfeedbackformat = FORMAT_HTML;
$expectedq->defaultmark = 1;
$expectedq->length = 1;
$expectedq->correctanswer = 0;
$expectedq->feedbacktrue = array(
'text' => '42 is the Ultimate Answer.',
'format' => FORMAT_HTML,
);
$expectedq->feedbackfalse = array(
'text' => 'You gave the right answer.',
'format' => FORMAT_HTML,
);
$this->assert(new question_check_specified_fields_expectation($expectedq), $q);
}
public function test_import_fill_in_the_blank(): void {
$xml = $this->make_test_xml();
$importer = new qformat_blackboard_six();
$questions = $importer->readquestions($xml);
$q = $questions[4];
$expectedq = new \stdClass();
$expectedq->qtype = 'shortanswer';
$expectedq->name = 'Name an amphibian: __________.';
$expectedq->questiontext = '<span style="font-size:12pt">Name an amphibian: __________.</span>';
$expectedq->questiontextformat = FORMAT_HTML;
$expectedq->generalfeedback = '';
$expectedq->generalfeedbackformat = FORMAT_HTML;
$expectedq->defaultmark = 1;
$expectedq->length = 1;
$expectedq->usecase = 0;
$expectedq->answer = array('frog', '*');
$expectedq->fraction = array(1, 0);
$expectedq->feedback = array(
0 => array(
'text' => '',
'format' => FORMAT_HTML,
),
1 => array(
'text' => '',
'format' => FORMAT_HTML,
)
);
$this->assert(new question_check_specified_fields_expectation($expectedq), $q);
}
public function test_import_essay(): void {
$xml = $this->make_test_xml();
$importer = new qformat_blackboard_six();
$questions = $importer->readquestions($xml);
$q = $questions[6];
$expectedq = new \stdClass();
$expectedq->qtype = 'essay';
$expectedq->name = 'How are you?';
$expectedq->questiontext = 'How are you?';
$expectedq->questiontextformat = FORMAT_HTML;
$expectedq->generalfeedback = '';
$expectedq->generalfeedbackformat = FORMAT_HTML;
$expectedq->defaultmark = 1;
$expectedq->length = 1;
$expectedq->responseformat = 'editor';
$expectedq->responsefieldlines = 15;
$expectedq->attachments = 0;
$expectedq->graderinfo = array(
'text' => 'Blackboard answer for essay questions will be imported as informations for graders.',
'format' => FORMAT_HTML,
);
$this->assert(new question_check_specified_fields_expectation($expectedq), $q);
}
public function test_import_category(): void {
$xml = $this->make_test_xml();
$importer = new qformat_blackboard_six();
$questions = $importer->readquestions($xml);
$q = $questions[0];
$expectedq = new \stdClass();
$expectedq->qtype = 'category';
$expectedq->category = 'exam 3 2008-9';
$this->assert(new question_check_specified_fields_expectation($expectedq), $q);
}
}
@@ -0,0 +1,334 @@
<?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 qformat_blackboard_six;
use qformat_blackboard_six;
use qformat_blackboard_six_file;
use question_bank;
use question_check_specified_fields_expectation;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->dirroot . '/question/format.php');
require_once($CFG->dirroot . '/question/format/blackboard_six/format.php');
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
/**
* Unit tests for the blackboard v6+ question import format.
*
* @package qformat_blackboard_six
* @copyright 2012 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class blackboardsixformatqti_test extends \question_testcase {
public function make_test_xml() {
$xmlfile = new qformat_blackboard_six_file();
$xmlfile->filetype = 1;
$xmlfile->text = file_get_contents(__DIR__ . '/fixtures/sample_blackboard_qti.dat');
return array(0 => $xmlfile);
}
public function test_import_match(): void {
$xml = $this->make_test_xml();
$importer = new qformat_blackboard_six();
$questions = $importer->readquestions($xml);
$q = $questions[4];
// If qtype_ddmatch is installed, the formatter produces ddmatch
// qtypes, not match ones.
$ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch');
$expectedq = new \stdClass();
$expectedq->qtype = $ddmatchisinstalled ? 'ddmatch' : 'match';
$expectedq->name = 'Classify the animals.';
$expectedq->questiontext = 'Classify the animals.';
$expectedq->questiontextformat = FORMAT_HTML;
$expectedq->correctfeedback = array('text' => '',
'format' => FORMAT_HTML, 'files' => array());
$expectedq->partiallycorrectfeedback = array('text' => '',
'format' => FORMAT_HTML, 'files' => array());
$expectedq->incorrectfeedback = array('text' => '',
'format' => FORMAT_HTML, 'files' => array());
$expectedq->generalfeedback = '';
$expectedq->generalfeedbackformat = FORMAT_HTML;
$expectedq->defaultmark = 1;
$expectedq->length = 1;
$expectedq->penalty = 0.3333333;
$expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers');
$expectedq->subquestions = array(
array('text' => '', 'format' => FORMAT_HTML),
array('text' => 'cat', 'format' => FORMAT_HTML),
array('text' => 'frog', 'format' => FORMAT_HTML),
array('text' => 'newt', 'format' => FORMAT_HTML));
if ($ddmatchisinstalled) {
$expectedq->subanswers = array(
array('text' => 'insect', 'format' => FORMAT_HTML),
array('text' => 'mammal', 'format' => FORMAT_HTML),
array('text' => 'amphibian', 'format' => FORMAT_HTML),
array('text' => 'amphibian', 'format' => FORMAT_HTML),
);
} else {
$expectedq->subanswers = array('insect', 'mammal', 'amphibian', 'amphibian');
}
$this->assert(new question_check_specified_fields_expectation($expectedq), $q);
}
public function test_import_multichoice_single(): void {
$xml = $this->make_test_xml();
$importer = new qformat_blackboard_six();
$questions = $importer->readquestions($xml);
$q = $questions[2];
$expectedq = new \stdClass();
$expectedq->qtype = 'multichoice';
$expectedq->single = 1;
$expectedq->name = 'What\'s between orange and green in the spectrum?';
$expectedq->questiontext = '<span style="font-size:12pt">What\'s between orange and green in the spectrum?</span>';
$expectedq->questiontextformat = FORMAT_HTML;
$expectedq->correctfeedback = array('text' => '',
'format' => FORMAT_HTML, 'files' => array());
$expectedq->partiallycorrectfeedback = array('text' => '',
'format' => FORMAT_HTML, 'files' => array());
$expectedq->incorrectfeedback = array('text' => '',
'format' => FORMAT_HTML, 'files' => array());
$expectedq->generalfeedback = '';
$expectedq->generalfeedbackformat = FORMAT_HTML;
$expectedq->defaultmark = 1;
$expectedq->length = 1;
$expectedq->penalty = 0.3333333;
$expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers');
$expectedq->answer = array(
0 => array(
'text' => '<span style="font-size:12pt">red</span>',
'format' => FORMAT_HTML,
),
1 => array(
'text' => '<span style="font-size:12pt">yellow</span>',
'format' => FORMAT_HTML,
),
2 => array(
'text' => '<span style="font-size:12pt">blue</span>',
'format' => FORMAT_HTML,
)
);
$expectedq->fraction = array(0, 1, 0);
$expectedq->feedback = array(
0 => array(
'text' => 'Red is not between orange and green in the spectrum but yellow is.',
'format' => FORMAT_HTML,
),
1 => array(
'text' => 'You gave the right answer.',
'format' => FORMAT_HTML,
),
2 => array(
'text' => 'Blue is not between orange and green in the spectrum but yellow is.',
'format' => FORMAT_HTML,
)
);
$this->assert(new question_check_specified_fields_expectation($expectedq), $q);
}
public function test_import_multichoice_multi(): void {
$xml = $this->make_test_xml();
$importer = new qformat_blackboard_six();
$questions = $importer->readquestions($xml);
$q = $questions[3];
$expectedq = new \stdClass();
$expectedq->qtype = 'multichoice';
$expectedq->single = 0;
$expectedq->name = 'What\'s between orange and green in the spectrum?';
$expectedq->questiontext = '<i>What\'s between orange and green in the spectrum?</i>';
$expectedq->questiontextformat = FORMAT_HTML;
$expectedq->correctfeedback = array(
'text' => '',
'format' => FORMAT_HTML,
'files' => array(),
);
$expectedq->partiallycorrectfeedback = array(
'text' => '',
'format' => FORMAT_HTML,
'files' => array(),
);
$expectedq->incorrectfeedback = array(
'text' => '',
'format' => FORMAT_HTML,
'files' => array(),
);
$expectedq->generalfeedback = '';
$expectedq->generalfeedbackformat = FORMAT_HTML;
$expectedq->defaultmark = 1;
$expectedq->length = 1;
$expectedq->penalty = 0.3333333;
$expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers');
$expectedq->answer = array(
0 => array(
'text' => '<span style="font-size:12pt">yellow</span>',
'format' => FORMAT_HTML,
),
1 => array(
'text' => '<span style="font-size:12pt">red</span>',
'format' => FORMAT_HTML,
),
2 => array(
'text' => '<span style="font-size:12pt">off-beige</span>',
'format' => FORMAT_HTML,
),
3 => array(
'text' => '<span style="font-size:12pt">blue</span>',
'format' => FORMAT_HTML,
)
);
$expectedq->fraction = array(0.5, 0, 0.5, 0);
$expectedq->feedback = array(
0 => array(
'text' => '',
'format' => FORMAT_HTML,
),
1 => array(
'text' => '',
'format' => FORMAT_HTML,
),
2 => array(
'text' => '',
'format' => FORMAT_HTML,
),
3 => array(
'text' => '',
'format' => FORMAT_HTML,
)
);
$this->assert(new question_check_specified_fields_expectation($expectedq), $q);
}
public function test_import_truefalse(): void {
$xml = $this->make_test_xml();
$importer = new qformat_blackboard_six();
$questions = $importer->readquestions($xml);
$q = $questions[1];
$expectedq = new \stdClass();
$expectedq->qtype = 'truefalse';
$expectedq->name = '42 is the Absolute Answer to everything.';
$expectedq->questiontext = '<span style="font-size:12pt">42 is the Absolute Answer to everything.</span>';
$expectedq->questiontextformat = FORMAT_HTML;
$expectedq->generalfeedback = '';
$expectedq->generalfeedbackformat = FORMAT_HTML;
$expectedq->defaultmark = 1;
$expectedq->length = 1;
$expectedq->correctanswer = 0;
$expectedq->feedbacktrue = array(
'text' => '42 is the <b>Ultimate</b> Answer.',
'format' => FORMAT_HTML,
);
$expectedq->feedbackfalse = array(
'text' => 'You gave the right answer.',
'format' => FORMAT_HTML,
);
$this->assert(new question_check_specified_fields_expectation($expectedq), $q);
}
public function test_import_fill_in_the_blank(): void {
$xml = $this->make_test_xml();
$importer = new qformat_blackboard_six();
$questions = $importer->readquestions($xml);
$q = $questions[5];
$expectedq = new \stdClass();
$expectedq->qtype = 'shortanswer';
$expectedq->name = 'Name an amphibian: __________.';
$expectedq->questiontext = '<span style="font-size:12pt">Name an amphibian: __________.</span>';
$expectedq->questiontextformat = FORMAT_HTML;
$expectedq->generalfeedback = '';
$expectedq->generalfeedbackformat = FORMAT_HTML;
$expectedq->defaultmark = 1;
$expectedq->length = 1;
$expectedq->usecase = 0;
$expectedq->answer = array('frog', '*');
$expectedq->fraction = array(1, 0);
$expectedq->feedback = array(
0 => array(
'text' => 'A frog is an amphibian.',
'format' => FORMAT_HTML,
),
1 => array(
'text' => 'A frog is an amphibian.',
'format' => FORMAT_HTML,
)
);
$this->assert(new question_check_specified_fields_expectation($expectedq), $q);
}
public function test_import_essay(): void {
$xml = $this->make_test_xml();
$importer = new qformat_blackboard_six();
$questions = $importer->readquestions($xml);
$q = $questions[6];
$expectedq = new \stdClass();
$expectedq->qtype = 'essay';
$expectedq->name = 'How are you?';
$expectedq->questiontext = 'How are you?';
$expectedq->questiontextformat = FORMAT_HTML;
$expectedq->generalfeedback = '';
$expectedq->generalfeedbackformat = FORMAT_HTML;
$expectedq->defaultmark = 1;
$expectedq->length = 1;
$expectedq->responseformat = 'editor';
$expectedq->responsefieldlines = 15;
$expectedq->attachments = 0;
$expectedq->graderinfo = array(
'text' => 'Blackboard answer for essay questions will be imported as informations for graders.',
'format' => FORMAT_HTML,
);
$this->assert(new question_check_specified_fields_expectation($expectedq), $q);
}
public function test_import_category(): void {
$xml = $this->make_test_xml();
$importer = new qformat_blackboard_six();
$questions = $importer->readquestions($xml);
$q = $questions[0];
$expectedq = new \stdClass();
$expectedq->qtype = 'category';
$expectedq->category = 'sample_blackboard_six';
$this->assert(new question_check_specified_fields_expectation($expectedq), $q);
}
}
@@ -0,0 +1,142 @@
<?xml version='1.0' encoding='utf-8'?>
<POOL>
<TITLE value='exam 3 2008-9'/>
<QUESTIONLIST>
<QUESTION id='q1' class='QUESTION_TRUEFALSE' points='1'/>
<QUESTION id='q7' class='QUESTION_MULTIPLECHOICE' points='1'/>
<QUESTION id='q8' class='QUESTION_MULTIPLEANSWER' points='1'/>
<QUESTION id='q39-44' class='QUESTION_MATCH' points='1'/>
<QUESTION id='q9' class='QUESTION_ESSAY' points='1'/>
<QUESTION id='q27' class='QUESTION_FILLINBLANK' points='1'/>
</QUESTIONLIST>
<QUESTION_TRUEFALSE id='q1'>
<BODY>
<TEXT><![CDATA[<span style="font-size:12pt">42 is the Absolute Answer to everything.</span>]]></TEXT>
<FLAGS>
<ISHTML value='true'/>
<ISNEWLINELITERAL value='false'/>
</FLAGS>
</BODY>
<ANSWER id='q1_a1'>
<TEXT>False</TEXT>
</ANSWER>
<ANSWER id='q1_a2'>
<TEXT>True</TEXT>
</ANSWER>
<GRADABLE>
<CORRECTANSWER answer_id='q1_a2'/>
<FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
<FEEDBACK_WHEN_INCORRECT><![CDATA[42 is the Ultimate Answer.]]></FEEDBACK_WHEN_INCORRECT>
</GRADABLE>
</QUESTION_TRUEFALSE>
<QUESTION_MULTIPLECHOICE id='q7'>
<BODY>
<TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>
<FLAGS>
<ISHTML value='true'/>
<ISNEWLINELITERAL value='false'/>
</FLAGS>
</BODY>
<ANSWER id='q7_a1' position='1'>
<TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>
</ANSWER>
<ANSWER id='q7_a2' position='2'>
<TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>
</ANSWER>
<ANSWER id='q7_a3' position='3'>
<TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>
</ANSWER>
<GRADABLE>
<CORRECTANSWER answer_id='q7_a2'/>
<FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
<FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow is between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>
</GRADABLE>
</QUESTION_MULTIPLECHOICE>
<QUESTION_MULTIPLEANSWER id='q8'>
<BODY>
<TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>
<FLAGS>
<ISHTML value='true'/>
<ISNEWLINELITERAL value='false'/>
</FLAGS>
</BODY>
<ANSWER id='q8_a1' position='1'>
<TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>
</ANSWER>
<ANSWER id='q8_a2' position='2'>
<TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>
</ANSWER>
<ANSWER id='q8_a3' position='3'>
<TEXT><![CDATA[<span style="font-size:12pt">off-beige</span>]]></TEXT>
</ANSWER>
<ANSWER id='q8_a4' position='4'>
<TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>
</ANSWER>
<GRADABLE>
<CORRECTANSWER answer_id='q8_a1'/>
<CORRECTANSWER answer_id='q8_a3'/>
<FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
<FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow and off-beige are between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>
</GRADABLE>
</QUESTION_MULTIPLEANSWER>
<QUESTION_MATCH id='q39-44'>
<BODY>
<TEXT><![CDATA[<i>Classify the animals.</i>]]></TEXT>
<FLAGS>
<ISHTML value='true'/>
<ISNEWLINELITERAL value='false'/>
</FLAGS>
</BODY>
<ANSWER id='q39-44_a1' position='1'>
<TEXT><![CDATA[frog]]></TEXT>
</ANSWER>
<ANSWER id='q39-44_a2' position='2'>
<TEXT><![CDATA[cat]]></TEXT>
</ANSWER>
<ANSWER id='q39-44_a3' position='3'>
<TEXT><![CDATA[newt]]></TEXT>
</ANSWER>
<CHOICE id='q39-44_c1' position='1'>
<TEXT><![CDATA[mammal]]></TEXT>
</CHOICE>
<CHOICE id='q39-44_c2' position='2'>
<TEXT><![CDATA[insect]]></TEXT>
</CHOICE>
<CHOICE id='q39-44_c3' position='3'>
<TEXT><![CDATA[amphibian]]></TEXT>
</CHOICE>
<GRADABLE>
<CORRECTANSWER answer_id='q39-44_a1' choice_id='q39-44_c3'/>
<CORRECTANSWER answer_id='q39-44_a2' choice_id='q39-44_c1'/>
<CORRECTANSWER answer_id='q39-44_a3' choice_id='q39-44_c3'/>
</GRADABLE>
</QUESTION_MATCH>
<QUESTION_ESSAY id='q9'>
<BODY>
<TEXT><![CDATA[How are you?]]></TEXT>
<FLAGS>
<ISHTML value='true'/>
<ISNEWLINELITERAL value='false'/>
</FLAGS>
</BODY>
<ANSWER id='q9_a1'>
<TEXT><![CDATA[Blackboard answer for essay questions will be imported as informations for graders.]]></TEXT>
</ANSWER>
<GRADABLE>
</GRADABLE>
</QUESTION_ESSAY>
<QUESTION_FILLINBLANK id='q27'>
<BODY>
<TEXT><![CDATA[<span style="font-size:12pt">Name an amphibian: __________.</span>]]></TEXT>
<FLAGS>
<ISHTML value='true'/>
<ISNEWLINELITERAL value='false'/>
</FLAGS>
</BODY>
<ANSWER id='q27_a1' position='1'>
<TEXT>frog</TEXT>
</ANSWER>
<GRADABLE>
</GRADABLE>
</QUESTION_FILLINBLANK>
</POOL>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
<?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 the blackboard_six question import format.
*
* @package qformat_blackboard_six
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qformat_blackboard_six';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for qformat_gift.
*
* @package qformat_gift
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qformat_gift\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qformat_gift implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @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';
}
}
+207
View File
@@ -0,0 +1,207 @@
// EXAMPLE QUESTIONS for the GIFT import filter
// by Paul Tsuchido Shew, January 2004.
//-----------------------------------------//
// Examples from the class description.
//-----------------------------------------//
Who's buried in Grant's tomb?{~Grant ~Jefferson =no one}
Grant is {~buried =entombed ~living} in Grant's tomb.
Grant is buried in Grant's tomb.{FALSE}
Who's buried in Grant's tomb?{=no one =nobody}
When was Ulysses S. Grant born?{#1822:1}
//-----------------------------------------//
// Examples from the documentation.
//-----------------------------------------//
// ===Multiple Choice===
Who's buried in Grant's tomb?{~Grant ~Jefferson =no one}
Grant is {~buried =entombed ~living} in Grant's tomb.
The American holiday of Thanksgiving is celebrated on the {
~second
~third
=fourth
} Thursday of November.
Japanese characters originally came from what country? {
~India
=China
~Korea
~Egypt}
// ===Short Answer===
Who's buried in Grant's tomb?{=no one =nobody}
Two plus two equals {=four =4}.
// ===True-False===
Grant is buried in Grant's tomb.{F}
The sun rises in the east.{T}
// ===Matching===
Matching Question. {
=subquestion1 -> subanswer1
=subquestion2 -> subanswer2
=subquestion3 -> subanswer3
}
Match the following countries with their corresponding capitals. {
=Canada -> Ottawa
=Italy -> Rome
=Japan -> Tokyo
=India -> New Delhi
}
// ===Numerical===
When was Ulysses S. Grant born? {#1822}
What is the value of pi (to 3 decimal places)? {#3.1415:0.0005}.
What is the value of pi (to 3 decimal places)? {#3.141..3.142}.
When was Ulysses S. Grant born? {#
=1822:0
=%50%1822:2}
// OPTIONS
// ===Line Comments===
// Subheading: Numerical questions below
What's 2 plus 2? {#4}
// ===Question Name===
::Kanji Origins::Japanese characters originally
came from what country? {=China}
::Thanksgiving Date::The American holiday of Thanksgiving is
celebrated on the {~second ~third =fourth} Thursday of November.
// ===Feedback===
What's the answer to this multiple-choice question?{
~wrong answer#feedback comment on the wrong answer
~another wrong answer#feedback comment on this wrong answer
=right answer#Very good!}
Who's buried in Grant's tomb?{
=no one#excellent answer!
=nobody#excellent answer!}
// ===Specify text format===
[markdown]Who's buried in **Grant's tomb**?{
=no one#excellent answer!
=nobody#excellent answer!}
// ===Percentage Answer Weights===
Grant is buried in Grant's tomb.{FALSE#No one is buried in Grant's tomb.}
Difficult question.{~wrong answer ~%50%half credit answer =full credit answer}
::Jesus' hometown::Jesus Christ was from {
~Jerusalem#This was an important city, but the wrong answer.
~%25%Bethlehem#He was born here, but not raised here.
~%50%Galilee#You need to be more specific.
=Nazareth#Yes! That's right!}.
::Jesus' hometown:: Jesus Christ was from {
=Nazareth#Yes! That's right!
=%75%Nazereth#Right, but misspelled.
=%25%Bethlehem#He was born here, but not raised here.}
// ===Multiple Answers===
What two people are entombed in Grant's tomb? {
~No one
~%50%Grant
~%50%Grant's wife
~Grant's father }
What two people are entombed in Grant's tomb? {
~%-50%No one
~%50%Grant
~%50%Grant's wife
~%-50%Grant's father }
// ===Special Characters===
Which answer equals 5? {
~ \= 2 + 2
= \= 2 + 3
~ \= 2 + 4 }
::GIFT Control Characters::
Which of the following is NOT a control character for the GIFT import format? {
~ \~ # \~ is a control character.
~ \= # \= is a control character.
~ \# # \# is a control character.
~ \{ # \{ is a control character.
~ \} # \} is a control character.
= \\ # Correct! \\ (backslash) is not a control character. BUT,
it is used to escape the control characters. So, to specify
a literal backslash, you must escape it with a backslash
(as shown in this example).
}
//-----------------------------------------//
// Examples from gift/format.php.
//-----------------------------------------//
Who's buried in Grant's tomb?{~Grant ~Jefferson =no one}
Grant is {~buried =entombed ~living} in Grant's tomb.
Grant is buried in Grant's tomb.{FALSE}
Who's buried in Grant's tomb?{=no one =nobody}
When was Ulysses S. Grant born?{#1822:5}
Match the following countries with their corresponding
capitals.{=Canada->Ottawa =Italy->Rome =Japan->Tokyo}
//-----------------------------------------//
// More complicated examples.
//-----------------------------------------//
::Grant's Tomb::Grant is {
~buried#No one is buried there.
=entombed#Right answer!
~living#We hope not!
} in Grant's tomb.
Difficult multiple choice question.{
~wrong answer #comment on wrong answer
~%50%half credit answer #comment on answer
=full credit answer #well done!}
::Jesus' hometown (Short answer ex.):: Jesus Christ was from {
=Nazareth#Yes! That's right!
=%75%Nazereth#Right, but misspelled.
=%25%Bethlehem#He was born here, but not raised here.
}.
//this comment will be ignored by the filter
::Numerical example::
When was Ulysses S. Grant born? {#
=1822:0 #Correct! 100% credit
=%50%1822:2 #He was born in 1822.
You get 50% credit for being close.
}
+885
View File
@@ -0,0 +1,885 @@
<?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/>.
/**
* GIFT format question importer/exporter.
*
* @package qformat_gift
* @copyright 2003 Paul Tsuchido Shew
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* The GIFT import filter was designed as an easy to use method
* for teachers writing questions as a text file. It supports most
* question types and the missing word format.
*
* Multiple Choice / Missing Word
* Who's buried in Grant's tomb?{~Grant ~Jefferson =no one}
* Grant is {~buried =entombed ~living} in Grant's tomb.
* True-False:
* Grant is buried in Grant's tomb.{FALSE}
* Short-Answer.
* Who's buried in Grant's tomb?{=no one =nobody}
* Numerical
* When was Ulysses S. Grant born?{#1822:5}
* Matching
* Match the following countries with their corresponding
* capitals.{=Canada->Ottawa =Italy->Rome =Japan->Tokyo}
*
* Comment lines start with a double backslash (//).
* Optional question names are enclosed in double colon(::).
* Answer feedback is indicated with hash mark (#).
* Percentage answer weights immediately follow the tilde (for
* multiple choice) or equal sign (for short answer and numerical),
* and are enclosed in percent signs (% %). See docs and examples.txt for more.
*
* This filter was written through the collaboration of numerous
* members of the Moodle community. It was originally based on
* the missingword format, which included code from Thomas Robb
* and others. Paul Tsuchido Shew wrote this filter in December 2003.
*
* @copyright 2003 Paul Tsuchido Shew
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qformat_gift extends qformat_default {
public function provide_import() {
return true;
}
public function provide_export() {
return true;
}
public function export_file_extension() {
return '.txt';
}
/**
* Validate the given file.
*
* For more expensive or detailed integrity checks.
*
* @param stored_file $file the file to check
* @return string the error message that occurred while validating the given file
*/
public function validate_file(stored_file $file): string {
return $this->validate_is_utf8_file($file);
}
protected function answerweightparser(&$answer) {
$answer = substr($answer, 1); // Removes initial %.
$endposition = strpos($answer, "%");
$answerweight = substr($answer, 0, $endposition); // Gets weight as integer.
$answerweight = $answerweight/100; // Converts to percent.
$answer = substr($answer, $endposition+1); // Removes comment from answer.
return $answerweight;
}
protected function commentparser($answer, $defaultformat) {
$bits = explode('#', $answer, 2);
$ans = $this->parse_text_with_format(trim($bits[0]), $defaultformat);
if (count($bits) > 1) {
$feedback = $this->parse_text_with_format(trim($bits[1]), $defaultformat);
} else {
$feedback = array('text' => '', 'format' => $defaultformat, 'files' => array());
}
return array($ans, $feedback);
}
protected function split_truefalse_comment($answer, $defaultformat) {
$bits = explode('#', $answer, 3);
$ans = $this->parse_text_with_format(trim($bits[0]), $defaultformat);
if (count($bits) > 1) {
$wrongfeedback = $this->parse_text_with_format(trim($bits[1]), $defaultformat);
} else {
$wrongfeedback = array('text' => '', 'format' => $defaultformat, 'files' => array());
}
if (count($bits) > 2) {
$rightfeedback = $this->parse_text_with_format(trim($bits[2]), $defaultformat);
} else {
$rightfeedback = array('text' => '', 'format' => $defaultformat, 'files' => array());
}
return array($ans, $wrongfeedback, $rightfeedback);
}
protected function escapedchar_pre($string) {
// Replaces escaped control characters with a placeholder BEFORE processing.
$escapedcharacters = array("\\:", "\\#", "\\=", "\\{", "\\}", "\\~", "\\n" );
$placeholders = array("&&058;", "&&035;", "&&061;", "&&123;", "&&125;", "&&126;", "&&010");
$string = str_replace("\\\\", "&&092;", $string);
$string = str_replace($escapedcharacters, $placeholders, $string);
$string = str_replace("&&092;", "\\", $string);
return $string;
}
protected function escapedchar_post($string) {
// Replaces placeholders with corresponding character AFTER processing is done.
$placeholders = array("&&058;", "&&035;", "&&061;", "&&123;", "&&125;", "&&126;", "&&010");
$characters = array(":", "#", "=", "{", "}", "~", "\n" );
$string = str_replace($placeholders, $characters, $string);
return $string;
}
protected function check_answer_count($min, $answers, $text) {
$countanswers = count($answers);
if ($countanswers < $min) {
$this->error(get_string('importminerror', 'qformat_gift'), $text);
return false;
}
return true;
}
protected function parse_text_with_format($text, $defaultformat = FORMAT_MOODLE) {
$result = array(
'text' => $text,
'format' => $defaultformat,
'files' => array(),
);
if (strpos($text, '[') === 0) {
$formatend = strpos($text, ']');
$result['format'] = $this->format_name_to_const(substr($text, 1, $formatend - 1));
if ($result['format'] == -1) {
$result['format'] = $defaultformat;
} else {
$result['text'] = substr($text, $formatend + 1);
}
}
$result['text'] = trim($this->escapedchar_post($result['text']));
return $result;
}
public function readquestion($lines) {
// Given an array of lines known to define a question in this format, this function
// converts it into a question object suitable for processing and insertion into Moodle.
$question = $this->defaultquestion();
// Define replaced by simple assignment, stop redefine notices.
$giftanswerweightregex = '/^%\-*([0-9]{1,2})\.?([0-9]*)%/';
// Separate comments and implode.
$comments = '';
foreach ($lines as $key => $line) {
$line = trim($line);
if (substr($line, 0, 2) == '//') {
$comments .= $line . "\n";
$lines[$key] = ' ';
}
}
$text = trim(implode("\n", $lines));
if ($text == '') {
return false;
}
// Substitute escaped control characters with placeholders.
$text = $this->escapedchar_pre($text);
// Look for category modifier.
if (preg_match('~^\$CATEGORY:~', $text)) {
$newcategory = trim(substr($text, 10));
// Build fake question to contain category.
$question->qtype = 'category';
$question->category = $newcategory;
return $question;
}
// Question name parser.
if (substr($text, 0, 2) == '::') {
$text = substr($text, 2);
$namefinish = strpos($text, '::');
if ($namefinish === false) {
$question->name = false;
// Name will be assigned after processing question text below.
} else {
$questionname = substr($text, 0, $namefinish);
$question->name = $this->clean_question_name($this->escapedchar_post($questionname));
$text = trim(substr($text, $namefinish+2)); // Remove name from text.
}
} else {
$question->name = false;
}
// Find the answer section.
$answerstart = strpos($text, '{');
$answerfinish = strpos($text, '}');
$description = false;
if ($answerstart === false && $answerfinish === false) {
// No answer means it's a description.
$description = true;
$answertext = '';
$answerlength = 0;
} else if ($answerstart === false || $answerfinish === false) {
$this->error(get_string('braceerror', 'qformat_gift'), $text);
return false;
} else {
$answerlength = $answerfinish - $answerstart;
$answertext = trim(substr($text, $answerstart + 1, $answerlength - 1));
}
// Format the question text, without answer, inserting "_____" as necessary.
if ($description) {
$questiontext = $text;
} else if (substr($text, -1) == "}") {
// No blank line if answers follow question, outside of closing punctuation.
$questiontext = substr_replace($text, "", $answerstart, $answerlength + 1);
} else {
// Inserts blank line for missing word format.
$questiontext = substr_replace($text, "_____", $answerstart, $answerlength + 1);
}
// Look to see if there is any general feedback.
$gfseparator = strrpos($answertext, '####');
if ($gfseparator === false) {
$generalfeedback = '';
} else {
$generalfeedback = substr($answertext, $gfseparator + 4);
$answertext = trim(substr($answertext, 0, $gfseparator));
}
// Get questiontext format from questiontext.
$text = $this->parse_text_with_format($questiontext);
$question->questiontextformat = $text['format'];
$question->questiontext = $text['text'];
// Get generalfeedback format from questiontext.
$text = $this->parse_text_with_format($generalfeedback, $question->questiontextformat);
$question->generalfeedback = $text['text'];
$question->generalfeedbackformat = $text['format'];
// Set question name if not already set.
if ($question->name === false) {
$question->name = $this->create_default_question_name($question->questiontext, get_string('questionname', 'question'));
}
// Determine question type.
$question->qtype = null;
// Extract any idnumber and tags from the comments.
list($question->idnumber, $question->tags) = $this->extract_idnumber_and_tags_from_comment($comments);
// Give plugins first try.
// Plugins must promise not to intercept standard qtypes
// MDL-12346, this could be called from lesson mod which has its own base class =(.
if (method_exists($this, 'try_importing_using_qtypes')
&& ($tryquestion = $this->try_importing_using_qtypes($lines, $question, $answertext))) {
return $tryquestion;
}
if ($description) {
$question->qtype = 'description';
} else if ($answertext == '') {
$question->qtype = 'essay';
} else if ($answertext[0] == '#') {
$question->qtype = 'numerical';
} else if (strpos($answertext, '~') !== false) {
// Only Multiplechoice questions contain tilde ~.
$question->qtype = 'multichoice';
} else if (strpos($answertext, '=') !== false
&& strpos($answertext, '->') !== false) {
// Only Matching contains both = and ->.
$question->qtype = 'match';
} else { // Either truefalse or shortanswer.
// Truefalse question check.
$truefalsecheck = $answertext;
if (strpos($answertext, '#') > 0) {
// Strip comments to check for TrueFalse question.
$truefalsecheck = trim(substr($answertext, 0, strpos($answertext, "#")));
}
$validtfanswers = array('T', 'TRUE', 'F', 'FALSE');
if (in_array($truefalsecheck, $validtfanswers)) {
$question->qtype = 'truefalse';
} else { // Must be shortanswer.
$question->qtype = 'shortanswer';
}
}
if (!isset($question->qtype)) {
$giftqtypenotset = get_string('giftqtypenotset', 'qformat_gift');
$this->error($giftqtypenotset, $text);
return false;
}
switch ($question->qtype) {
case 'description':
$question->defaultmark = 0;
$question->length = 0;
return $question;
case 'essay':
$question->responseformat = 'editor';
$question->responserequired = 1;
$question->responsefieldlines = 15;
$question->attachments = 0;
$question->attachmentsrequired = 0;
$question->graderinfo = array(
'text' => '', 'format' => FORMAT_HTML, 'files' => array());
$question->responsetemplate = array(
'text' => '', 'format' => FORMAT_HTML);
return $question;
case 'multichoice':
// "Temporary" solution to enable choice of answernumbering on GIFT import
// by respecting default set for multichoice questions (MDL-59447)
$question->answernumbering = get_config('qtype_multichoice', 'answernumbering');
if (strpos($answertext, "=") === false) {
$question->single = 0; // Multiple answers are enabled if no single answer is 100% correct.
} else {
$question->single = 1; // Only one answer allowed (the default).
}
$question = $this->add_blank_combined_feedback($question);
$answertext = str_replace("=", "~=", $answertext);
$answers = explode("~", $answertext);
if (isset($answers[0])) {
$answers[0] = trim($answers[0]);
}
if (empty($answers[0])) {
array_shift($answers);
}
$countanswers = count($answers);
if (!$this->check_answer_count(2, $answers, $text)) {
return false;
}
foreach ($answers as $key => $answer) {
$answer = trim($answer);
// Determine answer weight.
if ($answer[0] == '=') {
$answerweight = 1;
$answer = substr($answer, 1);
} else if (preg_match($giftanswerweightregex, $answer)) { // Check for properly formatted answer weight.
$answerweight = $this->answerweightparser($answer);
} else { // Default, i.e., wrong anwer.
$answerweight = 0;
}
list($question->answer[$key], $question->feedback[$key]) =
$this->commentparser($answer, $question->questiontextformat);
$question->fraction[$key] = $answerweight;
} // End foreach answer.
return $question;
case 'match':
$question = $this->add_blank_combined_feedback($question);
$answers = explode('=', $answertext);
if (isset($answers[0])) {
$answers[0] = trim($answers[0]);
}
if (empty($answers[0])) {
array_shift($answers);
}
if (!$this->check_answer_count(2, $answers, $text)) {
return false;
}
foreach ($answers as $key => $answer) {
$answer = trim($answer);
if (strpos($answer, "->") === false) {
$this->error(get_string('giftmatchingformat', 'qformat_gift'), $answer);
return false;
}
$marker = strpos($answer, '->');
$question->subquestions[$key] = $this->parse_text_with_format(
substr($answer, 0, $marker), $question->questiontextformat);
$question->subanswers[$key] = trim($this->escapedchar_post(
substr($answer, $marker + 2)));
}
return $question;
case 'truefalse':
list($answer, $wrongfeedback, $rightfeedback) =
$this->split_truefalse_comment($answertext, $question->questiontextformat);
if ($answer['text'] == "T" || $answer['text'] == "TRUE") {
$question->correctanswer = 1;
$question->feedbacktrue = $rightfeedback;
$question->feedbackfalse = $wrongfeedback;
} else {
$question->correctanswer = 0;
$question->feedbacktrue = $wrongfeedback;
$question->feedbackfalse = $rightfeedback;
}
$question->penalty = 1;
return $question;
case 'shortanswer':
// Shortanswer question.
$answers = explode("=", $answertext);
if (isset($answers[0])) {
$answers[0] = trim($answers[0]);
}
if (empty($answers[0])) {
array_shift($answers);
}
if (!$this->check_answer_count(1, $answers, $text)) {
return false;
}
foreach ($answers as $key => $answer) {
$answer = trim($answer);
// Answer weight.
if (preg_match($giftanswerweightregex, $answer)) { // Check for properly formatted answer weight.
$answerweight = $this->answerweightparser($answer);
} else { // Default, i.e., full-credit anwer.
$answerweight = 1;
}
list($answer, $question->feedback[$key]) = $this->commentparser(
$answer, $question->questiontextformat);
$question->answer[$key] = $answer['text'];
$question->fraction[$key] = $answerweight;
}
return $question;
case 'numerical':
// Note similarities to ShortAnswer.
$answertext = substr($answertext, 1); // Remove leading "#".
// If there is feedback for a wrong answer, store it for now.
if (($pos = strpos($answertext, '~')) !== false) {
$wrongfeedback = substr($answertext, $pos);
$answertext = substr($answertext, 0, $pos);
} else {
$wrongfeedback = '';
}
$answers = explode("=", $answertext);
if (isset($answers[0])) {
$answers[0] = trim($answers[0]);
}
if (empty($answers[0])) {
array_shift($answers);
}
if (count($answers) == 0) {
// Invalid question.
$giftnonumericalanswers = get_string('giftnonumericalanswers', 'qformat_gift');
$this->error($giftnonumericalanswers, $text);
return false;
}
foreach ($answers as $key => $answer) {
$answer = trim($answer);
// Answer weight.
if (preg_match($giftanswerweightregex, $answer)) { // Check for properly formatted answer weight.
$answerweight = $this->answerweightparser($answer);
} else { // Default, i.e., full-credit anwer.
$answerweight = 1;
}
list($answer, $question->feedback[$key]) = $this->commentparser(
$answer, $question->questiontextformat);
$question->fraction[$key] = $answerweight;
$answer = $answer['text'];
// Calculate Answer and Min/Max values.
if (strpos($answer, "..") > 0) { // Optional [min]..[max] format.
$marker = strpos($answer, "..");
$max = trim(substr($answer, $marker + 2));
$min = trim(substr($answer, 0, $marker));
$ans = ($max + $min)/2;
$tol = $max - $ans;
} else if (strpos($answer, ':') > 0) { // Standard [answer]:[errormargin] format.
$marker = strpos($answer, ':');
$tol = trim(substr($answer, $marker+1));
$ans = trim(substr($answer, 0, $marker));
} else { // Only one valid answer (zero errormargin).
$tol = 0;
$ans = trim($answer);
}
if (!(is_numeric($ans) || $ans = '*') || !is_numeric($tol)) {
$errornotnumbers = get_string('errornotnumbers');
$this->error($errornotnumbers, $text);
return false;
}
// Store results.
$question->answer[$key] = $ans;
$question->tolerance[$key] = $tol;
}
if ($wrongfeedback) {
$key += 1;
$question->fraction[$key] = 0;
list($notused, $question->feedback[$key]) = $this->commentparser(
$wrongfeedback, $question->questiontextformat);
$question->answer[$key] = '*';
$question->tolerance[$key] = '';
}
return $question;
default:
$this->error(get_string('giftnovalidquestion', 'qformat_gift'), $text);
return false;
}
}
protected function repchar($text, $notused = 0) {
// Escapes 'reserved' characters # = ~ {) :
// Removes new lines.
$reserved = array( '\\', '#', '=', '~', '{', '}', ':', "\n", "\r");
$escaped = array('\\\\', '\#', '\=', '\~', '\{', '\}', '\:', '\n', '');
$newtext = str_replace($reserved, $escaped, $text);
return $newtext;
}
/**
* @param int $format one of the FORMAT_ constants.
* @return string the corresponding name.
*/
protected function format_const_to_name($format) {
if ($format == FORMAT_MOODLE) {
return 'moodle';
} else if ($format == FORMAT_HTML) {
return 'html';
} else if ($format == FORMAT_PLAIN) {
return 'plain';
} else if ($format == FORMAT_MARKDOWN) {
return 'markdown';
} else {
return 'moodle';
}
}
/**
* @param int $format one of the FORMAT_ constants.
* @return string the corresponding name.
*/
protected function format_name_to_const($format) {
if ($format == 'moodle') {
return FORMAT_MOODLE;
} else if ($format == 'html') {
return FORMAT_HTML;
} else if ($format == 'plain') {
return FORMAT_PLAIN;
} else if ($format == 'markdown') {
return FORMAT_MARKDOWN;
} else {
return -1;
}
}
/**
* Extract any tags or idnumber declared in the question comment.
*
* @param string $comment E.g. "// Line 1.\n//Line 2.\n".
* @return array with two elements. string $idnumber (or '') and string[] of tags.
*/
public function extract_idnumber_and_tags_from_comment(string $comment): array {
// Find the idnumber, if any. There should not be more than one, but if so, we just find the first.
$idnumber = '';
if (preg_match('~
# Start of id token.
\[id:
# Any number of (non-control) characters, with any ] escaped.
# This is the bit we want so capture it.
(
(?:\\\\]|[^][:cntrl:]])+
)
# End of id token.
]
~x', $comment, $match)) {
$idnumber = str_replace('\]', ']', trim($match[1]));
}
// Find any tags.
$tags = [];
if (preg_match_all('~
# Start of tag token.
\[tag:
# Any number of allowed characters (see PARAM_TAG), with any ] escaped.
# This is the bit we want so capture it.
(
(?:\\\\]|[^]<>`[:cntrl:]]|)+
)
# End of tag token.
]
~x', $comment, $matches)) {
foreach ($matches[1] as $rawtag) {
$tags[] = str_replace('\]', ']', trim($rawtag));
}
}
return [$idnumber, $tags];
}
public function write_name($name) {
return '::' . $this->repchar($name) . '::';
}
public function write_questiontext($text, $format, $defaultformat = FORMAT_MOODLE) {
$output = '';
if ($text != '' && $format != $defaultformat) {
$output .= '[' . $this->format_const_to_name($format) . ']';
}
$output .= $this->repchar($text, $format);
return $output;
}
/**
* Outputs the general feedback for the question, if any. This needs to be the
* last thing before the }.
* @param object $question the question data.
* @param string $indent to put before the general feedback. Defaults to a tab.
* If this is not blank, a newline is added after the line.
*/
public function write_general_feedback($question, $indent = "\t") {
$generalfeedback = $this->write_questiontext($question->generalfeedback,
$question->generalfeedbackformat, $question->questiontextformat);
if ($generalfeedback) {
$generalfeedback = '####' . $generalfeedback;
if ($indent) {
$generalfeedback = $indent . $generalfeedback . "\n";
}
}
return $generalfeedback;
}
public function writequestion($question) {
// Start with a comment.
$expout = "// question: {$question->id} name: {$question->name}\n";
$expout .= $this->write_idnumber_and_tags($question);
// Output depends on question type.
switch($question->qtype) {
case 'category':
// Not a real question, used to insert category switch.
$expout .= "\$CATEGORY: $question->category\n";
break;
case 'description':
$expout .= $this->write_name($question->name);
$expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
break;
case 'essay':
$expout .= $this->write_name($question->name);
$expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
$expout .= "{";
$expout .= $this->write_general_feedback($question, '');
$expout .= "}\n";
break;
case 'truefalse':
$trueanswer = $question->options->answers[$question->options->trueanswer];
$falseanswer = $question->options->answers[$question->options->falseanswer];
if ($trueanswer->fraction == 1) {
$answertext = 'TRUE';
$rightfeedback = $this->write_questiontext($trueanswer->feedback,
$trueanswer->feedbackformat, $question->questiontextformat);
$wrongfeedback = $this->write_questiontext($falseanswer->feedback,
$falseanswer->feedbackformat, $question->questiontextformat);
} else {
$answertext = 'FALSE';
$rightfeedback = $this->write_questiontext($falseanswer->feedback,
$falseanswer->feedbackformat, $question->questiontextformat);
$wrongfeedback = $this->write_questiontext($trueanswer->feedback,
$trueanswer->feedbackformat, $question->questiontextformat);
}
$expout .= $this->write_name($question->name);
$expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
$expout .= '{' . $this->repchar($answertext);
if ($wrongfeedback) {
$expout .= '#' . $wrongfeedback;
} else if ($rightfeedback) {
$expout .= '#';
}
if ($rightfeedback) {
$expout .= '#' . $rightfeedback;
}
$expout .= $this->write_general_feedback($question, '');
$expout .= "}\n";
break;
case 'multichoice':
$expout .= $this->write_name($question->name);
$expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
$expout .= "{\n";
foreach ($question->options->answers as $answer) {
if ($answer->fraction == 1 && $question->options->single) {
$answertext = '=';
} else if ($answer->fraction == 0) {
$answertext = '~';
} else {
$weight = $answer->fraction * 100;
$answertext = '~%' . $weight . '%';
}
$expout .= "\t" . $answertext . $this->write_questiontext($answer->answer,
$answer->answerformat, $question->questiontextformat);
if ($answer->feedback != '') {
$expout .= '#' . $this->write_questiontext($answer->feedback,
$answer->feedbackformat, $question->questiontextformat);
}
$expout .= "\n";
}
$expout .= $this->write_general_feedback($question);
$expout .= "}\n";
break;
case 'shortanswer':
$expout .= $this->write_name($question->name);
$expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
$expout .= "{\n";
foreach ($question->options->answers as $answer) {
$weight = 100 * $answer->fraction;
$expout .= "\t=%" . $weight . '%' . $this->repchar($answer->answer) .
'#' . $this->write_questiontext($answer->feedback,
$answer->feedbackformat, $question->questiontextformat) . "\n";
}
$expout .= $this->write_general_feedback($question);
$expout .= "}\n";
break;
case 'numerical':
$expout .= $this->write_name($question->name);
$expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
$expout .= "{#\n";
foreach ($question->options->answers as $answer) {
if ($answer->answer != '' && $answer->answer != '*') {
$weight = 100 * $answer->fraction;
$expout .= "\t=%" . $weight . '%' . $answer->answer . ':' .
(float)$answer->tolerance . '#' . $this->write_questiontext($answer->feedback,
$answer->feedbackformat, $question->questiontextformat) . "\n";
} else {
$expout .= "\t~#" . $this->write_questiontext($answer->feedback,
$answer->feedbackformat, $question->questiontextformat) . "\n";
}
}
$expout .= $this->write_general_feedback($question);
$expout .= "}\n";
break;
case 'match':
$expout .= $this->write_name($question->name);
$expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
$expout .= "{\n";
foreach ($question->options->subquestions as $subquestion) {
$expout .= "\t=" . $this->write_questiontext($subquestion->questiontext,
$subquestion->questiontextformat, $question->questiontextformat) .
' -> ' . $this->repchar($subquestion->answertext) . "\n";
}
$expout .= $this->write_general_feedback($question);
$expout .= "}\n";
break;
default:
// Check for plugins.
if ($out = $this->try_exporting_using_qtypes($question->qtype, $question)) {
$expout .= $out;
}
}
// Add empty line to delimit questions.
$expout .= "\n";
return $expout;
}
/**
* Prepare any question idnumber or tags for export.
*
* @param stdClass $questiondata the question data we are exporting.
* @return string a string that can be written as a line in the GIFT file,
* e.g. "// [id:myid] [tag:some-tag]\n". Will be '' if none.
*/
public function write_idnumber_and_tags(stdClass $questiondata): string {
if ($questiondata->qtype == 'category') {
return '';
}
$bits = [];
if (isset($questiondata->idnumber) && $questiondata->idnumber !== '') {
$bits[] = '[id:' . str_replace(']', '\]', $questiondata->idnumber) . ']';
}
// Write the question tags.
if (core_tag_tag::is_enabled('core_question', 'question')) {
$tagobjects = core_tag_tag::get_item_tags('core_question', 'question', $questiondata->id);
if (!empty($tagobjects)) {
$context = context::instance_by_id($questiondata->contextid);
$sortedtagobjects = question_sort_tags($tagobjects, $context, [$this->course]);
// Currently we ignore course tags. This should probably be fixed in future.
if (!empty($sortedtagobjects->tags)) {
foreach ($sortedtagobjects->tags as $tag) {
$bits[] = '[tag:' . str_replace(']', '\]', $tag) . ']';
}
}
}
}
if (!$bits) {
return '';
}
return '// ' . implode(' ', $bits) . "\n";
}
}
@@ -0,0 +1,37 @@
<?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 'qformat_gift', language 'en', branch 'MOODLE_20_STABLE'
*
* @package qformat_gift
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['braceerror'] = 'Could not find {...} around answers';
$string['giftleftbraceerror'] = 'Could not find a {';
$string['giftmatchingformat'] = 'Matching question answers are improperly formatted';
$string['giftnonumericalanswers'] = 'No answers found for numerical question';
$string['giftnovalidquestion'] = 'No valid question found';
$string['giftqtypenotset'] = 'Question type is not set';
$string['giftrightbraceerror'] = 'Could not find a }';
$string['importminerror'] = 'There is an error in the question. There are not enough answers for this question type';
$string['nohandler'] = 'No handler for question type {$a}';
$string['pluginname'] = 'GIFT format';
$string['pluginname_help'] = 'GIFT format enables multiple choice, true/false, short answer, matching, missing word, numerical and essay questions to be imported or exported via text file.';
$string['pluginname_link'] = 'qformat/gift';
$string['privacy:metadata'] = 'The GIFT question format plugin does not store any personal data.';
@@ -0,0 +1,54 @@
@qformat @qformat_gift
Feature: Test importing questions from GIFT format.
In order to reuse questions
As an teacher
I need to be able to import them in GIFT format.
Background:
Given the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "users" exist:
| username | firstname |
| teacher | Teacher |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
And I am on the "Course 1" "core_question > course question import" page logged in as "teacher"
@javascript @_file_upload
Scenario: import some GIFT questions
When I set the field "id_format_gift" to "1"
And I upload "question/format/gift/tests/fixtures/questions.gift.txt" file to "Import" filemanager
And I press "id_submitbutton"
Then I should see "Parsing questions from import file."
And I should see "Importing 9 questions from file"
And I should see "What's between orange and green in the spectrum?"
When I press "Continue"
Then I should see "colours"
# Now export again.
And I am on the "Course 1" "core_question > course question export" page
And I set the field "id_format_gift" to "1"
And I press "Export questions to file"
And following "click here" should download a file that:
| Has mimetype | text/plain |
| Contains text | What's between orange and green in the spectrum? |
@javascript @_file_upload
Scenario: import a GIFT file which specifies the category
When I set the field "id_format_gift" to "1"
And I upload "question/format/gift/tests/fixtures/questions_in_category.gift.txt" file to "Import" filemanager
And I press "id_submitbutton"
Then I should see "Parsing questions from import file."
And I should see "Importing 4 questions from file"
And I should see "Match the activity to the description."
When I press "Continue"
Then I should see "Moodle activities"
@javascript @_file_upload
Scenario: import some GIFT questions with unsupported encoding
When I set the field "id_format_gift" to "1"
And I upload "question/format/gift/tests/fixtures/questions_encoding_windows-1252.gift.txt" file to "Import" filemanager
And I press "id_submitbutton"
Then I should see "The file you selected does not use UTF-8 character encoding. GIFT format files must use UTF-8."
+49
View File
@@ -0,0 +1,49 @@
// essay
::Q8:: How are you? {}
// question: 2 name: Moodle activities
::Moodle activities::[html]Match the <b>activity</b> to the description.{
=[html]An activity supporting asynchronous discussions. -> Forum
=[moodle]A teacher asks a question and specifies a choice of multiple responses. -> Choice
=[plain]A bank of record entries which participants can add to. -> Database
=[markdown]A collection of web pages that anyone can add to or edit. -> Wiki
= -> Chat
}
// multiple choice with specified feedback for right and wrong answers
::Q2:: What's between orange and green in the spectrum?
{
=yellow # right; good!
~red # [html]wrong, it's yellow
~[plain]blue # wrong, it's yellow
}
// multiple choice, multiple response with specified feedback for right and wrong answers
::colours:: What's between orange and green in the spectrum?
{
~%50%yellow # right; good!
~%-100%red # [html]wrong
~%50%off-beige # right; good!
~%-100%[plain]blue # wrong
}
// math range question
::Q5:: What is a number from 1 to 5? {#3:2~#Completely wrong}";
// question: 666 name: Shortanswer
::Shortanswer::Which is the best animal?{
=Frog#Good!
=%50%Cat#What is it with Moodlers and cats?
=%0%*#Completely wrong
}
// true/false, with general feedback
::Q1:: 42 is the Absolute Answer to everything.{
FALSE#42 is the Ultimate Answer.#You gave the right answer.
####This is, of course, a Hitchiker's Guide to the Galaxy reference.}";
// name 0-11
::2-08 TSL::TSL is blablabla.{T}
// name 0-11
::2-08 TSL::TSL is blablabla.{TRUE}
@@ -0,0 +1,18 @@
// question: 0 name: Switch category to $course$/top/Default for LTTEST
$CATEGORY: $course$/top/Default for LTTEST
// question: 19756780 name: asdf
::asdf::[html]<p>asdf<br></p>{}
// question: 19756810 name: test daniel
::test daniel::[html]<p>asdfasdf</p>{}
// question: 19756750 name: asdf
::asdf::[html]<p>asdf<br></p>{
=<p>aödf<br></p> -> asdf
=<p>asdf<br></p> -> asdf
=<p>asdf<br></p> -> asdf
}
@@ -0,0 +1,27 @@
// question: 0 name: switch category to $course$/Default for New Features
$CATEGORY: $course$/Default for New Features
// question: 44 name: Moodle activities
::Moodle activities::[html]Match the activity to the description.{
=An activity supporting asynchronous discussions. -> Forum
=A teacher asks a question and specifies a choice of multiple responses. -> Choice
=A bank of record entries which participants can add to. -> Database
=A collection of web pages that anyone can add to or edit. -> Wiki
= -> Chat
}
// question: 43 name: Greeting
::Greeting::[html]<a href\="http\://demo.moodle.net/file.php/5/media/bonjour.mp3">Listen to this greeting\:</a><br /><br />What language is being spoken?{
~English#Sorry, listen again.
=French#Yes, well done!
~German#Sorry, listen again.
~Spanish#Sorry, listen again.
}
// question: 46 name: Moodle user
::Moodle user::[html]Anyone who uses Moodle is a ...{
=%100%Moodler#
}
// question: 45 name: Moodle acronym
::Moodle acronym::[html]Moodle is an acronym for <span style\="font-style\: italic;">Modular Object-Oriented Dynamic Learning Environment</span>.{TRUE}
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
<?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 the calculated question type.
*
* @package qformat_gift
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qformat_gift';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for qformat_missingword.
*
* @package qformat_missingword
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qformat_missingword\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qformat_missingword implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @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';
}
}
+181
View File
@@ -0,0 +1,181 @@
<?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/>.
/**
* Missing word question importer.
*
* @package qformat_missingword
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Missing word question importer.
*
* This Moodle class provides all functions necessary to import and export
* one-correct-answer multiple choice questions in this format:
*
* As soon as we begin to explore our body parts as infants
* we become students of {=anatomy and physiology ~reflexology
* ~science ~experiment}, and in a sense we remain students for life.
*
* Each answer is separated with a tilde ~, and the correct answer is
* prefixed with an equals sign =
*
* Percentage weights can be included by following the tilde with the
* desired percent. Comments can be included for each choice by following
* the comment with a hash mark ("#") and the comment. Example:
*
* This is {=the best answer#comment on the best answer ~75%a good
* answer#comment on the good answer ~a wrong one#comment on the bad answer}
*
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qformat_missingword extends qformat_default {
public function provide_import() {
return true;
}
/**
* Validate the given file.
*
* For more expensive or detailed integrity checks.
*
* @param stored_file $file the file to check
* @return string the error message that occurred while validating the given file
*/
public function validate_file(stored_file $file): string {
return $this->validate_is_utf8_file($file);
}
public function readquestion($lines) {
// Given an array of lines known to define a question in
// this format, this function converts it into a question
// object suitable for processing and insertion into Moodle.
$question = $this->defaultquestion();
$comment = null; // Added by T Robb.
$text = implode(" ", $lines);
// Find answer section.
$answerstart = strpos($text, "{");
if ($answerstart === false) {
$this->error(get_string('beginanswernotfound', 'qformat_missingword'), $text);
return false;
}
$answerfinish = strpos($text, "}");
if ($answerfinish === false) {
$this->error(get_string('endanswernotfound', 'qformat_missingword'), $text);
return false;
}
$answerlength = $answerfinish - $answerstart;
$answertext = substr($text, $answerstart + 1, $answerlength - 1);
// Save the new question text.
$question->questiontext = substr_replace($text, "_____", $answerstart, $answerlength+1);
$question->name = $this->create_default_question_name($question->questiontext, get_string('questionname', 'question'));
// Parse the answers.
$answertext = str_replace("=", "~=", $answertext);
$answers = explode("~", $answertext);
if (isset($answers[0])) {
$answers[0] = trim($answers[0]);
}
if (empty($answers[0])) {
array_shift($answers);
}
$countanswers = count($answers);
switch ($countanswers) {
case 0: // Invalid question.
$this->error(get_string('noanswerfound', 'qformat_missingword'), $answertext);
return false;
case 1:
$question->qtype = 'shortanswer';
$answer = trim($answers[0]);
if ($answer[0] == "=") {
$answer = substr($answer, 1);
}
$question->answer[] = $answer;
$question->fraction[] = 1;
$question->feedback[] = array('text' => '', 'format' => FORMAT_HTML);
return $question;
default:
$question->qtype = 'multichoice';
$question = $this->add_blank_combined_feedback($question);
$question->single = 1; // Only one answer allowed.
foreach ($answers as $key => $answer) {
$answer = trim($answer);
// Tom's addition starts here.
$answeight = 0;
if (strspn($answer, "1234567890%") > 0) {
// Make sure that the percent sign is the last in the span.
if (strpos($answer, "%") == strspn($answer, "1234567890%") - 1) {
$answeight0 = substr($answer, 0, strspn($answer, "1234567890%"));
$answeight = round(($answeight0/100), 2);
$answer = substr($answer, (strspn($answer, "1234567890%")));
}
}
if ($answer[0] == "=") {
$answeight = 1;
}
// Remove the protective underscore for leading numbers in answers.
if ($answer[0] == "_") {
$answer = substr($answer, 1);
}
$answer = trim($answer);
if (strpos($answer, "#") > 0) {
$hashpos = strpos($answer, "#");
$comment = substr(($answer), $hashpos+1);
$answer = substr($answer, 0, $hashpos);
} else {
$comment = " ";
}
// End of Tom's addition.
if ($answer[0] == "=") {
$question->fraction[$key] = $answeight;
$answer = substr($answer, 1);
} else {
$question->fraction[$key] = $answeight;
}
$question->answer[$key] = array('text' => $answer, 'format' => FORMAT_HTML);
$question->feedback[$key] = array('text' => $comment, 'format' => FORMAT_HTML);
}
return $question;
}
}
}
@@ -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/>.
/**
* Strings for component 'qformat_missingword', language 'en', branch 'MOODLE_20_STABLE'
*
* @package qformat_missingword
* @copyright 2010 Helen Foster
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Missing word format';
$string['pluginname_help'] = 'Missing word format enables questions to be imported via text file.';
$string['pluginname_link'] = 'qformat/missingword';
$string['beginanswernotfound'] = 'Could not find a required "{" character in imported file content.';
$string['endanswernotfound'] = 'Could not find a required "}" character in imported file content.';
$string['noanswerfound'] = 'No answers found in question';
$string['privacy:metadata'] = 'The Missing word question format plugin does not store any personal data.';
@@ -0,0 +1,3 @@
As soon as we begin to explore our body parts as infants
we become students of {=anatomy and physiology ~reflexology
~science ~experiment}, and in a sense we remain students for life.
@@ -0,0 +1,2 @@
You can use the missing word format to import questions
into both Moodle's Question bank and {=Lesson} activity.
@@ -0,0 +1,2 @@
This is {=the best answer#comment on the best answer ~75%a good
answer#comment on the good answer ~a wrong one#comment on the bad answer}
+32
View File
@@ -0,0 +1,32 @@
<?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 the calculated question type.
*
* @package qformat_missingword
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qformat_missingword';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for qformat_multianswer.
*
* @package qformat_multianswer
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qformat_multianswer\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qformat_multianswer implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @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';
}
}
+94
View File
@@ -0,0 +1,94 @@
<?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/>.
/**
* Embedded answer (Cloze) question importer.
*
* @package qformat_multianswer
* @copyright 2003 Henrik Kaipe
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Importer that imports a text file containing a single Multianswer question
* from a text file.
*
* @copyright 2003 Henrik Kaipe
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qformat_multianswer extends qformat_default {
public function provide_import() {
return true;
}
/**
* Validate the given file.
*
* For more expensive or detailed integrity checks.
*
* @param stored_file $file the file to check
* @return string the error message that occurred while validating the given file
*/
public function validate_file(stored_file $file): string {
return $this->validate_is_utf8_file($file);
}
public function readquestions($lines) {
question_bank::get_qtype('multianswer'); // Ensure the multianswer code is loaded.
// For this class the method has been simplified as
// there can never be more than one question for a
// multianswer import.
$questions = array();
$questiontext = array();
$questiontext['text'] = implode('', $lines);
$questiontext['format'] = FORMAT_MOODLE;
$questiontext['itemid'] = '';
$question = qtype_multianswer_extract_question($questiontext);
$errors = qtype_multianswer_validate_question($question);
if ($errors) {
$this->error(get_string('invalidmultianswerquestion', 'qtype_multianswer', implode(' ', $errors)));
return array();
}
$question->questiontext = $question->questiontext['text'];
$question->questiontextformat = 0;
$question->qtype = 'multianswer';
$question->generalfeedback = '';
$question->generalfeedbackformat = FORMAT_MOODLE;
$question->length = 1;
$question->penalty = 0.3333333;
$question->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
$question->version = 1;
$question->versionid = 0;
$question->questionbankentryid = 0;
if (!empty($question)) {
$question->name = $this->create_default_question_name($question->questiontext, get_string('questionname', 'question'));
$questions[] = $question;
}
return $questions;
}
}
@@ -0,0 +1,28 @@
<?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 'qformat_multianswer', language 'en', branch 'MOODLE_20_STABLE'
*
* @package qformat_multianswer
* @copyright 2010 Helen Foster
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Embedded answers (Cloze)';
$string['pluginname_help'] = 'Embedded answers (Cloze) format enables the import of a passage of text with questions such as multiple-choice and short answer embedded within it.';
$string['pluginname_link'] = 'question/type/multianswer';
$string['privacy:metadata'] = 'The Embedded answers question plugin does not store any personal data.';
@@ -0,0 +1 @@
Please select the fruits {1:MULTICHOICE:=Apple#Correct}
@@ -0,0 +1 @@
Please select the fruits {1:MULTICHOICE:Pear#Incorrect~%50%Apple#Correct}
@@ -0,0 +1 @@
What grade would you give it? {3:NUMERICAL:=zero}
@@ -0,0 +1 @@
Please select the fruits.
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<quiz>
<!-- question: 3911 -->
<question type="cloze">
<name>
<text>All CLOZE types in one question</text>
</name>
<questiontext format="html">
<text><![CDATA[<h5>MULTICHOICE</h5>
<p>Whenever we talk about Asterix we have to imagine the times {1:MULTICHOICE:200 B.C.#too early~=50 B.C.#Correct~50 A.C.#100 years too late~200 A.C.#too late}</p>
<p>Asterix is best know to be a brave warrior. With his best friend {1:MULTICHOICE_H:Idefix#That's the dog of Obelix~=Obelix#His mate~Troubadix#He is the bard and sings horribly} he experiences many adventures.</p>
<p>According to the story Asterix has never been traveled to {1:MULTICHOICE_V:America#Asterix and the great crossing~=Australia#That was too far away at that time~India#Asterix and the Magic Carpet}</p>
<p>Many quotes in the comics are written in {1:MULTICHOICE_S:Greek#While the olympic games were known at the time and Asterix actually participated at the games, the greek language was not widely used.~Hebrew#The heroes haven't been to the middle east.~=Latin#Yes, many quotes are in latin.} language.</p>
<p>Obelix owns a company. In some of the episodes he first delivers {1:MULTICHOICE_HS:Fish#No, that's the quarrel between to other citizens of the village.~=a Menhir#Yes, he fabricates these types of obelisks.~a wild boar#No, he actually would eat, rather than delivering them.} before he can head off with Asterix to new adventures.</p>
<p>In the name of Obelix, Methusalix and Miraculix hides a roman number. Which one is it? {1:MULTICHOICE_VS:=59#Yes~51#No, that's LI~109#No, that's CIX~509#No, that's DIX}</p>
<hr>
<h5>MULTIRESPONSE</h5>
<p>To the Simpsons family belong the following persons: {1:MULTIRESPONSE:=Marge#Correct, she is the mother~Fred#No, he's not the husband of Marge~=Lisa#She's the daughter of Marge~Anna#Nope, she is from Frozen}</p>
<p>Marge is known for her {1:MULTIRESPONSE_H:%100%blue hair#yes, that's true~%0%being very greedy#Not that we know of~%50%being a soccer mom#Not in all aspects}</p>
<p>Whenever the children {1:MULTIRESPONSE_S:=Bart#first born son~=Lisa#first born daughter~Marge#Nope, that is the mother~=Maggie#Last born child~Abraham#He is the grandfather} need the help of her parents, they try to be there for them.</p>
<p>The Simpsons series is known to "forsee" the future. These events {1:MULTIRESPONSE_HS:%100%Trump becomes president#in "Bart to the future"~%100%20th Century Fox as a division of The Walt Disney Company.#in "When You Dish Upon a Star"~O.J Simpson was convicted as a murderer#no, that is made up~=The Titanic submarine implosion#In one episode in 2006} were a topic in an episode many years before they actually happened.</p>
<hr>
<h5>NUMERICAL and SHORTANSWER</h5>
<p>Let's start easy. Water freezes at 0°C and {1:NUMERICAL:=32:0#Must be exact} Farenheit. But, do you know that water is boiling at {1:NUMERICAL:=212:0#Exact~%50%212:10#Close, you get at least half the points.} Fahrenkeit?</p>
<p>Let's be fair: Anders Celsius, after whom a temperature scale was named was born in which year? {3:NUMERICAL:%100%1701:0#Exact match~%66%1701:20#Within the tolerance of 20 years.~%33%1701:50#Within the tolerance of 20 years.} This answer is worth 3 points when you know the exact year, 2 points when you are off by 20 years, and still 1 point when you are off by 50 years.</p>
<p>Fahrnheit lived some years before Celcius. You may have noticed already that the spelling of the scientist was different every time he was mentioned. Please spell his name correctly: {1:SHORTANSWER:=Fahrenheit#That's correct.}</p>
<p>Water consists of Hydrogen and Oxygen. Please write down the correct formula: {1:SHORTANSWER_C:=H<sub>2</sub>O#That is correct.~=H2O#That's correct (the 2 is usually subscripted but not possible during input).~h2O#Both Oxygen and Hydrogen are used with upper case letters.~H2o#Both Oxygen and Hydrogen are used with upper case letters.~h2o#Both Oxygen and Hydrogen are used with upper case letters.}</p>
<p>The word used for a famous beverage made from fermented grain mash was derived from "<i>uisge beatha</i>" or "<i>uisce beatha</i>" which means "water of life". Under which name is the beverage worldwide known for? {1:SHORTANSWER:=Whisky#english~=Whiskey#mostly in Scottland and the US.~%50%Wiskey#That's not spelled correctly.~%25%Wisky#That's hardly recognizable.}</p>]]></text>
</questiontext>
<generalfeedback format="html">
<text><![CDATA[<p>To get to know more about Asterix and his friends, read the more than 40 comic books.</p>]]></text>
</generalfeedback>
<penalty>0.3333333</penalty>
<hidden>0</hidden>
<idnumber></idnumber>
</question>
</quiz>
@@ -0,0 +1,8 @@
Match the following cities with the correct state:
* San Francisco: {1:MULTICHOICE:=California#OK~Arizona#Wrong}
* Tucson: {1:MULTICHOICE:California#Wrong~%100%Arizona#OK}
* Los Angeles: {1:MULTICHOICE:=California#OK~Arizona#Wrong}
* Phoenix: {1:MULTICHOICE:%0%California#Wrong~=Arizona#OK}
The capital of France is {1:SHORTANSWER:%100%Paris#Congratulations!
~%50%Marseille#No, that is the second largest city in France (after
Paris).~*#Wrong answer. The capital of France is Paris, of course.}.
@@ -0,0 +1,151 @@
<?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 qformat_multianswer;
use qformat_multianswer;
use question_check_specified_fields_expectation;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->dirroot . '/question/format.php');
require_once($CFG->dirroot . '/question/format/multianswer/format.php');
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
/**
* Unit tests for the Embedded answer (Cloze) question importer.
*
* @package qformat_multianswer
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class multianswerformat_test extends \question_testcase {
public function test_import(): void {
$lines = file(__DIR__ . '/fixtures/questions.multianswer.txt');
$importer = new qformat_multianswer();
$qs = $importer->readquestions($lines);
$expectedq = (object) array(
'name' => 'Match the following cities with the correct state:
* San Francisco: {#1}
* ...',
'questiontext' => 'Match the following cities with the correct state:
* San Francisco: {#1}
* Tucson: {#2}
* Los Angeles: {#3}
* Phoenix: {#4}
The capital of France is {#5}.
',
'questiontextformat' => FORMAT_MOODLE,
'generalfeedback' => '',
'generalfeedbackformat' => FORMAT_MOODLE,
'qtype' => 'multianswer',
'defaultmark' => 5,
'penalty' => 0.3333333,
'length' => 1,
);
$this->assertEquals(1, count($qs));
$this->assert(new question_check_specified_fields_expectation($expectedq), $qs[0]);
$this->assertEquals('multichoice', $qs[0]->options->questions[1]->qtype);
$this->assertEquals('multichoice', $qs[0]->options->questions[2]->qtype);
$this->assertEquals('multichoice', $qs[0]->options->questions[3]->qtype);
$this->assertEquals('multichoice', $qs[0]->options->questions[4]->qtype);
$this->assertEquals('shortanswer', $qs[0]->options->questions[5]->qtype);
}
public function test_read_brokencloze_1(): void {
$lines = file(__DIR__ . '/fixtures/broken_multianswer_1.txt');
$importer = new qformat_multianswer();
// The importer echoes some errors, so we need to capture and check that.
ob_start();
$questions = $importer->readquestions($lines);
$output = ob_get_contents();
ob_end_clean();
// Check that there were some expected errors.
$this->assertStringContainsString('Error importing question', $output);
$this->assertStringContainsString('Invalid embedded answers (Cloze) question', $output);
$this->assertStringContainsString('This type of question requires at least 2 choices', $output);
// No question have been imported.
$this->assertCount(0, $questions);
}
public function test_read_brokencloze_2(): void {
$lines = file(__DIR__ . '/fixtures/broken_multianswer_2.txt');
$importer = new qformat_multianswer();
// The importer echoes some errors, so we need to capture and check that.
ob_start();
$questions = $importer->readquestions($lines);
$output = ob_get_contents();
ob_end_clean();
// Check that there were some expected errors.
$this->assertStringContainsString('Error importing question', $output);
$this->assertStringContainsString('Invalid embedded answers (Cloze) question', $output);
$this->assertStringContainsString('One of the answers should have a score of 100% so it is possible to get full marks for this question.',
$output);
// No question have been imported.
$this->assertCount(0, $questions);
}
public function test_read_brokencloze_3(): void {
$lines = file(__DIR__ . '/fixtures/broken_multianswer_3.txt');
$importer = new qformat_multianswer();
// The importer echoes some errors, so we need to capture and check that.
ob_start();
$questions = $importer->readquestions($lines);
$output = ob_get_contents();
ob_end_clean();
// Check that there were some expected errors.
$this->assertStringContainsString('Error importing question', $output);
$this->assertStringContainsString('Invalid embedded answers (Cloze) question', $output);
$this->assertStringContainsString('The answer must be a number, for example -1.234 or 3e8, or \'*\'.', $output);
// No question have been imported.
$this->assertCount(0, $questions);
}
public function test_read_brokencloze_4(): void {
$lines = file(__DIR__ . '/fixtures/broken_multianswer_4.txt');
$importer = new qformat_multianswer();
// The importer echoes some errors, so we need to capture and check that.
ob_start();
$questions = $importer->readquestions($lines);
$output = ob_get_contents();
ob_end_clean();
// Check that there were some expected errors.
$this->assertStringContainsString('Error importing question', $output);
$this->assertStringContainsString('Invalid embedded answers (Cloze) question', $output);
$this->assertStringContainsString('The question text must include at least one embedded answer.', $output);
// No question have been imported.
$this->assertCount(0, $questions);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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 the calculated question type.
*
* @package qformat_multianswer
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qformat_multianswer';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
+65
View File
@@ -0,0 +1,65 @@
This files describes API changes for question import/export format plugins.
=== 4.0 ===
1) The major question bank changes should not affect import/export plugins.
The navigation changes may affect Behat tests. If you encounter this,
the best way to fix it is to use the new navigation steps in MDL-74130.
2) The new validate_file() method in question/format.php can be overwritten
to implement more expensive or detailed file integrity checks for question imports.
There is a simple way to do this if you just want to check that the file is valid UTF-8,
which you can see an example of in format_gift.
3) The ExamView question format has been completely removed.
(The last posts in http://forum.examview.com/index.php?
are from over 10 years ago and there are no moodle.org
discussions about Examview in the last 10 years.)
4) The WebCT question format has been completely removed (WebCT was acquired by Blackboard in 2006).
=== 3.6 ===
* Saving question category descriptions (info) is now supported in Moodle XML import/export format.
New xml-structure snippet for a question category:
<question type="category">
<category>
<text>${$contexttypename}$/{$category_path}</text>
</category>
<info format="{$format}">
<text>{$info_categorydescription}</text>
</info>
</question>
* The method importprocess() in question/format.php no longer accepts $category as a parameter.
If required in a plugin then please override this method.
=== 2.3 ===
* This plugin type now supports cron in the standard way. If required, Create a
lib.php file containing
function qformat_mypluginname_cron() {};
=== 2.1.5 / 2.2.3 / 2.3 ===
* The readquestions method used to take a second argument $context. However, at
the point where this method was called, it was impossible to know what
context the quetsions were going to be saved into, so the value could be
wrong. Also, none of the standard question formats were using this argument,
so it was removed. See MDL-32220.
=== 2.2 ===
* The plugin name used to be defined in a string called the same thing as the
format, with assoicated help strings, for example:
$string['aiken'] = 'Aiken format';
$string['aiken_help'] = 'This is a simple format ...';
$string['aiken_link'] = 'qformat/aiken';
This needs to be changed to use the standard string name pluginname, as for
other plugin types.
$string['pluginname'] = 'Aiken format';
$string['pluginname_help'] = 'This is a simple format ...';
$string['pluginname_link'] = 'qformat/aiken';
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for qformat_xhtml.
*
* @package qformat_xhtml
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qformat_xhtml\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qformat_xhtml implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @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';
}
}
+195
View File
@@ -0,0 +1,195 @@
<?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/>.
/**
* XHTML question exporter.
*
* @package qformat_xhtml
* @copyright 2005 Howard Miller
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* XHTML question exporter.
*
* Exports questions as static HTML.
*
* @copyright 2005 Howard Miller
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qformat_xhtml extends qformat_default {
public function provide_export() {
return true;
}
protected function repchar($text) {
return $text;
}
protected function writequestion($question) {
global $OUTPUT;
// Turns question into string.
// Question reflects database fields for general question and specific to type.
// If a category switch, just ignore.
if ($question->qtype=='category') {
return '';
}
// Initial string.
$expout = "";
$id = $question->id;
// Add comment and div tags.
$expout .= "<!-- question: {$id} name: {$question->name} -->\n";
$expout .= "<div class=\"question\">\n";
// Add header.
$expout .= "<h3>{$question->name}</h3>\n";
// Format and add the question text.
$text = question_rewrite_question_preview_urls($question->questiontext, $question->id,
$question->contextid, 'question', 'questiontext', $question->id,
$question->contextid, 'qformat_xhtml');
$expout .= '<p class="questiontext">' . format_text($text,
$question->questiontextformat, array('noclean' => true)) . "</p>\n";
// Selection depends on question type.
switch($question->qtype) {
case 'truefalse':
$sttrue = get_string('true', 'qtype_truefalse');
$stfalse = get_string('false', 'qtype_truefalse');
$expout .= "<ul class=\"truefalse\">\n";
$expout .= " <li><input name=\"quest_{$id}\" type=\"radio\" value=\"{$sttrue}\" />{$sttrue}</li>\n";
$expout .= " <li><input name=\"quest_{$id}\" type=\"radio\" value=\"{$stfalse}\" />{$stfalse}</li>\n";
$expout .= "</ul>\n";
break;
case 'multichoice':
$expout .= "<ul class=\"multichoice\">\n";
foreach ($question->options->answers as $answer) {
$answertext = $this->repchar( $answer->answer );
if ($question->options->single) {
$expout .= " <li><input name=\"quest_{$id}\" type=\"radio\" value=\""
. s($answertext) . "\" />{$answertext}</li>\n";
} else {
$expout .= " <li><input name=\"quest_{$id}\" type=\"checkbox\" value=\""
. s($answertext) . "\" />{$answertext}</li>\n";
}
}
$expout .= "</ul>\n";
break;
case 'shortanswer':
$expout .= html_writer::start_tag('ul', array('class' => 'shortanswer'));
$expout .= html_writer::start_tag('li');
$expout .= html_writer::label(get_string('answer'), 'quest_'.$id, false, array('class' => 'accesshide'));
$expout .= html_writer::empty_tag('input', array('id' => "quest_{$id}", 'name' => "quest_{$id}", 'type' => 'text'));
$expout .= html_writer::end_tag('li');
$expout .= html_writer::end_tag('ul');
break;
case 'numerical':
$expout .= html_writer::start_tag('ul', array('class' => 'numerical'));
$expout .= html_writer::start_tag('li');
$expout .= html_writer::label(get_string('answer'), 'quest_'.$id, false, array('class' => 'accesshide'));
$expout .= html_writer::empty_tag('input', array('id' => "quest_{$id}", 'name' => "quest_{$id}", 'type' => 'text'));
$expout .= html_writer::end_tag('li');
$expout .= html_writer::end_tag('ul');
break;
case 'match':
$expout .= html_writer::start_tag('ul', array('class' => 'match'));
// Build answer list.
$answerlist = array();
foreach ($question->options->subquestions as $subquestion) {
$answerlist[] = $this->repchar( $subquestion->answertext );
}
shuffle( $answerlist ); // Random display order.
// Build select options.
$selectoptions = array();
foreach ($answerlist as $ans) {
$selectoptions[s($ans)] = s($ans);
}
// Display.
$option = 0;
foreach ($question->options->subquestions as $subquestion) {
// Build drop down for answers.
$questiontext = $this->repchar( $subquestion->questiontext );
if ($questiontext != '') {
$dropdown = html_writer::label(get_string('answer', 'qtype_match', $option+1), 'quest_'.$id.'_'.$option,
false, array('class' => 'accesshide'));
$dropdown .= html_writer::select($selectoptions, "quest_{$id}_{$option}", '', false,
array('id' => "quest_{$id}_{$option}"));
$expout .= html_writer::tag('li', $questiontext);
$expout .= $dropdown;
$option++;
}
}
$expout .= html_writer::end_tag('ul');
break;
case 'description':
break;
case 'multianswer':
default:
$expout .= "<!-- export of {$question->qtype} type is not supported -->\n";
}
// Close off div.
$expout .= "</div>\n\n\n";
return $expout;
}
protected function presave_process($content) {
// Override method to allow us to add xhtml headers and footers.
global $CFG;
// Get css bit.
$csslines = file( "{$CFG->dirroot}/question/format/xhtml/xhtml.css" );
$css = implode( ' ', $csslines );
$xp = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n";
$xp .= " \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n";
$xp .= "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n";
$xp .= "<head>\n";
$xp .= "<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\" />\n";
$xp .= "<title>Moodle Quiz XHTML Export</title>\n";
$xp .= "<style type=\"text/css\">\n";
$xp .= $css;
$xp .= "</style>\n";
$xp .= "</head>\n";
$xp .= "<body>\n";
$xp .= "<form action=\"...REPLACE ME...\" method=\"post\">\n\n";
$xp .= $content;
$xp .= "<p class=\"submit\">\n";
$xp .= " <input type=\"submit\" />\n";
$xp .= "</p>\n";
$xp .= "</form>\n";
$xp .= "</body>\n";
$xp .= "</html>\n";
return $xp;
}
public function export_file_extension() {
return '.html';
}
}
@@ -0,0 +1,28 @@
<?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 'qformat_xhtml', language 'en', branch 'MOODLE_20_STABLE'
*
* @package qformat_xhtml
* @copyright 2010 Helen Foster
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'XHTML format';
$string['pluginname_help'] = 'XHTML format enables all questions in the category to be exported to a single page of strict XHTML for possible use in another application.';
$string['pluginname_link'] = 'qformat/xhtml';
$string['privacy:metadata'] = 'The XHTML question format plugin does not store any personal data.';
+58
View File
@@ -0,0 +1,58 @@
<?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/>.
/**
* Standard plugin entry points of the HTML question export format.
*
* @package qformat_xhtml
* @copyright 2013 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Serve question files when they are displayed in this export format.
*
* @param context $previewcontext the quiz context
* @param int $questionid the question id.
* @param context $filecontext the file (question) context
* @param string $filecomponent the component the file belongs to.
* @param string $filearea the file area.
* @param array $args remaining file args.
* @param bool $forcedownload.
* @param array $options additional options affecting the file serving.
*/
function qformat_xhtml_question_preview_pluginfile($previewcontext, $questionid,
$filecontext, $filecomponent, $filearea, $args, $forcedownload, $options = array()) {
global $CFG;
list($context, $course, $cm) = get_context_info_array($previewcontext->id);
require_login($course, false, $cm);
question_require_capability_on($questionid, 'view');
$fs = get_file_storage();
$relativepath = implode('/', $args);
$fullpath = "/{$filecontext->id}/{$filecomponent}/{$filearea}/{$relativepath}";
if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
send_file_not_found();
}
send_stored_file($file, 0, 0, $forcedownload, $options);
}
+32
View File
@@ -0,0 +1,32 @@
<?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 the calculated question type.
*
* @package qformat_xhtml
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qformat_xhtml';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
+20
View File
@@ -0,0 +1,20 @@
body {
font-family: verdana, helvetica, sans-serif;
background-color: #fff;
color: #000;
}
.question {
border: 1px solid #ddd;
margin: 5px;
padding: 3px;
}
.question h3 {
font-weight: normal;
font-size: 125%;
}
.question ul {
list-style-type: none;
}
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for qformat_xml.
*
* @package qformat_xml
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qformat_xml\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for qformat_xml implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @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';
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,34 @@
<?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 'qformat_xml', language 'en', branch 'MOODLE_20_STABLE'
*
* @package qformat_xml
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['invalidxml'] = 'Invalid XML file - string expected (use CDATA?)';
$string['pluginname'] = 'Moodle XML format';
$string['pluginname_help'] = 'This is a Moodle-specific format for importing and exporting questions.';
$string['pluginname_link'] = 'qformat/xml';
$string['privacy:metadata'] = 'The XML question format plugin does not store any personal data.';
$string['truefalseimporterror'] = '<b>Warning</b>: The true/false question \'{$a->questiontext}\' could not be imported properly. It was not clear whether the correct answer is true or false. The question has been imported assuming that the answer is \'{$a->answer}\'. If this is not correct, you will need to edit the question.';
$string['unsupportedexport'] = 'Question type {$a} is not supported by XML export';
$string['xmlimportnoname'] = 'Missing question name in XML file';
$string['xmlimportnoquestion'] = 'Missing question text in XML file';
$string['xmltypeunsupported'] = 'Question type {$a} is not supported by XML import';
@@ -0,0 +1,78 @@
@qformat @qformat_xml
Feature: Test importing questions from Moodle XML format.
In order to reuse questions
As an teacher
I need to be able to import them in XML format.
Background:
Given the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "users" exist:
| username | firstname |
| teacher | Teacher |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
@javascript @_file_upload
Scenario: import some true/false questions from Moodle XML format
When I am on the "Course 1" "core_question > course question import" page logged in as "teacher"
And I set the field "id_format_xml" to "1"
And I upload "question/format/xml/tests/fixtures/truefalse.xml" file to "Import" filemanager
And I press "id_submitbutton"
Then I should see "Parsing questions from import file."
And I should see "Importing 2 questions from file"
And I should see "is an acronym for Modular Object-Oriented Dynamic Learning Education"
And I should see "is an acronym for Modular Object-Oriented Dynamic Learning Environment"
When I press "Continue"
Then I should see "Moodle acronym (False)"
Then I should see "Moodle acronym (True)"
# Now export again.
When I am on the "Course 1" "core_question > course question export" page logged in as "teacher"
And I set the field "id_format_xml" to "1"
And I set the field "Export category" to "TrueFalse"
And I press "Export questions to file"
Then following "click here" should download a file that:
| Has mimetype | text/xml |
| Contains text in xml element | Moodle acronym (True) |
| Contains text in xml element | Moodle acronym (False) |
@javascript @_file_upload
Scenario: import some multiple choice questions from Moodle XML format
When I am on the "Course 1" "core_question > course question import" page logged in as "teacher"
And I set the field "id_format_xml" to "1"
And I upload "question/format/xml/tests/fixtures/multichoice.xml" file to "Import" filemanager
And I press "id_submitbutton"
Then I should see "Parsing questions from import file."
And I should see "Importing 1 questions from file"
And I should see "What language is being spoken?"
When I press "Continue"
Then I should see "Greeting"
@javascript @_file_upload
Scenario: import some multi-answer questions from Moodle XML format
When I am on the "Course 1" "core_question > course question import" page logged in as "teacher"
And I set the field "id_format_xml" to "1"
And I upload "question/format/xml/tests/fixtures/multianswer.xml" file to "Import" filemanager
And I press "id_submitbutton"
Then I should see "Parsing questions from import file."
And I should see "Importing 1 questions from file"
And I should see "Match the following cities with the correct state,"
When I press "Continue"
Then I should see "cloze with images"
@javascript @_file_upload
Scenario: import some questions with legacy-style images from Moodle XML format
When I am on the "Course 1" "core_question > course question import" page logged in as "teacher"
And I set the field "id_format_xml" to "1"
And I upload "question/format/xml/tests/fixtures/sample_questions_with_old_image_tag.xml" file to "Import" filemanager
And I press "id_submitbutton"
Then I should see "Parsing questions from import file."
And I should see "Importing 2 questions from file"
And I should see "This is a multianswer question with an image in the old"
And I should see "This is a multichoice question with an image in the old"
When I press "Continue"
Then I should see "cloze question with image"
Then I should see "mcq with image"
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<quiz>
<!-- question: 1 -->
<question type="cloze">
<name>
<text>Multianswer question without subquestions</text>
</name>
<questiontext format="html">
<text>Please select the fruits.</text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<penalty>0.3333333</penalty>
<hidden>0</hidden>
<idnumber></idnumber>
</question>
<!-- question: 2 -->
<question type="cloze">
<name>
<text>Multichoice subquestion without choices</text>
</name>
<questiontext format="html">
<text>Please select the fruits {1:MULTICHOICE}</text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<penalty>0.3333333</penalty>
<hidden>0</hidden>
<idnumber></idnumber>
</question>
<!-- question: 3 -->
<question type="cloze">
<name>
<text>Multichoice subquestion with only one choice</text>
</name>
<questiontext format="html">
<text>Please select the fruits {1:MULTICHOICE:=Apple#Correct}</text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<penalty>0.3333333</penalty>
<hidden>0</hidden>
<idnumber></idnumber>
</question>
<!-- question: 4 -->
<question type="cloze">
<name>
<text>Multichoice subquestion with no completely correct answer</text>
</name>
<questiontext format="html">
<text>Please select the fruits {1:MULTICHOICE:Pear#Incorrect~%50%Apple#Correct}</text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<penalty>0.3333333</penalty>
<hidden>0</hidden>
<idnumber></idnumber>
</question>
<!-- question: 5 -->
<question type="cloze">
<name>
<text>Numerical subquestion with buggy answer</text>
</name>
<questiontext format="html">
<text>What grade would you give it? {3:NUMERICAL:=zero}</text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<penalty>0.3333333</penalty>
<hidden>0</hidden>
<idnumber></idnumber>
</question>
</quiz>
@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<quiz>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Sigma/Tau</text>
</category>
<info format="html">
<text>This is Tau category for test</text>
</info>
</question>
<!-- question: 106 -->
<question type="essay">
<name>
<text>Tau Question</text>
</name>
<questiontext format="moodle_auto_format">
<text>Testing Tau Question</text>
</questiontext>
<generalfeedback format="moodle_auto_format">
<text></text>
</generalfeedback>
<defaultgrade>1.0000000</defaultgrade>
<penalty>0.0000000</penalty>
<hidden>0</hidden>
<responseformat>editor</responseformat>
<responserequired>1</responserequired>
<responsefieldlines>15</responsefieldlines>
<attachments>0</attachments>
<attachmentsrequired>0</attachmentsrequired>
<graderinfo format="html">
<text></text>
</graderinfo>
<responsetemplate format="html">
<text></text>
</responsetemplate>
</question>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Sigma</text>
</category>
<info format="html">
<text>This is Sigma category for test</text>
</info>
</question>
<!-- question: 105 -->
<question type="shortanswer">
<name>
<text>Sigma Question</text>
</name>
<questiontext format="moodle_auto_format">
<text>Testing Sigma Question</text>
</questiontext>
<generalfeedback format="moodle_auto_format">
<text></text>
</generalfeedback>
<defaultgrade>1.0000000</defaultgrade>
<penalty>0.3333333</penalty>
<hidden>0</hidden>
<usecase>0</usecase>
<answer fraction="100" format="moodle_auto_format">
<text>yes</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="0" format="moodle_auto_format">
<text>no</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="0" format="moodle_auto_format">
<text>may be</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
</question>
</quiz>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<quiz>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Alpha</text>
</category>
<info format="moodle_auto_format">
<text>This is Alpha category for test</text>
</info>
<idnumber>alpha-idnumber</idnumber>
</question>
<!-- question: 91 -->
<question type="truefalse">
<name>
<text>Alpha Question</text>
</name>
<questiontext format="html">
<text><![CDATA[<p>Testing Alpha Question</p>]]></text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<defaultgrade>1.0000000</defaultgrade>
<penalty>1.0000000</penalty>
<hidden>0</hidden>
<answer fraction="100" format="moodle_auto_format">
<text>true</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="0" format="moodle_auto_format">
<text>false</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
</question>
</quiz>
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<quiz>
<!-- question: 1 -->
<question type="multichoice">
<name>
<text><![CDATA[Question with <strong>invalid grades</strong> : x > 1 & x < 2]]></text>
</name>
<questiontext format="html">
<text><![CDATA[Select all prime numbers :<br>]]></text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<defaultgrade>1.0000000</defaultgrade>
<penalty>0.3333333</penalty>
<hidden>0</hidden>
<idnumber></idnumber>
<single>false</single>
<shuffleanswers>true</shuffleanswers>
<answernumbering>abc</answernumbering>
<showstandardinstruction>0</showstandardinstruction>
<correctfeedback format="html">
<text>Your answer is correct.</text>
</correctfeedback>
<partiallycorrectfeedback format="html">
<text>Your answer is partially correct.</text>
</partiallycorrectfeedback>
<incorrectfeedback format="html">
<text>Your answer is incorrect.</text>
</incorrectfeedback>
<shownumcorrect/>
<answer fraction="-50" format="html">
<text><![CDATA[<p dir="ltr" style="text-align: left;">1<br></p>]]></text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="33" format="html"> <!-- Invalid grade. -->
<text><![CDATA[<p dir="ltr" style="text-align: left;">2<br></p>]]></text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="33.33333" format="html">
<text><![CDATA[<p dir="ltr" style="text-align: left;">3<br></p>]]></text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="-50" format="html">
<text><![CDATA[<p dir="ltr" style="text-align: left;">4<br></p>]]></text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="33.33333" format="html">
<text><![CDATA[<p dir="ltr" style="text-align: left;">5<br></p>]]></text>
<feedback format="html">
<text></text>
</feedback>
</answer>
</question>
</quiz>
+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<quiz>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Alpha</text>
</category>
<info format="moodle_auto_format">
<text>This is Alpha category for test</text>
</info>
<idnumber>alpha-idnumber</idnumber>
</question>
<!-- question: 91 -->
<question type="truefalse">
<name>
<text>Alpha Question</text>
</name>
<questiontext format="html">
<text><![CDATA[<p>Testing Alpha Question</p>]]></text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<defaultgrade>1.0000000</defaultgrade>
<penalty>1.0000000</penalty>
<hidden>0</hidden>
<idnumber></idnumber>
<answer fraction="100" format="moodle_auto_format">
<text>true</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="0" format="moodle_auto_format">
<text>false</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
</question>
</quiz>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<quiz>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Alpha</text>
</category>
<info format="moodle_auto_format">
<text>This is Alpha category for test</text>
</info>
<idnumber>The inequalities &lt; &amp; &gt;</idnumber>
</question>
<!-- question: 91 -->
<question type="truefalse">
<name>
<text>Alpha Question</text>
</name>
<questiontext format="html">
<text><![CDATA[<p>Testing Alpha Question</p>]]></text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<defaultgrade>1.0000000</defaultgrade>
<penalty>1.0000000</penalty>
<hidden>0</hidden>
<idnumber>T &amp; F</idnumber>
<answer fraction="100" format="moodle_auto_format">
<text>true</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="0" format="moodle_auto_format">
<text>false</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
</question>
</quiz>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<quiz>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Delta</text>
</category>
<info format="plain_text">
<text>This is Delta category for test</text>
</info>
<idnumber></idnumber>
</question>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Delta/Epsilon</text>
</category>
<info format="markdown">
<text>This is Epsilon category for test</text>
</info>
<idnumber></idnumber>
</question>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Delta/Epsilon/Zeta</text>
</category>
<info format="moodle_auto_format">
<text>This is Zeta category for test</text>
</info>
<idnumber></idnumber>
</question>
<!-- question: 93 -->
<question type="truefalse">
<name>
<text>Zeta Question</text>
</name>
<questiontext format="html">
<text><![CDATA[<p>Testing Zeta Question</p>]]></text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<defaultgrade>1.0000000</defaultgrade>
<penalty>1.0000000</penalty>
<hidden>0</hidden>
<idnumber></idnumber>
<answer fraction="100" format="moodle_auto_format">
<text>true</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="0" format="moodle_auto_format">
<text>false</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
</question>
</quiz>
@@ -0,0 +1,195 @@
<?xml version="1.0" encoding="UTF-8"?>
<quiz>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Iota</text>
</category>
<info format="plain_text">
<text>This is Iota category for test</text>
</info>
<idnumber></idnumber>
</question>
<!-- question: 96 -->
<question type="truefalse">
<name>
<text>Iota Question</text>
</name>
<questiontext format="html">
<text><![CDATA[<p>Testing Iota Question</p>]]></text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<defaultgrade>1.0000000</defaultgrade>
<penalty>1.0000000</penalty>
<hidden>0</hidden>
<idnumber></idnumber>
<answer fraction="100" format="moodle_auto_format">
<text>true</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="0" format="moodle_auto_format">
<text>false</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
</question>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Iota/Kappa</text>
</category>
<info format="markdown">
<text>This is Kappa category for test</text>
</info>
<idnumber></idnumber>
</question>
<!-- question: 106 -->
<question type="essay">
<name>
<text>Kappa Essay Question</text>
</name>
<questiontext format="moodle_auto_format">
<text>Testing Kappa Essay Question</text>
</questiontext>
<generalfeedback format="moodle_auto_format">
<text></text>
</generalfeedback>
<defaultgrade>1.0000000</defaultgrade>
<penalty>0.0000000</penalty>
<hidden>0</hidden>
<idnumber></idnumber>
<responseformat>editor</responseformat>
<responserequired>1</responserequired>
<responsefieldlines>10</responsefieldlines>
<minwordlimit></minwordlimit>
<maxwordlimit></maxwordlimit>
<attachments>0</attachments>
<attachmentsrequired>0</attachmentsrequired>
<maxbytes>0</maxbytes>
<filetypeslist></filetypeslist>
<graderinfo format="html">
<text></text>
</graderinfo>
<responsetemplate format="html">
<text></text>
</responsetemplate>
</question>
<!-- question: 97 -->
<question type="truefalse">
<name>
<text>Kappa Question</text>
</name>
<questiontext format="html">
<text><![CDATA[<p>Testing Kappa Question</p>]]></text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<defaultgrade>1.0000000</defaultgrade>
<penalty>1.0000000</penalty>
<hidden>0</hidden>
<idnumber></idnumber>
<answer fraction="100" format="moodle_auto_format">
<text>true</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="0" format="moodle_auto_format">
<text>false</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
</question>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Iota/Kappa/Lambda</text>
</category>
<info format="moodle_auto_format">
<text>This is Lambda category for test</text>
</info>
<idnumber></idnumber>
</question>
<!-- question: 98 -->
<question type="truefalse">
<name>
<text>Lambda Question</text>
</name>
<questiontext format="html">
<text><![CDATA[<p>Testing Lambda Question</p>]]></text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<defaultgrade>1.0000000</defaultgrade>
<penalty>1.0000000</penalty>
<hidden>0</hidden>
<idnumber></idnumber>
<answer fraction="100" format="moodle_auto_format">
<text>true</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="0" format="moodle_auto_format">
<text>false</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
</question>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Iota/Mu</text>
</category>
<info format="moodle_auto_format">
<text>This is Mu category for test</text>
</info>
<idnumber></idnumber>
</question>
<!-- question: 99 -->
<question type="truefalse">
<name>
<text>Mu Question</text>
</name>
<questiontext format="html">
<text><![CDATA[<p>Testing Mu Question</p>]]></text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<defaultgrade>1.0000000</defaultgrade>
<penalty>1.0000000</penalty>
<hidden>0</hidden>
<idnumber></idnumber>
<answer fraction="100" format="moodle_auto_format">
<text>true</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="0" format="moodle_auto_format">
<text>false</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
</question>
</quiz>
+72
View File
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<quiz>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Pi</text>
</category>
</question>
<!-- question: 103 -->
<question type="truefalse">
<name>
<text>Pi Question</text>
</name>
<questiontext format="html">
<text><![CDATA[<p>Testing Pi Question</p>]]></text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<defaultgrade>1.0000000</defaultgrade>
<penalty>1.0000000</penalty>
<hidden>0</hidden>
<answer fraction="100" format="moodle_auto_format">
<text>true</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="0" format="moodle_auto_format">
<text>false</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
</question>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Pi/Rho</text>
</category>
</question>
<!-- question: 104 -->
<question type="truefalse">
<name>
<text>Rho Question</text>
</name>
<questiontext format="html">
<text><![CDATA[<p>Testing Rho Question</p>]]></text>
</questiontext>
<generalfeedback format="html">
<text></text>
</generalfeedback>
<defaultgrade>1.0000000</defaultgrade>
<penalty>1.0000000</penalty>
<hidden>0</hidden>
<answer fraction="100" format="moodle_auto_format">
<text>true</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
<answer fraction="0" format="moodle_auto_format">
<text>false</text>
<feedback format="html">
<text></text>
</feedback>
</answer>
</question>
</quiz>
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<quiz>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Default for Test images in backup</text>
</category>
</question>
<!-- question: 5 -->
<question type="cloze">
<name><text>cloze question with image</text>
</name>
<questiontext>
<text><![CDATA[This is a multianswer question with an image in the old &quot;image&quot; field that was used in moodle 1.9.<br />Match the following cities with the correct state:<br />* San Francisco: {1:MULTICHOICE:=California#OK~Arizona#Wrong}<br />* Tucson: {1:MULTICHOICE:California#Wrong~%100%Arizona#OK}<br />* Los Angeles: {1:MULTICHOICE:=California#OK~Arizona#Wrong}<br />* Phoenix: {1:MULTICHOICE:%0%California#Wrong~=Arizona#OK}<br />The capital of France is {1:SHORTANSWER:%100%Paris#Congratulations!<br />~%50%Marseille#No, that is the second largest city in France (after<br />Paris).~*#Wrong answer. The capital of France is Paris, of course.}.<br />]]></text>
</questiontext>
<image>moodle.png</image>
<image_base64>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAHzSURBVDhPxZFbSJNhGIDXERsVGbKawWJaoyAYGeqNYgdl6zenrhhUlB0I6iJEbUl0WF1UUK2DYhDkGghu7Z+HraisPLAsu+0ygi66kCioqwg2t6f///YhzAVBXfTc/HzP9/J8L/wG/pG8QDw+TE+Xn/O+dva07MW5vZayys3yNp/ZwPPRBPuOHqGisordDc00Ki42bqhAjQ3Jid8jAvoL9vIt4qs463G73VRtrcFiseDvuSYG+TwB7y/zfdzHpzeDWachAjeuXyWkPqZ21zZ2Nrioa/JQarOjlK8jes7Eh/s18GwHRMxkgka+3lvEi1ubcgM63Zea6W5v5ESLB4fDwRVvNam+eRBey3TAxqObNl76rXwLFvAlsIyhC2W5gZ+JJmYixQS9q8RZZzpghwEr/R2LpYGPIYWZqJnYSWNugCkPPDDx1JddTyeZqCetasMdS6WB9OQB7Xet5kmbITeQntoP/YWEfSZxFoy5SA2YCXeukAJ+vD5MMl7Cw9YF2UAkGhIXmYSbZLgI9WKxOOukx52kVAvq6eXS6HOHyAyuRz0lN5hlwg19hcTOWqXQ0FxaXZPnGC4V7s+BsToRUM8YpdA2eHswG/DO2eDV7WpGWg30dpZIA5Nddkba5nP3+Epp4F2vwqh3CXeOLZyzwV/wvwPwC58bSNEdAkhaAAAAAElFTkSuQmCC
</image_base64>
<generalfeedback>
<text></text>
</generalfeedback>
<shuffleanswers>0</shuffleanswers>
</question>
<!-- question: 4 -->
<question type="multichoice">
<name><text>mcq with image</text>
</name>
<questiontext format="html">
<text><![CDATA[This is a multichoice question with an image in the old &quot;image&quot; field that was used in Moodle 1.9]]></text>
</questiontext>
<image>moodle.png</image>
<image_base64>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAHzSURBVDhPxZFbSJNhGIDXERsVGbKawWJaoyAYGeqNYgdl6zenrhhUlB0I6iJEbUl0WF1UUK2DYhDkGghu7Z+HraisPLAsu+0ygi66kCioqwg2t6f///YhzAVBXfTc/HzP9/J8L/wG/pG8QDw+TE+Xn/O+dva07MW5vZayys3yNp/ZwPPRBPuOHqGisordDc00Ki42bqhAjQ3Jid8jAvoL9vIt4qs463G73VRtrcFiseDvuSYG+TwB7y/zfdzHpzeDWachAjeuXyWkPqZ21zZ2Nrioa/JQarOjlK8jes7Eh/s18GwHRMxkgka+3lvEi1ubcgM63Zea6W5v5ESLB4fDwRVvNam+eRBey3TAxqObNl76rXwLFvAlsIyhC2W5gZ+JJmYixQS9q8RZZzpghwEr/R2LpYGPIYWZqJnYSWNugCkPPDDx1JddTyeZqCetasMdS6WB9OQB7Xet5kmbITeQntoP/YWEfSZxFoy5SA2YCXeukAJ+vD5MMl7Cw9YF2UAkGhIXmYSbZLgI9WKxOOukx52kVAvq6eXS6HOHyAyuRz0lN5hlwg19hcTOWqXQ0FxaXZPnGC4V7s+BsToRUM8YpdA2eHswG/DO2eDV7WpGWg30dpZIA5Nddkba5nP3+Epp4F2vwqh3CXeOLZyzwV/wvwPwC58bSNEdAkhaAAAAAElFTkSuQmCC
</image_base64>
<generalfeedback>
<text></text>
</generalfeedback>
<defaultgrade>1</defaultgrade>
<penalty>0.1</penalty>
<hidden>0</hidden>
<shuffleanswers>1</shuffleanswers>
<single>true</single>
<shuffleanswers>true</shuffleanswers>
<correctfeedback> <text></text>
</correctfeedback>
<partiallycorrectfeedback> <text></text>
</partiallycorrectfeedback>
<incorrectfeedback> <text></text>
</incorrectfeedback>
<answernumbering>abc</answernumbering>
<answer fraction="0">
<text>
wrong answer
</text>
<feedback>
<text>
</text>
</feedback>
</answer>
<answer fraction="0">
<text>
another wrong answer
</text>
<feedback>
<text>
</text>
</feedback>
</answer>
<answer fraction="100">
<text>
right answer
</text>
<feedback>
<text>
</text>
</feedback>
</answer>
</question>
</quiz>
File diff suppressed because one or more lines are too long
@@ -0,0 +1,526 @@
<?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/>.
/**
* Unit tests for export/import description (info) for question category in the Moodle XML format.
*
* @package qformat_xml
* @copyright 2014 Nikita Nikitsky, Volgograd State Technical University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core_question\local\bank\question_edit_contexts;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->dirroot . '/question/format/xml/format.php');
require_once($CFG->dirroot . '/question/format.php');
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
require_once($CFG->dirroot . '/question/editlib.php');
/**
* Unit tests for the XML question format import and export.
*
* @copyright 2014 Nikita Nikitsky, Volgograd State Technical University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qformat_xml_import_export_test extends advanced_testcase {
/**
* Create object qformat_xml for test.
* @param string $filename with name for testing file.
* @param stdClass $course
* @return qformat_xml XML question format object.
*/
public function create_qformat($filename, $course) {
$qformat = new qformat_xml();
$qformat->setContexts((new question_edit_contexts(context_course::instance($course->id)))->all());
$qformat->setCourse($course);
$qformat->setFilename(__DIR__ . '/fixtures/' . $filename);
$qformat->setRealfilename($filename);
$qformat->setMatchgrades('error');
$qformat->setCatfromfile(1);
$qformat->setContextfromfile(1);
$qformat->setStoponerror(1);
$qformat->setCattofile(1);
$qformat->setContexttofile(1);
$qformat->set_display_progress(false);
return $qformat;
}
/**
* Check xml for compliance.
* @param string $expectedxml with correct string.
* @param string $xml you want to check.
*/
public function assert_same_xml($expectedxml, $xml) {
$this->assertEquals($this->normalise_xml($expectedxml),
$this->normalise_xml($xml));
}
/**
* Clean up some XML to remove irrelevant differences, before it is compared.
* @param string $xml some XML.
* @return string cleaned-up XML.
*/
protected function normalise_xml($xml) {
// Normalise line endings.
$xml = phpunit_util::normalise_line_endings($xml);
$xml = preg_replace("~\n$~", "", $xml); // Strip final newline in file.
// Replace all numbers in question id comments with 0.
$xml = preg_replace('~(?<=<!-- question: )([0-9]+)(?= -->)~', '0', $xml);
// Deal with how different databases output numbers. Only match when only thing in a tag.
$xml = preg_replace("~>.0000000<~", '>0<', $xml); // How Oracle outputs 0.0000000.
$xml = preg_replace("~(\.(:?[0-9]*[1-9])?)0*<~", '$1<', $xml); // Other cases of trailing 0s
$xml = preg_replace("~([0-9]).<~", '$1<', $xml); // Stray . in 1. after last step.
return $xml;
}
/**
* Check imported category.
* @param string $name imported category name.
* @param string $info imported category info field (description of category).
* @param int $infoformat imported category info field format.
*/
public function assert_category_imported($name, $info, $infoformat, $idnumber = null) {
global $DB;
$category = $DB->get_record('question_categories', ['name' => $name], '*', MUST_EXIST);
$this->assertEquals($info, $category->info);
$this->assertEquals($infoformat, $category->infoformat);
$this->assertSame($idnumber, $category->idnumber);
}
/**
* Check a question category has a given parent.
* @param string $catname Name of the question category
* @param string $parentname Name of the parent category
* @throws dml_exception
*/
public function assert_category_has_parent($catname, $parentname) {
global $DB;
$sql = 'SELECT qc1.*
FROM {question_categories} qc1
JOIN {question_categories} qc2 ON qc1.parent = qc2.id
WHERE qc1.name = ?
AND qc2.name = ?';
$categories = $DB->get_records_sql($sql, [$catname, $parentname]);
$this->assertTrue(count($categories) == 1);
}
/**
* Check a question exists in a category.
* @param string $qname The name of the question
* @param string $catname The name of the category
* @throws dml_exception
*/
public function assert_question_in_category($qname, $catname) {
global $DB;
$sql = "SELECT q.*, qbe.questioncategoryid AS category
FROM {question} q
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
WHERE q.name = :name";
$question = $DB->get_record_sql($sql, ['name' => $qname], MUST_EXIST);
$category = $DB->get_record('question_categories', ['name' => $catname], '*', MUST_EXIST);
$this->assertEquals($category->id, $question->category);
}
/**
* Simple check for importing a category with a description.
*/
public function test_import_category(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$this->setAdminUser();
$qformat = $this->create_qformat('category_with_description.xml', $course);
$imported = $qformat->importprocess();
$this->assertTrue($imported);
$this->assert_category_imported('Alpha',
'This is Alpha category for test', FORMAT_MOODLE, 'alpha-idnumber');
$this->assert_category_has_parent('Alpha', 'top');
}
/**
* Check importing nested categories.
*/
public function test_import_nested_categories(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$this->setAdminUser();
$qformat = $this->create_qformat('nested_categories.xml', $course);
$imported = $qformat->importprocess();
$this->assertTrue($imported);
$this->assert_category_imported('Delta', 'This is Delta category for test', FORMAT_PLAIN);
$this->assert_category_imported('Epsilon', 'This is Epsilon category for test', FORMAT_MARKDOWN);
$this->assert_category_imported('Zeta', 'This is Zeta category for test', FORMAT_MOODLE);
$this->assert_category_has_parent('Delta', 'top');
$this->assert_category_has_parent('Epsilon', 'Delta');
$this->assert_category_has_parent('Zeta', 'Epsilon');
}
/**
* Check importing nested categories contain the right questions.
*/
public function test_import_nested_categories_with_questions(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$this->setAdminUser();
$qformat = $this->create_qformat('nested_categories_with_questions.xml', $course);
$imported = $qformat->importprocess();
$this->assertTrue($imported);
$this->assert_category_imported('Iota', 'This is Iota category for test', FORMAT_PLAIN);
$this->assert_category_imported('Kappa', 'This is Kappa category for test', FORMAT_MARKDOWN);
$this->assert_category_imported('Lambda', 'This is Lambda category for test', FORMAT_MOODLE);
$this->assert_category_imported('Mu', 'This is Mu category for test', FORMAT_MOODLE);
$this->assert_question_in_category('Iota Question', 'Iota');
$this->assert_question_in_category('Kappa Question', 'Kappa');
$this->assert_question_in_category('Lambda Question', 'Lambda');
$this->assert_question_in_category('Mu Question', 'Mu');
$this->assert_category_has_parent('Iota', 'top');
$this->assert_category_has_parent('Kappa', 'Iota');
$this->assert_category_has_parent('Lambda', 'Kappa');
$this->assert_category_has_parent('Mu', 'Iota');
}
/**
* Check import of an old file (without format), for backward compatability.
*/
public function test_import_old_format(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$this->setAdminUser();
$qformat = $this->create_qformat('old_format_file.xml', $course);
$imported = $qformat->importprocess();
$this->assertTrue($imported);
$this->assert_category_imported('Pi', '', FORMAT_MOODLE);
$this->assert_category_imported('Rho', '', FORMAT_MOODLE);
$this->assert_question_in_category('Pi Question', 'Pi');
$this->assert_question_in_category('Rho Question', 'Rho');
$this->assert_category_has_parent('Pi', 'top');
$this->assert_category_has_parent('Rho', 'Pi');
}
/**
* Check the import of an xml file where the child category exists before the parent category.
*/
public function test_import_categories_in_reverse_order(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$this->setAdminUser();
$qformat = $this->create_qformat('categories_reverse_order.xml', $course);
$imported = $qformat->importprocess();
$this->assertTrue($imported);
$this->assert_category_imported('Sigma', 'This is Sigma category for test', FORMAT_HTML);
$this->assert_category_imported('Tau', 'This is Tau category for test', FORMAT_HTML);
$this->assert_question_in_category('Sigma Question', 'Sigma');
$this->assert_question_in_category('Tau Question', 'Tau');
$this->assert_category_has_parent('Sigma', 'top');
$this->assert_category_has_parent('Tau', 'Sigma');
}
/**
* Check exception when importing questions with invalid grades.
*
* @covers \qformat_default::importprocess
*/
public function test_import_invalid_grades(): void {
global $OUTPUT;
$this->resetAfterTest(true);
$course = $this->getDataGenerator()->create_course();
$this->setAdminUser();
$qformat = $this->create_qformat('error_invalid_grades.xml', $course);
ob_start();
$imported = $qformat->importprocess();
$output = ob_get_clean();
$a = ['grades' => '0.33', 'question' => 'Question with invalid grades : x > 1 & x < 2'];
$expectedoutput = $OUTPUT->notification(get_string('invalidgradequestion', 'question', $a));
$expectedoutput .= $OUTPUT->notification(get_string('importparseerror', 'question'));
$this->assertFalse($imported);
$this->assertEquals($expectedoutput, $output);
}
/**
* Simple check for exporting a category.
*/
public function test_export_category(): void {
global $SITE;
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$this->resetAfterTest();
$this->setAdminUser();
// Note while this loads $qformat with all the 'right' data from the xml file,
// the call to setCategory, followed by exportprocess will actually only export data
// from the database (created by the generator).
$qformat = $this->create_qformat('export_category.xml', $SITE);
$category = $generator->create_question_category([
'name' => 'Alpha',
'contextid' => context_course::instance($SITE->id)->id,
'info' => 'This is Alpha category for test',
'infoformat' => '0',
'idnumber' => 'alpha-idnumber',
'stamp' => make_unique_id_code(),
'parent' => '0',
'sortorder' => '999']);
$question = $generator->create_question('truefalse', null, [
'category' => $category->id,
'name' => 'Alpha Question',
'questiontext' => ['format' => '1', 'text' => '<p>Testing Alpha Question</p>'],
'generalfeedback' => ['format' => '1', 'text' => ''],
'correctanswer' => '1',
'feedbacktrue' => ['format' => '1', 'text' => ''],
'feedbackfalse' => ['format' => '1', 'text' => ''],
'penalty' => '1']);
$qformat->setCategory($category);
$expectedxml = file_get_contents(__DIR__ . '/fixtures/export_category.xml');
$this->assert_same_xml($expectedxml, $qformat->exportprocess());
}
/**
* Check exporting nested categories.
*/
public function test_export_nested_categories(): void {
global $SITE;
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$qformat = $this->create_qformat('nested_categories.zml', $SITE);
$categorydelta = $generator->create_question_category([
'name' => 'Delta',
'contextid' => context_course::instance($SITE->id)->id,
'info' => 'This is Delta category for test',
'infoformat' => '2',
'stamp' => make_unique_id_code(),
'parent' => '0',
'sortorder' => '999']);
$categoryepsilon = $generator->create_question_category([
'name' => 'Epsilon',
'contextid' => context_course::instance($SITE->id)->id,
'info' => 'This is Epsilon category for test',
'infoformat' => '4',
'stamp' => make_unique_id_code(),
'parent' => $categorydelta->id,
'sortorder' => '999']);
$categoryzeta = $generator->create_question_category([
'name' => 'Zeta',
'contextid' => context_course::instance($SITE->id)->id,
'info' => 'This is Zeta category for test',
'infoformat' => '0',
'stamp' => make_unique_id_code(),
'parent' => $categoryepsilon->id,
'sortorder' => '999']);
$question = $generator->create_question('truefalse', null, [
'category' => $categoryzeta->id,
'name' => 'Zeta Question',
'questiontext' => [
'format' => '1',
'text' => '<p>Testing Zeta Question</p>'],
'generalfeedback' => ['format' => '1', 'text' => ''],
'correctanswer' => '1',
'feedbacktrue' => ['format' => '1', 'text' => ''],
'feedbackfalse' => ['format' => '1', 'text' => ''],
'penalty' => '1']);
$qformat->setCategory($categorydelta);
$qformat->setCategory($categoryepsilon);
$qformat->setCategory($categoryzeta);
$expectedxml = file_get_contents(__DIR__ . '/fixtures/nested_categories.xml');
$this->assert_same_xml($expectedxml, $qformat->exportprocess());
}
/**
* Check exporting nested categories contain the right questions.
*/
public function test_export_nested_categories_with_questions(): void {
global $SITE;
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$qformat = $this->create_qformat('nested_categories_with_questions.xml', $SITE);
$categoryiota = $generator->create_question_category([
'name' => 'Iota',
'contextid' => context_course::instance($SITE->id)->id,
'info' => 'This is Iota category for test',
'infoformat' => '2',
'stamp' => make_unique_id_code(),
'parent' => '0',
'sortorder' => '999']);
$iotaquestion = $generator->create_question('truefalse', null, [
'category' => $categoryiota->id,
'name' => 'Iota Question',
'questiontext' => [
'format' => '1',
'text' => '<p>Testing Iota Question</p>'],
'generalfeedback' => ['format' => '1', 'text' => ''],
'correctanswer' => '1',
'feedbacktrue' => ['format' => '1', 'text' => ''],
'feedbackfalse' => ['format' => '1', 'text' => ''],
'penalty' => '1']);
$categorykappa = $generator->create_question_category([
'name' => 'Kappa',
'contextid' => context_course::instance($SITE->id)->id,
'info' => 'This is Kappa category for test',
'infoformat' => '4',
'stamp' => make_unique_id_code(),
'parent' => $categoryiota->id,
'sortorder' => '999']);
$kappaquestion = $generator->create_question('essay', null, [
'category' => $categorykappa->id,
'name' => 'Kappa Essay Question',
'questiontext' => ['text' => 'Testing Kappa Essay Question'],
'generalfeedback' => '',
'responseformat' => 'editor',
'responserequired' => 1,
'responsefieldlines' => 10,
'attachments' => 0,
'attachmentsrequired' => 0,
'graderinfo' => ['format' => '1', 'text' => ''],
'responsetemplate' => ['format' => '1', 'text' => ''],
'idnumber' => '']);
$kappaquestion1 = $generator->create_question('truefalse', null, [
'category' => $categorykappa->id,
'name' => 'Kappa Question',
'questiontext' => [
'format' => '1',
'text' => '<p>Testing Kappa Question</p>'],
'generalfeedback' => ['format' => '1', 'text' => ''],
'correctanswer' => '1',
'feedbacktrue' => ['format' => '1', 'text' => ''],
'feedbackfalse' => ['format' => '1', 'text' => ''],
'penalty' => '1',
'idnumber' => '']);
$categorylambda = $generator->create_question_category([
'name' => 'Lambda',
'contextid' => context_course::instance($SITE->id)->id,
'info' => 'This is Lambda category for test',
'infoformat' => '0',
'stamp' => make_unique_id_code(),
'parent' => $categorykappa->id,
'sortorder' => '999']);
$lambdaquestion = $generator->create_question('truefalse', null, [
'category' => $categorylambda->id,
'name' => 'Lambda Question',
'questiontext' => [
'format' => '1',
'text' => '<p>Testing Lambda Question</p>'],
'generalfeedback' => ['format' => '1', 'text' => ''],
'correctanswer' => '1',
'feedbacktrue' => ['format' => '1', 'text' => ''],
'feedbackfalse' => ['format' => '1', 'text' => ''],
'penalty' => '1']);
$categorymu = $generator->create_question_category([
'name' => 'Mu',
'contextid' => context_course::instance($SITE->id)->id,
'info' => 'This is Mu category for test',
'infoformat' => '0',
'stamp' => make_unique_id_code(),
'parent' => $categoryiota->id,
'sortorder' => '999']);
$muquestion = $generator->create_question('truefalse', null, [
'category' => $categorymu->id,
'name' => 'Mu Question',
'questiontext' => [
'format' => '1',
'text' => '<p>Testing Mu Question</p>'],
'generalfeedback' => ['format' => '1', 'text' => ''],
'correctanswer' => '1',
'feedbacktrue' => ['format' => '1', 'text' => ''],
'feedbackfalse' => ['format' => '1', 'text' => ''],
'penalty' => '1']);
$qformat->setCategory($categoryiota);
$expectedxml = file_get_contents(__DIR__ . '/fixtures/nested_categories_with_questions.xml');
$this->assert_same_xml($expectedxml, $qformat->exportprocess());
}
/**
* Simple check for exporting a category.
*/
public function test_export_category_with_special_chars(): void {
global $SITE;
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$this->resetAfterTest();
$this->setAdminUser();
// Note while this loads $qformat with all the 'right' data from the xml file,
// the call to setCategory, followed by exportprocess will actually only export data
// from the database (created by the generator).
$qformat = $this->create_qformat('export_category.xml', $SITE);
$category = $generator->create_question_category([
'name' => 'Alpha',
'contextid' => context_course::instance($SITE->id)->id,
'info' => 'This is Alpha category for test',
'infoformat' => '0',
'idnumber' => 'The inequalities < & >',
'stamp' => make_unique_id_code(),
'parent' => '0',
'sortorder' => '999']);
$generator->create_question('truefalse', null, [
'category' => $category->id,
'name' => 'Alpha Question',
'questiontext' => ['format' => '1', 'text' => '<p>Testing Alpha Question</p>'],
'generalfeedback' => ['format' => '1', 'text' => ''],
'idnumber' => 'T & F',
'correctanswer' => '1',
'feedbacktrue' => ['format' => '1', 'text' => ''],
'feedbackfalse' => ['format' => '1', 'text' => ''],
'penalty' => '1']);
$qformat->setCategory($category);
$expectedxml = file_get_contents(__DIR__ . '/fixtures/html_chars_in_idnumbers.xml');
$this->assert_same_xml($expectedxml, $qformat->exportprocess());
}
/**
* Test that bad multianswer questions are not imported.
*/
public function test_import_broken_multianswer_questions(): void {
$lines = file(__DIR__ . '/fixtures/broken_cloze_questions.xml');
$importer = $qformat = new qformat_xml();
// The importer echoes some errors, so we need to capture and check that.
ob_start();
$questions = $importer->readquestions($lines);
$output = ob_get_contents();
ob_end_clean();
// Check that there were some expected errors.
$this->assertStringContainsString('Error importing question', $output);
$this->assertStringContainsString('Invalid embedded answers (Cloze) question', $output);
$this->assertStringContainsString('This type of question requires at least 2 choices', $output);
$this->assertStringContainsString('The answer must be a number, for example -1.234 or 3e8, or \'*\'.', $output);
$this->assertStringContainsString('One of the answers should have a score of 100% so it is possible to get full marks for this question.',
$output);
$this->assertStringContainsString('The question text must include at least one embedded answer.', $output);
// No question have been imported.
$this->assertCount(0, $questions);
}
}
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
<?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 the calculated question type.
*
* @package qformat_xml
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qformat_xml';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;