first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,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;