MDL-20636 work-in-progress on converting the Opaque question type.

This commit is contained in:
Tim Hunt 2011-01-14 19:06:52 +00:00
parent 24ef491d8f
commit 2086d94077
19 changed files with 2087 additions and 18 deletions

View File

@ -36,7 +36,8 @@ class qbehaviour_opaque_test extends qbehaviour_walkthrough_test_base {
* @return unknown_type
*/
protected function make_standard_om_question() {
$engineid = get_field('question_opaque_engines', 'MIN(id)', '', '');
global $DB;
$engineid = $DB->get_field('question_opaque_engines', 'MIN(id)', array());
if (empty($engineid)) {
throw new Exception('Cannot test Opaque. No question engines configured.');
}

View File

@ -372,29 +372,29 @@ DONE question/type/multichoice/version.php | 6 +-
question/type/numerical/simpletest/testquestiontype.php | 206 +-
question/type/numerical/version.php | 4 +-
question/type/opaque/db/install.xml | 44 +
question/type/opaque/db/upgrade.php | 69 +
question/type/opaque/edit_opaque_form.php | 114 +
DONE question/type/opaque/db/install.xml | 44 +
DONE question/type/opaque/db/upgrade.php | 69 +
DONE question/type/opaque/edit_opaque_form.php | 114 +
question/type/opaque/editengine.php | 152 +
question/type/opaque/engines.php | 78 +
DONE question/type/opaque/engines.php | 78 +
question/type/opaque/file.php | 31 +
question/type/opaque/icon.gif | Bin 0 -> 97 bytes
question/type/opaque/lang/en_utf8/help/opaque/configuredengines.html | 6 +
question/type/opaque/lang/en_utf8/help/opaque/editengine.html | 16 +
question/type/opaque/lang/en_utf8/help/opaque/opaque.html | 8 +
question/type/opaque/lang/en_utf8/help/opaque/passkey.html | 6 +
question/type/opaque/lang/en_utf8/help/opaque/questionengine.html | 7 +
question/type/opaque/lang/en_utf8/help/opaque/questionid.html | 5 +
question/type/opaque/lang/en_utf8/qtype_opaque.php | 57 +
DONE question/type/opaque/icon.gif | Bin 0 -> 97 bytes
DONE question/type/opaque/lang/en_utf8/help/opaque/configuredengines.html | 6 +
DONE question/type/opaque/lang/en_utf8/help/opaque/editengine.html | 16 +
DONE question/type/opaque/lang/en_utf8/help/opaque/opaque.html | 8 +
DONE question/type/opaque/lang/en_utf8/help/opaque/passkey.html | 6 +
DONE question/type/opaque/lang/en_utf8/help/opaque/questionengine.html | 7 +
DONE question/type/opaque/lang/en_utf8/help/opaque/questionid.html | 5 +
DONE question/type/opaque/lang/en_utf8/qtype_opaque.php | 57 +
question/type/opaque/locallib.php | 826 ++++++
question/type/opaque/question.php | 55 +
question/type/opaque/questiontype.php | 220 ++
question/type/opaque/renderer.php | 37 +
DONE question/type/opaque/question.php | 55 +
DONE question/type/opaque/questiontype.php | 220 ++
DONE question/type/opaque/renderer.php | 37 +
question/type/opaque/simpletest/testlocallib.php | 60 +
question/type/opaque/simpletest/testquestiontype.php | 303 ++
question/type/opaque/styles.css | 14 +
DONE question/type/opaque/styles.css | 14 +
question/type/opaque/testengine.php | 70 +
question/type/opaque/version.php | 5 +
DONE question/type/opaque/version.php | 5 +
question/type/oumultiresponse/db/install.xml | 24 +
question/type/oumultiresponse/db/upgrade.php | 58 +

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="question/type/opaque/db" VERSION="20110114" COMMENT="XMLDB file for Moodle question/type/opaque"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="question_opaque_engines" COMMENT="Remote engines that the Opaque question type can connect to. Each engine will comprise one or more servers stored in the question_opaque_servers table" NEXT="question_opaque_servers">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="true" COMMENT="id of the table, please edit me" NEXT="name"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The name used to identify this engine in the interface." PREVIOUS="id" NEXT="passkey"/>
<FIELD NAME="passkey" TYPE="char" LENGTH="8" NOTNULL="false" SEQUENCE="false" COMMENT="The passkey for the QE if required." PREVIOUS="name"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="primary key of the table, please edit me"/>
</KEYS>
</TABLE>
<TABLE NAME="question_opaque_servers" COMMENT="This table stores the individual servers that make up an Opaque remote engine." PREVIOUS="question_opaque_engines" NEXT="question_opaque">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="true" COMMENT="id of the table, please edit me" NEXT="engineid"/>
<FIELD NAME="engineid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="false" COMMENT="The Opaque engine this server is part of." PREVIOUS="id" NEXT="type"/>
<FIELD NAME="type" TYPE="char" LENGTH="16" NOTNULL="true" SEQUENCE="false" COMMENT="The type of this server - question engine or question bank." PREVIOUS="engineid" NEXT="url"/>
<FIELD NAME="url" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The base URL of this server." PREVIOUS="type"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="primary key of the table, please edit me" NEXT="quesopaqserv_eng_fk"/>
<KEY NAME="quesopaqserv_eng_fk" TYPE="foreign" FIELDS="engineid" REFTABLE="question_opaque_engines" REFFIELDS="id" PREVIOUS="primary"/>
</KEYS>
</TABLE>
<TABLE NAME="question_opaque" COMMENT="Extra infomation required to define an Opaque question. This table extends the question table." PREVIOUS="question_opaque_servers">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="true" COMMENT="id of the table, please edit me" NEXT="questionid"/>
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="false" COMMENT="The id of the question this row relates to." PREVIOUS="id" NEXT="engineid"/>
<FIELD NAME="engineid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="false" SEQUENCE="false" COMMENT="The remote engine that provides this question. Refers to question_opaaque_engines.id." PREVIOUS="questionid" NEXT="remoteid"/>
<FIELD NAME="remoteid" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The id of this question - meaningful to the remote engine." PREVIOUS="engineid" NEXT="remoteversion"/>
<FIELD NAME="remoteversion" TYPE="char" LENGTH="16" NOTNULL="true" SEQUENCE="false" COMMENT="The version of this question - meaningful to the remote engine." PREVIOUS="remoteid"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="primary key of the table, please edit me" NEXT="quesopaq_eng_fk"/>
<KEY NAME="quesopaq_eng_fk" TYPE="foreign" FIELDS="engineid" REFTABLE="question_opaque_engines" REFFIELDS="id" PREVIOUS="primary" NEXT="quesopaq_que_fk"/>
<KEY NAME="quesopaq_que_fk" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id" PREVIOUS="quesopaq_eng_fk"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>

View File

@ -0,0 +1,118 @@
<?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/>.
/**
* Defines the editing form for the Opaque question type.
*
* @package qtype
* @subpackage opaque
* @copyright 2006 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/locallib.php');
/**
* Form definition base class. This defines the common fields that
* all question types need. Question types should define their own
* class that inherits from this one, and implements the definition_inner()
* method.
*
* @copyright 2006 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_edit_opaque_form extends question_edit_form {
function definition() {
parent::definition();
$mform = $this->_form;
$mform->removeElement('questiontext');
$mform->removeElement('generalfeedback');
$mform->removeElement('defaultmark');
$mform->addElement('hidden', 'defaultmark');
$mform->setType('defaultmark', PARAM_INT);
$mform->setDefault('defaultmark', 1);
}
function definition_inner($mform) {
$mform->addElement('select', 'engineid', get_string('questionengine', 'qtype_opaque'), installed_engine_choices());
$mform->setType('engineid', PARAM_INT);
$mform->addRule('engineid', null, 'required', null, 'client');
$mform->addHelpButton('engineid', 'questionengine', 'qtype_opaque');
$mform->addElement('text', 'remoteid', get_string('questionid', 'qtype_opaque'), array('size' => 50));
$mform->setType('remoteid', PARAM_RAW);
$mform->addRule('remoteid', null, 'required', null, 'client');
$mform->addHelpButton('remoteid', 'questionid', 'qtype_opaque');
$mform->addElement('text', 'remoteversion', get_string('questionversion', 'qtype_opaque'), array('size' => 3));
$mform->setType('remoteversion', PARAM_RAW);
$mform->addRule('remoteversion', null, 'required', null, 'client');
}
function validation($data, $files) {
$errors = parent::validation($data, $files);
// Check we can connect to this questoin engine.
$engine = load_engine_def($data['engineid']);
if (is_string($engine)) {
$errors['engineid'] = $engine;
}
$remoteidok = true;
$partregexp = '[_a-z][_a-zA-Z0-9]*';
if (!preg_match("/^$partregexp(\\.$partregexp)*\$/", $data['remoteid'])) {
$errors['remoteid'] = get_string('invalidquestionidsyntax', 'qtype_opaque');
$remoteidok = false;
}
if (!preg_match('/^\d+\.\d+$/', $data['remoteversion'])) {
$errors['remoteversion'] = get_string('invalidquestionversionsyntax', 'qtype_opaque');
$remoteidok = false;
}
// Try connecting to the remote question engine both as extra validation of the id, and
// also to get the default grade.
if ($remoteidok) {
$metadata = get_question_metadata($engine, $data['remoteid'], $data['remoteversion']);
if (is_string($metadata)) {
$errors['remoteid'] = $metadata;
} else if (!isset($metadata['questionmetadata']['#']['scoring'][0]['#']['marks'][0]['#'])) {
$errors['remoteid'] = get_string('maxgradenotreturned');
} else {
$this->_defaultmark = $metadata['questionmetadata']['#']['scoring'][0]['#']['marks'][0]['#'];
}
}
return $errors;
}
function get_data($slashed = true) {
// We override get_data to to add the defaultmark, which was determined during validation,
// to the data that is returned.
$data = parent::get_data($slashed);
if (is_object($data) && isset($this->_defaultmark)) {
$data->defaultmark = $this->_defaultmark;
}
return $data;
}
function qtype() {
return 'opaque';
}
}

View File

@ -0,0 +1,161 @@
<?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/>.
/**
* Page for editing the configuration of a particular Opaque engine.
*
* @package qtype
* @subpackage opaque
* @copyright 2006 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../../../config.php');
require_once($CFG->libdir . '/adminlib.php');
require_once($CFG->libdir . '/formslib.php');
include_once($CFG->libdir . '/validateurlsyntax.php');
require_once(dirname(__FILE__) . '/locallib.php');
$engineid = optional_param('engineid', 0, PARAM_INT);
// Check the user is logged in.
require_login();
$context = get_context_instance(CONTEXT_SYSTEM);
require_capability('moodle/question:config', $context);
admin_externalpage_setup('qtypesettingopaque', '', null,
new moodle_url('/question/type/opaque/editengine.php', array('engineid' => $engineid)));
$PAGE->set_title(get_string('editquestionengine', 'qtype_opaque'));
$PAGE->navbar->add(get_string('editquestionengineshort', 'qtype_opaque'));
/**
* Form definition class.
*
* @copyright 2006 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_opaque_engine_edit_form extends moodleform {
public function definition() {
$mform = $this->_form;
$mform->addElement('text', 'enginename', get_string('enginename', 'qtype_opaque'));
$mform->addRule('enginename', get_string('missingenginename', 'qtype_opaque'), 'required', null, 'client');
$mform->setType('enginename', PARAM_MULTILANG);
$mform->addElement('textarea', 'questionengineurls', get_string('questionengineurls', 'qtype_opaque'),
'rows="5" cols="80"');
$mform->addRule('questionengineurls', get_string('missingengineurls', 'qtype_opaque'), 'required', null, 'client');
$mform->setType('questionengineurls', PARAM_RAW);
$mform->addElement('textarea', 'questionbankurls', get_string('questionbankurls', 'qtype_opaque'),
'rows="5" cols="80"');
$mform->setType('questionbankurls', PARAM_RAW);
$mform->addElement('text', 'passkey', get_string('passkey', 'qtype_opaque'));
$mform->setType('passkey', PARAM_MULTILANG);
$mform->addHelpButton('passkey', 'passkey', 'qtype_opaque');
$mform->addElement('hidden', 'engineid');
$mform->setType('engineid', PARAM_INT);
$this->add_action_buttons();
}
/**
* Validate the contents of a textarea field, which should be a newline-separated list of URLs.
*
* @param $data the form data.
* @param $field the field to validate.
* @param $errors any error messages are added to this array.
*/
protected function validateurllist(&$data, $field, &$errors) {
$urls = preg_split('/[\r\n]+/', $data[$field]);
foreach ($urls as $url) {
$url = trim($url);
if ($url && !validateUrlSyntax($url, 's?H?S?u-P-a?I?p?f?q?r?')) {
$errors[$field] = get_string('urlsinvalid', 'qtype_opaque');
}
}
}
/**
* Extract the contents of a textarea field, which should be a newline-separated list of URLs.
*
* @param $data the form data.
* @param $field the field to extract.
* @param @return array those lines from the form field that are valid URLs.
*/
public function extracturllist($data, $field) {
$rawurls = preg_split('/[\r\n]+/', $data->$field);
$urls = array();
foreach ($rawurls as $url) {
$url = clean_param(trim($url), PARAM_URL);
if ($url) {
$urls[] = $url;
}
}
return $urls;
}
public function validation($data) {
$errors = parent::validation($data, $files);
$this->validateurllist($data, 'questionengineurls', $errors);
$this->validateurllist($data, 'questionbankurls', $errors);
return $errors;
}
}
$mform = new qtype_opaque_engine_edit_form('editengine.php');
if ($mform->is_cancelled()){
redirect(new moodle_url('/question/type/opaque/engines.php'));
} else if ($data = $mform->get_data()){
$engine = new stdClass;
if (!empty($data->engineid)) {
$engine->id = $data->engineid;
}
$engine->name = $data->enginename;
$engine->passkey = trim($data->passkey);
$engine->questionengines = $mform->extracturllist($data, 'questionengineurls');
$engine->questionbanks = $mform->extracturllist($data, 'questionbankurls');
save_engine_def($engine);
redirect(new moodle_url('/question/type/opaque/engines.php'));
}
// Prepare defaults.
$defaults = new stdClass;
$defaults->engineid = $engineid;
if ($engineid) {
$engine = load_engine_def($engineid);
$defaults->enginename = $engine->name;
$defaults->questionengineurls = implode("\n", $engine->questionengines);
$defaults->questionbankurls = implode("\n", $engine->questionbanks);
$defaults->passkey = $engine->passkey;
}
$mform->set_data($defaults);
// Display the form.
echo $OUTPUT->header();
echo $OUTPUT->heading_with_help(get_string('editquestionengine', 'qtype_opaque'),
'editquestionengine', 'qtype_opaque');
$mform->display();
echo $OUTPUT->footer();

View File

@ -0,0 +1,92 @@
<?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/>.
/**
* This page lets admins manage the list of known remote Opaque engines.
*
* @package qtype
* @subpackage opaque
* @copyright 2006 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../../../config.php');
require_once($CFG->libdir . '/adminlib.php');
require_once(dirname(__FILE__) . '/locallib.php');
// Check the user is logged in.
require_login();
$context = get_context_instance(CONTEXT_SYSTEM);
require_capability('moodle/question:config', $context);
admin_externalpage_setup('qtypesettingopaque');
// See if any action was requested.
$delete = optional_param('delete', 0, PARAM_INT);
if ($delete) {
$engine = $DB->get_record('question_opaque_engines', array('id' => $delete), '*', MUST_EXIST);
if (optional_param('confirm', false, PARAM_BOOL) && confirm_sesskey()) {
delete_engine_def($delete);
redirect($PAGE->url);
} else {
echo $OUTPUT->header();
echo $OUTPUT->confirm(get_string('deleteconfigareyousure', 'qtype_opaque', format_string($engine->name)),
new moodle_url('/question/type/opaque/engines.php', array('delete' => $delete, 'confirm' => 'yes', 'sesskey' => sesskey())),
$PAGE->url);
echo $OUTPUT->footer();
exit;
}
}
// Get the list of configured engines.
$engines = $DB->get_records('question_opaque_engines', array(), 'id ASC');
// Header.
echo $OUTPUT->header();
echo $OUTPUT->heading_with_help(get_string('configuredquestionengines', 'qtype_opaque'),
'configuredquestionengines', 'qtype_opaque');
// List of configured engines.
if ($engines) {
$strtest = get_string('testconnection', 'qtype_opaque');
$stredit = get_string('edit');
$strdelete = get_string('delete');
foreach ($engines as $engine) {
echo html_writer::tag('p', format_string($engine->name) .
$OUTPUT->action_icon(new moodle_url('/question/type/opaque/testengine.php', array('delete' => $engine->id)),
$OUTPUT->pix_icon('t/preview'), $strtest) .
$OUTPUT->action_icon(new moodle_url('/question/type/opaque/editengine.php', array('delete' => $engine->id)),
$OUTPUT->pix_icon('t/edit'), $stredit) .
$OUTPUT->action_icon(new moodle_url('/question/type/opaque/engines.php', array('delete' => $engine->id)),
$OUTPUT->pix_icon('t/delete'), $strdelete));
}
} else {
echo html_writer::tag('p', get_string('noengines', 'qtype_opaque'));
}
// Add new engine link.
echo html_writer::tag('p', html_writer::link(new moodle_url('/question/type/opaque/editengine.php'),
get_string('addengine', 'qtype_opaque')));
// Footer.
echo $OUTPUT->footer();
?>

View File

@ -0,0 +1,31 @@
<?php
/**
* Serves files from the Opaque resource cache.
*
* @copyright &copy; 2007 The Open University
* @author T.J.Hunt@open.ac.uk
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package package_name
*//** */
require_once(dirname(__FILE__) . '/../../../config.php');
require_once(dirname(__FILE__) . '/locallib.php');
$engineid = required_param('engineid', PARAM_INT);
$remoteid = required_param('remoteid', PARAM_PATH);
$remoteversion = required_param('remoteversion', PARAM_PATH);
$filename = required_param('filename', PARAM_FILE);
// The Open University found it necessary to comment out the whole of the following if statement
// to make things work reliably. However, I think that was only problems with synchronising
// the session between our load-balanced servers, and I think it is better to leave
// this code in. (OU bug 7991.)
global $SESSION;
if ($SESSION->cached_opaque_state->engineid != $engineid ||
$SESSION->cached_opaque_state->remoteid != $remoteid ||
$SESSION->cached_opaque_state->remoteversion != $remoteversion) {
print_error('cannotaccessfile');
}
$resourcecache = new opaque_resource_cache($engineid, $remoteid, $remoteversion);
$resourcecache->serve_file($filename);
?>

View File

@ -0,0 +1,65 @@
<?php
$string['accessoutofsequence'] = 'You have accessed this page out of sequence. Please do not use the Back button when attempting quizzes.';
$string['addengine'] = 'Add another engine';
$string['addingopaque'] = 'Adding an Opaque question';
$string['cannotaccessfile'] = 'You are not allowed to access this file.';
$string['configuredquestionengines'] = 'Configured question engines';
$string['configuredquestionengines_help'] = 'Opaque is a way of connecting other compatible question engines into Moodle. For Moodle to use another question engine, it needs to be set up here. This screen lists all the question engines that have been configured. Lets you edit their configurations, delete configurations, and create new ones.';
$string['couldnotconnect'] = 'Could not connect to the opaque server {$a}.';
$string['couldnotgetengineinfo'] = 'Could not get the remote server information for engine id {$a}.';
$string['couldnotloadenginename'] = 'Could not load the engine name from the database for engine id {$a}.';
$string['couldnotloadengineservers'] = 'Could not load the servers list from the database for engine id {$a}.';
$string['couldnotsaveengineinfo'] = 'Could not save the details of the question engine to the database.';
$string['deleteconfigareyousure'] = 'Are you sure you want to delete the configuration of engine {$a}?';
$string['deletefailed'] = 'Error when trying to delete the engine configuration.';
$string['editingopaque'] = 'Editing an opaque question';
$string['editquestionengine'] = 'Editing Opaque question engine configuration';
$string['editquestionengine_help'] = 'Each remote system you configure must have a name, which will be used to identify it within Moodle. You must specify at least one question engine URL. You can also specify question bank URLs, if your remote question engine uses a separate question bank. When specifying URLs, you may specify several, one per line. Do this when you have several load-balanced servers. Calls to the question engines will be distributed approximately evenly over the different remote servers.';
$string['editquestionengineshort'] = 'Editing engine';
$string['enginedeleted'] = 'Engine configuration deleted.';
$string['enginename'] = 'Engine name';
$string['getmetadatacallfailed'] = 'Failed to retrieve the metadata for this question. Are you sure the remote id and version are correct?';
$string['invalidquestionidsyntax'] = 'This does not match the syntax for a question id';
$string['invalidquestionversionsyntax'] = 'The question version should be of the form major.minor, where major and minor are integers.';
$string['lTRYAGAIN'] = 'Try again';
$string['lGIVEUP'] = 'Pass';
$string['lNEXTQUESTION'] = 'Next';
$string['lENTERANSWER'] = 'Check';
$string['lCLEAR'] = 'Clear';
$string['managequestionengines'] = 'Manage the list of installed question engines.';
$string['maxgradenotreturned'] = 'The question engine was not able to return the maximum grades for this question. Are you sure the remote id and version are correct?';
$string['missingenginename'] = 'Missing engine name';
$string['missingengineurls'] = 'Missing question engine URLs';
$string['missingremoteidinimport'] = 'Missing remote id in import file.';
$string['missingremoteversioninimport'] = 'Missing remote version in import file.';
$string['noengines'] = 'Currenly, there are no configured remote engines.';
$string['notcompleted'] = '[Not completed]';
$string['notcompletedmessage'] = 'You did not complete this question during the attempt. No review is possible.';
$string['onequestionperpage'] = 'For technical reasons, this question cannot be shown here at this time. (Only one one question of this type can be displayed on each screen.) Please review one question at a time by clicking on the question number in the navigation panel.';
$string['opaque'] = 'Opaque';
$string['opaque_help'] = 'Opaque is a way of connecting other compatible question engines into Moodle. This screen lets you create an Opaque question by identifying which remote question engine to connect to, and giving the identity of the question on that remote engine, as explained in the Opaque documentation. Question engines need to be configured on the question engine configuration admin screen.';
$string['opaquesummary'] = 'Use a question provided by another question engine system.';
$string['passkey'] = 'Pass key';
$string['passkey_help'] = 'A pass key is a security measure that some question engines implement. You will only be able to connect to that question engine if you know the pass key. Consult the documentation for the particular type of question engine you are trying to connect to.';
$string['pluginname'] = 'Opaque';
$string['processcallfailed'] = 'Failed to process a response. {$a}';
$string['questionbankurls'] = 'Question bank URLs';
$string['questionengineurls'] = 'Question engine URLs';
$string['questionengine'] = 'Question engine';
$string['questionengine_help'] = 'Select the remote question engine that hosts the question you wish to use.';
$string['questionid'] = 'Question id';
$string['questionid_help'] = 'Opaque questions are identified by both a question id and a question version number. The person who created the question you are trying to refer to will be able to tell you these.';
$string['questionversion'] = 'Question version';
$string['soapfault'] = 'Soap fault: Fault code: {$a->faultcode}. Fault actor: {$a->faultactor}. Fault string: {$a->faultstring}. Fault detail: {$a->faultdetail}.';
$string['startcallfailed'] = 'Failed to start a question session. {$a}';
$string['stopcallfailed'] = 'Failed to close question session. {$a}';
$string['testconnection'] = 'Test connection';
$string['testconnectionfailed'] = 'Connection test failed.';
$string['testconnectionpassed'] = 'Connection test passed.';
$string['testconnectionto'] = 'Test connection to question engine {$a}';
$string['testingengine'] = 'Testing question engine';
$string['unknownengine'] = 'Unknown engine. {$a}';
$string['unrecognisedservertype'] = 'Unrecognised server type read from the database.';
$string['urlsinvalid'] = 'You must enter a list of URLs, one per line.';

View File

@ -0,0 +1,847 @@
<?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/>.
/**
* Library routines used by the Opaque question type.
*
* @package qtype
* @subpackage opaque
* @copyright 2006 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once($CFG->libdir . '/soaplib.php');
require_once($CFG->libdir . '/xmlize.php');
/** User passed on question. Should match the definition in Om.question.Results. */
define('OPAQUE_ATTEMPTS_PASS', 0);
/** User got question wrong after all attempts. Should match the definition in om.question.Results. */
define('OPAQUE_ATTEMPTS_WRONG', -1);
/** User got question partially correct after all attempts. Should match the definition in om.question.Results. */
define('OPAQUE_ATTEMPTS_PARTIALLYCORRECT', -2);
/** If developer hasn't set the value. Should match the definition in om.question.Results. */
define('OPAQUE_ATTEMPTS_UNSET', -99);
/** Prefix used for CSS files. */
define('OPAQUE_CSS_FILENAME_PREFIX', '__styles_');
/**
* @return an array id -> enginename, that can be used to build a dropdown
* menu of installed question types.
*/
function installed_engine_choices() {
global $DB;
return $DB->get_records_menu('question_opaque_engines', array(), 'name ASC', 'id, name');
}
function format_opaque_error($message, $a = NULL) {
return get_string($message, 'qtype_opaque', $a);
}
function format_soap_fault($fault) {
foreach (array('faultcode', 'faultactor', 'faultstring', 'faultdetail') as $field) {
if (empty($fault->$field)) {
$fault->$field = '';
}
}
return get_string('soapfault', 'qtype_opaque', $fault);
}
/**
* Manages loading and saving question engine definitions to and from the database.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_opaque_engine_manager {
/**
* Load the definition of an engine from the database.
* @param integer $engineid the id of the engine to load.
* @return mixed On success, and object with fields id, name, questionengines and questionbanks.
* The last two fields are arrays of URLs. On an error, returns a string to look up in the
* qtype_opaque language file as an error message.
*/
public function load_engine_def($engineid) {
$engine = get_record('question_opaque_engines', 'id', $engineid);
if (!$engine) {
return format_opaque_error('couldnotloadenginename', $engineid);
}
$engine->questionengines = array();
$engine->questionbanks = array();
$servers = get_records('question_opaque_servers', 'engineid', $engineid, 'id ASC');
if (!$servers) {
return format_opaque_error('couldnotloadengineservers', $engineid);
}
foreach ($servers as $server) {
if ($server->type == 'qe') {
$engine->questionengines[] = $server->url;
} else if ($server->type == 'qb') {
$engine->questionbanks[] = $server->url;
} else {
return format_opaque_error('unrecognisedservertype', $engineid);
}
}
return $engine;
}
/**
* Save or update an engine definition in the database, and returm the engine id. The definition
* will be created if $engine->id is not set, and updated if it is.
*
* @param object $engine the definition to save.
* @return integer the id of the saved definition.
*/
public function save_engine_def($engine) {
global $db;
$db->StartTrans();
if (!empty($engine->id)) {
update_record('question_opaque_engines', $engine);
} else {
$engine->id = insert_record('question_opaque_engines', $engine);
}
delete_records('question_opaque_servers', 'engineid', $engine->id);
$this->store_opaque_servers($engine->questionengines, 'qe', $engine->id);
$this->store_opaque_servers($engine->questionbanks, 'qb', $engine->id);
if ($db->CompleteTrans()) {
return $engine->id;
} else {
print_error('couldnotsaveengineinfo', 'qtype_opaque', 'engines.php');
}
}
/**
* Save a list of servers of a given type in the question_opaque_servers table.
*
* @param array $urls an array of URLs.
* @param string $type 'qe' or 'qb'.
* @param int $engineid
*/
protected function store_opaque_servers($urls, $type, $engineid) {
foreach ($urls as $url) {
$server = new stdClass;
$server->engineid = $engineid;
$server->type = $type;
$server->url = $url;
insert_record('question_opaque_servers', $server, false);
}
}
/**
* Delete the definition of an engine from the database.
* @param integer $engineid the id of the engine to delete.
* @return boolean whether the delete succeeded.
*/
public function delete_engine_def($engineid) {
global $DB;
$transaction = $DB->start_delegated_transaction();
$DB->delete_records('question_opaque_servers', array('engineid' => $engineid));
$DB->sdelete_records('question_opaque_engines', array('id' => $engineid));
$transaction->allow_commit();
}
protected function get_possibly_matching_engines($engine) {
global $CFG;
// First we try to get a reasonably accurate guess with SQL - we load the id of all
// engines with the same passkey and which use the first questionengine and questionbank
// (if any).
$where = "WHERE e.passkey = '$engine->passkey'";
$sql = "SELECT e.id,1 FROM {$CFG->prefix}question_opaque_engines e ";
if (!empty($engine->questionengines)) {
$qeurl = reset($engine->questionengines);
$sql .= "JOIN {$CFG->prefix}question_opaque_servers qe ON
qe.engineid = e.id AND qe.type = 'qe'";
$where .= " AND qe.url = '$qeurl'";
}
if (!empty($engine->questionbanks)) {
$qburl = reset($engine->questionbanks);
$sql .= "JOIN {$CFG->prefix}question_opaque_servers qb ON
qb.engineid = e.id AND qb.type = 'qb'";
$where .= " AND qb.url = '$qburl'";
}
$sql .= $where;
return get_records_sql_menu($sql);
}
/**
* If an engine definition like this one (same passkey and server lists) already exists
* in the database, then return its id, otherwise save this one to the database and
* return the new engine id.
*
* @param object $engine the engine to ensure is in the databse.
* @return integer its id.
*/
public function find_or_create_engineid($engine) {
$possibleengineids = $this->get_possibly_matching_engines($engine);
// Then we loop through the possibilities loading the full definition and comparing it.
if ($possibleengineids) {
foreach ($possibleengineids as $engineid => $ignored) {
$testengine = $this->load_engine_def($engineid);
$testengine->passkey = addslashes($testengine->passkey);
if ($this->is_same_engine($engine, $testengine)) {
return $engineid;
}
}
}
return $this->save_engine_def($engine);
}
/**
* Are these two engine definitions essentially the same (same passkey and server lists)?
*
* @param object $engine1 one engine definition.
* @param object $engine2 another engine definition.
* @return boolean whether they are the same.
*/
public function is_same_engine($engine1, $engine2) {
// Same passkey.
$ans = $engine1->passkey == $engine2->passkey &&
// Same question engines.
!array_diff($engine1->questionengines, $engine2->questionengines) &&
!array_diff($engine2->questionengines, $engine1->questionengines) &&
// Same question banks.
!array_diff($engine1->questionbanks, $engine2->questionbanks) &&
!array_diff($engine2->questionbanks, $engine1->questionbanks);
return $ans;
}
}
/**
* Load the definition of an engine from the database.
* @param integer $engineid the id of the engine to load.
* @return mixed On success, and object with fields id, name, questionengines and questionbanks.
* The last two fields are arrays of URLs. On an error, returns a string to look up in the
* qtype_opaque language file as an error message.
*/
function load_engine_def($engineid) {
$manager = new qtype_opaque_engine_manager();
return $manager->load_engine_def($engineid);
}
/**
* Save or update an engine definition in the database, and returm the engine id. The definition
* will be created if $engine->id is not set, and updated if it is.
*
* @param object $engine the definition to save.
* @return integer the id of the saved definition.
*/
function save_engine_def($engine) {
$manager = new qtype_opaque_engine_manager();
return $manager->save_engine_def($engine);
}
/**
* Delete the definition of an engine from the database.
* @param integer $engineid the id of the engine to delete.
* @return boolean whether the delete succeeded.
*/
function delete_engine_def($engineid) {
$manager = new qtype_opaque_engine_manager();
return $manager->delete_engine_def($engineid);
}
/**
* If an engine definition like this one (same passkey and server lists) already exists
* in the database, then return its id, otherwise save this one to the database and
* return the new engine id.
*
* @param object $engine the engine to ensure is in the databse.
* @return integer its id.
*/
function find_or_create_engineid($engine) {
$manager = new qtype_opaque_engine_manager();
return $manager->find_or_create_engineid($engine);
}
/**
* @param mixed $engine either an $engine object, or the URL of a particular question engine server.
* @return a soap connection, either to the specific URL give, or to to one of the question engine servers
* of this $engine object picked at random. returns a string to look up in the qtype_opaque
* language file as an error message if a problem arises.
*/
function connect_to_engine(&$engine) {
if (is_string($engine)) {
$url = $engine;
} else if (!empty($engine->urlused)) {
$url = $engine->urlused;
} else {
$url = $engine->questionengines[array_rand($engine->questionengines)];
}
$connection = soap_connect($url . '?wsdl');
if (is_soap_fault($connection)) {
return format_opaque_error('couldnotconnect', $url);
}
if (!is_string($engine)) {
$engine->urlused = $url;
}
return $connection;
}
/**
* @param mixed $engine either an $engine object, or the URL of a particular question engine server.
* @return some XML, as parsed by xmlize, on success, or a string to look up in the qtype_opaque
* language file as an error message.
*/
function get_engine_info($engine) {
$connection = connect_to_engine($engine);
if (is_string($connection)) {
return $connection;
}
$getengineinforesult = soap_call($connection, 'getEngineInfo', array());
if (is_soap_fault($getengineinforesult)) {
return format_opaque_error('couldnotgetengineinfo', format_soap_fault($getengineinforesult));
}
$xmlarr = xmlize($getengineinforesult);
return $xmlarr;
}
/**
* @param mixed $engine either an $engine object, or the URL of a particular question engine server.
* @return The question metadata, as an xmlised array, so, for example,
* $metadata[questionmetadata][@][#][scoring][0][#][marks][0][#] is the maximum possible score for
* this question.
*/
function get_question_metadata(&$engine, $remoteid, $remoteversion) {
$connection = connect_to_engine($engine);
$questionbaseurl = $engine->questionbanks[array_rand($engine->questionbanks)];
$getmetadataresult = soap_call($connection, 'getQuestionMetadata',
array($remoteid, $remoteversion, $questionbaseurl));
if (is_soap_fault($getmetadataresult)) {
return format_opaque_error('getmetadatacallfailed', format_soap_fault($getmetadataresult));
}
return xmlize($getmetadataresult);
}
/**
* @param object $engine the engine to connect to.
* @param string $remoteid
* @param string $remoteversion
* @param int $randomseed
* @return mixed the result of the soap call on success, or a string error message on failure.
*/
function start_question_session(&$engine, $remoteid, $remoteversion, $data, $cached_resources) {
$connection = connect_to_engine($engine);
if (is_string($connection)) {
return format_opaque_error('startcallfailed', $connection);
}
$questionbaseurl = '';
if (!empty($engine->questionbanks)) {
$questionbaseurl = $engine->questionbanks[array_rand($engine->questionbanks)];
}
$startresult = soap_call($connection, 'start', array(
$remoteid,
$remoteversion,
$questionbaseurl,
array('randomseed', 'userid', 'language', 'passKey', 'preferredbehaviour'),
array($data['-_randomseed'], $data['-_userid'], $data['-_language'],
generate_passkey($engine->passkey, $data['-_userid']), $data['-_preferredbehaviour']),
$cached_resources
));
if (is_soap_fault($startresult)) {
return format_opaque_error('startcallfailed', format_soap_fault($startresult));
}
return $startresult;
}
function opaque_process(&$engine, $questionsessionid, $response) {
$connection = connect_to_engine($engine);
if (is_string($connection)) {
return format_opaque_error('processcallfailed', $connection);
}
$processresult = soap_call($connection, 'process', array(
$questionsessionid,
array_keys($response),
array_values($response)
));
if (is_soap_fault($processresult)) {
return format_opaque_error('processcallfailed', format_soap_fault($processresult));
}
return $processresult;
}
/**
* @param string $questionsessionid the question session to stop.
* @return true on success, or a string error message on failure.
*/
function stop_question_session($engine, $questionsessionid) {
$connection = connect_to_engine($engine);
if (is_string($connection)) {
return format_opaque_error('stopcallfailed', $connection);
}
$stopresult = soap_call($connection, 'stop', array($questionsessionid));
if (is_soap_fault($stopresult)) {
return format_opaque_error('stopcallfailed', format_soap_fault($stopresult));
} else {
return true;
}
}
/**
* Get a step from $qa, as if $pendingstep had already been added at the end
* of the list, if it is not null.
* @param integer $seq
* @param question_attempt $qa
* @param question_attempt_step|null $pendingstep
* @return question_attempt_step
*/
function opaque_get_step($seq, question_attempt $qa, $pendingstep) {
if ($seq < $qa->get_num_steps()) {
return $qa->get_step($seq);
}
if ($seq == $qa->get_num_steps() && !is_null($pendingstep)) {
return $pendingstep;
}
throw new Exception('Sequence number ' . $seq . ' out of range.');
}
/**
* Update the $SESSION->cached_opaque_state to show the current status of $question for state
* $state.
* @param object $question the question
* @param object $state
* @return mixed $SESSION->cached_opaque_state on success, a string error message on failure.
*/
function update_opaque_state(question_attempt $qa, question_attempt_step $pendingstep = null) {
global $SESSION;
$question = $qa->get_question();
$targetseq = $qa->get_num_steps() - 1;
if (!is_null($pendingstep)) {
$targetseq += 1;
}
if (empty($SESSION->cached_opaque_state) ||
empty($SESSION->cached_opaque_state->qaid) ||
empty($SESSION->cached_opaque_state->sequencenumber)) {
$cachestatus = 'empty';
} else if ($SESSION->cached_opaque_state->qaid != $qa->get_database_id() ||
$SESSION->cached_opaque_state->sequencenumber > $targetseq) {
if (!empty($SESSION->cached_opaque_state->questionsessionid)) {
$error = stop_question_session($SESSION->cached_opaque_state->engine,
$SESSION->cached_opaque_state->questionsessionid);
if (is_string($error)) {
unset($SESSION->cached_opaque_state);
return $error;
}
}
unset($SESSION->cached_opaque_state);
$cachestatus = 'empty';
} else if ($SESSION->cached_opaque_state->sequencenumber < $targetseq) {
$cachestatus = 'catchup';
} else {
$cachestatus = 'good';
}
$resourcecache = new opaque_resource_cache($question->engineid,
$question->remoteid, $question->remoteversion);
if ($cachestatus == 'empty') {
$SESSION->cached_opaque_state = new stdClass;
$opaquestate = $SESSION->cached_opaque_state;
$opaquestate->qaid = $qa->get_database_id();
$opaquestate->remoteid = $question->remoteid;
$opaquestate->remoteversion = $question->remoteversion;
$opaquestate->engineid = $question->engineid;
$opaquestate->nameprefix = $qa->get_field_prefix();
$opaquestate->questionended = false;
$opaquestate->sequencenumber = -1;
$opaquestate->resultssequencenumber = -1;
$engine = load_engine_def($question->engineid);
if (is_string($engine)) {
unset($SESSION->cached_opaque_state);
return $engine;
}
$opaquestate->engine = $engine;
$step = opaque_get_step(0, $qa, $pendingstep);
$startreturn = start_question_session($engine, $question->remoteid,
$question->remoteversion, $step->get_all_data(), $resourcecache->list_cached_resources());
if (is_string($startreturn)) {
unset($SESSION->cached_opaque_state);
return $startreturn;
}
extract_stuff_from_response($opaquestate, $startreturn, $resourcecache);
$opaquestate->sequencenumber++;
$cachestatus = 'catchup';
} else {
$opaquestate = $SESSION->cached_opaque_state;
}
if ($cachestatus == 'catchup') {
if ($opaquestate->sequencenumber >= $targetseq) {
$error = stop_question_session($opaquestate->engine,
$opaquestate->questionsessionid);
}
while ($opaquestate->sequencenumber < $targetseq) {
$step = opaque_get_step($opaquestate->sequencenumber + 1, $qa, $pendingstep);
$processreturn = opaque_process($opaquestate->engine, $opaquestate->questionsessionid, $step->get_submitted_data());
if (is_string($processreturn)) {
unset($SESSION->cached_opaque_state);
return $processreturn;
}
if (!empty($processreturn->results)) {
$opaquestate->resultssequencenumber = $opaquestate->sequencenumber + 1;
$opaquestate->results = $processreturn->results;
}
if ($processreturn->questionEnd) {
$opaquestate->questionended = true;
$opaquestate->sequencenumber = $targetseq;
$opaquestate->xhtml = strip_omact_buttons($opaquestate->xhtml);
unset($opaquestate->questionsessionid);
break;
}
extract_stuff_from_response($opaquestate, $processreturn, $resourcecache);
$opaquestate->sequencenumber++;
}
$cachestatus = 'good';
}
return $opaquestate;
}
/**
* File name used to store the CSS of the question, question session id is appended.
*/
function opaque_stylesheet_filename($questionsessionid) {
return OPAQUE_CSS_FILENAME_PREFIX . $questionsessionid . '.css';
}
/**
* Pulls out the fields common to StartResponse and ProcessResponse.
* @param object $opaquestate should be $SESSION->cached_opaque_state, or equivalent.
* @param object $response a StartResponse or ProcessResponse.
* @param object $resourcecache the resource cache for this question.
* @return true on success, or a string error message on failure.
*/
function extract_stuff_from_response(&$opaquestate, $response, &$resourcecache) {
global $CFG;
static $replaces;
if (empty($replaces)) {
$replaces = array(
'%%RESOURCES%%' => '', // Filled in below.
'%%IDPREFIX%%' => '', // Filled in below.
'%%%%' => '%%'
);
$strings = array('lTRYAGAIN', 'lGIVEUP', 'lNEXTQUESTION', 'lENTERANSWER', 'lCLEAR');
foreach ($strings as $string) {
$replaces["%%$string%%"] = get_string($string, 'qtype_opaque');
}
}
// Process the XHTML, replacing the strings that need to be replaced.
$xhtml = $response->XHTML;
$replaces['%%RESOURCES%%'] = $resourcecache->file_url('');
$replaces['%%IDPREFIX%%'] = $opaquestate->nameprefix;
$xhtml = str_replace(array_keys($replaces), $replaces, $xhtml);
// TODO this is a nasty hack. Flash uses & as a separator in the FlashVars string,
// so we have to replce the &amp;s with %26s in this one place only. So for now
// do it with a regexp. Longer term, it might be better to changes the file.php urls
// so they don't contain &s.
$xhtml = preg_replace_callback(
'/name="FlashVars" value="TheSound=[^"]+"/',
create_function('$matches', 'return str_replace("&amp;", "%26", $matches[0]);'),
$xhtml);
// Another hack to take out the next button that most OM questions include, but which does not work in Moodle.
// Actually, we remove any non-disabled buttons, and the following script tag.
// TODO think of a better way to do this.
if ($opaquestate->resultssequencenumber >= 0) {
$xhtml = strip_omact_buttons($xhtml);
}
$opaquestate->xhtml = $xhtml;
// Process the CSS (only when we have a StartResponse).
if (!empty($response->CSS)) {
$opaquestate->cssfilename = opaque_stylesheet_filename($response->questionSession);
$resourcecache->cache_file($opaquestate->cssfilename, 'text/css;charset=UTF-8', $response->CSS);
}
// Process the resources.
$resourcecache->cache_resources($response->resources);
// Process the other bits.
$opaquestate->progressinfo = $response->progressInfo;
if (!empty($response->questionSession)) {
$opaquestate->questionsessionid = $response->questionSession;
}
if(!empty($response->head)) {
$opaquestate->headXHTML = $response->head;
}
return true;
}
/**
* Strip any buttons, followed by script tags, where the button has an id containing _omact_, and is not disabled.
*/
function strip_omact_buttons($xhtml) {
return preg_replace('|<input(?:(?!disabled=)[^>])*? id="[^"]*_omact_[^"]*"(?:(?!disabled=)[^>])*?><script type="text/javascript">[^<]*</script>|', '', $xhtml);
}
/**
* @param string $secret the secret string for this question engine.
* @param int $userid the id of the user attempting this question.
* @return string the passkey that needs to be sent to the quetion engine to show that
* we are allowed to start a question session for this user.
*/
function generate_passkey($secret, $userid) {
return md5($secret . $userid);
}
/**
* OpenMark relies on certain browser-specific class names to be present in the
* HTML outside the question, in order to apply certian browser-specific layout
* work-arounds. This function re-implements Om's browser sniffing rules. See
* https://openmark.dev.java.net/source/browse/openmark/trunk/src/util/misc/UserAgent.java?view=markup
* @return string class to add to the HTML.
*/
function opaque_browser_type() {
$useragent = $_SERVER['HTTP_USER_AGENT'];
// Filter troublemakers
if (strpos($useragent, 'KHTML') !== false) {
return "khtml";
}
if (strpos($useragent, 'Opera') !== false) {
return "opera";
}
// Check version of our two supported browsers
$matches = array();
if (preg_match('/"^.*rv:(\d+)\\.(\d+)\D.*$"/', $useragent, $matches)) {
return 'gecko-' . $matches[1] . '-' . $matches[2];
}
if (preg_match('/^.*MSIE (\d+)\\.(\d+)\D.*Windows.*$/', $useragent, $matches)) {
return 'winie-' . $matches[1]; // Major verison only
}
return '';
}
/**
* This class caches the resources belonging a particular question.
*
* There are synchronisation issues if two students are doing the same question at the same time.
*/
class opaque_resource_cache {
var $folder; // Path to the folder where resources for this question are cached.
var $metadatafolder; // Path to the folder where mime types are stored.
var $baseurl; // initial part of the URL to link to a file in the cache.
/**
* Create a new opaque_resource_cache for a particular remote question.
* @param integer $engineid the id of the question engine.
* @param string $remoteid remote question id, as per Opaque spec.
* @param string $remoteversion remote question version, as per Opaque spec.
*/
function opaque_resource_cache($engineid, $remoteid, $remoteversion) {
global $CFG;
$folderstart = $CFG->dataroot . '/opaqueresources/' . $engineid . '/' . $remoteid . '/' . $remoteversion;
$this->folder = $folderstart . '/files';
if (!is_dir($this->folder)) {
$this->mkdir_recursive($this->folder);
}
$this->metadatafolder = $folderstart . '/meta';
if (!is_dir($this->metadatafolder)) {
$this->mkdir_recursive($this->metadatafolder);
}
$this->baseurl = $CFG->wwwroot . '/question/type/opaque/file.php?engineid=' .
$engineid . '&amp;remoteid=' . $remoteid .
'&amp;remoteversion=' . $remoteversion . '&amp;filename=';
}
/**
* @param string $filename the file name.
* @return the full path of a file with the given name.
*/
function file_path($filename) {
return $this->folder . '/' . $filename;
}
/**
* @param string $filename the file name.
* @return the full path of a file with the given name.
*/
function file_meta_path($filename) {
return $this->metadatafolder . '/' . $filename;
}
/**
* @param string $filename the file name.
* @return the URL to access this file.
*/
function file_url($filename) {
return $this->baseurl . $filename;
}
/**
* @param string $filename the file name.
* @return the URL to access this file.
*/
function file_mime_type($filename) {
$metapath = $this->file_meta_path($filename);
if (file_exists($metapath)) {
return file_get_contents($metapath);
}
return mimeinfo('type', $filename);
}
/**
* @param string $filename the name of the file to look for.
* @return true if this named file is in the cache, otherwise false.
*/
function file_in_cache($filename) {
return file_exists($this->file_path($filename));
}
/**
* Serve a file from the cache.
* @param string $filename the file name.
*/
function serve_file($filename) {
if (!$this->file_in_cache($filename)) {
header('HTTP/1.0 404 Not Found');
header('Content-Type: text/plain;charset=UTF-8');
echo 'File not found';
exit;
}
$mimetype = $this->file_mime_type($filename);
// Handle If-Modified-Since
$file = $this->file_path($filename);
$filedate = filemtime($file);
$ifmodifiedsince = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : false;
if($ifmodifiedsince && strtotime($ifmodifiedsince)>=$filedate) {
header('HTTP/1.0 304 Not Modified');
exit;
}
header('Last-Modified: '.gmdate('D, d M Y H:i:s',$filedate).' GMT');
// // Send expire headers
// $lifetime=4*60*60;
// header('Cache-Control: max-age='.$lifetime);
// header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
// Type
header('Content-Type: ' . $mimetype);
header('Content-Length: ' . filesize($file));
// Output file
$handle = fopen($file,'r');
session_write_close(); // unlock session during fileserving
fpassthru($handle);
fclose($handle);
}
/**
* Store a file in the cache.
*
* @param string $filename the name of the file to cache.
* @param string $mimetype the type of the file to cache.
* @param string $content the contents to write to the file.
*/
function cache_file($filename, $mimetype, $content) {
file_put_contents($this->file_path($filename), $content);
file_put_contents($this->file_meta_path($filename), $mimetype);
}
/**
* Add the resources from a particular response to the cache.
* @param array $resources as returned from start or process Opaque methods.
*/
function cache_resources($resources) {
if(!empty($resources)) {
foreach ($resources as $resource) {
$mimetype = $resource->mimeType;
if (strpos($resource->mimeType, 'text/') === 0 && !empty($resource->encoding)) {
$mimetype .= ';charset=' . $resource->encoding;
}
$this->cache_file($resource->filename, $mimetype, $resource->content);
}
}
}
/**
* List the resources cached for this question.
* @return array list of resource names.
*/
function list_cached_resources() {
$filepaths = glob($this->folder . '/*');
if (!is_array($filepaths)) {
// If an error occurrs, say that we have no files cached.
$filepaths = array();
}
$pathlen = strlen($this->folder . '/');
$files = array();
foreach ($filepaths as &$filepath) {
$file = substr($filepath, $pathlen);
if (strpos($file, OPAQUE_CSS_FILENAME_PREFIX) !== 0) {
$files[] = $file;
}
}
return $files;
}
/**
* This function exists because mkdir(folder,mode,TRUE) doesn't work on our server.
* Safe to call even if folder already exists (checks)
* @param string $folder Folder to create
* @param int $mode Mode for creation (default 0755)
* @return boolean True if folder (now) exists, false if there was a failure
*/
function mkdir_recursive($folder, $mode='') {
if(is_dir($folder)) {
return true;
}
if ($mode == '') {
global $CFG;
$mode = $CFG->directorypermissions;
}
if(!$this->mkdir_recursive(dirname($folder),$mode)) {
return false;
}
return mkdir($folder,$mode);
}
}
?>

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

