Commit afa2a7f4 authored by Plugins bot's avatar Plugins bot
Browse files

PLUGIN-26928 qbehaviour_opaque: cibot precheck request

parent ceb41588
name: Moodle plugin CI
on: [push, pull_request]
jobs:
test:
runs-on: 'ubuntu-latest'
strategy:
fail-fast: false
matrix:
include:
- php: '7.4'
moodle-branch: 'master'
database: 'pgsql'
- php: '7.4'
moodle-branch: 'MOODLE_311_STABLE'
database: 'mariadb'
services:
postgres:
image: postgres
env:
POSTGRES_USER: 'postgres'
POSTGRES_HOST_AUTH_METHOD: 'trust'
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 3
ports:
- 5432:5432
mariadb:
image: mariadb
env:
MYSQL_USER: 'root'
MYSQL_ALLOW_EMPTY_PASSWORD: "true"
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 3
steps:
- name: Checkout
uses: actions/checkout@v2
with:
path: plugin
- name: Install node
uses: actions/setup-node@v1
with:
node-version: '14.15.0'
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mbstring, pgsql, mysqli
- name: Deploy moodle-plugin-ci
run: |
composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^3
# Add dirs to $PATH
echo $(cd ci/bin; pwd) >> $GITHUB_PATH
echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH
# PHPUnit depends on en_AU.UTF-8 locale
sudo locale-gen en_AU.UTF-8
- name: Install Moodle
run: |
moodle-plugin-ci add-plugin --branch main moodleou/moodle-qtype_opaque
moodle-plugin-ci install --plugin ./plugin --db-host=127.0.0.1
env:
DB: ${{ matrix.database }}
MOODLE_BRANCH: ${{ matrix.moodle-branch }}
- name: phplint
if: ${{ always() }}
run: moodle-plugin-ci phplint
- name: phpcpd
if: ${{ always() }}
run: moodle-plugin-ci phpcpd || true
- name: phpmd
if: ${{ always() }}
run: moodle-plugin-ci phpmd
- name: codechecker
if: ${{ always() }}
run: moodle-plugin-ci codechecker
- name: validate
if: ${{ always() }}
run: moodle-plugin-ci validate
- name: savepoints
if: ${{ always() }}
run: moodle-plugin-ci savepoints
- name: mustache
if: ${{ always() }}
run: moodle-plugin-ci mustache
- name: grunt
if: ${{ always() }}
run: moodle-plugin-ci grunt
- name: phpunit
if: ${{ always() }}
run: moodle-plugin-ci phpunit
- name: behat
if: ${{ always() }}
run: moodle-plugin-ci behat --profile chrome
The Opaque question type and behaviour
https://moodle.org/plugins/qtype_opaque
Opaque (http://docs.moodle.org/en/Development:Opaque) is the Open protocol for
accessing question engines.
The Opaque protocol was originally created by sam marshall of the Open
University (http://www.open.ac.uk/) as part of the OpenMark project
(http://java.net/projects/openmark/). The Moodle implementation of Opaque was
done by Tim Hunt.
As well as OpenMark, this question type can also be used to connect to
ounit (http://code.google.com/p/ounit/) and possibly other question systems
we don't know about.
Opaque has been available since Moodle 1.8, but this version is compatible with
Moodle 3.4.
This question behaviour also requires the Opaque question type to be installed.
https://moodle.org/plugins/qbehaviour_opaque
You can install from the Moodle plugins database using the links above.
Or to install using git, type this command in the root of your Moodle install
git clone https://github.com/moodleou/moodle-qtype_opaque.git question/type/opaque
echo '/question/type/opaque/' >> .git/info/exclude
git clone https://github.com/moodleou/moodle-qbehaviour_opaque.git question/behaviour/opaque
echo '/question/behaviour/opaque/' >> .git/info/exclude
Once installed you need to go to the question type settings page
(Site administration -> Plugins -> Question types -> Opaque) to
set up the URLs of the question engines you wish to use.
https://github.com/moodleou/moodle-local_testopaqueqe can be used to test that
Opaque is working.
To be able to run all the unit tests, you need a working OpenMark install, then you need to add
define('QTYPE_OPAQUE_TEST_ENGINE_QE', 'http://example.com/om-qe/services/Om');
define('QTYPE_OPAQUE_TEST_ENGINE_TN', 'http://example.com/openmark/!question');
define('QTYPE_OPAQUE_TEST_ENGINE_PASSKEY', 'abc123');
define('QTYPE_OPAQUE_TEST_ENGINE_TIMEOUT', '5');
to your config.php file. Of these, only the first is required. The remaining
ones are optional. Specify them if your set-up needs them.
<?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 behaviour specifically for use with the Opaque question type.
*
* @package qbehaviour_opaque
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/type/opaque/connection.php');
require_once($CFG->dirroot . '/question/behaviour/opaque/connection.php');
require_once($CFG->dirroot . '/question/behaviour/opaque/statecache.php');
require_once($CFG->dirroot . '/question/behaviour/opaque/resourcecache.php');
require_once($CFG->dirroot . '/question/behaviour/opaque/legacy.php');
require_once($CFG->dirroot . '/question/behaviour/opaque/opaquestate.php');
/**
* This behaviour is specifically for use with the Opaque question type.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_opaque extends question_behaviour {
/** @var string */
protected $preferredbehaviour;
/** @var string */
protected $questionsummary;
public function __construct(question_attempt $qa, $preferredbehaviour) {
parent::__construct($qa, $preferredbehaviour);
$this->preferredbehaviour = $preferredbehaviour;
}
public function is_compatible_question(question_definition $question) {
return $question instanceof qtype_opaque_question;
}
public function get_state_string($showcorrectness) {
$state = $this->qa->get_state();
$omstate = $this->qa->get_last_behaviour_var('_statestring');
if ($state->is_finished()) {
return $state->default_string($showcorrectness);
} else if ($omstate) {
return $omstate;
} else {
return get_string('notcomplete', 'qbehaviour_opaque');
}
}
public function init_first_step(question_attempt_step $step, $variant) {
global $USER;
parent::init_first_step($step, $variant);
// We no longer set the random seed as such (we pass a fixed conventional
// value instead). Instead we pass the variant as OpenMark's 'attempt'
// paramter.
$step->set_behaviour_var('_randomseed', 123456789);
$step->set_behaviour_var('_attempt', $variant);
$step->set_behaviour_var('_userid', $USER->id);
$step->set_behaviour_var('_language', current_language());
$step->set_behaviour_var('_preferredbehaviour', $this->preferredbehaviour);
$opaquestate = new qbehaviour_opaque_state($this->qa, $step);
$step->set_behaviour_var('_statestring', $opaquestate->get_progress_info());
// Remember the question summary.
$this->questionsummary = html_to_text($opaquestate->get_xhtml(), 0, false);
}
public function adjust_display_options(question_display_options $options) {
if (!$this->qa->has_marks()) {
$options->correctness = false;
}
if ($this->qa->get_state()->is_finished()) {
$options->readonly = true;
}
}
public function get_question_summary() {
return $this->questionsummary;
}
protected function is_same_response(question_attempt_step $pendingstep) {
$newdata = $pendingstep->get_submitted_data();
$newdata = qbehaviour_opaque_fix_up_submitted_data($newdata, $pendingstep);
// If an omact_ button has been clicked, never treat this as a duplicate submission.
if (qbehaviour_opaque_response_contains_om_action($newdata)) {
return false;
}
$laststep = $this->qa->get_last_step();
$olddata = $laststep->get_submitted_data();
$olddata = qbehaviour_opaque_fix_up_submitted_data($olddata, $laststep);
return question_utils::arrays_have_same_keys_and_values($newdata, $olddata);
}
public function summarise_action(question_attempt_step $step) {
if ($step->has_behaviour_var('_randomseed')) {
return $this->summarise_start($step);
} else if ($step->has_behaviour_var('finish')) {
return $this->summarise_finish($step);
} else if ($step->has_behaviour_var('comment')) {
return $this->summarise_manual_comment($step);
} else if ($step->has_qt_var('omact_ok')) {
// This is always a 'Try again' event in OpenMark. In this case we
// use button label.
return $step->get_qt_var('omact_ok');
} else {
$summary = $this->question->summarise_response(qbehaviour_opaque_state::submitted_data($step));
if ($this->step_has_a_submitted_response($step)) {
return get_string('submitted', 'question', $summary);
} else {
return get_string('saved', 'question', $summary);
}
}
}
public function process_action(question_attempt_pending_step $pendingstep) {
if ($pendingstep->has_behaviour_var('finish')) {
return $this->process_finish($pendingstep);
} else if ($pendingstep->has_behaviour_var('comment')) {
return $this->process_comment($pendingstep);
} else if ($this->is_same_response($pendingstep) ||
$this->qa->get_state()->is_finished()) {
return question_attempt::DISCARD;
} else {
return $this->process_remote_action($pendingstep);
}
}
public function process_finish(question_attempt_pending_step $pendingstep) {
if ($this->qa->get_state()->is_finished()) {
return question_attempt::DISCARD;
}
// Try to get the question to stop.
$result = $this->process_remote_action($pendingstep);
if ($result == question_attempt::KEEP && !$pendingstep->get_state()->is_finished()) {
// They tried to finish but the question is not finished, so all we
// can do is to set the state to gave up. This lets the renderer
// handle the review page appropriately.
$pendingstep->set_state(question_state::$gaveup);
}
return $result;
}
public function process_remote_action(question_attempt_pending_step $pendingstep) {
$opaquestate = new qbehaviour_opaque_state($this->qa, $pendingstep);
if ($opaquestate->get_results_sequence_number() != $this->qa->get_num_steps()) {
if ($opaquestate->get_progress_info() === 'Answer saved') {
$pendingstep->set_state(question_state::$complete);
} else {
$pendingstep->set_state(question_state::$todo);
}
$pendingstep->set_behaviour_var('_statestring', $opaquestate->get_progress_info());
} else {
// Look for a score on the default axis.
$pendingstep->set_fraction(0);
$results = $opaquestate->get_results();
foreach ($results->scores as $score) {
if ($score->axis == '') {
$pendingstep->set_fraction($score->marks / $this->question->defaultmark);
}
}
if ($results->attempts > 0) {
$pendingstep->set_state(question_state::$gradedright);
} else {
$pendingstep->set_state(
question_state::graded_state_for_fraction($pendingstep->get_fraction()));
}
if (!empty($results->questionLine)) {
$this->qa->set_question_summary(
$this->cleanup_results($results->questionLine));
if (preg_match('~variant\s*=\s*(\d+)~i', $results->questionLine, $matches)) {
$pendingstep->set_new_variant_number($matches[1]);
} else {
$pendingstep->set_new_variant_number(1);
}
}
if (!empty($results->answerLine)) {
$pendingstep->set_new_response_summary(
$this->cleanup_results($results->answerLine));
}
if (!empty($results->actionSummary)) {
$pendingstep->set_behaviour_var('_actionsummary',
$this->cleanup_results($results->actionSummary));
}
}
return question_attempt::KEEP;
}
protected function cleanup_results($line) {
return preg_replace('/\\s+/', ' ', $line);
}
public function step_has_a_submitted_response($step) {
if ($step->has_behaviour_var('finish')) {
return false;
} else if ($step->has_behaviour_var('comment')) {
return false;
} else {
$foundomact = false;
foreach (qbehaviour_opaque_state::submitted_data($step) as $name => $value) {
if ($name === 'omact_ok') {
// This is a Try again state.
return false;
}
if (substr($name, 0, 6) === 'omact_') {
$foundomact = true;
}
}
return $foundomact;
}
}
}
<?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/>.
/**
* Question behaviour type specifically for use with the Opaque question type.
*
* @package qbehaviour_opaque
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Question behaviour type information specifically for use with the Opaque question type.
*
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_opaque_type extends question_behaviour_type {
public function can_questions_finish_during_the_attempt() {
return true;
}
}
# Change log for the The Opaque question behaviour
## Changes in 2.9
* This version works with Moodle 4.0.
## Changes in 2.8
* Privacy API implementation.
* Update to use the newer editor_ousupsub, instead of editor_supsub.
* Setup Travis-CI automated testing integration.
* Fix some coding style.
* Due to privacy API support, this version now only works in Moodle 3.4+
For older Moodles, you will need to use a previous version of this plugin.
## 2.7 and before
Changes were not documented here.
<?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 qbehaviour_opaque.
*
* @package qbehaviour_opaque
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qbehaviour_opaque\privacy;
/**
* Privacy Subsystem for qbehaviour_opaque implementing null_provider.
*
* @copyright 2018 The Open University
* @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';
}
}
<?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 qbehaviour_opaque_connection class.
*
* @package qbehaviour_opaque
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// In config.php, you can set
// $CFG->qtype_opaque_soap_class = 'qtype_opaque_soap_client_with_logging';
// To log every SOAP call in huge detail. Lots are writted to moodledata/temp.
/**
* Wraps the SOAP connection to the question engine, exposing the methods used
* when processing question attempts.
*
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qbehaviour_opaque_connection extends qtype_opaque_connection {
/**
* @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.
*/
protected function generate_passkey($userid) {
return md5($this->passkeysalt . $userid);
}
/**
* @param string $remoteid identifies the question.
* @param string $remoteversion identifies the specific version of the quetsion.
* @param aray $data feeds into the initialParams.
* @param question_display_options|null $options controls how the question is displayed.
* @return object and Opaque StartReturn structure.
*/
public function start($remoteid, $remoteversion, $data, $cachedresources, $options = null) {
$initialparams = array(
'randomseed' => $data['-_randomseed'],
'userid' => $data['-_userid'],
'language' => $data['-_language'],
'passKey' => $this->generate_passkey($data['-_userid']),
'preferredbehaviour' => $data['-_preferredbehaviour'],
);
if (array_key_exists('-_attempt', $data)) {
$initialparams['attempt'] = $data['-_attempt'];
$initialparams['navigatorVersion'] = '1.9.9';
}
if (!is_null($options)) {
$initialparams['display_readonly'] = (int) $options->readonly;
$initialparams['display_marks'] = (int) $options->marks;
$initialparams['display_markdp'] = (int) $options->markdp;
$initialparams['display_correctness'] = (int) $options->correctness;
$initialparams['display_feedback'] = (int) $options->feedback;
$initialparams['display_generalfeedback'] = (int) $options->generalfeedback;
}
return $this->soapclient->start($remoteid, $remoteversion, $this->question_base_url(),
array_keys($initialparams), array_values($initialparams), $cachedresources);
}
/**
* @param string $questionsessionid the question session.
* @param array $respones the post date to process.
*/
public function process($questionsessionid, $response) {
return $this->soapclient->process($questionsessionid,
array_keys($response), array_values($response));
}
/**
* @param string $questionsessionid the question session to stop.
*/
public function stop($questionsessionid) {