View File

@ -0,0 +1,56 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Opaque question definition class.
*
* @package qtype
* @subpackage opaque
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Represents an Opaque question.
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_opaque_question extends question_definition {
/** @var integer the ID of the question engine that serves this question. */
public $engineid;
/** @var string the id by which the question engine knows this question. */
public $remoteid;
/** @var string the version number of this question to use. */
public $remoteversion;
public function make_behaviour(question_attempt $qa, $preferredbehaviour) {
question_engine::load_behaviour_class('opaque');
return new qbehaviour_opaque($qa, $preferredbehaviour);
}
public function get_expected_data() {
return question_attempt::USE_RAW_DATA;
}
public function get_correct_response() {
// Not possible to say, so just return nothing.
return array();
}
}

View File

@ -0,0 +1,135 @@
<?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/>.
/**
* The questiontype class for the Opaque question type.
*
* @package qtype
* @subpackage opaque
* @copyright &copy; 2006 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/locallib.php');
/**
* The Opaque question type.
*
* @copyright &copy; 2006 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_opaque extends question_type {
/** @var qtype_opaque_engine_manager */
protected $enginemanager;
public function __construct() {
parent::__construct();
$this->enginemanager = new qtype_opaque_engine_manager();
}
/**
* Set the engine manager to used. You should not need to call this except
* when testing.
* @param qtype_opaque_engine_manager $manager
*/
public function set_engine_manager(qtype_opaque_engine_manager $manager) {
$this->enginemanager = $manager;
}
public function can_analyse_responses() {
return false;
}
function extra_question_fields() {
return array('question_opaque', 'engineid', 'remoteid', 'remoteversion');
}
function save_question($question, $form, $course) {
$form->questiontext = '';
$form->questiontextformat = FORMAT_MOODLE;
$form->unlimited = 0;
$form->penalty = 0;
return parent::save_question($question, $form, $course);
}
protected function initialise_question_instance(question_definition $question, $questiondata) {
parent::initialise_question_instance($question, $questiondata);
$question->engineid = $questiondata->options->engineid;
$question->remoteid = $questiondata->options->remoteid;
$question->remoteversion = $questiondata->options->remoteversion;
}
public function get_random_guess_score($questiondata) {
return null;
}
function export_to_xml($question, $format, $extra=null) {
$expout = '';
$expout .= ' <remoteid>' . $question->options->remoteid . "</remoteid>\n";
$expout .= ' <remoteversion>' . $question->options->remoteversion . "</remoteversion>\n";
$expout .= " <engine>\n";
$engine = $this->enginemanager->load_engine_def($question->options->engineid);
$expout .= " <name>\n" . $format->writetext($engine->name, 4) . " </name>\n";
$expout .= " <passkey>\n" . $format->writetext($engine->passkey, 4) . " </passkey>\n";
foreach ($engine->questionengines as $qe) {
$expout .= " <qe>\n" . $format->writetext($qe, 4) . " </qe>\n";
}
foreach ($engine->questionbanks as $qb) {
$expout .= " <qb>\n" . $format->writetext($qb, 4) . " </qb>\n";
}
$expout .= " </engine>\n";
return $expout;
}
function import_from_xml($data, $question, $format, $extra = null) {
if (!isset($data['@']['type']) || $data['@']['type'] != 'opaque') {
return false;
}
$question = $format->import_headers($data);
$question->qtype = 'opaque';
$question->remoteid = $format->getpath($data, array('#', 'remoteid', 0, '#'),
'', false, get_string('missingremoteidinimport', 'qtype_opaque'));
$question->remoteversion = $format->getpath($data, array('#', 'remoteversion', 0, '#'),
'', false, get_string('missingremoteversioninimport', 'qtype_opaque'));
// Engine bit.
$strerror = get_string('missingenginedetailsinimport', 'qtype_opaque');
if (!isset($data['#']['engine'][0])) {
$format->error($strerror);
}
$enginedata = $data['#']['engine'][0];
$engine = new stdClass();
$engine->name = $format->import_text($enginedata['#']['name'][0]['#']['text']);
$engine->passkey = $format->import_text($enginedata['#']['passkey'][0]['#']['text']);
$engine->questionengines = array();
$engine->questionbanks = array();
if (isset($enginedata['#']['qe'])) {
foreach ($enginedata['#']['qe'] as $qedata) {
$engine->questionengines[] = $format->import_text($qedata['#']['text']);
}
}
if (isset($enginedata['#']['qb'])) {
foreach ($enginedata['#']['qb'] as $qbdata) {
$engine->questionbanks[] = $format->import_text($qbdata['#']['text']);
}
}
$question->engineid = $this->enginemanager->find_or_create_engineid($engine);
return $question;
}
}

View File

@ -0,0 +1,38 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Opaque question renderer class.
*
* @package qtype
* @subpackage opaque
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Generates the output for Opaque questions.
*
* // TODO if we delete this class, does it cause any errors?
*
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_opaque_renderer extends qtype_renderer {
}

View File

@ -0,0 +1,6 @@
<?php
$settings = new admin_externalpage('qtypesettingopaque',
get_string('pluginname', 'qtype_opaque'),
new moodle_url('/question/type/opaque/engines.php'),
'moodle/question:config');

View File

@ -0,0 +1,60 @@
<?php
/**
* Unit tests for (some of) mod/quiz/accessrules.php.
*
* @copyright &copy; 2008 The Open University
* @author T.J.Hunt@open.ac.uk
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package quiz
*/
if (!defined('MOODLE_INTERNAL')) {
die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page.
}
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
class opaque_locallib_test extends UnitTestCase {
function test_is_same_engine() {
$manager = new qtype_opaque_engine_manager();
$engine1 = new stdClass;
$engine1->name = 'OpenMark live servers';
$engine1->passkey = '';
$engine1->questionengines = array(
'http://ltsweb1.open.ac.uk/om-qe/services/Om',
'http://ltsweb2.open.ac.uk/om-qe/services/Om');
$engine1->questionbanks = array(
'https://ltsweb1.open.ac.uk/openmark/!question',
'https://ltsweb2.open.ac.uk/openmark/!question');
$engine2 = new stdClass;
$engine2->name = 'OpenMark live servers';
$engine2->passkey = '';
$engine2->questionengines = array(
'http://ltsweb1.open.ac.uk/om-qe/services/Om',
'http://ltsweb2.open.ac.uk/om-qe/services/Om');
$engine2->questionbanks = array(
'https://ltsweb1.open.ac.uk/openmark/!question',
'https://ltsweb2.open.ac.uk/openmark/!question');
$this->assertTrue($manager->is_same_engine($engine1, $engine2));
$engine2->questionbanks = array(
'https://ltsweb2.open.ac.uk/openmark/!question',
'https://ltsweb1.open.ac.uk/openmark/!question');
$this->assertTrue($manager->is_same_engine($engine1, $engine2));
$engine2->name = 'Frog';
$this->assertTrue($manager->is_same_engine($engine1, $engine2));
$engine2->passkey = 'newt';
$this->assertFalse($manager->is_same_engine($engine1, $engine2));
$engine2->passkey = '';
$engine2->questionengines = array(
'http://ltsweb2.open.ac.uk/om-qe/services/Om');
$this->assertFalse($manager->is_same_engine($engine1, $engine2));
}
}
?>

View File

@ -0,0 +1,303 @@
<?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 opaque question type class.
*
* @package qtype_opaque
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->dirroot . '/question/type/opaque/questiontype.php');
require_once($CFG->dirroot . '/question/format/xml/format.php');
class qtype_opaque_engine_manager_mock extends qtype_opaque_engine_manager {
protected $knownengines = array();
public function add_test_engine($id, $engine) {
$this->knownengines[$id] = $engine;
}
public function load_engine_def($engineid) {
if (isset($this->knownengines[$engineid])) {
return $this->knownengines[$engineid];
} else {
return format_opaque_error('unrecognisedservertype', $engineid);
}
}
public function save_engine_def($engine) {
$this->knownengines[] = $engine;
return end(array_keys($this->knownengines));
}
protected function store_opaque_servers($urls, $type, $engineid) {
// Should not be used, but override to avoid accidental DB writes.
}
public function delete_engine_def($engineid) {
unset($this->knownengines[$engineid]);
return true;
}
protected function get_possibly_matching_engines($engine) {
return $this->knownengines;
}
}
/**
* Unit tests for the opaque question type class.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_opaque_test extends UnitTestCase {
var $qtype;
public function setUp() {
$this->qtype = new qtype_opaque();
}
public function tearDown() {
$this->qtype = null;
}
public function assert_same_xml($expectedxml, $xml) {
$this->assertEqual(str_replace("\r\n", "\n", $expectedxml),
str_replace("\r\n", "\n", $xml));
}
public function test_name() {
$this->assertEqual($this->qtype->name(), 'opaque');
}
public function test_can_analyse_responses() {
$this->assertFalse($this->qtype->can_analyse_responses());
}
public function test_get_random_guess_score() {
$this->assertNull($this->qtype->get_random_guess_score(null));
}
public function test_get_possible_responses() {
$this->assertEqual(array(), $this->qtype->get_possible_responses(null));
}
public function test_xml_import_known_engine() {
// This relies on the fact that the question_bank only creates one
// copy of each question type class.
$manager = new qtype_opaque_engine_manager_mock();
question_bank::get_qtype('opaque')->set_engine_manager($manager);
$engine = new stdClass;
$engine->name = 'A question engine';
$engine->questionengines = array('http://example.com/');
$engine->questionbanks = array();
$engine->passkey = 'secret';
$manager->add_test_engine(123, $engine);
$xml = ' <question type="opaque">
<name>
<text>An Opaque question</text>
</name>
<questiontext format="moodle_auto_format">
<text></text>
</questiontext>
<generalfeedback>
<text></text>
</generalfeedback>
<defaultgrade>3</defaultgrade>
<penalty>0</penalty>
<hidden>0</hidden>
<remoteid>example.question</remoteid>
<remoteversion>1.0</remoteversion>
<engine>
<name>
<text>Question engine</text>
</name>
<passkey>
<text>secret</text>
</passkey>
<qe>
<text>http://example.com/</text>
</qe>
</engine>
</question>';
$xmldata = xmlize($xml);
$importer = new qformat_xml();
$q = $importer->try_importing_using_qtypes(
$xmldata['question'], null, null, 'opaque');
$expectedq = new stdClass;
$expectedq->qtype = 'opaque';
$expectedq->name = 'An Opaque question';
$expectedq->questiontext = '';
$expectedq->questiontextformat = FORMAT_MOODLE;
$expectedq->generalfeedback = '';
$expectedq->defaultmark = 3;
$expectedq->length = 1;
$expectedq->penalty = 0;
$expectedq->remoteid = 'example.question';
$expectedq->remoteversion = '1.0';
$expectedq->engineid = 123;
$this->assert(new CheckSpecifiedFieldsExpectation($expectedq), $q);
}
public function test_xml_import_unknown_engine() {
// This relies on the fact that the question_bank only creates one
// copy of each question type class.
$manager = new qtype_opaque_engine_manager_mock();
question_bank::get_qtype('opaque')->set_engine_manager($manager);
$engine = new stdClass;
$engine->name = 'A question engine';
$engine->questionengines = array('http://example.com/qe2', 'http://example.com/qe1');
$engine->questionbanks = array('http://example.com/qb');
$engine->passkey = 'secret';
$xml = ' <question type="opaque">
<name>
<text>An Opaque question</text>
</name>
<questiontext format="moodle_auto_format">
<text></text>
</questiontext>
<generalfeedback>
<text></text>
</generalfeedback>
<defaultgrade>3</defaultgrade>
<penalty>0</penalty>
<hidden>0</hidden>
<remoteid>example.question</remoteid>
<remoteversion>1.0</remoteversion>
<engine>
<name>
<text>Question engine</text>
</name>
<passkey>
<text>secret</text>
</passkey>
<qe>
<text>http://example.com/qe1</text>
</qe>
<qe>
<text>http://example.com/qe2</text>
</qe>
<qb>
<text>http://example.com/qb</text>
</qb>
</engine>
</question>';
$xmldata = xmlize($xml);
$importer = new qformat_xml();
$q = $importer->try_importing_using_qtypes(
$xmldata['question'], null, null, 'opaque');
$expectedq = new stdClass;
$expectedq->qtype = 'opaque';
$expectedq->name = 'An Opaque question';
$expectedq->questiontext = '';
$expectedq->questiontextformat = FORMAT_MOODLE;
$expectedq->generalfeedback = '';
$expectedq->defaultmark = 3;
$expectedq->length = 1;
$expectedq->penalty = 0;
$expectedq->remoteid = 'example.question';
$expectedq->remoteversion = '1.0';
$expectedq->engineid = 0;
$this->assert(new CheckSpecifiedFieldsExpectation($expectedq), $q);
$this->assertTrue($manager->is_same_engine(
$engine, $manager->load_engine_def($q->engineid)));
}
public function test_xml_export() {
// This relies on the fact that the question_bank only creates one
// copy of each question type class.
$manager = new qtype_opaque_engine_manager_mock();
question_bank::get_qtype('opaque')->set_engine_manager($manager);
$engine = new stdClass;
$engine->name = 'A question engine';
$engine->questionengines = array('http://example.com/');
$engine->questionbanks = array();
$engine->passkey = 'secret';
$manager->add_test_engine(123, $engine);
$qdata = new stdClass;
$qdata->id = 321;
$qdata->qtype = 'opaque';
$qdata->name = 'An Opaque question';
$qdata->questiontext = '';
$qdata->questiontextformat = FORMAT_MOODLE;
$qdata->generalfeedback = '';
$qdata->defaultmark = 3;
$qdata->length = 1;
$qdata->penalty = 0;
$qdata->hidden = 0;
$qdata->options->remoteid = 'example.question';
$qdata->options->remoteversion = '1.0';
$qdata->options->engineid = 123;
$exporter = new qformat_xml();
$xml = $exporter->writequestion($qdata);
$expectedxml = '<!-- question: 321 -->
<question type="opaque">
<name>
<text>An Opaque question</text>
</name>
<questiontext format="moodle_auto_format">
<text></text>
</questiontext>
<generalfeedback>
<text></text>
</generalfeedback>
<defaultgrade>3</defaultgrade>
<penalty>0</penalty>
<hidden>0</hidden>
<remoteid>example.question</remoteid>
<remoteversion>1.0</remoteversion>
<engine>
<name>
<text>A question engine</text>
</name>
<passkey>
<text>secret</text>
</passkey>
<qe>
<text>http://example.com/</text>
</qe>
</engine>
</question>
';
$this->assert_same_xml($expectedxml, $xml);
}
}

View File

@ -0,0 +1,14 @@
.que.opaque .formulation {
padding: 0;
background: transparent;
}
.que .im-controls {
margin: 0;
}
.que.opaque .question_aborted {
padding: 7em 1em;
text-align: center;
background: #eeeeee;
}

View File

@ -0,0 +1,70 @@
<?php // $Id$
/**
* Page for configuring the list Opaque question engines we can connect to.
*
* @copyright &copy; 2006 The Open University
* @author T.J.Hunt@open.ac.uk
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package opaquequestiontype
*//** */
require_once(dirname(__FILE__) . '/../../../config.php');
require_once($CFG->libdir . '/formslib.php');
include_once($CFG->libdir . '/validateurlsyntax.php');
require_once(dirname(__FILE__) . '/locallib.php');
// Check the user is logged in.
require_login();
if (!has_capability('moodle/question:config', get_context_instance(CONTEXT_SYSTEM, SITEID))) {
print_error('restricteduser');
}
// Load the engine definition.
$engineid = required_param('engineid', PARAM_INT);
$engine = load_engine_def($engineid);
if (is_string($engine)) {
print_error('unknownengine', 'qtype_opaque', 'engines.php', $engine);
}
// Do the test.
$ok = true;
$strtitle = get_string('testingengine', 'qtype_opaque');
$navlinks[] = array('name' => get_string('configuredquestionengines', 'qtype_opaque'), 'link' => "$CFG->wwwroot/question/type/opaque/engines.php", 'type' => 'misc');
$navlinks[] = array('name' => $strtitle, 'link' => '', 'type' => 'title');
print_header_simple($strtitle, '', build_navigation($navlinks));
print_heading($strtitle);
foreach ($engine->questionengines as $engineurl) {
print_box_start();
print_heading(get_string('testconnectionto', 'qtype_opaque', $engineurl), '', 3);
$info = get_engine_info($engineurl);
if (is_array($info) && isset($info['engineinfo']['#'])) {
xml_to_dl($info['engineinfo']['#']);
} else {
notify($info);
$ok = false;
}
print_box_end();
}
if ($ok) {
notify(get_string('testconnectionpassed', 'qtype_opaque'), 'notifysuccess');
} else {
notify(get_string('testconnectionfailed', 'qtype_opaque'));
}
print_continue('engines.php');
print_footer();
/**
* @param output some XML as a <dl>.
*/
function xml_to_dl($xml) {
echo '<dl>';
foreach ($xml as $element => $content) {
echo "<dt>$element</dt><dd>" . $content['0']['#'] . "</dd>\n";
}
echo '</dl>';
}
?>

View File

@ -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/>.
/**
* Version information for the Opaque question type.
*
* @package qtype
* @subpackage opaque
* @copyright 2006 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$plugin->version = 2011011400;
$plugin->requires = 2011011200;