diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml index 0bbb9fe29b2063cec971295a66462cb107e0d665..09e9d1e6ab677a45e5ec1f5817163e2910e37aa5 100644 --- a/.github/workflows/moodle-ci.yml +++ b/.github/workflows/moodle-ci.yml @@ -32,34 +32,29 @@ jobs: matrix: # I don't know why, but mariadb is much slower, so mostly use pgsql. # We use a mix of SBCL and GCL, but mostly prefer SBCL as it is faster. include: + # There is a known problem with PHP 8.2. + # Remove testing PHP 8.2 for now as this failure in CI will obscure other problems. - php: '8.1' moodle-branch: 'master' database: 'pgsql' maxima: 'SBCL' - - php: '8.0' - moodle-branch: 'master' - database: 'pgsql' - maxima: 'SBCL' - php: '8.1' moodle-branch: 'MOODLE_402_STABLE' database: 'pgsql' - maxima: 'SBCL' - - php: '8.1' - moodle-branch: 'MOODLE_402_STABLE' - database: 'mariadb' - maxima: 'SBCL' + maxima: 'GCL' - php: '8.0' moodle-branch: 'MOODLE_402_STABLE' database: 'pgsql' maxima: 'SBCL' + # Edinburgh is planning to run the setup below for 2023-24. - php: '7.4' moodle-branch: 'MOODLE_401_STABLE' - database: 'pgsql' - maxima: 'SBCL' + database: 'mariadb' + maxima: 'GCL' - php: '7.4' moodle-branch: 'MOODLE_400_STABLE' database: 'pgsql' - maxima: 'GCL' + maxima: 'SBCL' - php: '7.3' moodle-branch: 'MOODLE_311_STABLE' database: 'pgsql' diff --git a/Readme.md b/Readme.md index f522eeff8487ee0e8d59dfcb5549797d6cd2cab2..4a8390e24a9788ccfd9df0286cdd208a483b5702 100644 --- a/Readme.md +++ b/Readme.md @@ -1,4 +1,4 @@ -# STACK 4.4.4 +# STACK 4.4.6 STACK is an assessment system for mathematics, science and related disciplines. STACK is a question type for the Moodle learning management system, and also the ILIAS learning management system. diff --git a/adminui/caschat.php b/adminui/caschat.php index 2f3cb3f6dd2371bca52430aab0f9eedb34ab5f02..85656af98451a412b8daa8bad4b28a63431a7cf1 100644 --- a/adminui/caschat.php +++ b/adminui/caschat.php @@ -27,6 +27,7 @@ require_once(__DIR__.'/../../../../config.php'); require_once($CFG->libdir . '/questionlib.php'); require_once(__DIR__ . '/../locallib.php'); +require_once(__DIR__ . '/../vle_specific.php'); require_once(__DIR__ . '/../stack/utils.class.php'); require_once(__DIR__ . '/../stack/options.class.php'); require_once(__DIR__ . '/../stack/cas/secure_loader.class.php'); @@ -53,22 +54,27 @@ if (!$questionid) { list($context, $seed, $urlparams) = qtype_stack_setup_question_test_page($question); // Check permissions. - question_require_capability_on($questiondata, 'view'); + question_require_capability_on($questiondata, 'edit'); } $PAGE->set_url('/question/type/stack/adminui/caschat.php', $urlparams); $title = stack_string('chattitle'); $PAGE->set_title($title); - $displaytext = ''; $debuginfo = ''; $errs = ''; $varerrs = array(); $vars = optional_param('maximavars', '', PARAM_RAW); +$inps = optional_param('inputs', '', PARAM_RAW); $string = optional_param('cas', '', PARAM_RAW); $simp = optional_param('simp', '', PARAM_RAW); +$savedb = false; +$savedmsg = ''; +if (trim(optional_param('action', '', PARAM_RAW)) == trim(stack_string('savechat'))) { + $savedb = true; +} // Always fix dollars in this script. // Very useful for converting existing text for use elswhere in Moodle, such as in pages of text. @@ -91,8 +97,8 @@ if ($string) { $options->set_option('simplify', $simp); $session = new stack_cas_session2(array(), $options); - if ($vars) { - $keyvals = new stack_cas_keyval($vars, $options, 0); + if ($vars || $inps) { + $keyvals = new stack_cas_keyval($vars . "\n" . $inps, $options, 0); $keyvals->get_valid(); $varerrs = $keyvals->get_errors(); if ($keyvals->get_valid()) { @@ -128,17 +134,40 @@ if ($string) { $errs = ''; } $debuginfo = $session->get_debuginfo(); + + // Save updated data in the DB when everything is valid. + if ($questionid && $savedb) { + $DB->set_field('question', 'generalfeedback', $string, + array('id' => $questionid)); + $DB->set_field('qtype_stack_options', 'questionvariables', $vars, + array('questionid' => $questionid)); + $DB->set_field('qtype_stack_options', 'compiledcache', null, array('questionid' => $questionid)); + // Invalidate the question definition cache. + stack_clear_vle_question_cache($questionid); + + $savedmsg = stack_string('savechatmsg'); + } } } echo $OUTPUT->header(); echo $OUTPUT->heading($title); -echo html_writer::tag('p', stack_string('chatintro')); // If we are editing the General Feedback from a question it is very helpful to see the question text. if ($questionid) { - echo $OUTPUT->heading(stack_string('questiontext'), 3); - echo html_writer::tag('pre', $question->questiontext, array('class' => 'questiontext')); + + $qtype = new qtype_stack(); + $qtestlink = html_writer::link($qtype->get_question_test_url($question), stack_string('runquestiontests'), + array('class' => 'nav-link')); + echo html_writer::tag('nav', $qtestlink, array('class' => 'nav')); + + if ($savedmsg) { + echo html_writer::tag('p', $savedmsg, array('class' => 'overallresult pass')); + } + + $out = html_writer::tag('summary', stack_string('questiontext')); + $out .= html_writer::tag('pre', $question->questiontext, array('class' => 'questiontext')); + echo html_writer::tag('details', $out); } if (!$varerrs) { @@ -147,39 +176,45 @@ if (!$varerrs) { } } + +$fout = html_writer::tag('h2', stack_string('questionvariables')); +$fout .= html_writer::tag('p', implode($varerrs)); +$varlen = substr_count($vars, "\n") + 3; +$fout .= html_writer::tag('p', html_writer::tag('textarea', $vars, + array('cols' => 100, 'rows' => $varlen, 'name' => 'maximavars'))); +if ($questionid) { + $inplen = substr_count($inps, "\n"); + $fout .= html_writer::tag('p', html_writer::tag('textarea', $inps, + array('cols' => 100, 'rows' => $inplen, 'name' => 'inputs'))); +} if ($simp) { - $simp = stack_string('autosimplify').' '. - html_writer::empty_tag('input', array('type' => 'checkbox', 'checked' => $simp, 'name' => 'simp')); + $fout .= stack_string('autosimplify').' '. + html_writer::empty_tag('input', array('type' => 'checkbox', 'checked' => $simp, 'name' => 'simp')); } else { - $simp = stack_string('autosimplify').' '.html_writer::empty_tag('input', array('type' => 'checkbox', 'name' => 'simp')); + $fout .= stack_string('autosimplify').' '.html_writer::empty_tag('input', array('type' => 'checkbox', 'name' => 'simp')); } - -$varlen = substr_count($vars, "\n") + 3; +if ($questionid) { + $fout .= html_writer::tag('h2', stack_string('generalfeedback')); +} else { + $fout .= html_writer::tag('h2', stack_string('castext')); +} +$fout .= html_writer::tag('p', $errs); $stringlen = max(substr_count($string, "\n") + 3, 8); - -echo html_writer::tag('form', - html_writer::tag('h2', stack_string('questionvariables')) . - html_writer::tag('p', implode($varerrs)) . - html_writer::tag('p', html_writer::tag('textarea', $vars, - array('cols' => 100, 'rows' => $varlen, 'name' => 'maximavars'))) . - html_writer::tag('p', $simp) . - html_writer::tag('h2', stack_string('castext')) . - html_writer::tag('p', $errs) . - html_writer::tag('p', html_writer::tag('textarea', $string, - array('cols' => 100, 'rows' => $stringlen, 'name' => 'cas'))) . - html_writer::tag('p', html_writer::empty_tag('input', - array('type' => 'submit', 'value' => stack_string('chat')))), - array('action' => $PAGE->url, 'method' => 'post')); - -if ($string) { - echo $OUTPUT->heading(stack_string('questionvariablevalues'), 3); - echo html_writer::start_tag('div', array('class' => 'questionvariables')); - echo html_writer::tag('pre', $session->get_keyval_representation(true)); - echo html_writer::end_tag('div'); +$fout .= html_writer::tag('p', html_writer::tag('textarea', $string, + array('cols' => 100, 'rows' => $stringlen, 'name' => 'cas'))); +$fout .= html_writer::start_tag('p'); +$fout .= html_writer::empty_tag('input', + array('type' => 'submit', 'name' => 'action', 'value' => stack_string('chat'))); +if ($questionid && !$varerrs) { + $fout .= html_writer::empty_tag('input', + array('type' => 'submit', 'name' => 'action', 'value' => stack_string('savechat'))); } +$fout .= html_writer::end_tag('p'); +echo html_writer::tag('form', $fout, array('action' => $PAGE->url, 'method' => 'post')); if ('' != trim($debuginfo)) { echo $OUTPUT->box($debuginfo); } +echo html_writer::tag('p', stack_string('chatintro')); echo $OUTPUT->footer(); diff --git a/adminui/dependencies.php b/adminui/dependencies.php index 3cd4705ae5f4f7de4070e9abd38dddb7608fad3c..16072fb4d87eaa9cf45812d212a9687ce2531520 100644 --- a/adminui/dependencies.php +++ b/adminui/dependencies.php @@ -109,6 +109,9 @@ echo $OUTPUT->single_button( echo $OUTPUT->single_button( new moodle_url($PAGE->url, array('langs' => 1, 'sesskey' => sesskey())), 'Find "langs"'); +echo $OUTPUT->single_button( + new moodle_url($PAGE->url, array('todo' => 1, 'sesskey' => sesskey())), + 'Find "todo"'); if (data_submitted() && optional_param('includes', false, PARAM_BOOL)) { /* @@ -309,7 +312,7 @@ if (data_submitted() && optional_param('langs', false, PARAM_BOOL)) { $qs = $DB->get_recordset_sql('SELECT q.id as questionid FROM {question} q, {qtype_stack_options} o WHERE ' . 'q.id = o.questionid AND ' . $DB->sql_like('o.compiledcache', ':trg') . ' AND NOT ' . $DB->sql_like('o.compiledcache', ':other') . ';', ['trg' => '%"langs":[%', 'other' => '%"langs":[]%']); - echo '<h4>Questions containing that have localisation using means we understand.</h4>'; + echo '<h4>Questions containing localisation using means we understand.</h4>'; echo '<table><thead><tr><th>Question</th><th>Langs</th></thead><tbody>'; // Load the whole question, simpler to get the contexts correct that way. foreach ($qs as $item) { @@ -329,4 +332,29 @@ if (data_submitted() && optional_param('langs', false, PARAM_BOOL)) { echo '</tbody></table>'; } +if (data_submitted() && optional_param('todo', false, PARAM_BOOL)) { + /* + * Todo blocks present in the question. + */ + $qs = $DB->get_recordset_sql('SELECT q.id as questionid FROM {question} q, {qtype_stack_options} o WHERE ' . + 'q.id = o.questionid AND ' . + $DB->sql_like('o.compiledcache', ':trg') . ';', ['trg' => '%stack_todo%']); + echo '<h4>Questions containing [[todo]] blocks</h4>'; + echo '<table><thead><tr><th>Question</th></thead><tbody>'; + // Load the whole question, simpler to get the contexts correct that way. + foreach ($qs as $item) { + $q = question_bank::load_question($item->questionid); + list($context, $seed, $urlparams) = qtype_stack_setup_question_test_page($q); + if (stack_determine_moodle_version() < 400) { + $qurl = question_preview_url($item->questionid, null, null, null, null, $context); + } else { + $qurl = qbank_previewquestion\helper::question_preview_url($item->questionid, + null, null, null, null, $context); + } + echo "<tr><td>" . $q->name . ' ' . + $OUTPUT->action_icon($qurl, new pix_icon('t/preview', get_string('preview'))) . '</td></tr>'; + } + echo '</tbody></table>'; +} + echo $OUTPUT->footer(); diff --git a/adminui/healthcheck.php b/adminui/healthcheck.php index 0379a11b774aaa99add1d8fb2ccca00ea39e63e6..0a9a4073ea710ca32ba18dfe4ebaa4b5b5c3f2ca 100644 --- a/adminui/healthcheck.php +++ b/adminui/healthcheck.php @@ -28,15 +28,8 @@ define('NO_OUTPUT_BUFFERING', true); require_once(__DIR__.'/../../../../config.php'); require_once($CFG->dirroot .'/course/lib.php'); require_once($CFG->libdir .'/filelib.php'); - require_once(__DIR__ . '/../locallib.php'); -require_once(__DIR__ . '/../stack/utils.class.php'); -require_once(__DIR__ . '/../stack/options.class.php'); -require_once(__DIR__ . '/../stack/cas/cassession2.class.php'); -require_once(__DIR__ . '/../stack/cas/castext2/castext2_evaluatable.class.php'); -require_once(__DIR__ . '/../stack/cas/connector.dbcache.class.php'); -require_once(__DIR__ . '/../stack/cas/installhelper.class.php'); - +require_once(__DIR__ . '/../stack/cas/connector.healthcheck.class.php'); // Check permissions. require_login(); @@ -49,23 +42,12 @@ $PAGE->set_url('/question/type/stack/adminui/healthcheck.php'); $title = stack_string('healthcheck'); $PAGE->set_title($title); +$config = stack_utils::get_config(); + // Start output. echo $OUTPUT->header(); echo $OUTPUT->heading($title); -$config = stack_utils::get_config(); - -// This array holds summary info, for a table at the end of the pager. -$summary = array(); -$summary[] = array('', $config->platform ); - -// Mbstring. -if (!extension_loaded('mbstring')) { - echo $OUTPUT->heading(stack_string('healthchecknombstring'), 3); - echo $OUTPUT->footer(); - exit; -} - // Clear the cache if requested. if (data_submitted() && optional_param('clearcache', false, PARAM_BOOL)) { require_sesskey(); @@ -92,121 +74,48 @@ if (data_submitted() && optional_param('createmaximaimage', false, PARAM_BOOL)) exit; } -// LaTeX. -echo $OUTPUT->heading(stack_string('healthchecklatex'), 3); -echo html_writer::tag('p', stack_string('healthcheckmathsdisplaymethod', - stack_maths::configured_output_name())); -echo html_writer::tag('p', stack_string('healthchecklatexintro')); - -echo html_writer::tag('dt', stack_string('texdisplaystyle')); -echo html_writer::tag('dd', stack_string('healthchecksampledisplaytex')); - -echo html_writer::tag('dt', stack_string('texinlinestyle')); -echo html_writer::tag('dd', stack_string('healthchecksampleinlinetex')); +// From this point do all health-related actions. -if ($config->mathsdisplay === 'mathjax') { - echo html_writer::tag('p', stack_string('healthchecklatexmathjax')); -} else { - $settingsurl = new moodle_url('/admin/filters.php'); - echo html_writer::tag('p', stack_string('healthcheckfilters', - array('filter' => stack_maths::configured_output_name(), 'url' => $settingsurl->out()))); -} - -// Maxima config. -echo $OUTPUT->heading(stack_string('healthcheckconfig'), 3); - -// Try to list available versions of Maxima (linux only, without the DB). -if ($config->platform !== 'win') { - $connection = stack_connection_helper::make(); - if (is_a($connection, 'stack_cas_connection_linux')) { - echo html_writer::tag('pre', $connection->get_maxima_available()); - } -} - -// Check for location of Maxima. -$maximalocation = stack_cas_configuration::confirm_maxima_win_location(); -if ('' != $maximalocation) { - $message = stack_string('healthcheckconfigintro1').' '.html_writer::tag('tt', $maximalocation); - echo html_writer::tag('p', $message); - $summary[] = array(null, $message); -} - -// Check if the current options for library packages are permitted (maximalibraries). -list($valid, $message) = stack_cas_configuration::validate_maximalibraries(); -if (!$valid) { - echo html_writer::tag('p', $message); - $summary[] = array(false, $message); +// Mbstring. This is an install requirement, rather than a CAS healtcheck. +if (!extension_loaded('mbstring')) { + echo $OUTPUT->heading(stack_string('healthchecknombstring'), 3); + echo $OUTPUT->footer(); + exit; } -// Try to connect to create maxima local. -echo html_writer::tag('p', stack_string('healthcheckconfigintro2')); -stack_cas_configuration::create_maximalocal(); - -echo html_writer::tag('textarea', stack_cas_configuration::generate_maximalocal_contents(), - array('readonly' => 'readonly', 'wrap' => 'virtual', 'rows' => '32', 'cols' => '100')); // Maxima config. -if (stack_cas_configuration::maxima_bat_is_missing()) { - echo $OUTPUT->heading(stack_string('healthcheckmaximabat'), 3); - $message = stack_string('healthcheckmaximabatinfo', $CFG->dataroot); - echo html_writer::tag('p', $message); - $summary[] = array(false, $message); +$healthcheck = new stack_cas_healthcheck($config); +$tab = ''; +foreach ($healthcheck->get_test_results() as $test) { + $tl = ''; + if (true === $test['result']) { + $tl .= html_writer::tag('td', stack_string('testsuitepass')); + } else if (false === $test['result']) { + $tl .= html_writer::tag('td', stack_string('testsuitefail')); + } else { + $tl .= html_writer::tag('td', ' '); + } + $tl .= html_writer::tag('td', $test['summary']); + $tab .= html_writer::tag('tr', $tl)."\n"; } - -// Test an *uncached* call to the CAS. I.e. a genuine call to the process. -echo $OUTPUT->heading(stack_string('healthuncached'), 3); -echo html_writer::tag('p', stack_string('healthuncachedintro')); -list($message, $genuinedebug, $result) = stack_connection_helper::stackmaxima_genuine_connect(); -$summary[] = array($result, $message); -echo html_writer::tag('p', $message); -echo output_debug(stack_string('debuginfo'), $genuinedebug); -$genuinecascall = $result; - -// Test Maxima connection. -// Intentionally use get_string for the sample CAS and plots, so we don't render -// the maths too soon. -output_cas_text(stack_string('healthcheckconnect'), - stack_string('healthcheckconnectintro'), get_string('healthchecksamplecas', 'qtype_stack')); - -// If we have a linux machine, and we are testing the raw connection then we should -// attempt to automatically create an optimized maxima image on the system. -if ($config->platform === 'linux' && $genuinecascall) { - echo $OUTPUT->heading(stack_string('healthautomaxopt'), 3); - echo html_writer::tag('p', stack_string('healthautomaxoptintro')); - list($message, $debug, $result, $commandline, $rawcommand) - = stack_connection_helper::stackmaxima_auto_maxima_optimise($genuinedebug); - $summary[] = array($result, $message); - echo html_writer::tag('p', $message); - echo output_debug(stack_string('debuginfo'), $debug); +echo html_writer::tag('table', $tab); +if ($healthcheck->get_overall_result()) { + echo html_writer::tag('p', stack_string('healthcheckpass'), array('class' => 'overallresult pass')); +} else { + echo html_writer::tag('p', stack_string('healthcheckfail'), array('class' => 'overallresult fail')); } - - -// Test the version of the STACK libraries that Maxima is using. -// When Maxima is being run pre-compiled (maxima-optimise) or on a server, -// it is possible for the version of the Maxima libraries to get out of synch -// with the qtype_stack code. - -echo $OUTPUT->heading(stack_string('healthchecksstackmaximaversion'), 3); -list($message, $details, $result) = stack_connection_helper::stackmaxima_version_healthcheck(); -$summary[] = array($result, stack_string($message, $details)); -$summary[] = array(null, stack_string('settingmaximalibraries') . ' ' . $config->maximalibraries); -echo html_writer::tag('p', stack_string($message, $details)); - -// Test plots. -output_cas_text(stack_string('healthcheckplots'), - stack_string('healthcheckplotsintro'), get_string('healthchecksampleplots', 'qtype_stack')); +echo html_writer::tag('p', get_string('healthcheckfaildocs', 'qtype_stack', + array('link' => (string) new moodle_url('/question/type/stack/doc/doc.php/Installation/Testing_installation.md'))) + ); // State of the cache. -echo $OUTPUT->heading(stack_string('settingcasresultscache'), 3); -$message = stack_string('healthcheckcache_' . $config->casresultscache); -$summary[] = array(null, $message); -echo html_writer::tag('p', $message); if ('db' == $config->casresultscache) { echo html_writer::tag('p', stack_string('healthcheckcachestatus', - stack_cas_connection_db_cache::entries_count($DB))); + stack_cas_connection_db_cache::entries_count($DB))); echo $OUTPUT->single_button( - new moodle_url($PAGE->url, array('clearcache' => 1, 'sesskey' => sesskey())), - stack_string('clearthecache')); + new moodle_url($PAGE->url, array('clearcache' => 1, 'sesskey' => sesskey())), + stack_string('clearthecache')); } // Option to auto-create the Maxima image and store the results. @@ -216,49 +125,38 @@ if ($config->platform != 'win') { stack_string('healthcheckcreateimage')); } - echo '<hr />'; -$tab = ''; -foreach ($summary as $line) { - $tl = ''; - if (true === $line[0]) { - $tl .= html_writer::tag('td', '<span class="ok">OK</span>'); - } else if (false === $line[0]) { - $tl .= html_writer::tag('td', '<span class="error">FAILED</span>'); - } else { - $tl .= html_writer::tag('td', ' '); - - } - $tl .= html_writer::tag('td', $line[1]); - $tab .= html_writer::tag('tr', $tl)."\n"; -} -echo html_writer::tag('table', $tab); - -echo $OUTPUT->footer(); - -function output_cas_text($title, $intro, $castext) { - global $OUTPUT; +// LaTeX. This is an install requirement, rather than a CAS healtcheck. +echo $OUTPUT->heading(stack_string('healthchecklatex'), 3); +echo html_writer::tag('p', stack_string('healthcheckmathsdisplaymethod', + stack_maths::configured_output_name())); +echo html_writer::tag('p', stack_string('healthchecklatexintro')); - echo $OUTPUT->heading($title, 3); - echo html_writer::tag('p', $intro); - echo html_writer::tag('pre', s($castext)); +echo html_writer::tag('dt', stack_string('texdisplaystyle')); +echo html_writer::tag('dd', stack_string('healthchecksampledisplaytex')); - $ct = castext2_evaluatable::make_from_source($castext, 'healthcheck'); - $session = new stack_cas_session2([$ct]); - $session->instantiate(); +echo html_writer::tag('dt', stack_string('texinlinestyle')); +echo html_writer::tag('dd', stack_string('healthchecksampleinlinetex')); - echo html_writer::tag('p', stack_ouput_castext($ct->get_rendered())); - echo output_debug(stack_string('errors'), $ct->get_errors()); - echo output_debug(stack_string('debuginfo'), $session->get_debuginfo()); +if ($config->mathsdisplay === 'mathjax') { + echo html_writer::tag('p', stack_string('healthchecklatexmathjax')); +} else { + $settingsurl = new moodle_url('/admin/filters.php'); + echo html_writer::tag('p', stack_string('healthcheckfilters', + array('filter' => stack_maths::configured_output_name(), 'url' => $settingsurl->out()))); } - -function output_debug($title, $message) { - global $OUTPUT; - - if (!$message) { - return; +// Output details. +foreach ($healthcheck->get_test_results() as $test) { + if ($test['details'] !== null) { + echo '<hr />'; + $heading = stack_string($test['tag']); + if ($test['result'] === false) { + $heading = stack_string('testsuitefail') . ' ' . $heading; + } + echo $OUTPUT->heading($heading, 3); + echo $test['details']; } - - return $OUTPUT->box($OUTPUT->heading($title) . $message); } + +echo $OUTPUT->footer(); diff --git a/amd/build/stackjsvle.min.js b/amd/build/stackjsvle.min.js index 363c1980f1b0de590354c10d162ee5c0ad83e626..70fdaff834b83a6ded2c1fd10409e79c48f00923 100644 --- a/amd/build/stackjsvle.min.js +++ b/amd/build/stackjsvle.min.js @@ -31,6 +31,6 @@ function _createForOfIteratorHelper(o,allowArrayLike){var it="undefined"!=typeof * @module qtype_stack/stackjsvle * @copyright 2023 Aalto University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */define("qtype_stack/stackjsvle",["core/event"],(function(CustomEvents){var IFRAMES={},INPUTS={},INPUTS_INPUT_EVENT={},DISABLE_CHANGES=!1;function vle_get_element(id){for(var candidate=document.getElementById(id),iter=candidate;iter&&!iter.classList.contains("formulation");)iter=iter.parentElement;return iter&&iter.classList.contains("formulation")?candidate:null}function vle_get_input_element(name,srciframe){for(var iter=document.getElementById(srciframe);iter&&!iter.classList.contains("formulation");)iter=iter.parentElement;if(iter&&iter.classList.contains("formulation")){var _possible=iter.querySelector('input[id$="_'+name+'"]');if(null!==_possible)return _possible;if(null!==(_possible=iter.querySelector('input[id$="_'+name+'_1"][type=radio]')))return _possible;if(null!==(_possible=iter.querySelector('select[id$="_'+name+'"]')))return _possible}var possible=document.querySelector('.formulation input[id$="_'+name+'"]');return null!==possible||null!==(possible=document.querySelector('.formulation input[id$="_'+name+'_1"][type=radio]'))?possible:possible=document.querySelector('.formulation select[id$="_'+name+'"]')}function vle_update_dom(modifiedsubtreerootelement){CustomEvents.notifyFilterContentUpdated(modifiedsubtreerootelement)}function is_evil_attribute(name,value){var lcname=name.toLowerCase();if(lcname.startsWith("on"))return!0;if("src"===lcname||lcname.endsWith("href")){var lcvalue=value.replace(/\s+/g,"").toLowerCase();if(lcvalue.includes("javascript:")||lcvalue.includes("data:text"))return!0}return!1}return window.addEventListener("message",(function(e){if("string"==typeof e.data||e.data instanceof String){var msg=null;try{msg=JSON.parse(e.data)}catch(e){return}if("version"in msg&&msg.version.startsWith("STACK-JS")&&"src"in msg&&"type"in msg&&msg.src in IFRAMES){var element=null,input=null,response={version:"STACK-JS:1.0.0"};switch(msg.type){case"register-input-listener":if(null===(input=vle_get_input_element(msg.name,msg.src)))return response.type="error",response.msg='Failed to connect to input: "'+msg.name+'"',response.tgt=msg.src,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");if(response.type="initial-input",response.name=msg.name,response.tgt=msg.src,"select"===input.nodeName.toLowerCase()?(response.value=input.value,response["input-type"]="select"):"checkbox"===input.type?(response.value=input.checked,response["input-type"]="checkbox"):(response.value=input.value,response["input-type"]=input.type),"radio"===input.type){response.value="";var _step4,_iterator4=_createForOfIteratorHelper(document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]"));try{for(_iterator4.s();!(_step4=_iterator4.n()).done;){var inp=_step4.value;inp.checked&&(response.value=inp.value)}}catch(err){_iterator4.e(err)}finally{_iterator4.f()}}if(input.id in INPUTS){if(msg.src in INPUTS[input.id])return;if("radio"!==input.type)INPUTS[input.id].push(msg.src);else{var _step5,_iterator5=_createForOfIteratorHelper(document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]"));try{for(_iterator5.s();!(_step5=_iterator5.n()).done;){var _inp=_step5.value;INPUTS[_inp.id].push(msg.src)}}catch(err){_iterator5.e(err)}finally{_iterator5.f()}}}else{if("radio"!==input.type)INPUTS[input.id]=[msg.src];else{var _step6,_iterator6=_createForOfIteratorHelper(document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]"));try{for(_iterator6.s();!(_step6=_iterator6.n()).done;){var _inp2=_step6.value;INPUTS[_inp2.id]=[msg.src]}}catch(err){_iterator6.e(err)}finally{_iterator6.f()}}if("radio"!==input.type)input.addEventListener("change",(function(){if(!DISABLE_CHANGES){var resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};"checkbox"===input.type?resp.value=input.checked:resp.value=input.value;var _step7,_iterator7=_createForOfIteratorHelper(INPUTS[input.id]);try{for(_iterator7.s();!(_step7=_iterator7.n()).done;){var tgt=_step7.value;resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}}catch(err){_iterator7.e(err)}finally{_iterator7.f()}}}));else document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]").forEach((function(inp){inp.addEventListener("change",(function(){if(!DISABLE_CHANGES){var resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};if(inp.checked){resp.value=inp.value;var _step8,_iterator8=_createForOfIteratorHelper(INPUTS[inp.id]);try{for(_iterator8.s();!(_step8=_iterator8.n()).done;){var tgt=_step8.value;resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}}catch(err){_iterator8.e(err)}finally{_iterator8.f()}}}}))}))}if("track-input"in msg&&msg["track-input"]&&"radio"!==input.type)if(input.id in INPUTS_INPUT_EVENT){if(msg.src in INPUTS_INPUT_EVENT[input.id])return;INPUTS_INPUT_EVENT[input.id].push(msg.src)}else INPUTS_INPUT_EVENT[input.id]=[msg.src],input.addEventListener("input",(function(){if(!DISABLE_CHANGES){var resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};"checkbox"===input.type?resp.value=input.checked:resp.value=input.value;var _step9,_iterator9=_createForOfIteratorHelper(INPUTS_INPUT_EVENT[input.id]);try{for(_iterator9.s();!(_step9=_iterator9.n()).done;){var tgt=_step9.value;resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}}catch(err){_iterator9.e(err)}finally{_iterator9.f()}}}));msg.src in INPUTS[input.id]||IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");break;case"changed-input":if(null===(input=vle_get_input_element(msg.name,msg.src))){var ret={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to modify input: "'+msg.name+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret),"*")}DISABLE_CHANGES=!0,"checkbox"===input.type?input.checked=msg.value:input.value=msg.value,function(inputelement){var c=new Event("change");inputelement.dispatchEvent(c);var i=new Event("input");inputelement.dispatchEvent(i)}(input),DISABLE_CHANGES=!1,response.type="changed-input",response.name=msg.name,response.value=msg.value;var _step10,_iterator10=_createForOfIteratorHelper(INPUTS[input.id]);try{for(_iterator10.s();!(_step10=_iterator10.n()).done;){var tgt=_step10.value;tgt!==msg.src&&(response.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(response),"*"))}}catch(err){_iterator10.e(err)}finally{_iterator10.f()}break;case"toggle-visibility":if(null===(element=vle_get_element(msg.target))){var _ret={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to find element: "'+msg.target+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(_ret),"*")}"show"===msg.set?(element.style.display="block",vle_update_dom(element)):"hide"===msg.set&&(element.style.display="none");break;case"change-content":if(null===(element=vle_get_element(msg.target))){var _ret2={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to find element: "'+msg.target+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(_ret2),"*")}element.replaceChildren(function(src){var _step,doc=(new DOMParser).parseFromString(src),_iterator=_createForOfIteratorHelper(doc.querySelectorAll("script, style"));try{for(_iterator.s();!(_step=_iterator.n()).done;)_step.value.remove()}catch(err){_iterator.e(err)}finally{_iterator.f()}var _step2,_iterator2=_createForOfIteratorHelper(doc.querySelectorAll("*"));try{for(_iterator2.s();!(_step2=_iterator2.n()).done;){var _step3,_el=_step2.value,_iterator3=_createForOfIteratorHelper(_el.attributes);try{for(_iterator3.s();!(_step3=_iterator3.n()).done;){var _step3$value=_step3.value,name=_step3$value.name;is_evil_attribute(name,_step3$value.value)&&_el.removeAttribute(name)}}catch(err){_iterator3.e(err)}finally{_iterator3.f()}}}catch(err){_iterator2.e(err)}finally{_iterator2.f()}return doc.body}(msg.content)),vle_update_dom(element);break;case"resize-frame":(element=IFRAMES[msg.src].parentElement).style.width=msg.width,element.style.height=msg.height,IFRAMES[msg.src].style.width="100%",IFRAMES[msg.src].style.height="100%",vle_update_dom(element);break;case"ping":return response.type="ping",response.tgt=msg.src,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");case"initial-input":case"error":break;default:response.type="error",response.msg='Unknown message-type: "'+msg.type+'"',response.tgt=msg.src,IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*")}}}})),{create_iframe:function(iframeid,content,targetdivid,title,scrolling,evil){var frm=document.createElement("iframe");frm.id=iframeid,frm.style.width="100%",frm.style.height="100%",frm.style.border=0,!1===scrolling?(frm.scrolling="no",frm.style.overflow="hidden"):frm.scrolling="yes",frm.title=title,frm.referrerpolicy="no-referrer",evil||(frm.sandbox="allow-scripts allow-downloads"),frm.csp="script-src: 'unsafe-inline' 'self' '*';img-src: '*';",document.getElementById(targetdivid).replaceChildren(frm),IFRAMES[iframeid]=frm;var src=new Blob([content],{type:"text/html; charset=utf-8"});frm.src=URL.createObjectURL(src)}}})); + */define("qtype_stack/stackjsvle",["core/event"],(function(CustomEvents){var IFRAMES={},INPUTS={},INPUTS_INPUT_EVENT={},DISABLE_CHANGES=!1;function vle_get_element(id){for(var candidate=document.getElementById(id),iter=candidate;iter&&!iter.classList.contains("formulation");)iter=iter.parentElement;return iter&&iter.classList.contains("formulation")?candidate:null}function vle_get_input_element(name,srciframe){for(var iter=document.getElementById(srciframe);iter&&!iter.classList.contains("formulation");)iter=iter.parentElement;if(iter&&iter.classList.contains("formulation")){var _possible=iter.querySelector('input[id$="_'+name+'"]');if(null!==_possible)return _possible;if(null!==(_possible=iter.querySelector('input[id$="_'+name+'_1"][type=radio]')))return _possible;if(null!==(_possible=iter.querySelector('select[id$="_'+name+'"]')))return _possible}var possible=document.querySelector('.formulation input[id$="_'+name+'"]');return null!==possible||null!==(possible=document.querySelector('.formulation input[id$="_'+name+'_1"][type=radio]'))?possible:possible=document.querySelector('.formulation select[id$="_'+name+'"]')}function vle_update_dom(modifiedsubtreerootelement){CustomEvents.notifyFilterContentUpdated(modifiedsubtreerootelement)}function is_evil_attribute(name,value){var lcname=name.toLowerCase();if(lcname.startsWith("on"))return!0;if("src"===lcname||lcname.endsWith("href")){var lcvalue=value.replace(/\s+/g,"").toLowerCase();if(lcvalue.includes("javascript:")||lcvalue.includes("data:text"))return!0}return!1}return window.addEventListener("message",(function(e){if("string"==typeof e.data||e.data instanceof String){var msg=null;try{msg=JSON.parse(e.data)}catch(e){return}if("version"in msg&&msg.version.startsWith("STACK-JS")&&"src"in msg&&"type"in msg&&msg.src in IFRAMES){var element=null,input=null,response={version:"STACK-JS:1.0.0"};switch(msg.type){case"register-input-listener":if(null===(input=vle_get_input_element(msg.name,msg.src)))return response.type="error",response.msg='Failed to connect to input: "'+msg.name+'"',response.tgt=msg.src,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");if(response.type="initial-input",response.name=msg.name,response.tgt=msg.src,"select"===input.nodeName.toLowerCase()?(response.value=input.value,response["input-type"]="select"):"checkbox"===input.type?(response.value=input.checked,response["input-type"]="checkbox"):(response.value=input.value,response["input-type"]=input.type),"radio"===input.type){response.value="";var _step4,_iterator4=_createForOfIteratorHelper(document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]"));try{for(_iterator4.s();!(_step4=_iterator4.n()).done;){var inp=_step4.value;inp.checked&&(response.value=inp.value)}}catch(err){_iterator4.e(err)}finally{_iterator4.f()}}if(input.id in INPUTS){if(msg.src in INPUTS[input.id])return;if("radio"!==input.type)INPUTS[input.id].push(msg.src);else{var _step5,_iterator5=_createForOfIteratorHelper(document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]"));try{for(_iterator5.s();!(_step5=_iterator5.n()).done;){var _inp=_step5.value;INPUTS[_inp.id].push(msg.src)}}catch(err){_iterator5.e(err)}finally{_iterator5.f()}}}else{if("radio"!==input.type)INPUTS[input.id]=[msg.src];else{var _step6,_iterator6=_createForOfIteratorHelper(document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]"));try{for(_iterator6.s();!(_step6=_iterator6.n()).done;){var _inp2=_step6.value;INPUTS[_inp2.id]=[msg.src]}}catch(err){_iterator6.e(err)}finally{_iterator6.f()}}if("radio"!==input.type)input.addEventListener("change",(function(){if(!DISABLE_CHANGES){var resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};"checkbox"===input.type?resp.value=input.checked:resp.value=input.value;var _step7,_iterator7=_createForOfIteratorHelper(INPUTS[input.id]);try{for(_iterator7.s();!(_step7=_iterator7.n()).done;){var tgt=_step7.value;resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}}catch(err){_iterator7.e(err)}finally{_iterator7.f()}}}));else document.querySelectorAll("input[type=radio][name="+CSS.escape(input.name)+"]").forEach((function(inp){inp.addEventListener("change",(function(){if(!DISABLE_CHANGES){var resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};if(inp.checked){resp.value=inp.value;var _step8,_iterator8=_createForOfIteratorHelper(INPUTS[inp.id]);try{for(_iterator8.s();!(_step8=_iterator8.n()).done;){var tgt=_step8.value;resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}}catch(err){_iterator8.e(err)}finally{_iterator8.f()}}}}))}))}if("track-input"in msg&&msg["track-input"]&&"radio"!==input.type)if(input.id in INPUTS_INPUT_EVENT){if(msg.src in INPUTS_INPUT_EVENT[input.id])return;INPUTS_INPUT_EVENT[input.id].push(msg.src)}else INPUTS_INPUT_EVENT[input.id]=[msg.src],input.addEventListener("input",(function(){if(!DISABLE_CHANGES){var resp={version:"STACK-JS:1.0.0",type:"changed-input",name:msg.name};"checkbox"===input.type?resp.value=input.checked:resp.value=input.value;var _step9,_iterator9=_createForOfIteratorHelper(INPUTS_INPUT_EVENT[input.id]);try{for(_iterator9.s();!(_step9=_iterator9.n()).done;){var tgt=_step9.value;resp.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp),"*")}}catch(err){_iterator9.e(err)}finally{_iterator9.f()}}}));msg.src in INPUTS[input.id]||IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");break;case"changed-input":if(null===(input=vle_get_input_element(msg.name,msg.src))){var ret={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to modify input: "'+msg.name+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret),"*")}DISABLE_CHANGES=!0,"checkbox"===input.type?input.checked=msg.value:input.value=msg.value,function(inputelement){var c=new Event("change");inputelement.dispatchEvent(c);var i=new Event("input");inputelement.dispatchEvent(i)}(input),DISABLE_CHANGES=!1,response.type="changed-input",response.name=msg.name,response.value=msg.value;var _step10,_iterator10=_createForOfIteratorHelper(INPUTS[input.id]);try{for(_iterator10.s();!(_step10=_iterator10.n()).done;){var tgt=_step10.value;tgt!==msg.src&&(response.tgt=tgt,IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(response),"*"))}}catch(err){_iterator10.e(err)}finally{_iterator10.f()}break;case"toggle-visibility":if(null===(element=vle_get_element(msg.target))){var _ret={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to find element: "'+msg.target+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(_ret),"*")}"show"===msg.set?(element.style.display="block",vle_update_dom(element)):"hide"===msg.set&&(element.style.display="none");break;case"change-content":if(null===(element=vle_get_element(msg.target))){var _ret2={version:"STACK-JS:1.0.0",type:"error",msg:'Failed to find element: "'+msg.target+'"',tgt:msg.src};return void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(_ret2),"*")}element.replaceChildren(function(src){var _step,doc=(new DOMParser).parseFromString(src),_iterator=_createForOfIteratorHelper(doc.querySelectorAll("script, style"));try{for(_iterator.s();!(_step=_iterator.n()).done;)_step.value.remove()}catch(err){_iterator.e(err)}finally{_iterator.f()}var _step2,_iterator2=_createForOfIteratorHelper(doc.querySelectorAll("*"));try{for(_iterator2.s();!(_step2=_iterator2.n()).done;){var _step3,_el=_step2.value,_iterator3=_createForOfIteratorHelper(_el.attributes);try{for(_iterator3.s();!(_step3=_iterator3.n()).done;){var _step3$value=_step3.value,name=_step3$value.name;is_evil_attribute(name,_step3$value.value)&&_el.removeAttribute(name)}}catch(err){_iterator3.e(err)}finally{_iterator3.f()}}}catch(err){_iterator2.e(err)}finally{_iterator2.f()}return doc.body}(msg.content)),vle_update_dom(element);break;case"resize-frame":(element=IFRAMES[msg.src].parentElement).style.width=msg.width,element.style.height=msg.height,IFRAMES[msg.src].style.width="100%",IFRAMES[msg.src].style.height="100%",vle_update_dom(element);break;case"ping":return response.type="ping",response.tgt=msg.src,void IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*");case"initial-input":case"error":break;default:response.type="error",response.msg='Unknown message-type: "'+msg.type+'"',response.tgt=msg.src,IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response),"*")}}}})),{create_iframe:function(iframeid,content,targetdivid,title,scrolling,evil){var frm=document.createElement("iframe");frm.id=iframeid,frm.style.width="100%",frm.style.height="100%",frm.style.border=0,!1===scrolling?(frm.scrolling="no",frm.style.overflow="hidden"):frm.scrolling="yes",frm.title=title,frm.referrerpolicy="no-referrer",evil||(frm.sandbox="allow-scripts allow-downloads"),frm.srcdoc=content,document.getElementById(targetdivid).replaceChildren(frm),IFRAMES[iframeid]=frm}}})); //# sourceMappingURL=stackjsvle.min.js.map \ No newline at end of file diff --git a/amd/build/stackjsvle.min.js.map b/amd/build/stackjsvle.min.js.map index 465d0a28ff703877f2bdf2c723a049fcd7106f45..0189f3910d106200fa0800fadae0c3c70fc8abdd 100644 --- a/amd/build/stackjsvle.min.js.map +++ b/amd/build/stackjsvle.min.js.map @@ -1 +1 @@ -{"version":3,"file":"stackjsvle.min.js","sources":["../src/stackjsvle.js"],"sourcesContent":["/**\n * A javascript module to handle separation of author sourced scripts into\n * IFRAMES. All such scripts will have limited access to the actual document\n * on the VLE side and this script represents the VLE side endpoint for\n * message handling needed to give that access. When porting STACK onto VLEs\n * one needs to map this script to do the following:\n *\n * 1. Ensure that searches for target elements/inputs are limited to questions\n * and do not return any elements outside them.\n *\n * 2. Map any identifiers needed to identify inputs by name.\n *\n * 3. Any change handling related to input value modifications through this\n * logic gets connected to any such handling on the VLE side.\n *\n *\n * This script is intenttionally ordered so that the VLE specific bits should\n * be at the top.\n *\n *\n * This script assumes the following:\n *\n * 1. Each relevant IFRAME has an `id`-attribute that will be told to this\n * script.\n *\n * 2. Each such IFRAME exists within the question itself, so that one can\n * traverse up the DOM tree from that IFRAME to find the border of\n * the question.\n *\n * @module qtype_stack/stackjsvle\n * @copyright 2023 Aalto University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\"qtype_stack/stackjsvle\", [\"core/event\"], function(CustomEvents) {\n \"use strict\";\n // Note the VLE specific include of logic.\n\n /* All the IFRAMES have unique identifiers that they give in their\n * messages. But we only work with those that have been created by\n * our logic and are found from this map.\n */\n let IFRAMES = {};\n\n /* For event handling, lists of IFRAMES listening particular inputs.\n */\n let INPUTS = {};\n\n /* For event handling, lists of IFRAMES listening particular inputs\n * and their input events. By default we only listen to changes.\n * We report input events as changes to the other side.\n */\n let INPUTS_INPUT_EVENT = {};\n\n /* A flag to disable certain things. */\n let DISABLE_CHANGES = false;\n\n\n /**\n * Returns an element with a given id, if an only if that element exists\n * inside a portion of DOM that represents a question.\n *\n * If not found or exists outside the restricted area then returns `null`.\n *\n * @param {String} id the identifier of the element we want.\n */\n function vle_get_element(id) {\n /* In the case of Moodle we are happy as long as the element is inside\n something with the `formulation`-class. */\n let candidate = document.getElementById(id);\n let iter = candidate;\n while (iter && !iter.classList.contains('formulation')) {\n iter = iter.parentElement;\n }\n if (iter && iter.classList.contains('formulation')) {\n return candidate;\n }\n\n return null;\n }\n\n /**\n * Returns an input element with a given name, if an only if that element\n * exists inside a portion of DOM that represents a question.\n *\n * Note that, the input element may have a name that multiple questions\n * use and to pick the preferred element one needs to pick the one\n * within the same question as the IFRAME.\n *\n * Note that the input can also be a select. In the case of radio buttons\n * returning one of the possible buttons is enough.\n *\n * If not found or exists outside the restricted area then returns `null`.\n *\n * @param {String} name the name of the input we want\n * @param {String} srciframe the identifier of the iframe wanting it\n */\n function vle_get_input_element(name, srciframe) {\n /* In the case of Moodle we are happy as long as the element is inside\n something with the `formulation`-class. */\n let initialcandidate = document.getElementById(srciframe);\n let iter = initialcandidate;\n while (iter && !iter.classList.contains('formulation')) {\n iter = iter.parentElement;\n }\n if (iter && iter.classList.contains('formulation')) {\n // iter now represents the borders of the question containing\n // this IFRAME.\n let possible = iter.querySelector('input[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n // Radios have interesting ids, but the name makes sense\n possible = iter.querySelector('input[id$=\"_' + name + '_1\"][type=radio]');\n if (possible !== null) {\n return possible;\n }\n possible = iter.querySelector('select[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n }\n // If none found within the question itself, search everywhere.\n let possible = document.querySelector('.formulation input[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n // Radios have interesting ids, but the name makes sense\n possible = document.querySelector('.formulation input[id$=\"_' + name + '_1\"][type=radio]');\n if (possible !== null) {\n return possible;\n }\n possible = document.querySelector('.formulation select[id$=\"_' + name + '\"]');\n return possible;\n }\n\n /**\n * Triggers any VLE specific scripting related to updates of the given\n * input element.\n *\n * @param {HTMLElement} inputelement the input element that has changed\n */\n function vle_update_input(inputelement) {\n // Triggering a change event may be necessary.\n const c = new Event('change');\n inputelement.dispatchEvent(c);\n // Also there are those that listen to input events.\n const i = new Event('input');\n inputelement.dispatchEvent(i);\n }\n\n /**\n * Triggers any VLE specific scripting related to DOM updates.\n *\n * @param {HTMLElement} modifiedsubtreerootelement element under which changes may have happened.\n */\n function vle_update_dom(modifiedsubtreerootelement) {\n CustomEvents.notifyFilterContentUpdated(modifiedsubtreerootelement);\n }\n\n /**\n * Does HTML-string cleaning, i.e., removes any script payload. Returns\n * a DOM version of the given input string.\n *\n * This is used when receiving replacement content for a div.\n *\n * @param {String} src a raw string to sanitise\n */\n function vle_html_sanitize(src) {\n // This can be implemented with many libraries or by custom code\n // however as this is typically a thing that a VLE might already have\n // tools for we have it at this level so that the VLE can use its own\n // tools that do things that the VLE developpers consider safe.\n\n // As Moodle does not currently seem to have such a sanitizer in\n // the core libraries, here is one implementation that shows what we\n // are looking for.\n\n // TODO: look into replacing this with DOMPurify or some such.\n\n let parser = new DOMParser();\n let doc = parser.parseFromString(src);\n\n // First remove all <script> tags. Also <style> as we do not want\n // to include too much style.\n for (let el of doc.querySelectorAll('script, style')) {\n el.remove();\n }\n\n // Check all elements for attributes.\n for (let el of doc.querySelectorAll('*')) {\n for (let {name, value} of el.attributes) {\n if (is_evil_attribute(name, value)) {\n el.removeAttribute(name);\n }\n }\n }\n\n return doc.body;\n }\n\n /**\n * Utility function trying to determine if a given attribute is evil\n * when sanitizing HTML-fragments.\n *\n * @param {String} name the name of an attribute.\n * @param {String} value the value of an attribute.\n */\n function is_evil_attribute(name, value) {\n const lcname = name.toLowerCase();\n if (lcname.startsWith('on')) {\n // We do not allow event listeners to be defined.\n return true;\n }\n if (lcname === 'src' || lcname.endsWith('href')) {\n // Do not allow certain things in the urls.\n const lcvalue = value.replace(/\\s+/g, '').toLowerCase();\n // Ignore es-lint false positive.\n /* eslint-disable no-script-url */\n if (lcvalue.includes('javascript:') || lcvalue.includes('data:text')) {\n return true;\n }\n }\n\n return false;\n }\n\n\n /*************************************************************************\n * Above this are the bits that one would probably tune when porting.\n *\n * Below is the actuall message handling and it should be left alone.\n */\n window.addEventListener(\"message\", (e) => {\n // NOTE! We do not check the source or origin of the message in\n // the normal way. All actions that can bypass our filters to trigger\n // something are largely irrelevant and all traffic will be kept\n // \"safe\" as anyone could be listening.\n\n // All messages we receive are strings, anything else is for someone\n // else and will be ignored.\n if (!(typeof e.data === 'string' || e.data instanceof String)) {\n return;\n }\n\n // That string is a JSON encoded dictionary.\n let msg = null;\n try {\n msg = JSON.parse(e.data);\n } catch (e) {\n // Only JSON objects that are parseable will work.\n return;\n }\n\n // All messages we handle contain a version field with a particular\n // value, for now we leave the possibility open for that value to have\n // an actual version number suffix...\n if (!(('version' in msg) && msg.version.startsWith('STACK-JS'))) {\n return;\n }\n\n // All messages we handle must have a source and a type,\n // and that source must be one of the registered ones.\n if (!(('src' in msg) && ('type' in msg) && (msg.src in IFRAMES))) {\n return;\n }\n let element = null;\n let input = null;\n\n let response = {\n version: 'STACK-JS:1.0.0'\n };\n\n switch (msg.type) {\n case 'register-input-listener':\n // 1. Find the input.\n input = vle_get_input_element(msg.name, msg.src);\n\n if (input === null) {\n // Requested something that is not available.\n response.type = 'error';\n response.msg = 'Failed to connect to input: \"' + msg.name + '\"';\n response.tgt = msg.src;\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n return;\n }\n\n response.type = 'initial-input';\n response.name = msg.name;\n response.tgt = msg.src;\n\n // 2. What type of an input is this? Note that we do not\n // currently support all types in sensible ways. In particular,\n // anything with multiple values will be a problem.\n if (input.nodeName.toLowerCase() === 'select') {\n response.value = input.value;\n response['input-type'] = 'select';\n } else if (input.type === 'checkbox') {\n response.value = input.checked;\n response['input-type'] = 'checkbox';\n } else {\n response.value = input.value;\n response['input-type'] = input.type;\n }\n if (input.type === 'radio') {\n response.value = '';\n for (let inp of document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']')) {\n if (inp.checked) {\n response.value = inp.value;\n }\n }\n }\n\n // 3. Add listener for changes of this input.\n if (input.id in INPUTS) {\n if (msg.src in INPUTS[input.id]) {\n // DO NOT BIND TWICE!\n return;\n }\n if (input.type !== 'radio') {\n INPUTS[input.id].push(msg.src);\n } else {\n let radgroup = document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']');\n for (let inp of radgroup) {\n INPUTS[inp.id].push(msg.src);\n }\n }\n } else {\n if (input.type !== 'radio') {\n INPUTS[input.id] = [msg.src];\n } else {\n let radgroup = document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']');\n for (let inp of radgroup) {\n INPUTS[inp.id] = [msg.src];\n }\n }\n if (input.type !== 'radio') {\n input.addEventListener('change', () => {\n if (DISABLE_CHANGES) {\n return;\n }\n let resp = {\n version: 'STACK-JS:1.0.0',\n type: 'changed-input',\n name: msg.name\n };\n if (input.type === 'checkbox') {\n resp['value'] = input.checked;\n } else {\n resp['value'] = input.value;\n }\n for (let tgt of INPUTS[input.id]) {\n resp['tgt'] = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');\n }\n });\n } else {\n // Assume that if we received a radio button that is safe\n // then all its friends are also safe.\n let radgroup = document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']');\n radgroup.forEach((inp) => {\n inp.addEventListener('change', () => {\n if (DISABLE_CHANGES) {\n return;\n }\n let resp = {\n version: 'STACK-JS:1.0.0',\n type: 'changed-input',\n name: msg.name\n };\n if (inp.checked) {\n resp.value = inp.value;\n } else {\n // What about unsetting?\n return;\n }\n for (let tgt of INPUTS[inp.id]) {\n resp['tgt'] = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');\n }\n });\n });\n }\n }\n\n if (('track-input' in msg) && msg['track-input'] && input.type !== 'radio') {\n if (input.id in INPUTS_INPUT_EVENT) {\n if (msg.src in INPUTS_INPUT_EVENT[input.id]) {\n // DO NOT BIND TWICE!\n return;\n }\n INPUTS_INPUT_EVENT[input.id].push(msg.src);\n } else {\n INPUTS_INPUT_EVENT[input.id] = [msg.src];\n\n input.addEventListener('input', () => {\n if (DISABLE_CHANGES) {\n return;\n }\n let resp = {\n version: 'STACK-JS:1.0.0',\n type: 'changed-input',\n name: msg.name\n };\n if (input.type === 'checkbox') {\n resp['value'] = input.checked;\n } else {\n resp['value'] = input.value;\n }\n for (let tgt of INPUTS_INPUT_EVENT[input.id]) {\n resp['tgt'] = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');\n }\n });\n }\n }\n\n // 4. Let the requester know that we have bound things\n // and let it know the initial value.\n if (!(msg.src in INPUTS[input.id])) {\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n\n break;\n case 'changed-input':\n // 1. Find the input.\n input = vle_get_input_element(msg.name, msg.src);\n\n if (input === null) {\n // Requested something that is not available.\n const ret = {\n version: 'STACK-JS:1.0.0',\n type: 'error',\n msg: 'Failed to modify input: \"' + msg.name + '\"',\n tgt: msg.src\n };\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), '*');\n return;\n }\n\n // Disable change events.\n DISABLE_CHANGES = true;\n\n // TODO: Radio buttons should we check that value is possible?\n if (input.type === 'checkbox') {\n input.checked = msg.value;\n } else {\n input.value = msg.value;\n }\n\n // Trigger VLE side actions.\n vle_update_input(input);\n\n // Enable change tracking.\n DISABLE_CHANGES = false;\n\n // Tell all other frames, that care, about this.\n response.type = 'changed-input';\n response.name = msg.name;\n response.value = msg.value;\n\n for (let tgt of INPUTS[input.id]) {\n if (tgt !== msg.src) {\n response.tgt = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n }\n\n break;\n case 'toggle-visibility':\n // 1. Find the element.\n element = vle_get_element(msg.target);\n\n if (element === null) {\n // Requested something that is not available.\n const ret = {\n version: 'STACK-JS:1.0.0',\n type: 'error',\n msg: 'Failed to find element: \"' + msg.target + '\"',\n tgt: msg.src\n };\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), '*');\n return;\n }\n\n // 2. Toggle display setting.\n if (msg.set === 'show') {\n element.style.display = 'block';\n // If we make something visible we should let the VLE know about it.\n vle_update_dom(element);\n } else if (msg.set === 'hide') {\n element.style.display = 'none';\n }\n\n break;\n case 'change-content':\n // 1. Find the element.\n element = vle_get_element(msg.target);\n\n if (element === null) {\n // Requested something that is not available.\n const ret = {\n version: 'STACK-JS:1.0.0',\n type: 'error',\n msg: 'Failed to find element: \"' + msg.target + '\"',\n tgt: msg.src\n };\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), '*');\n return;\n }\n\n // 2. Secure content.\n // 3. Switch the content.\n element.replaceChildren(vle_html_sanitize(msg.content));\n // If we tune something we should let the VLE know about it.\n vle_update_dom(element);\n\n break;\n case 'resize-frame':\n // 1. Find the frames wrapper div.\n element = IFRAMES[msg.src].parentElement;\n\n // 2. Set the wrapper size.\n element.style.width = msg.width;\n element.style.height = msg.height;\n\n // 3. Reset the frame size.\n IFRAMES[msg.src].style.width = '100%';\n IFRAMES[msg.src].style.height = '100%';\n\n // Only touching the size but still let the VLE know.\n vle_update_dom(element);\n break;\n case 'ping':\n // This is for testing the connection. The other end will\n // send these untill it receives a reply.\n // Part of the logic for startup.\n response.type = 'ping';\n response.tgt = msg.src;\n\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n return;\n case 'initial-input':\n case 'error':\n // These message types are for the other end.\n break;\n\n default:\n // If we see something unexpected, lets let the other end know\n // and make sure that they know our version. Could be that this\n // end has not been upgraded.\n response.type = 'error';\n response.msg = 'Unknown message-type: \"' + msg.type + '\"';\n response.tgt = msg.src;\n\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n\n });\n\n\n return {\n /* To avoid any logic that forbids IFRAMEs in the VLE output one can\n also create and register that IFRAME through this logic. This\n also ensures that all relevant security settigns for that IFRAME\n have been correctly tuned.\n\n Here the IDs are for the secrect identifier that may be present\n inside the content of that IFRAME and for the question that contains\n it. One also identifies a DIV element that marks the position of\n the IFRAME and limits the size of the IFRAME (all IFRAMEs this\n creates will be 100% x 100%).\n\n @param {String} iframeid the id that the IFRAME has stored inside\n it and uses for communication.\n @param {String} the full HTML content of that IFRAME.\n @param {String} targetdivid the id of the element (div) that will\n hold the IFRAME.\n @param {String} title a descriptive name for the iframe.\n @param {bool} scrolling whether we have overflow:scroll or\n overflow:hidden.\n @param {bool} evil allows certain special cases to act without\n sandboxing, this is a feature that will be removed so do\n not rely on it only use it to test STACK-JS before you get your\n thing to run in a sandbox.\n */\n create_iframe(iframeid, content, targetdivid, title, scrolling, evil) {\n const frm = document.createElement('iframe');\n frm.id = iframeid;\n frm.style.width = '100%';\n frm.style.height = '100%';\n frm.style.border = 0;\n if (scrolling === false) {\n frm.scrolling = 'no';\n frm.style.overflow = 'hidden';\n } else {\n frm.scrolling = 'yes';\n }\n frm.title = title;\n // Somewhat random limitation.\n frm.referrerpolicy = 'no-referrer';\n // We include that allow-downloads as an example of XLS-\n // document building in JS has been seen.\n // UNDER NO CIRCUMSTANCES DO WE ALLOW-SAME-ORIGIN!\n // That would defeat the whole point of this.\n if (!evil) {\n frm.sandbox = 'allow-scripts allow-downloads';\n }\n\n // As the SOP is intentionally broken we need to allow\n // scripts from everywhere.\n frm.csp = \"script-src: 'unsafe-inline' 'self' '*';img-src: '*';\";\n\n // The target DIV will have its children removed.\n // This allows that div to contain some sort of loading\n // indicator until we plug in the frame.\n // Naturally the frame will then start to load itself.\n document.getElementById(targetdivid).replaceChildren(frm);\n IFRAMES[iframeid] = frm;\n\n // Move the content over.\n const src = new Blob([content], {type: 'text/html; charset=utf-8'});\n frm.src = URL.createObjectURL(src);\n }\n\n };\n});"],"names":["define","CustomEvents","IFRAMES","INPUTS","INPUTS_INPUT_EVENT","DISABLE_CHANGES","vle_get_element","id","candidate","document","getElementById","iter","classList","contains","parentElement","vle_get_input_element","name","srciframe","possible","querySelector","vle_update_dom","modifiedsubtreerootelement","notifyFilterContentUpdated","is_evil_attribute","value","lcname","toLowerCase","startsWith","endsWith","lcvalue","replace","includes","window","addEventListener","e","data","String","msg","JSON","parse","version","src","element","input","response","type","tgt","contentWindow","postMessage","stringify","nodeName","checked","querySelectorAll","CSS","escape","inp","push","resp","forEach","ret","inputelement","c","Event","dispatchEvent","i","vle_update_input","target","set","style","display","replaceChildren","doc","DOMParser","parseFromString","remove","el","attributes","removeAttribute","body","vle_html_sanitize","content","width","height","create_iframe","iframeid","targetdivid","title","scrolling","evil","frm","createElement","border","overflow","referrerpolicy","sandbox","csp","Blob","URL","createObjectURL"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"stackjsvle.min.js","sources":["../src/stackjsvle.js"],"sourcesContent":["/**\n * A javascript module to handle separation of author sourced scripts into\n * IFRAMES. All such scripts will have limited access to the actual document\n * on the VLE side and this script represents the VLE side endpoint for\n * message handling needed to give that access. When porting STACK onto VLEs\n * one needs to map this script to do the following:\n *\n * 1. Ensure that searches for target elements/inputs are limited to questions\n * and do not return any elements outside them.\n *\n * 2. Map any identifiers needed to identify inputs by name.\n *\n * 3. Any change handling related to input value modifications through this\n * logic gets connected to any such handling on the VLE side.\n *\n *\n * This script is intenttionally ordered so that the VLE specific bits should\n * be at the top.\n *\n *\n * This script assumes the following:\n *\n * 1. Each relevant IFRAME has an `id`-attribute that will be told to this\n * script.\n *\n * 2. Each such IFRAME exists within the question itself, so that one can\n * traverse up the DOM tree from that IFRAME to find the border of\n * the question.\n *\n * @module qtype_stack/stackjsvle\n * @copyright 2023 Aalto University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\"qtype_stack/stackjsvle\", [\"core/event\"], function(CustomEvents) {\n \"use strict\";\n // Note the VLE specific include of logic.\n\n /* All the IFRAMES have unique identifiers that they give in their\n * messages. But we only work with those that have been created by\n * our logic and are found from this map.\n */\n let IFRAMES = {};\n\n /* For event handling, lists of IFRAMES listening particular inputs.\n */\n let INPUTS = {};\n\n /* For event handling, lists of IFRAMES listening particular inputs\n * and their input events. By default we only listen to changes.\n * We report input events as changes to the other side.\n */\n let INPUTS_INPUT_EVENT = {};\n\n /* A flag to disable certain things. */\n let DISABLE_CHANGES = false;\n\n\n /**\n * Returns an element with a given id, if an only if that element exists\n * inside a portion of DOM that represents a question.\n *\n * If not found or exists outside the restricted area then returns `null`.\n *\n * @param {String} id the identifier of the element we want.\n */\n function vle_get_element(id) {\n /* In the case of Moodle we are happy as long as the element is inside\n something with the `formulation`-class. */\n let candidate = document.getElementById(id);\n let iter = candidate;\n while (iter && !iter.classList.contains('formulation')) {\n iter = iter.parentElement;\n }\n if (iter && iter.classList.contains('formulation')) {\n return candidate;\n }\n\n return null;\n }\n\n /**\n * Returns an input element with a given name, if an only if that element\n * exists inside a portion of DOM that represents a question.\n *\n * Note that, the input element may have a name that multiple questions\n * use and to pick the preferred element one needs to pick the one\n * within the same question as the IFRAME.\n *\n * Note that the input can also be a select. In the case of radio buttons\n * returning one of the possible buttons is enough.\n *\n * If not found or exists outside the restricted area then returns `null`.\n *\n * @param {String} name the name of the input we want\n * @param {String} srciframe the identifier of the iframe wanting it\n */\n function vle_get_input_element(name, srciframe) {\n /* In the case of Moodle we are happy as long as the element is inside\n something with the `formulation`-class. */\n let initialcandidate = document.getElementById(srciframe);\n let iter = initialcandidate;\n while (iter && !iter.classList.contains('formulation')) {\n iter = iter.parentElement;\n }\n if (iter && iter.classList.contains('formulation')) {\n // iter now represents the borders of the question containing\n // this IFRAME.\n let possible = iter.querySelector('input[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n // Radios have interesting ids, but the name makes sense\n possible = iter.querySelector('input[id$=\"_' + name + '_1\"][type=radio]');\n if (possible !== null) {\n return possible;\n }\n possible = iter.querySelector('select[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n }\n // If none found within the question itself, search everywhere.\n let possible = document.querySelector('.formulation input[id$=\"_' + name + '\"]');\n if (possible !== null) {\n return possible;\n }\n // Radios have interesting ids, but the name makes sense\n possible = document.querySelector('.formulation input[id$=\"_' + name + '_1\"][type=radio]');\n if (possible !== null) {\n return possible;\n }\n possible = document.querySelector('.formulation select[id$=\"_' + name + '\"]');\n return possible;\n }\n\n /**\n * Triggers any VLE specific scripting related to updates of the given\n * input element.\n *\n * @param {HTMLElement} inputelement the input element that has changed\n */\n function vle_update_input(inputelement) {\n // Triggering a change event may be necessary.\n const c = new Event('change');\n inputelement.dispatchEvent(c);\n // Also there are those that listen to input events.\n const i = new Event('input');\n inputelement.dispatchEvent(i);\n }\n\n /**\n * Triggers any VLE specific scripting related to DOM updates.\n *\n * @param {HTMLElement} modifiedsubtreerootelement element under which changes may have happened.\n */\n function vle_update_dom(modifiedsubtreerootelement) {\n CustomEvents.notifyFilterContentUpdated(modifiedsubtreerootelement);\n }\n\n /**\n * Does HTML-string cleaning, i.e., removes any script payload. Returns\n * a DOM version of the given input string.\n *\n * This is used when receiving replacement content for a div.\n *\n * @param {String} src a raw string to sanitise\n */\n function vle_html_sanitize(src) {\n // This can be implemented with many libraries or by custom code\n // however as this is typically a thing that a VLE might already have\n // tools for we have it at this level so that the VLE can use its own\n // tools that do things that the VLE developpers consider safe.\n\n // As Moodle does not currently seem to have such a sanitizer in\n // the core libraries, here is one implementation that shows what we\n // are looking for.\n\n // TODO: look into replacing this with DOMPurify or some such.\n\n let parser = new DOMParser();\n let doc = parser.parseFromString(src);\n\n // First remove all <script> tags. Also <style> as we do not want\n // to include too much style.\n for (let el of doc.querySelectorAll('script, style')) {\n el.remove();\n }\n\n // Check all elements for attributes.\n for (let el of doc.querySelectorAll('*')) {\n for (let {name, value} of el.attributes) {\n if (is_evil_attribute(name, value)) {\n el.removeAttribute(name);\n }\n }\n }\n\n return doc.body;\n }\n\n /**\n * Utility function trying to determine if a given attribute is evil\n * when sanitizing HTML-fragments.\n *\n * @param {String} name the name of an attribute.\n * @param {String} value the value of an attribute.\n */\n function is_evil_attribute(name, value) {\n const lcname = name.toLowerCase();\n if (lcname.startsWith('on')) {\n // We do not allow event listeners to be defined.\n return true;\n }\n if (lcname === 'src' || lcname.endsWith('href')) {\n // Do not allow certain things in the urls.\n const lcvalue = value.replace(/\\s+/g, '').toLowerCase();\n // Ignore es-lint false positive.\n /* eslint-disable no-script-url */\n if (lcvalue.includes('javascript:') || lcvalue.includes('data:text')) {\n return true;\n }\n }\n\n return false;\n }\n\n\n /*************************************************************************\n * Above this are the bits that one would probably tune when porting.\n *\n * Below is the actuall message handling and it should be left alone.\n */\n window.addEventListener(\"message\", (e) => {\n // NOTE! We do not check the source or origin of the message in\n // the normal way. All actions that can bypass our filters to trigger\n // something are largely irrelevant and all traffic will be kept\n // \"safe\" as anyone could be listening.\n\n // All messages we receive are strings, anything else is for someone\n // else and will be ignored.\n if (!(typeof e.data === 'string' || e.data instanceof String)) {\n return;\n }\n\n // That string is a JSON encoded dictionary.\n let msg = null;\n try {\n msg = JSON.parse(e.data);\n } catch (e) {\n // Only JSON objects that are parseable will work.\n return;\n }\n\n // All messages we handle contain a version field with a particular\n // value, for now we leave the possibility open for that value to have\n // an actual version number suffix...\n if (!(('version' in msg) && msg.version.startsWith('STACK-JS'))) {\n return;\n }\n\n // All messages we handle must have a source and a type,\n // and that source must be one of the registered ones.\n if (!(('src' in msg) && ('type' in msg) && (msg.src in IFRAMES))) {\n return;\n }\n let element = null;\n let input = null;\n\n let response = {\n version: 'STACK-JS:1.0.0'\n };\n\n switch (msg.type) {\n case 'register-input-listener':\n // 1. Find the input.\n input = vle_get_input_element(msg.name, msg.src);\n\n if (input === null) {\n // Requested something that is not available.\n response.type = 'error';\n response.msg = 'Failed to connect to input: \"' + msg.name + '\"';\n response.tgt = msg.src;\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n return;\n }\n\n response.type = 'initial-input';\n response.name = msg.name;\n response.tgt = msg.src;\n\n // 2. What type of an input is this? Note that we do not\n // currently support all types in sensible ways. In particular,\n // anything with multiple values will be a problem.\n if (input.nodeName.toLowerCase() === 'select') {\n response.value = input.value;\n response['input-type'] = 'select';\n } else if (input.type === 'checkbox') {\n response.value = input.checked;\n response['input-type'] = 'checkbox';\n } else {\n response.value = input.value;\n response['input-type'] = input.type;\n }\n if (input.type === 'radio') {\n response.value = '';\n for (let inp of document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']')) {\n if (inp.checked) {\n response.value = inp.value;\n }\n }\n }\n\n // 3. Add listener for changes of this input.\n if (input.id in INPUTS) {\n if (msg.src in INPUTS[input.id]) {\n // DO NOT BIND TWICE!\n return;\n }\n if (input.type !== 'radio') {\n INPUTS[input.id].push(msg.src);\n } else {\n let radgroup = document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']');\n for (let inp of radgroup) {\n INPUTS[inp.id].push(msg.src);\n }\n }\n } else {\n if (input.type !== 'radio') {\n INPUTS[input.id] = [msg.src];\n } else {\n let radgroup = document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']');\n for (let inp of radgroup) {\n INPUTS[inp.id] = [msg.src];\n }\n }\n if (input.type !== 'radio') {\n input.addEventListener('change', () => {\n if (DISABLE_CHANGES) {\n return;\n }\n let resp = {\n version: 'STACK-JS:1.0.0',\n type: 'changed-input',\n name: msg.name\n };\n if (input.type === 'checkbox') {\n resp['value'] = input.checked;\n } else {\n resp['value'] = input.value;\n }\n for (let tgt of INPUTS[input.id]) {\n resp['tgt'] = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');\n }\n });\n } else {\n // Assume that if we received a radio button that is safe\n // then all its friends are also safe.\n let radgroup = document.querySelectorAll('input[type=radio][name=' + CSS.escape(input.name) + ']');\n radgroup.forEach((inp) => {\n inp.addEventListener('change', () => {\n if (DISABLE_CHANGES) {\n return;\n }\n let resp = {\n version: 'STACK-JS:1.0.0',\n type: 'changed-input',\n name: msg.name\n };\n if (inp.checked) {\n resp.value = inp.value;\n } else {\n // What about unsetting?\n return;\n }\n for (let tgt of INPUTS[inp.id]) {\n resp['tgt'] = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');\n }\n });\n });\n }\n }\n\n if (('track-input' in msg) && msg['track-input'] && input.type !== 'radio') {\n if (input.id in INPUTS_INPUT_EVENT) {\n if (msg.src in INPUTS_INPUT_EVENT[input.id]) {\n // DO NOT BIND TWICE!\n return;\n }\n INPUTS_INPUT_EVENT[input.id].push(msg.src);\n } else {\n INPUTS_INPUT_EVENT[input.id] = [msg.src];\n\n input.addEventListener('input', () => {\n if (DISABLE_CHANGES) {\n return;\n }\n let resp = {\n version: 'STACK-JS:1.0.0',\n type: 'changed-input',\n name: msg.name\n };\n if (input.type === 'checkbox') {\n resp['value'] = input.checked;\n } else {\n resp['value'] = input.value;\n }\n for (let tgt of INPUTS_INPUT_EVENT[input.id]) {\n resp['tgt'] = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(resp), '*');\n }\n });\n }\n }\n\n // 4. Let the requester know that we have bound things\n // and let it know the initial value.\n if (!(msg.src in INPUTS[input.id])) {\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n\n break;\n case 'changed-input':\n // 1. Find the input.\n input = vle_get_input_element(msg.name, msg.src);\n\n if (input === null) {\n // Requested something that is not available.\n const ret = {\n version: 'STACK-JS:1.0.0',\n type: 'error',\n msg: 'Failed to modify input: \"' + msg.name + '\"',\n tgt: msg.src\n };\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), '*');\n return;\n }\n\n // Disable change events.\n DISABLE_CHANGES = true;\n\n // TODO: Radio buttons should we check that value is possible?\n if (input.type === 'checkbox') {\n input.checked = msg.value;\n } else {\n input.value = msg.value;\n }\n\n // Trigger VLE side actions.\n vle_update_input(input);\n\n // Enable change tracking.\n DISABLE_CHANGES = false;\n\n // Tell all other frames, that care, about this.\n response.type = 'changed-input';\n response.name = msg.name;\n response.value = msg.value;\n\n for (let tgt of INPUTS[input.id]) {\n if (tgt !== msg.src) {\n response.tgt = tgt;\n IFRAMES[tgt].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n }\n\n break;\n case 'toggle-visibility':\n // 1. Find the element.\n element = vle_get_element(msg.target);\n\n if (element === null) {\n // Requested something that is not available.\n const ret = {\n version: 'STACK-JS:1.0.0',\n type: 'error',\n msg: 'Failed to find element: \"' + msg.target + '\"',\n tgt: msg.src\n };\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), '*');\n return;\n }\n\n // 2. Toggle display setting.\n if (msg.set === 'show') {\n element.style.display = 'block';\n // If we make something visible we should let the VLE know about it.\n vle_update_dom(element);\n } else if (msg.set === 'hide') {\n element.style.display = 'none';\n }\n\n break;\n case 'change-content':\n // 1. Find the element.\n element = vle_get_element(msg.target);\n\n if (element === null) {\n // Requested something that is not available.\n const ret = {\n version: 'STACK-JS:1.0.0',\n type: 'error',\n msg: 'Failed to find element: \"' + msg.target + '\"',\n tgt: msg.src\n };\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(ret), '*');\n return;\n }\n\n // 2. Secure content.\n // 3. Switch the content.\n element.replaceChildren(vle_html_sanitize(msg.content));\n // If we tune something we should let the VLE know about it.\n vle_update_dom(element);\n\n break;\n case 'resize-frame':\n // 1. Find the frames wrapper div.\n element = IFRAMES[msg.src].parentElement;\n\n // 2. Set the wrapper size.\n element.style.width = msg.width;\n element.style.height = msg.height;\n\n // 3. Reset the frame size.\n IFRAMES[msg.src].style.width = '100%';\n IFRAMES[msg.src].style.height = '100%';\n\n // Only touching the size but still let the VLE know.\n vle_update_dom(element);\n break;\n case 'ping':\n // This is for testing the connection. The other end will\n // send these untill it receives a reply.\n // Part of the logic for startup.\n response.type = 'ping';\n response.tgt = msg.src;\n\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n return;\n case 'initial-input':\n case 'error':\n // These message types are for the other end.\n break;\n\n default:\n // If we see something unexpected, lets let the other end know\n // and make sure that they know our version. Could be that this\n // end has not been upgraded.\n response.type = 'error';\n response.msg = 'Unknown message-type: \"' + msg.type + '\"';\n response.tgt = msg.src;\n\n IFRAMES[msg.src].contentWindow.postMessage(JSON.stringify(response), '*');\n }\n\n });\n\n\n return {\n /* To avoid any logic that forbids IFRAMEs in the VLE output one can\n also create and register that IFRAME through this logic. This\n also ensures that all relevant security settigns for that IFRAME\n have been correctly tuned.\n\n Here the IDs are for the secrect identifier that may be present\n inside the content of that IFRAME and for the question that contains\n it. One also identifies a DIV element that marks the position of\n the IFRAME and limits the size of the IFRAME (all IFRAMEs this\n creates will be 100% x 100%).\n\n @param {String} iframeid the id that the IFRAME has stored inside\n it and uses for communication.\n @param {String} the full HTML content of that IFRAME.\n @param {String} targetdivid the id of the element (div) that will\n hold the IFRAME.\n @param {String} title a descriptive name for the iframe.\n @param {bool} scrolling whether we have overflow:scroll or\n overflow:hidden.\n @param {bool} evil allows certain special cases to act without\n sandboxing, this is a feature that will be removed so do\n not rely on it only use it to test STACK-JS before you get your\n thing to run in a sandbox.\n */\n create_iframe(iframeid, content, targetdivid, title, scrolling, evil) {\n const frm = document.createElement('iframe');\n frm.id = iframeid;\n frm.style.width = '100%';\n frm.style.height = '100%';\n frm.style.border = 0;\n if (scrolling === false) {\n frm.scrolling = 'no';\n frm.style.overflow = 'hidden';\n } else {\n frm.scrolling = 'yes';\n }\n frm.title = title;\n // Somewhat random limitation.\n frm.referrerpolicy = 'no-referrer';\n // We include that allow-downloads as an example of XLS-\n // document building in JS has been seen.\n // UNDER NO CIRCUMSTANCES DO WE ALLOW-SAME-ORIGIN!\n // That would defeat the whole point of this.\n if (!evil) {\n frm.sandbox = 'allow-scripts allow-downloads';\n }\n\n // As the SOP is intentionally broken we need to allow\n // scripts from everywhere.\n\n // NOTE: this bit commented out as long as the csp-attribute\n // is not supported by more browsers.\n // frm.csp = \"script-src: 'unsafe-inline' 'self' '*';\";\n // frm.csp = \"script-src: 'unsafe-inline' 'self' '*';img-src: '*';\";\n\n // Plug the content into the frame.\n frm.srcdoc = content;\n\n // The target DIV will have its children removed.\n // This allows that div to contain some sort of loading\n // indicator until we plug in the frame.\n // Naturally the frame will then start to load itself.\n document.getElementById(targetdivid).replaceChildren(frm);\n IFRAMES[iframeid] = frm;\n }\n\n };\n});"],"names":["define","CustomEvents","IFRAMES","INPUTS","INPUTS_INPUT_EVENT","DISABLE_CHANGES","vle_get_element","id","candidate","document","getElementById","iter","classList","contains","parentElement","vle_get_input_element","name","srciframe","possible","querySelector","vle_update_dom","modifiedsubtreerootelement","notifyFilterContentUpdated","is_evil_attribute","value","lcname","toLowerCase","startsWith","endsWith","lcvalue","replace","includes","window","addEventListener","e","data","String","msg","JSON","parse","version","src","element","input","response","type","tgt","contentWindow","postMessage","stringify","nodeName","checked","querySelectorAll","CSS","escape","inp","push","resp","forEach","ret","inputelement","c","Event","dispatchEvent","i","vle_update_input","target","set","style","display","replaceChildren","doc","DOMParser","parseFromString","remove","el","attributes","removeAttribute","body","vle_html_sanitize","content","width","height","create_iframe","iframeid","targetdivid","title","scrolling","evil","frm","createElement","border","overflow","referrerpolicy","sandbox","srcdoc"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/stackjsvle.js b/amd/src/stackjsvle.js index da700968c746491bd8f3985d92cb40c027285740..5b829bb6c6e28d6d20a02bc15f18d9db6bc5f216 100644 --- a/amd/src/stackjsvle.js +++ b/amd/src/stackjsvle.js @@ -608,7 +608,14 @@ define("qtype_stack/stackjsvle", ["core/event"], function(CustomEvents) { // As the SOP is intentionally broken we need to allow // scripts from everywhere. - frm.csp = "script-src: 'unsafe-inline' 'self' '*';img-src: '*';"; + + // NOTE: this bit commented out as long as the csp-attribute + // is not supported by more browsers. + // frm.csp = "script-src: 'unsafe-inline' 'self' '*';"; + // frm.csp = "script-src: 'unsafe-inline' 'self' '*';img-src: '*';"; + + // Plug the content into the frame. + frm.srcdoc = content; // The target DIV will have its children removed. // This allows that div to contain some sort of loading @@ -616,10 +623,6 @@ define("qtype_stack/stackjsvle", ["core/event"], function(CustomEvents) { // Naturally the frame will then start to load itself. document.getElementById(targetdivid).replaceChildren(frm); IFRAMES[iframeid] = frm; - - // Move the content over. - const src = new Blob([content], {type: 'text/html; charset=utf-8'}); - frm.src = URL.createObjectURL(src); } }; diff --git a/deploy.php b/deploy.php index fb43b60aceda289195e163dad4abd997bca8ae7b..5c17e1ae7807af969523abc093eaf424369dc17f 100644 --- a/deploy.php +++ b/deploy.php @@ -80,13 +80,18 @@ if (!is_null($undeploy) && $question->deployedseeds) { // Process undeployall if applicable. $deployfromlist = optional_param('deployfromlist', null, PARAM_INT); -if (!is_null($deployfromlist)) { +$deploysystematic = optional_param('deploysystematic', null, PARAM_INT); +if (!is_null($deployfromlist) || !is_null($deploysystematic)) { // Check data integrity. $dataproblem = false; - $deploytxt = optional_param('deployfromlist', null, PARAM_TEXT); - $baseseeds = explode("\n", trim($deploytxt)); + if (!is_null($deployfromlist)) { + $deploytxt = optional_param('deployfromlist', null, PARAM_TEXT); + $baseseeds = explode("\n", trim($deploytxt)); + } else { + $baseseeds = range(1, $deploysystematic); + } $newseeds = array(); foreach ($baseseeds as $newseed) { // Now also explode over commas. diff --git a/doc/en/Authoring/Answer_Tests/Results/NumRelative.md b/doc/en/Authoring/Answer_Tests/Results/NumRelative.md index 7f2a43a013db25e8bec397e0ededdbd4c235a97c..fa1d7be91cb06379c004f8fd14ac2a86e0bbb4f7 100644 --- a/doc/en/Authoring/Answer_Tests/Results/NumRelative.md +++ b/doc/en/Authoring/Answer_Tests/Results/NumRelative.md @@ -403,4 +403,20 @@ This page exposes the results of running answer tests on STACK test cases. This <td class="cell c4"><pre>0.1</pre></td> <td class="cell c5">1</td> <td class="cell c6"></td> +</tr> +<tr class="notes"> + <td class="cell c0"><td colspan="6">Complex numbers</td></td> +</tr> +<tr class="pass"> + <td class="cell c0">NumRelative</td> + <td class="cell c1"><span style="color:green;"><i class="fa fa-check"></i></span></td> + <td class="cell c2"><pre>0.99*%i</pre></td> + <td class="cell c3"><pre>%i</pre></td> + <td class="cell c4"><pre>0.1</pre></td> + <td class="cell c5">0</td> + <td class="cell c6">ATNumerical_SA_not_number.</td> +</tr> +<tr class="pass"> + <td class="cell c0"><td colspan="2"></td></td> + <td class="cell c1"><td colspan="4">Your answer should be a floating point number, but is not.</td></td> </tr></tbody></table></div> \ No newline at end of file diff --git a/doc/en/Authoring/Answer_Tests/Results/SubstEquiv.md b/doc/en/Authoring/Answer_Tests/Results/SubstEquiv.md index 7d495484dc99416beba2c55728947fa6d1fa11ee..3efa29bffa9b27bc13f7e93fd92084cf174473ff 100644 --- a/doc/en/Authoring/Answer_Tests/Results/SubstEquiv.md +++ b/doc/en/Authoring/Answer_Tests/Results/SubstEquiv.md @@ -383,6 +383,28 @@ This page exposes the results of running answer tests on STACK test cases. This <td class="cell c0"><td colspan="2"></td></td> <td class="cell c1"><td colspan="4">Your answer would be correct if you used the following substitution of variables. <span class="filter_mathjaxloader_equation"><span class="nolink">\[\left[ A=a , B=b , C=C \right] \]</span></span></td></td> </tr> +<tr class="pass"> + <td class="cell c0">SubstEquiv</td> + <td class="cell c1"><span style="color:green;"><i class="fa fa-check"></i></span></td> + <td class="cell c2"><pre>y=A+B</pre></td> + <td class="cell c3"><pre>x=a+b</pre></td> + <td class="cell c4"><pre>[x]</pre></td> + <td class="cell c5">0</td> + <td class="cell c6">ATEquation_default</td> +</tr> +<tr class="pass"> + <td class="cell c0">SubstEquiv</td> + <td class="cell c1"><span style="color:green;"><i class="fa fa-check"></i></span></td> + <td class="cell c2"><pre>y=A+B</pre></td> + <td class="cell c3"><pre>x=a+b</pre></td> + <td class="cell c4"><pre>[z]</pre></td> + <td class="cell c5">1</td> + <td class="cell c6">ATSubstEquiv_Subst [A = a,B = b,y = x].</td> +</tr> +<tr class="pass"> + <td class="cell c0"><td colspan="2"></td></td> + <td class="cell c1"><td colspan="4">Your answer would be correct if you used the following substitution of variables. <span class="filter_mathjaxloader_equation"><span class="nolink">\[\left[ A=a , B=b , y=x \right] \]</span></span></td></td> +</tr> <tr class="pass"> <td class="cell c0">SubstEquiv</td> <td class="cell c1"><span style="color:green;"><i class="fa fa-check"></i></span></td> @@ -412,6 +434,19 @@ This page exposes the results of running answer tests on STACK test cases. This <tr class="notes"> <td class="cell c0"><td colspan="6">Fix some variables.</td></td> </tr> +<tr class="pass"> + <td class="cell c0">SubstEquiv</td> + <td class="cell c1"><span style="color:green;"><i class="fa fa-check"></i></span></td> + <td class="cell c2"><pre>A*cos(x)+B*sin(x)</pre></td> + <td class="cell c3"><pre>P*cos(x)+Q*sin(x)</pre></td> + <td class="cell c4"><pre>[x]</pre></td> + <td class="cell c5">1</td> + <td class="cell c6">ATSubstEquiv_Subst [A = P,B = Q].</td> +</tr> +<tr class="pass"> + <td class="cell c0"><td colspan="2"></td></td> + <td class="cell c1"><td colspan="4">Your answer would be correct if you used the following substitution of variables. <span class="filter_mathjaxloader_equation"><span class="nolink">\[\left[ A=P , B=Q \right] \]</span></span></td></td> +</tr> <tr class="pass"> <td class="cell c0">SubstEquiv</td> <td class="cell c1"><span style="color:green;"><i class="fa fa-check"></i></span></td> @@ -427,6 +462,15 @@ This page exposes the results of running answer tests on STACK test cases. This <td class="cell c2"><pre>A*cos(t)+B*sin(t)</pre></td> <td class="cell c3"><pre>P*cos(x)+Q*sin(x)</pre></td> <td class="cell c4"><pre>[t]</pre></td> + <td class="cell c5">0</td> + <td class="cell c6"></td> +</tr> +<tr class="pass"> + <td class="cell c0">SubstEquiv</td> + <td class="cell c1"><span style="color:green;"><i class="fa fa-check"></i></span></td> + <td class="cell c2"><pre>A*cos(t)+B*sin(t)</pre></td> + <td class="cell c3"><pre>P*cos(x)+Q*sin(x)</pre></td> + <td class="cell c4"><pre>[z]</pre></td> <td class="cell c5">1</td> <td class="cell c6">ATSubstEquiv_Subst [A = P,B = Q,t = x].</td> </tr> @@ -474,4 +518,64 @@ n(2*x)+S*cos(2*x)</pre></td> <tr class="pass"> <td class="cell c0"><td colspan="2"></td></td> <td class="cell c1"><td colspan="4">Your answer would be correct if you used the following substitution of variables. <span class="filter_mathjaxloader_equation"><span class="nolink">\[\left[ y=x \right] \]</span></span></td></td> +</tr> +<tr class="pass"> + <td class="cell c0">SubstEquiv</td> + <td class="cell c1"><span style="color:green;"><i class="fa fa-check"></i></span></td> + <td class="cell c2"><pre>C1*%e^x*sin(4*x)+C2*%e^x*cos(4 +*x)+C4*x*%e^-x+C3*%e^-x</pre></td> + <td class="cell c3"><pre>e^(x)*A*cos(4*x)+B*e^(x)*sin(4 +*x)+C*e^(-x)+D*x*e^(-x)</pre></td> + <td class="cell c4"><pre>[x]</pre></td> + <td class="cell c5">1</td> + <td class="cell c6">ATSubstEquiv_Subst [C1 = B,C2 = A,C3 = C,C4 = D].</td> +</tr> +<tr class="pass"> + <td class="cell c0"><td colspan="2"></td></td> + <td class="cell c1"><td colspan="4">Your answer would be correct if you used the following substitution of variables. <span class="filter_mathjaxloader_equation"><span class="nolink">\[\left[ C_{1}=B , C_{2}=A , C_{3}=C , C_{4}=D \right] \]</span></span></td></td> +</tr> +<tr class="pass"> + <td class="cell c0">SubstEquiv</td> + <td class="cell c1"><span style="color:green;"><i class="fa fa-check"></i></span></td> + <td class="cell c2"><pre>C1*%e^x*sin(4*x)+C2*%e^x*cos(4 +*x)+C4*x*%e^-x+C3*%e^-x</pre></td> + <td class="cell c3"><pre>C4*x*e^(-x)+e^(x)*C1*cos(4*x)+ +C2*e^(x)*sin(4*x)+C3*e^(-x)</pre></td> + <td class="cell c4"><pre>[x]</pre></td> + <td class="cell c5">1</td> + <td class="cell c6">ATSubstEquiv_Subst [C1 = C2,C2 = C1,C3 = C3,C4 = C4].</td> +</tr> +<tr class="pass"> + <td class="cell c0"><td colspan="2"></td></td> + <td class="cell c1"><td colspan="4">Your answer would be correct if you used the following substitution of variables. <span class="filter_mathjaxloader_equation"><span class="nolink">\[\left[ C_{1}=C_{2} , C_{2}=C_{1} , C_{3}=C_{3} , C_{4}=C_{4} \right] \]</span></span></td></td> +</tr> +<tr class="pass"> + <td class="cell c0">SubstEquiv</td> + <td class="cell c1"><span style="color:green;"><i class="fa fa-check"></i></span></td> + <td class="cell c2"><pre>C1*%e^x*sin(4*x)+C2*%e^x*cos(4 +*x)+C4*x*%e^-x+C3*%e^-x</pre></td> + <td class="cell c3"><pre>A*x*e^(-x)+e^(x)*B*cos(4*x)+C* +e^(x)*sin(4*x)+D*e^(-x)</pre></td> + <td class="cell c4"><pre>[x]</pre></td> + <td class="cell c5">1</td> + <td class="cell c6">ATSubstEquiv_Subst [C1 = C,C2 = B,C3 = D,C4 = A].</td> +</tr> +<tr class="pass"> + <td class="cell c0"><td colspan="2"></td></td> + <td class="cell c1"><td colspan="4">Your answer would be correct if you used the following substitution of variables. <span class="filter_mathjaxloader_equation"><span class="nolink">\[\left[ C_{1}=C , C_{2}=B , C_{3}=D , C_{4}=A \right] \]</span></span></td></td> +</tr> +<tr class="pass"> + <td class="cell c0">SubstEquiv</td> + <td class="cell c1"><span style="color:green;"><i class="fa fa-check"></i></span></td> + <td class="cell c2"><pre>C1*%e^x*sin(4*x)+C2*%e^x*cos(4 +*x)+C4*x*%e^-x+C3*%e^-x</pre></td> + <td class="cell c3"><pre>e^(x)*C1*cos(4*x)+C2*e^(x)*sin +(4*x)+C3*e^(-x)+C4*x*e^(-x)</pre></td> + <td class="cell c4"><pre>[x]</pre></td> + <td class="cell c5">1</td> + <td class="cell c6">ATSubstEquiv_Subst [C1 = C2,C2 = C1,C3 = C3,C4 = C4].</td> +</tr> +<tr class="pass"> + <td class="cell c0"><td colspan="2"></td></td> + <td class="cell c1"><td colspan="4">Your answer would be correct if you used the following substitution of variables. <span class="filter_mathjaxloader_equation"><span class="nolink">\[\left[ C_{1}=C_{2} , C_{2}=C_{1} , C_{3}=C_{3} , C_{4}=C_{4} \right] \]</span></span></td></td> </tr></tbody></table></div> \ No newline at end of file diff --git a/doc/en/Authoring/Author_FAQ.md b/doc/en/Authoring/Author_FAQ.md index d9d33686403d78e5d1cbdb61cb07153d24f481b4..ec28274cecf1eaac70d8ebf4fd7e6ede361c1d5c 100644 --- a/doc/en/Authoring/Author_FAQ.md +++ b/doc/en/Authoring/Author_FAQ.md @@ -98,10 +98,6 @@ Then, you should get error reporting. As an example navigate to Site administration -> Plugins -> Question types -> STACK -> Healthcheck -There you can see an example of an expression sent to Maxima. Namely: - - cab:block([ RANDOM_SEED, OPT_NoFloats, sqrtdispflag, simp, assume_pos, caschat0, caschat1], stack_randseed(0), make_multsgn(dot), make_complexJ(i), OPT_NoFloats:true, sqrtdispflag:true, simp:true, assume_pos:false, print("[STACKSTART Locals= [ ") , print("0=[ error= ["), cte("caschat0",errcatch(caschat0:plot([x^4/(1+x^4),diff(x^4/(1+x^4),x)],[x,-3,3]))) , print("1=[ error= ["), cte("caschat1",errcatch(caschat1:plot([sin(x),x,x^2,x^3],[x,-3,3],[y,-3,3]))) , print("] ]") , return(true) ); - -Expressions such as this can be copied into the [STACK-Maxima sandbox](../CAS/STACK-Maxima_sandbox.md) and evaluated. The errors returned here might help track down the problem. +There you can see an example of an expression sent to Maxima. Expressions such as this can be copied into the [STACK-Maxima sandbox](../CAS/STACK-Maxima_sandbox.md) and evaluated. The errors returned here might help track down the problem. The issue is normally that you have tried to create a _syntactically invalid_ maxima command. For example `[a,,b]` will crash Maxima. Since we have not created a full parser, all syntax errors like this are not yet trapped. diff --git a/doc/en/Authoring/Deploying.md b/doc/en/Authoring/Deploying.md index c16356cef98341efa94cd00e91d1caad5b60e094..50dd47d8fcfe8da22bac9183f7f85032113f4a74 100644 --- a/doc/en/Authoring/Deploying.md +++ b/doc/en/Authoring/Deploying.md @@ -17,6 +17,7 @@ Notes: 3. Variants are different if and only if the evaluated [question note](Question_note.md) is different. Any number of instances can be requested and deployed but only one instance of each [question note](Question_note.md) can be deployed. It is possible to deploy \(n\) variants in one go, but the system will give up if too many duplicate question notes are generated. The teacher is responsible to ensure question variants are different if and only if the question notes are different. The deployment management also allows specific variants to be dropped. You can also return to the question preview window and try a specific deployed variant. 4. Deployment is not required for authors to test questions: an instance is generated on-the-fly. 5. Once a quiz is underway it is still possible in Moodle to edit a question, and to re-grade students' attempts. This is useful in rare cases where there is a mistake, you want to improve the worked solution, you would like to add better feedback/partial credit for a particular etc. However, do not change anything related to the random generation of questions! Results are unpredictable, and may well result in a situation when the modified question is different to that answered by students taking the test prior to modifications... +6. It is possible to [systematically deploy](../CAS/Systematic_deployment.md) all variants of a question in a simple manner. ## How to deploy question variants ## @@ -26,8 +27,3 @@ Notes: 1. You can click on the seed numbers to view a particular random variant. The testing page lists values of all the variables, displays the question and the worked solution. The testing page is a very efficient way to look at your random variants. 2. When you deploy new variants STACK will run all the question tests. If a test fails, the generation process will stop with an error message, showing the failing test. - - -## Limitations ## - -There is currently no way to loop systematically over all variants and deploy them all. diff --git a/doc/en/Authoring/Fact_sheets.md b/doc/en/Authoring/Fact_sheets.md index 230837b503de1bd4a822feb39ca19faea2e94161..771ffce5d87cec2320a2f1798f185ae6a7135d15 100644 --- a/doc/en/Authoring/Fact_sheets.md +++ b/doc/en/Authoring/Fact_sheets.md @@ -1,6 +1,8 @@ -# In-built "facts" +# Hints -STACK contains a "formula sheet" of useful fragments which a teacher may wish to include in a consistent way. These "facts" can be included in any [CASText](CASText.md). +STACK contains a "formula sheet" of useful fragments which a teacher may wish to include in a consistent way. This is achieved through the "hints" system. + +Hints can be included in any [CASText](CASText.md). To include a hint, use the syntax diff --git a/doc/en/Authoring/Inputs.md b/doc/en/Authoring/Inputs.md index 7d7a70e2a74a1d8ad24e8c0e74555b069c409170..1734b0e03cdda3290e205ff36f107cf22cd9c1bf 100644 --- a/doc/en/Authoring/Inputs.md +++ b/doc/en/Authoring/Inputs.md @@ -80,11 +80,15 @@ We cannot use the `EMPTYANSWER` tag for the teacher's answer with the matrix inp This input allows the user to type in multiple lines, where each line must be a valid algebraic expression. STACK passes the result to [Maxima](../CAS/Maxima.md) as a list. Note, the teacher's answer and any syntax hint must be a list, of valid Maxima exprssions! If you just pass in an expression strange behaviour may result. +If the `allowempty` tag is used the student's answer will be `[EMPTYANSWER]` to ensure the type of the student's answer is always a list. + #### Equivalence reasoning input #### The purpose of this input type is to enable students to work line by line and reason by equivalence. See the specific documentation for more information: [Equivalence reasoning](../CAS/Equivalence_reasoning.md). Note, the teacher's answer and any syntax hint must be a list! If you just pass in an expression strange behaviour may result. +If the `allowempty` tag is used the student's answer will be `[EMPTYANSWER]` to ensure the type of the student's answer is always a list. + #### True/False #### Simple drop down. A Boolean value is assigned to the variable name. @@ -254,6 +258,8 @@ The "compact" version removes most of the styling. This is needed when the answ Users are increasingly using inputs to store _state_, which makes no sense for a user to see. For example, when using [JSXGraph](JSXGraph.md) or [GeoGebra](GeoGebra.md) users transfer the configuration of the diagram into an input via JavaScript. In many situations, it makes no sense for the student to see anything about this input. The validation can be switched off with the regular "show validation" option, the input box itself can be hidden with JavaScript/CSS. Putting `hideanswer` in the extra options stops displaying the "teacher's answer", e.g. at the end of the process. +All input types should support this extra option. + Do not use this option in questions in place of the normal quiz settings. ### Extra option: allowempty ### @@ -392,8 +398,6 @@ min/max sf/dp | . | Y | Y | . | . | . | . | . | `rationalnum` | . | Y | . | . | . | . | . | . | . | . | . | . `consolidatesubscripts` | Y | . | Y | Y | . | . | . | . | . | . | . | . `negpow` | . | . | Y | . | . | . | . | . | . | . | . | . -`allowempty` | Y | Y | Y | Y | . | . | . | Y | . | . | Y | . -`hideanswer` | Y | Y | . | . | . | . | . | Y | . | . | Y | Y `simp` | Y | Y | Y | Y | . | . | . | . | Y | . | . | . `align` | Y | Y | Y | . | . | . | . | . | . | . | . | . `nounits` | Y | Y | Y | Y | Y | Y | Y | . | . | Y | . | . diff --git a/doc/en/Authoring/Question_blocks/Static_blocks.md b/doc/en/Authoring/Question_blocks/Static_blocks.md index a7ecbcf08bb8f9d0ad3bae82242f71c6462a05be..b574e7fc8e67a7c62f277079d8006cc7315c896b 100644 --- a/doc/en/Authoring/Question_blocks/Static_blocks.md +++ b/doc/en/Authoring/Question_blocks/Static_blocks.md @@ -18,6 +18,24 @@ Comment blocks allow you to put content into CASText which will not be seen by s Before 4.4 the contents of the block needed to be syntactically correct CASText. That is no longer the case and you can much more easily use this block to comment our unfinished stuff. +## Todo blocks ## + +"todo" blocks allow you to put items into CASText which indicate future work needed. This will not be seen by students. + + [[ todo ]] Place requests to collaborators here. This will not appear to students. [[/ todo ]] + +Any question with a todo will flag an error in the bulk tester. This will _not_ throw an error in the editing form. These blocks can also be found by the dependency checker. + +The todo block is similar to the comments block. A different block is provided to facilitate searching for questions with specific "todo" items remaining. The contents must be valid castext (unlike the comments block which can be anything) because in the future we may extend the functionality to display todo items in a teacher preview. If you need to include invalid content either use the comment block, or escape block inside the todo, e.g. + + [[todo]][[escape]]...[[/escape]][[/todo]] + +The contents of this block are replaced by the static + + <!--- stack_todo ---> + +to provide a searchable tag in instantiated text which is not visible in regular html, e.g. in the dependency checker. + ## The debug block ## The special "debug" block allows question authors to see all the values of variables created during a session in a table. Do not leave this block in a live question! diff --git a/doc/en/Authoring/Testing.md b/doc/en/Authoring/Testing.md index 5b67e6cf63e08b770527f9d0b430c2eee5cd406f..00a2e6d53fb2c1cb899c0ddb475a09171e89ad54 100644 --- a/doc/en/Authoring/Testing.md +++ b/doc/en/Authoring/Testing.md @@ -2,39 +2,9 @@ This page deals with testing questions and quality control. This is largely done through the question test functionality. -We have separate advice on [fixing broken questions](Fixing_broken_questions.md) in a live quiz. - -## Question authoring checklist ## - -High-quality question production needs care at each stage. - -__Minimal requirements__ - -1. The question name should be meaningful and consistent, i.e. match up to course, section and topic. E.g. `2018ILA-Wk3-Q2: equation of plane`. -2. Is the phrasing of the question clear to students? -3. Will students know how to input an answer? - * Could a "syntax hint" or message in the question help? - * Can "validation" help, e.g. by telling students how many significant figures are expected? (See the "numbers" input type.) -4. Use question variable stubs throughout, to enable efficient random generation. (E.g. define the correct answer in question variables, rather than hard-wiring a specific expression). -5. Add a meaingful question note which will make sense later, not just a list of randomly generated numbers. This could be an abreviated form of the question together with the answer. -6. Add question tests for one correct and at least one incorrect variant. (See below.) Always make sure the question marks the correct answer as correct! -7. Check all options in the question, inputs and PRTs. +High-quality question production needs care at each stage. An [authoring workflow](Workflow.md) is described separately. -__Phase 1__ - -1. Minimal random variants. -2. Worked solution ("General feedback") reflecting the random variables. -3. Consider likely mistakes, and add feedback to test for this. -4. Add at least one question test to test for each eventuality identified above. - -__Phase 2__ - -Use data obtained from one cycle of attempts by students. - -1. Did the question operate correctly? E.g. were correct answers correctly marked, and incorrect answers rejected? -2. What did students get wrong? Is there a reason for these answers such as a common misconception? If so, add nodes to the PRTs to test for this and improve feedback. -3. Add further question tests to test each misconception. -4. Is there any significant difference between random variants? +We have separate advice on [fixing broken questions](Fixing_broken_questions.md) in a live quiz. ## Testing for quality control ## diff --git a/doc/en/Authoring/Trees.md b/doc/en/Authoring/Trees.md index 2a08f5e4ef55c4b23c62ff5dbab0e4ac5a84465a..ce30475a5a34d9490b86bc33961b88a5ae75fc7a 100644 --- a/doc/en/Authoring/Trees.md +++ b/doc/en/Authoring/Trees.md @@ -5,7 +5,7 @@ It is sometime very useful to display the tree structure of an algebraic express For example, the HTML code for the tree of \(1+2x^3\) is given below. ``` -<ul class='tree'> +<ul class='algebratree'> <li><code>+</code> <ul> <li><span class='atom'>\(1\)</span></li> @@ -27,7 +27,7 @@ This is displayed as follows. <p> <figure> -<ul class='tree'> +<ul class='algebratree'> <li><code>+</code> <ul> <li><span class='atom'>\(1\)</span></li> @@ -47,7 +47,7 @@ This is displayed as follows. </figure> </p> -The tree is displayed in pure HTML using unordered lists `<ul>` and styled with CSS via the `<ul class='tree'>`. Therefore, such trees could be written in HTML by hand. +The tree is displayed in pure HTML using unordered lists `<ul>` and styled with CSS via the `<ul class='algebratree'>`. Therefore, such trees could be written in HTML by hand. STACK provides a function `disptree` to generate the above tree diagram from a Maxima expression. For example, use `{@disptree(1+2+pi*x^3)@}` in castext. This function generates a string representing the tree of that expression, and is not an inert function. @@ -55,7 +55,7 @@ STACK provides a function `treestop` to stop traversing the tree, and use the La <p> <figure> -<ul class='tree'> +<ul class='algebratree'> <li><code>=</code> <ul> <li><code>/</code> @@ -91,7 +91,7 @@ with the following castext: `{@p1@}: {@disptree(p1)@} <br/> {@p2@}: {@disptree( ## Styles -In order to correctly display list items within the `<ul class='tree'>` list, additional styling is needed. All list items must be styled with one of the following tags. The Maxima code ensures that operator nodes are styled slightly differently from atoms/terminal nodes. Some operators, such as integrals and sums, have special style rules applied. +In order to correctly display list items within the `<ul class='algebratree'>` list, additional styling is needed. All list items must be styled with one of the following tags. The Maxima code ensures that operator nodes are styled slightly differently from atoms/terminal nodes. Some operators, such as integrals and sums, have special style rules applied. 1. `<code>` is used to display operators as html code. 1. `<span class='op'>` is used to display operators as LaTeX. diff --git a/doc/en/Authoring/Workflow.md b/doc/en/Authoring/Workflow.md new file mode 100644 index 0000000000000000000000000000000000000000..fe6187a4f87a5febbcd2939edb0098b76642f125 --- /dev/null +++ b/doc/en/Authoring/Workflow.md @@ -0,0 +1,85 @@ +# Question authoring workflow + +This document contains suggestions for effective question authoring workflow, especially when working collaboratively. + +### 1. Minimal working question + +The first task is to create a minimal working question. At the outset + + +1. add minimal [question variables](Variables.md) to prevent repetition of information in the "Model answer" field of the inputs, and PRT nodes; +2. use question variables for key information in the question, especially if you intend to create [random variants](../CAS/Random.md) later; +3. add minimal feedback in the PRTs. + +It is helpful if the question name is meaningful and consistent in a course. E.g. it can match up to course, section and topic. E.g. `2018ILA-Wk3-Q2: equation of plane`. This makes finding questions later much easier. + +Create any multi-parts needed, but we recommend at this stage _not_ to add random variants. Random variants will be added later. + +If you already have a good idea of common mistakes, misconception, or intended feedback then you can use the potential response trees to test for this. This can be added now, however it might be better to wait until step 5 below to add feedback. + +Consider + +1. Is the phrasing of the question clear to students? +2. Will students know how to input an answer? + * Could a "syntax hint" or message in the question help? + * Can "validation" help, e.g. by telling students how many significant figures are expected? Advanced users might consider [bespoke validation](../CAS/Validator.md) functions. + + +### 2. Add basic question tests + +It is important to test questions for quality control, as described in the [testing](Testing.md) documentation. Navigate to the "STACK question dashboard" page by following the link at the top of the question editing page, or on the preview page. At this stage the preview question page will warn "Question is missing tests or variants." + +We recommend the following two test cases: + +1. Ensure what you have given in the inputs as the "Model answer" is really awarded full marks by your PRT. +2. Ensure that not every answer is given full marks! + +The STACK question dashboard makes it relatively easy to check the mode answer: press the button marked "Add test case assuming teacher's answer gets full marks". You will have to think about an answer which is wrong, and the format of this will depend on the types of inputs used. + +### 3. Write a robust worked solution + +From the STACK question dashboard follow the link to "Send general feedback to the CAS". This is a special page, only available to the STACK question type. The page is loaded with the question variables, model answers, and the general feedback (worked solution) fields. The page allows you to immediately preview the general feedback, edit variables, and then preview the instantiated general feedback. + +Add in further question variables, calculations etc. as needed and use these in the worked solution. _An effective way to create equivalent random variants is to ensure invariance of the worked solution._ If you cannot create a single worked solution for all variants then you can consider using question blocks, or separate questions and selecting questions in the quiz. A single question need not be the most general possible. + +Once the you are happy with the general feedback press the "Save back to question" button. This replaces the question variables and general feedback. Be aware that if you still have the edit form open, and you subsequently save the edit form again, then you will replace your question variables and general feedback with the contents of the edit form. + +### 4. Add random variants + +If you need to do so, at this point add in random variants. + +1. Edit the question variables to add in randomisation (if you did not do so in step 3.) +2. Add a question note. +3. Return to the Stack question dashboard and _deploy variants_. The STACK question dashboard link on the edit form will change to "No variants of this question have been deployed yet." to remind you to deploy variants. +4. Confirm any question tests added in step 2 continue to work for all variants. There is a button "Run all tests on all deployed variants (Slow)" to help you check this. + +### 5. Add better feedback + +Add in better feedback to the question. + +_Feedback is likely to be effective when it is specific in helping student improve on the task._ + +It is sensible to use the question variables to create variables for each answer which will trigger feedback. This answer can be based on any random variables, and can be used both in the potential response tree and to create a test case to confirm the feedback and any partial credit is really awarded for each random variant. + +In theory one test case should be created for each anticipated response which gets feedback. In very complex PRTs this is sometimes not possible/practical. + +When updating a PRT at this stage we would _expect_ test cases added in step 2 to fail. This is reassuring as it indicates something significant has changed! You can easily confirm the new behaviour of the testcase is now what is intended. + +### 6. Use data obtained from one cycle of attempts by students. + +Rather than second-guess what students _might_ get wrong it is more effective to look at what they _do_. See the section on [reporting](Reporting.md) for documentation on how to review students' answers. When feedback/marks are delayed (e.g. online exam) this can be done between students taking the assessment and results being released. If feedback/marks are immediate then better quality feedback can still be usefully added later. + +1. Did the question operate correctly? E.g. were correct answers correctly marked, and incorrect answers rejected? +2. What did students get wrong? Is there a reason for these answers such as a common misconception? If so, add nodes to the PRTs to test for this and improve feedback. +3. Add further question tests to test each misconception. +4. Is there any significant difference between random variants? + + +# Authoring collaboratively + +The question description, and descriptions within PRT nodes, can be used to describe intentions and problems to other team members. These fields are only available question authors, and are never shown to students. + +You can use the `[[todo]]...[[/todo]]` question block to indicate unfinished questions. Authors with the capability to use the "STACK diagnostic tools" can create a list of questions containing this block, making it easy for them to locate questions needing attention. + +You can use the `[[escape]]...[[/escape]]` and `[[comment]]...[[comment]]` blocks to remove broken content which is preventing a question from being saved. Maxima code can be removed with code comments: `/* .... */`. + diff --git a/doc/en/CAS/Random.md b/doc/en/CAS/Random.md index ee7ef0a2af57297e993667d525e213f6e578e3f8..e5c073076d9ff631e391983c01c19eff4ef611ed 100644 --- a/doc/en/CAS/Random.md +++ b/doc/en/CAS/Random.md @@ -10,6 +10,8 @@ For the purposes of learning and teaching, we do not need an algorithm which is It is very important to test each random version a student is likely to see and not to leave this to chance. To pre-generate and test random variants see the separate documentation on [deploying random variants](../Authoring/Deploying.md). +Users may also [systematically deploy](Systematic_deployment.md) all variants of a question in a simple manner. + ## rand() {#rand} STACK provides its own function `rand()`. diff --git a/doc/en/CAS/Systematic_deployment.md b/doc/en/CAS/Systematic_deployment.md new file mode 100644 index 0000000000000000000000000000000000000000..90e6fd9f62f5180ff794f905ca25c8679ee07607 --- /dev/null +++ b/doc/en/CAS/Systematic_deployment.md @@ -0,0 +1,42 @@ +# Systematic deployment + +STACK has the option to create [random variants](Random.md) of questions. However, users may need to systematically deploy all variants of a question in a simple manner. For example, where there is a small number of cases and all should be readily available. + +Every CAS (Maxima) session contains a constant `stack_seed` which holds the integer value of the seed used by that variant of the question. + +## Deploying every variant + +The constant `stack_seed` can be used to deploy every variant of a question. As an example, consider the data below (from https://nssdc.gsfc.nasa.gov/planetary/factsheet/) + + planet:["Mercury", "Venus", "Earth", "Moon", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"]; + /* mass 10^(24)kg. */ + mass:[0.330, 4.87, 5.97, 0.073, 0.642, 1898.0, 568.0, 86.8, 102.0, 0.0130]; + /* Orbital Period (days). */ + period[88.0, 224.7, 365.2, 27.3, 687.0, 4331, 10747, 30589, 59800, 90560]; + /* Distance from Sun (106 km) */ + dist:[57.9, 108.2, 149.6, 0.384, 228.0, 778.5, 1432.0, 2867.0, 4515.0, 5906.4]; + +If you want to use this data in a question, you can use a variable to index elements in these lists. In particular, you can deploy seeds 1,2,3,4,5,6,7,8,9,10. Then in the question variables or castext you can use the variable `stack_seed` as an index to the data, e.g. + + The planet {@planet[stack_seed]@} has mass \({@mass[stack_seed]*10^(24)@} \mathrm{kg}\). + +We recommend you add in a question variable, e.g. `n1:stack_seed;` and then use this variable (e.g. `n1`) as your index. + +If you want to exclude the moon, then you can omit seed 4, and deploy only seeds 1,2,3,5,6,7,8,9,10. (Of course, alternatively you could delete the entries for the moon from the list!) + +Note, in Maxima the list index starts at 1. I.e the first element of a list is `l[1]` (not zero). + +It is your responsibility to make sure the index remains within range! You can ensure this by creating an index variable such as `n1:mod(stack_seed-1,10)+1;` and then using this + + The planet {@planet[n1]@} has mass \({@mass[n1]*10^(24)@} \mathrm{kg}\). + +It is sensible to always ensure your `stack_seed` does not create run-time errors. Notice that although the `mod` function does return `0` we have avoided possible zero indexes when defining `n1`. + +Of course, there are many other ways to map deployed seeds onto systematic deployment of variants. Using consecutive integers from \(1, \ldots, n\) as the starting point is probably simplest and easiest to maintain. For this reason there is a special option to do this on the deploy variants page. + +Notes + +1. You can combine use of `stack_seed` with random functions. There is nothing wrong with seeding the random number generator from a small integer! +2. STACK auto-detects random functions. You must refer to `stack_seed`, or a random function, in the _question variables_ to trigger use of deployed variants. Otherwise STACK will think deployed variants are not needed. If necessary add in a variable `n1:stack_seed;` and then use `n1` as your index to make sure you have explicitly made use of `stack_seed`. +3. The variable `stack_seed` is a constant. You cannot reassign values to this variable within the question. +4. Just as with randomisation, you must create a question note to distinguish between variants of a question. diff --git a/doc/en/Developer/Development_history.md b/doc/en/Developer/Development_history.md index 4d8511084263a59d11410d464f92e56d018f2af8..5e9ceb6d0bf73ba25b7598b770445ca48a8fc086 100644 --- a/doc/en/Developer/Development_history.md +++ b/doc/en/Developer/Development_history.md @@ -2,6 +2,18 @@ For current and future plans, see [Development track](Development_track.md) and [Future plans](Future_plans.md). +## Version 4.4.5 + +Released July 2023. + +1. Add in the `s_assert` function to allow teachers to unit-test individual question variable values. +2. Add in the `[[hint]]` [question block](../Authoring/Question_blocks/Dynamic_blocks.md). Fixes issue #968, thanks to Michael Kallweit. +3. Add in the `stack_include_contrib()` for easier inclusion of libraries. +4. Add in the `[[todo]]` [question block](../Authoring/Question_blocks/Static_blocks.md). +5. Caschat page now saves question variables and general feedback back into the question. Fixes issue #984. +6. Confirm support for Maxima 5.46.0 and 5.47.0. +7. All inputs now "allowempty" and "hideanswer" as extra options. Fixes issue #997. + ## Version 4.4.4 Released June 2023. diff --git a/doc/en/Developer/Development_track.md b/doc/en/Developer/Development_track.md index 8e97e822b69a02c6a76e70e6b0fa3fea60cba52f..789296da7c50ee5d2fdf0108860e67b679e7b155 100644 --- a/doc/en/Developer/Development_track.md +++ b/doc/en/Developer/Development_track.md @@ -1,21 +1,22 @@ + # Development track for STACK Requests for features and ideas for developing STACK are all recorded in [Future plans](Future_plans.md). The past development history is documented on [Development history](Development_history.md). -## Version 4.4.5 +## Version 4.4.6 -DONE -1. Add in the `s_assert` function to allow teachers to unit-test individual question variable values. -2. Add in the `hint` [question block](../Authoring/Question_blocks/Dynamic_blocks.md). Fixes issue #968, thanks to Michael Kallweit. -3. Add in the `stack_include_contrib()` for easier inclusion of libraries. +1. Refactor the healthcheck scripts, especially to make unicode requirements for maxima more prominent. +2. Allow users to [systematically deploy](../CAS/Systematic_deployment.md) all variants of a question in a simple manner. +3. Tag inputs with 'aria-live' is 'assertive' for better screen reader support. -TODO: List of long lasting issues dealt with, that might need to be notified/closed, note that some of these have connecting issues: - #671, #420 +TODO: -1. Error messages: use caserror.class more fully to use user information to target error messages. -2. Remove all "cte" code from Maxima - mostly install. +1. Support for PHP 8.2. See issue #986. +2. Fix markdown problems. See issue #420. +3. Error messages: use caserror.class more fully to use user information to target error messages. +4. Remove all "cte" code from Maxima - mostly install. Done: diff --git a/doc/en/Developer/Future_plans.md b/doc/en/Developer/Future_plans.md index 6da5c73dd752aa329963ed974db825156c87ae04..0f3d64552dbc17da033b709293a2eab2cba112d3 100644 --- a/doc/en/Developer/Future_plans.md +++ b/doc/en/Developer/Future_plans.md @@ -69,7 +69,6 @@ Note, where the feature is listed as "(done)" means we have prototype code in th * WIRIS * Possible Maxima packages: * Better support for rational expressions, in particular really firm up the PartFrac and SingleFrac functions with better support. -* Auto deploy. E.g. if the first variable in the question variables is a single a:rand(n), then loop a=0..(n-1). * When validating the editing form, also evaluate the Maxima code in the PRTs, using the teacher's model answers. diff --git a/doc/en/Installation/Maxima.md b/doc/en/Installation/Maxima.md index 3655cc5a72048c48a3a0f150f6b4678bd7c62084..baf6e91243c1f2c370f1db63f4f55c2f7e5c0687 100644 --- a/doc/en/Installation/Maxima.md +++ b/doc/en/Installation/Maxima.md @@ -1,9 +1,9 @@ # Compiling Maxima from source. -You are strongly advised to read the installation instructions for the various components you need! - As of 21st Dec 2015 the following has been used to compile Maxima from source. +If you compile Maxima from source you _must_ include unicode support. This is essential even if you only use Maxima in English. Students' answers, and teacher's content, increasingly uses unicode which inevitably passes through Maxima. + ### You will need the following, and GNU autotools sudo apt-get install texinfo diff --git a/doc/en/Installation/Optimising_Maxima.md b/doc/en/Installation/Optimising_Maxima.md index 4837f54c0783e753b135203dc34fc2b3c87ad070..89c480b819682784d179314b7f7cc685e67e0404 100644 --- a/doc/en/Installation/Optimising_Maxima.md +++ b/doc/en/Installation/Optimising_Maxima.md @@ -24,7 +24,7 @@ It is important that the timeout time is *longer* than the CAS connection timeou The above can be used with either a direct Maxima connection, or with the image created as described below. -## Compiled Lisp ## +## Compiled Lisp image of the STACK libraries ## [Maxima](../CAS/Maxima.md) can be run with a number of different [Lisp implementations](http://maxima.sourceforge.net/lisp.html). Although CLISP is the most portable - due to being interpreted - other Lisps can give faster execution. diff --git a/doc/en/Installation/Testing_installation.md b/doc/en/Installation/Testing_installation.md index 82d4d3c84b4752604ebdbb6ecc90182917721d6f..793127d78ec435a6df4e566789451c41b9375366 100644 --- a/doc/en/Installation/Testing_installation.md +++ b/doc/en/Installation/Testing_installation.md @@ -17,6 +17,7 @@ The healthcheck script checks the following. * Check LaTeX is being converted correctly? Check [MathJax](Mathjax.md) or another LaTeX converter. * Can PHP call external applications? No, then change PHP settings. * Can PHP call Maxima? No, then see below. +* Does Maxima support unicode? Distributed versions of Maxima do (as of July 2023) but if you compile Maxima from source then you must include unicode support. * Graph plotting. Are auto-generated plots being created correctly? There should be two different graphs. If not, check the gnuplot settings, and directory permissions. The CAS-debug option in the STACK settings will provide a very verbose output which is indispensable at this stage. Turn this off for production servers, as it is wasteful of storage, particularly when caching results. @@ -64,13 +65,15 @@ For example you may be evaluating the latest release of STACK on a test server, like to know if the upgrade will break any of your existing questions. (And you don't want to do a lot of exporting and importing.) -# Troubleshooting an upgrade +# Troubleshooting an install/upgrade When you upgrade, the STACK plugin will try to automatically recreate the optimised Maxima image. Occasionally this will not work and you will need to troubleshoot why. ### 1. GOAL: maxima works on the server -Check Maxima is installed and working. E.g. type `maxima` on the command line, and try a non-trivial calculation such as `diff(sin(x^2),x);` to confirm Maxima is working. Use `quit();` to exit. +Check Maxima is installed and working. E.g. type `maxima` on the command line, and try a non-trivial calculation such as `diff(sin(x^2),x);` to confirm Maxima is working. + +Use `quit();` to exit. ### 2. GOAL: STACK works! diff --git a/doc/en/Installation/index.md b/doc/en/Installation/index.md index 943be566871effae9f7e688e8cc39bd8f9f5fb4d..0a6cef4292d9a5af9e5f0593de69a9761c4cf3d9 100644 --- a/doc/en/Installation/index.md +++ b/doc/en/Installation/index.md @@ -41,9 +41,13 @@ to `filter_mathjaxloader | mathjaxconfig` in the filter settings: Dashboard > Si ## 2. Install gnuplot and Maxima -Ensure gcc, gnuplot and [Maxima](http://maxima.sourceforge.net) are installed on your server. Currently Maxima 5.38.1 to 5.44.0 are supported. Please contact the developers to request support for other versions. (Newer versions will be supported, and prompts to test them are welcome.) +Ensure gcc, gnuplot and [Maxima](http://maxima.sourceforge.net) are installed on your server. Currently Maxima 5.38.1 to 5.47.0 are supported. Please contact the developers to request support for other versions. (Newer versions will be supported, and prompts to test them are welcome.) We currently recommend that you use any version of Maxima after 5.43.0. -We currently recommend that you use Maxima 5.41.0. +Maxima can be installed via a package manager on most Linux distributions (e.g. `sudo apt-get install maxima` on Debian/Ubuntu), [downloaded](http://maxima.sourceforge.net/download.html) as a self-contained installer program for Windows, or [compiled from source](Maxima.md). + +To check your version of maxima, run `maxima --version`. If Moodle is set up using Apache, STACK will run maxima through the Apache user (`www-data/apache2`). To check that this works, run maxima as the apache user (e.g. `sudo -u www-data maxima`). Later versions of maxima create a cache and thus the executing user needs to have write access to a temporary folder, see [#731](https://github.com/maths/moodle-qtype_stack/issues/731) for more details and troubleshooting. + +Alternatively, Maxima can also be run on a separate server via [GoeMaxima](https://github.com/mathinstitut/goemaxima) or [MaximaPool](https://github.com/maths/stack_util_maximapool). Please note @@ -51,10 +55,6 @@ Please note * Older versions of Maxima: in particular, Maxima 5.23.2 has some differences which result in \(1/\sqrt{x} \neq \sqrt{1/x}\), and similar problems. This means that we have an inconsistency between questions between versions of maxima. Of course, we can argue about which values of \(x\) make \(1/\sqrt{x} = \sqrt{1/x}\), but currently the unit tests and assumption is that these expressions should be considered to be algebraically equivalent! So, older versions of Maxima are not supported for a reason. Please test thoroughly if you try to use an older version, and expect some errors in the mathematical parts of the code. * If you install more than one version of Maxima then you will need to tell STACK which version to use. Otherwise just use the "default" option. -The documentation on [Installing Maxima](Maxima.md) includes code for compiling Maxima from source. - -Maxima can also be [downloaded](http://maxima.sourceforge.net/download.html) as a self-contained installer program for Windows, RPMs for Linux or as source for all platforms. - Instructions for installing a more recent version of Maxima on CentOS 6 are available on the [Moodle forum](https://moodle.org/mod/forum/discuss.php?d=270956) (Oct 2014). ## 3. Add some additional question behaviours @@ -170,4 +170,4 @@ If STACK is already installed, as described above, it can be updated via git, li It is a good idea to bulk test your materials with the new version. -If you are upgrading from much older versions please look at the [migrations page](Migration.md). \ No newline at end of file +If you are upgrading from much older versions please look at the [migrations page](Migration.md). diff --git a/doc/en/Reference/index.md b/doc/en/Reference/index.md index 703c8af9049904c0144fec0886ea84d2f9984a7f..9c51c14db7abf43878d0ef7ffaefca5db439e440 100644 --- a/doc/en/Reference/index.md +++ b/doc/en/Reference/index.md @@ -4,3 +4,4 @@ This section contains reference materials which are useful, but generally not pa 1. [Actuarial notation](Actuarial.md) 2. The [HELM](HELM.md) project. +2. Using [LaTeX](Latex.md) and [HTML](HTML.md) in questions. diff --git a/doc/en/Topics/Units.md b/doc/en/Topics/Units.md index 2c9959509b82e66cb1af81e9e3585b5c803eac65..20d775e7a5770afb7e5d8399c46f5ae268d82b04 100644 --- a/doc/en/Topics/Units.md +++ b/doc/en/Topics/Units.md @@ -44,17 +44,18 @@ Let us assume that the correct answer is `12.1*m/s^2`. Stack provides an input type to enable teachers to support students in entering answers with scientific units. -This input type is built closely on the algebraic input type with the following differences. +The goal of this input is to validate against the pattern `number * units`. 1. The input type will check both the teacher's answer and the student's answer for units. The input will require the student's answer to have units if and only if the teacher's answer also has units. This normally forces the student to use units. Also, students sometimes add units to dimensionless quantities (such as pH) and this input type will also enable a teacher to reject such input as invalid when the teacher does not use units. -2. This input type *always accepts floating-point numbers*, regardless of the option set on the edit form. The input type should display the same number of significant figures as typed in by the student. Note that all other input types truncate the display of unnecessary trailing zeros in floating point numbers, loosing information about significant figures. If you want to specifically test for significant figures, use this input type, with the teacher's answer having no units. -3. The student must type a number of some kind. Entering units on their own will be invalid. If you want to ask a student for units, then use the algebraic input type. Units on their own are a not valid expression for this input. -4. If the teacher shows the validation, "with variable list" this will be displayed as "the units found in your answer are"... -5. The student is permitted to use variable names in this input type. -6. The "insert stars" option is unchanged. You may or may not want your students to type a `*` or space between the numbers and units for implied multiplication. -7. You may want the single letter variable names options here. Note that since `km` literally means `k*m=1000*m` this is not a problem with most units. -8. The input type checks for units in a case sensitive way. If there is more than one option then STACK suggests a list. E.g. if the student types `mhz` then STACK suggests `MHz` or `mHz`. -9. You can require numerical accuracy at validation by using the `mindp`, `maxdp`, `minsf` and `maxsf` extra options, as documented in the [numerical input](../Authoring/Numerical_input.md). +2. In validating against the pattern `number * units` we do not accept complex expressions which might simplify to this with some additional calculations. For example, an answer such as `9.4*m-53*cm` is not considered valid by this input. +3. This input type *always accepts floating-point numbers*, regardless of the option set on the edit form. The input type should display the same number of significant figures as typed in by the student. Note that all other input types truncate the display of unnecessary trailing zeros in floating point numbers, loosing information about significant figures. If you want to specifically test for significant figures, use this input type, with the teacher's answer having no units. +4. The student must type a number of some kind. Entering units on their own will be invalid. If you want to ask a student for units, then use the algebraic input type. Units on their own are a not valid expression for this input. +5. If the teacher shows the validation, "with variable list" this will be displayed as "the units found in your answer are"... +6. The student is permitted to use variable names in this input type. +7. The "insert stars" option is unchanged. You may or may not want your students to type a `*` or space between the numbers and units for implied multiplication. +8. You may want the single letter variable names options here. Note that since `km` literally means `k*m=1000*m` this is not a problem with most units. +9. The input type checks for units in a case sensitive way. If there is more than one option then STACK suggests a list. E.g. if the student types `mhz` then STACK suggests `MHz` or `mHz`. +10. You can require numerical accuracy at validation by using the `mindp`, `maxdp`, `minsf` and `maxsf` extra options, as documented in the [numerical input](../Authoring/Numerical_input.md). There are surprisingly few ambiguities in the units set up, but there will be some that the developers have missed (correctly dealing with ambiguous input is by definition an impossible problem!). Please contact us with suggestions for improvements. diff --git a/doc/meta_en.json b/doc/meta_en.json index cdd1caca2e1b12e69ed9d5a7ef57683178fa229d..1cb8a2b567a1f17cc8ce5bf1905a9835208448c6 100644 --- a/doc/meta_en.json +++ b/doc/meta_en.json @@ -533,6 +533,11 @@ "description":"Functions for displaying the tree representation of a mathematical expression." }, { + "file":"Workflow.md", + "title":"Workflow - STACK Documentation", + "description":"Suggestions for effective question authoring workflow, especially when working collaboratively." + }, + { "file":"Numerical_input.md", "title":"Numerical Input - STACK Documentation", "description":"Information on STACK's numerical input type." @@ -617,6 +622,11 @@ "description":"Information on using different functions in STACK to generate random values." }, { + "file":"Systematic_deployment.md", + "title":"Systematic deployment - STACK Documentation", + "description":"Information on how to systematically deploy all variants of a question." + }, + { "file":"Real_Intervals.md", "title":"Real intervals - STACK Documentation", "description":"Information on representing, displaying and manipulating intervals in the real line." @@ -650,6 +660,11 @@ "file":"Validator.md", "title":"Creating bespoke validation - STACK Documentation", "description":"How to create bespoke validation of student's input." + }, + { + "file":"Workflow.md", + "title":"Question authoring workflow - STACK Documentation", + "description":"This page suggests workflow for authoring STACK questions." } ] }, diff --git a/lang/en/qtype_stack.php b/lang/en/qtype_stack.php index ae5bc560a822ba53671f891f58c0b96f2ad8c1ce..15075f5d3cbf419e5f99caeaa499efd08886c5c9 100644 --- a/lang/en/qtype_stack.php +++ b/lang/en/qtype_stack.php @@ -412,6 +412,7 @@ $string['languageproblemsmissing'] = 'The language tag {$a->lang} is missing fro $string['languageproblemsextra'] = 'The field {$a->field} has the following languages not in the question text: {$a->langs}.'; $string['alttextmissing'] = 'One or more images appears to have a missing or empty \'alt\' tag in "{$a->field}" ({$a->num}).'; +$string['todowarning'] = 'You have un-resolved todo blocks in "{$a->field}".'; // Admin settings. $string['settingajaxvalidation'] = 'Instant validation'; @@ -464,9 +465,9 @@ $string['settingserveruserpass'] = 'Server username:password'; $string['settingserveruserpass_desc'] = 'If you are using Platform type: Server, and if you have set up your Maxima pool server with HTTP authentication, then you can put the username and password here. That is slighly safer than putting them in the URL. The format is username:password.'; $string['settingusefullinks'] = 'Useful links'; $string['settingmaximalibraries'] = 'Load optional Maxima libraries:'; -$string['settingmaximalibraries_desc'] = 'This is a comma separated list of Maxima library names which will be automatically loaded into Maxima. Only supported library names can be used: "stats, distrib, descriptive, simplex". These libraries will not be loaded if you have saved a maxima image to optimise performance.'; -$string['settingmaximalibraries_error'] = 'The following package is not supported: {$a}'; -$string['settingmaximalibraries_failed'] = 'It appears as if some of the Maxima packages you have asked for have failed to load. Please refer to the installation instructions for notes about this error.'; +$string['settingmaximalibraries_desc'] = 'This is a comma separated list of Maxima library names which will be automatically loaded into Maxima. Only supported library names can be used: "stats, distrib, descriptive, simplex". When you change the listed libraties you must rebuild the Maxima optimised image.'; +$string['settingmaximalibraries_error'] = 'Please edit the STACK plugin setting <tt>qtype_stack | maximalibraries</tt>. The following package is not supported: {$a}'; +$string['settingmaximalibraries_failed'] = 'It appears as if some of the Maxima packages you have asked for have failed to load.'; // Strings used by replace dollars script. $string['replacedollarscount'] = 'This category contains {$a} STACK questions.'; @@ -515,6 +516,7 @@ $string['deployedvariants'] = 'Deployed variants'; $string['deployedvariantsn'] = 'Deployed variants ({$a})'; $string['deploymanybtn'] = 'Deploy # of variants:'; $string['deploymanyerror'] = 'Error in user input: cannot deploy "{$a->err}" variants.'; +$string['deploysystematicbtn'] = 'Deploy seeds from 1 to: '; $string['deployduplicateerror'] = 'Duplicate question notes detected in the deployed variants. We strongly recommend each question note is only deployed once, otherwise you will have difficulty collecting meaningful stats when grouping by variant. Please consider deleting some variants with duplicate notes.'; $string['deploytoomanyerror'] = 'STACK will try to deploy up to at most 100 new variants in any one request. No new variants deployed.'; $string['deploymanynonew'] = 'Too many repeated existing question notes were generated.'; @@ -594,6 +596,8 @@ $string['equivfirstline'] = 'You have used the wrong first line in your argument // Support scripts: CAS chat, healthcheck, etc. $string['all'] = 'All'; $string['chat'] = 'Send to the CAS'; +$string['savechat'] = 'Save back to question'; +$string['savechatmsg'] = 'Question variables and general feedback saved back to the question.'; $string['castext'] = 'CAS text'; $string['chat_desc'] = 'The <a href="{$a->link}">CAS chat script</a> lets you test the connection to the CAS, and try out Maxima syntax.'; $string['chatintro'] = 'This page enables CAS text to be evaluated directly. It is a simple script which is a useful minimal example, and a handy way to check if the CAS is working, and to test various inputs. The first text box enables variables to be defined, the second is for the CAS text itself.'; @@ -608,9 +612,7 @@ $string['healthcheckcache_db'] = 'CAS results are being cached in the database.' $string['healthcheckcache_none'] = 'CAS results are not being cached.'; $string['healthcheckcache_otherdb'] = 'CAS results are being cached in another database.'; $string['healthcheckcachestatus'] = 'The cache currently contains {$a} entries.'; -$string['healthcheckconfig'] = 'Maxima configuration file'; $string['healthcheckconfigintro1'] = 'Found, and using, Maxima in the following directory:'; -$string['healthcheckconfigintro2'] = 'Trying to automatically write the Maxima configuration file.'; $string['healthcheckconnect'] = 'Trying to connect to the CAS'; $string['healthcheckconnectintro'] = 'We are trying to evaluate the following CAS text:'; $string['healthcheckfilters'] = 'Please ensure that the {$a->filter} is enabled on the <a href="{$a->url}">Manage filters</a> page.'; @@ -621,9 +623,12 @@ $string['healthchecklatexmathjax'] = 'STACK relies on the Moodle MathJax filter. $string['healthcheckmathsdisplaymethod'] = 'Maths display method being used: {$a}.'; $string['healthcheckmaximabat'] = 'The maxima.bat file is missing'; $string['healthcheckmaximabatinfo'] = 'This script tried to automatically copy the maxima.bat script from inside "C:\Program files\Maxima-1.xx.y\bin" into "{$a}\stack". However, this seems not to have worked. Please copy this file manually.'; -$string['healthchecksamplecas'] = 'The derivative of {@ x^4/(1+x^4) @} is \[ \frac{d}{dx} \frac{x^4}{1+x^4} = {@ diff(x^4/(1+x^4),x) @}. \] Confirm if unicode is supported: \(\forall\) should be displayed {@unicode(8704)@}.'; +$string['healthchecksamplecas'] = 'The derivative of {@ x^4/(1+x^4) @} is \[ \frac{d}{dx} \frac{x^4}{1+x^4} = {@ diff(x^4/(1+x^4),x) @}. \]'; +$string['healthcheckconnectunicode'] = 'Trying to send unicode to the CAS'; +$string['healthchecksamplecasunicode'] = 'Confirm if unicode is supported: \(\forall\) should be displayed {@unicode(8704)@}.'; $string['healthchecksampledisplaytex'] = '\[\sum_{n=1}^\infty \frac{1}{n^2} = \frac{\pi^2}{6}.\]'; $string['healthchecksampleinlinetex'] = '\(\sum_{n=1}^\infty \frac{1}{n^2} = \frac{\pi^2}{6}\).'; +$string['healthcheckmaximalocal'] = 'Contents of the maximalocal file'; $string['healthcheckplots'] = 'Graph plotting'; $string['healthcheckplotsintro'] = 'There should be two different plots. If two identical plots are seen then this is an error in naming the plot files. If no errors are returned, but a plot is not displayed then one of the following may help. (i) check read permissions on the two temporary directories. (ii) change the options used by GNUPlot to create the plot. Currently there is no web interface to these options.'; $string['healthchecksampleplots'] = 'Two example plots below. {@plot([x^4/(1+x^4),diff(x^4/(1+x^4),x)],[x,-3,3])@} {@plot([sin(x),x,x^2,x^3],[x,-3,3],[y,-3,3],grid2d)@} A third, smaller, plot should be displayed below with traditional axes. {@plot([x,2*x^2-1,x*(4*x^2-3),8*x^4-8*x^2+1,x*(16*x^4-20*x^2+5),(2*x^2-1)*(16*x^4-16*x^2+1)],[x,-1,1],[y,-1.2,1.2],[box, false],[yx_ratio, 1],[axes, solid],[xtics, -3, 1, 3],[ytics, -3, 1, 3],[size,250,250])@}'; @@ -652,6 +657,10 @@ $string['healthautomaxopt_notok'] = 'Maxima image not created automatically.'; $string['healthautomaxopt_nolisp'] = 'Unable to determine LISP version, so Maxima image not created automatically.'; $string['healthautomaxopt_nolisprun'] = 'Unable to automatically locate lisp.run. Try "sudo updatedb" from the command line and refer to the optimization docs.'; $string['healthcheckcreateimage'] = 'Create Maxima image'; +$string['healthcheckmaximaavailable'] = 'Versions of Maxima available on this server'; +$string['healthcheckpass'] = 'The healthcheck passed without detecting any issues. However, please read the detail below carefully. Not every problem can be automatically detected.'; +$string['healthcheckfail'] = 'The healthcheck detected serious problems. Please read the diagnostic information below for more detail.'; +$string['healthcheckfaildocs'] = 'Detailed notes and trouble-shooting advice is given in the documentation under <a href="{$a->link}">Installation > Testing installation</a>.'; $string['stackInstall_replace_dollars_desc'] = 'The <a href="{$a->link}">fix maths delimiters script</a> can be used to replace old-style delimiters like <code>@...@</code>, <code>$...$</code> and <code>$$...$$</code> in your questions with the new recommended <code>{@...@}</code>, <code>\(...\)</code> and <code>\[...\]</code>.'; $string['stackInstall_testsuite_title'] = 'A test suite for STACK Answer tests'; $string['stackInstall_testsuite_title_desc'] = 'The <a href="{$a->link}">answer-tests script</a> verifies that the answer tests are performing correctly. They are also useful to learn by example how each answer-test can be used.'; @@ -738,6 +747,7 @@ $string['stackCas_trigop'] = 'You must apply {$a->trig} to an a $string['stackCas_trigexp'] = 'You cannot take a power of a trig function by writing {$a->forbid}. The square of the value of \(\{$a->identifier}(x)\) is typed in as <tt>{$a->identifier}(x)^2</tt>. The inverse of \(\{$a->identifier}(x)\) is written <tt>a{$a->identifier}(x)</tt> and not \(\{$a->identifier}^{-1}(x)\) .'; $string['stackCas_trigparens'] = 'When you apply a trig function to its arguments you must use round parentheses not square brackets. E.g {$a->forbid}.'; $string['stackCas_triginv'] = 'Inverse trig functions are written {$a->goodinv} not {$a->badinv}.'; +$string['stackCas_baddotdot'] = 'Using matrix multiplication "." with scalar floats is forbidden, use normal multiplication "*" instead for the same result. '; $string['stackCas_badLogIn'] = 'You have typed in the expression <tt>In</tt>. The natural logarithm is entered as <tt>ln</tt> in lower case. ("Lima November" not "India November")'; $string['stackCas_unitssynonym'] = 'You appear to have units {$a->forbid}. Did you mean {$a->unit}?'; $string['stackCas_unknownUnitsCase'] = 'Input of units is case sensitive: {$a->forbid} is an unknown unit. Did you mean one from the following list {$a->unit}?'; diff --git a/question.php b/question.php index 1554a98574b39a1db9e391b16441af23c3aaee54..08c8bf9c885daef66289def966953ff614685591 100644 --- a/question.php +++ b/question.php @@ -1271,11 +1271,12 @@ class qtype_stack_question extends question_graded_automatically_with_countback } /** - * @param string Input text (raw keyvals) to check for random functions. + * @param string Input text (raw keyvals) to check for random functions, or use of stack_seed. * @return bool Actual test of whether text uses randomisation. */ public static function random_variants_check($text) { - return preg_match('~\brand~', $text) || preg_match('~\bmultiselqn~', $text); + return preg_match('~\brand~', $text) || preg_match('~\bmultiselqn~', $text) + || preg_match('~\bstack_seed~', $text); } public function get_num_variants() { @@ -1458,7 +1459,7 @@ class qtype_stack_question extends question_graded_automatically_with_countback foreach ($patterns as $checkpat) { if ($stackversion < $checkpat['ver']) { foreach ($qfields as $field) { - if (strstr($this->$field, $checkpat['pat'])) { + if (strstr($this->$field ?? '', $checkpat['pat'])) { $a = array('pat' => $checkpat['pat'], 'ver' => $checkpat['ver'], 'qfield' => stack_string($field)); $err = stack_string('stackversionerror', $a); if (array_key_exists('alt', $checkpat)) { @@ -1492,7 +1493,7 @@ class qtype_stack_question extends question_graded_automatically_with_countback } $options = $input->get_parameter('options'); - if (trim($options) !== '') { + if (trim($options ?? '') !== '') { $options = explode(',', $options); foreach ($options as $opt) { $opt = strtolower(trim($opt)); @@ -1516,7 +1517,7 @@ class qtype_stack_question extends question_graded_automatically_with_countback $fields = array('questiontext', 'specificfeedback', 'generalfeedback', 'questiondescription'); foreach ($fields as $field) { $text = $this->$field; - $filesexpected = preg_match($pat, $text); + $filesexpected = preg_match($pat, $text ?? ''); $filesfound = $fs->get_area_files($context->id, 'question', $field, $this->id); if (!$filesexpected && $filesfound != array()) { $errors[] = stack_string('stackfileuseerror', stack_string($field)); @@ -1555,7 +1556,6 @@ class qtype_stack_question extends question_graded_automatically_with_countback // 2. Check alt-text exists. // Reminder: previous approach in Oct 2021 tried to use libxml_use_internal_errors, but this was a dead end. - $tocheck = array(); $text = ''; if ($this->questiontextinstantiated !== null) { @@ -1587,7 +1587,26 @@ class qtype_stack_question extends question_graded_automatically_with_countback } } - // 3. Language warning checks. + // 3. Check for todo blocks. + $tocheck = array(); + $fields = array('questiontext', 'specificfeedback', 'generalfeedback', 'questiondescription'); + foreach ($fields as $field) { + $tocheck[stack_string($field)] = $this->$field; + } + foreach ($this->prts as $prt) { + $text = trim($prt->get_feedback_test()); + if ($text !== '') { + $tocheck[$prt->get_name()] = $text; + } + } + $pat = '/\[\[todo/'; + foreach ($tocheck as $field => $text) { + if (preg_match($pat, $text ?? '')) { + $warnings[] = stack_string_error('todowarning', array('field' => $field)); + } + } + + // 4. Language warning checks. // Put language warning checks last (see guard clause below). // Check multi-language versions all have the same languages. $ml = new stack_multilang(); @@ -1710,7 +1729,7 @@ class qtype_stack_question extends question_graded_automatically_with_countback // Invalidate the question definition cache. // First from the next sessions. - cache::make('core', 'questiondata')->delete($this->id); + stack_clear_vle_question_cache($this->id); } } catch (stack_exception $e) { // TODO: what exactly do we use here as the key diff --git a/questiontestrun.php b/questiontestrun.php index 830d02fc2c177365d06d29e4cf8faea5605a288c..561bff39075822c411a8d00112dc123ec2bebb3c 100644 --- a/questiontestrun.php +++ b/questiontestrun.php @@ -141,7 +141,7 @@ if ($question->options->get_option('simplify')) { $simp = ''; } -$questionvarsinputs = $question->questionvariables; +$questionvarsinputs = ''; foreach ($question->get_correct_response() as $key => $val) { if (substr($key, -4, 4) !== '_val') { $questionvarsinputs .= "\n{$key}:{$val};"; @@ -150,7 +150,8 @@ foreach ($question->get_correct_response() as $key => $val) { // We've chosen not to send a specific seed since it is helpful to test the general feedback in a random context. $chatparams = $urlparams; -$chatparams['maximavars'] = $questionvarsinputs; +$chatparams['maximavars'] = $question->questionvariables; +$chatparams['inputs'] = $questionvarsinputs; $chatparams['simp'] = $simp; $chatparams['cas'] = $question->generalfeedback; $chatlink = new moodle_url('/question/type/stack/adminui/caschat.php', $chatparams); @@ -430,6 +431,16 @@ if ($question->has_random_variants()) { echo ' ' . stack_string('deploymanynotes'); echo html_writer::end_tag('form'); + // Systematic deployment of variants. + echo html_writer::start_tag('form', array('method' => 'get', 'class' => 'deploysystematic', + 'action' => new moodle_url('/question/type/stack/deploy.php', $urlparams))); + echo html_writer::input_hidden_params(new moodle_url($PAGE->url, array('sesskey' => sesskey())), array('seed')); + echo ' ' . html_writer::empty_tag('input', array('type' => 'submit', 'class' => 'btn btn-secondary', + 'value' => stack_string('deploysystematicbtn'))); + echo ' ' . html_writer::empty_tag('input', array('type' => 'text', 'size' => 3, + 'id' => 'deploysystematicfield', 'name' => 'deploysystematic', 'value' => '')); + echo html_writer::end_tag('form'); + // Deploy many from a CS list of integer seeds. echo "\n" . html_writer::start_tag('form', array('method' => 'get', 'class' => 'deployfromlist', 'action' => new moodle_url('/question/type/stack/deploy.php', $urlparams))); diff --git a/questiontype.php b/questiontype.php index e53c8074e000743d51c7cef86436d31a2608b0c7..5ca867b8e1ae521fe24bb6fa8fb94f45b4e5e2e5 100644 --- a/questiontype.php +++ b/questiontype.php @@ -94,12 +94,11 @@ class qtype_stack extends question_type { */ protected function fix_dollars_in_form_data($fromform) { $questionfields = array('questiontext', 'generalfeedback', 'specificfeedback', - 'prtcorrect', 'prtpartiallycorrect', 'prtincorrect'); + 'prtcorrect', 'prtpartiallycorrect', 'prtincorrect', 'questiondescription'); foreach ($questionfields as $field) { $fromform->{$field}['text'] = stack_maths::replace_dollars($fromform->{$field}['text']); } $fromform->questionnote = stack_maths::replace_dollars($fromform->questionnote); - $fromform->questiondescription = stack_maths::replace_dollars($fromform->questiondescription['text']); $prtnames = array_keys($this->get_prt_names_from_question($fromform->questiontext['text'], $fromform->specificfeedback['text'])); @@ -380,7 +379,7 @@ class qtype_stack extends question_type { } if (isset($fromform->testcases)) { - // If the data includes the defintion of the question tests that there + // If the data includes the definition of the question tests that there // should be (i.e. when doing import) then replace the existing set // of tests with the new one. $this->save_question_tests($fromform->id, $fromform->testcases); diff --git a/settings.php b/settings.php index 3ca30b81e77327bb3b4338723a9f90689f1ca1bf..efcf9868fdb1d964d447cebe759cd5c65746f551 100644 --- a/settings.php +++ b/settings.php @@ -73,7 +73,8 @@ $settings->add(new admin_setting_configselect('qtype_stack/maximaversion', array('5.40.0' => '5.40.0', '5.41.0' => '5.41.0', '5.42.0' => '5.42.0', '5.42.1' => '5.42.1', '5.42.2' => '5.42.2', '5.43.0' => '5.43.0', '5.43.1' => '5.43.1', '5.43.2' => '5.43.2', - '5.44.0' => '5.44.0', 'default' => 'default'))); + '5.44.0' => '5.44.0', '5.46.0' => '5.46.0', '5.47.0' => '5.47.0', + 'default' => 'default'))); $settings->add(new admin_setting_configtext('qtype_stack/castimeout', get_string('settingcastimeout', 'qtype_stack'), diff --git a/stack/cas/cassession2.class.php b/stack/cas/cassession2.class.php index 55a6537b6f8ae28c7d12c0bf9a21e32e687d7443..c9723cc345367c1824555e7d47b09f57ef42a2be 100644 --- a/stack/cas/cassession2.class.php +++ b/stack/cas/cassession2.class.php @@ -315,8 +315,10 @@ class stack_cas_session2 { // We will build the whole command here. // No protection in the block. $preblock = ''; - $command = 'block([]' . + $command = 'block([stack_seed]' . self::SEP . 'stack_randseed(' . $this->seed . ')'; + // Make the value of the seed available in the session. + $command .= self::SEP . 'stack_seed:' . $this->seed; // The options. $command .= $this->options->get_cas_commands()['commands']; // Some parts of logic storage. diff --git a/stack/cas/castext2/autogen/parser-grammar.pegjs b/stack/cas/castext2/autogen/parser-grammar.pegjs index 09e6c502cc0bee12b5d4534d1d6e1422250d544f..e2499e11b3f9cc4ff9d7b210ef536509cc2cad51 100644 --- a/stack/cas/castext2/autogen/parser-grammar.pegjs +++ b/stack/cas/castext2/autogen/parser-grammar.pegjs @@ -79,8 +79,6 @@ RawIfChars / !RawBlock c:'{#' { /** <?php return $c; ?> **/ return c; } / !(IOBlock/GenericBlock/EndBlock/ElIf/Else) c:'[[' { /** <?php return $c; ?> **/ return c; } - - CommentBlock = '[[' _ 'comment' _ ']]' content:CommentChars* '[[/' _ 'comment' _ ']]' { /** <?php @@ -101,7 +99,6 @@ CommentChars return c; } - EscapeBlock = '[[' _ 'escape' value:(__+ 'value' _ '=' _ String)? _ ']]' content:EscapeChars* '[[/' _ 'escape' _ ']]' { // Value parameter here is for backwards compatibility. Maybe we need a deprecation warning @@ -131,8 +128,6 @@ EscapeChars return c; } - - IOBlock = '[[' _ channel:Identifier _ ':' _ variable:Identifier _ ']]' { /** <?php @@ -191,7 +186,6 @@ ParamIdentifier "identifier" return char+morechars.join(""); } - // Unicode categories stolen from here https://github.com/pegjs/pegjs/blob/master/examples/javascript.pegjs#L374 // Letter, Lowercase ULetter "unicode letter character" @@ -201,7 +195,6 @@ ULetter "unicode letter character" / [\u01C5\u01C8\u01CB\u01F2\u1F88-\u1F8F\u1F98-\u1F9F\u1FA8-\u1FAF\u1FBC\u1FCC\u1FFC] / [\u0041-\u005A\u00C0-\u00D6\u00D8-\u00DE\u0100\u0102\u0104\u0106\u0108\u010A\u010C\u010E\u0110\u0112\u0114\u0116\u0118\u011A\u011C\u011E\u0120\u0122\u0124\u0126\u0128\u012A\u012C\u012E\u0130\u0132\u0134\u0136\u0139\u013B\u013D\u013F\u0141\u0143\u0145\u0147\u014A\u014C\u014E\u0150\u0152\u0154\u0156\u0158\u015A\u015C\u015E\u0160\u0162\u0164\u0166\u0168\u016A\u016C\u016E\u0170\u0172\u0174\u0176\u0178-\u0179\u017B\u017D\u0181-\u0182\u0184\u0186-\u0187\u0189-\u018B\u018E-\u0191\u0193-\u0194\u0196-\u0198\u019C-\u019D\u019F-\u01A0\u01A2\u01A4\u01A6-\u01A7\u01A9\u01AC\u01AE-\u01AF\u01B1-\u01B3\u01B5\u01B7-\u01B8\u01BC\u01C4\u01C7\u01CA\u01CD\u01CF\u01D1\u01D3\u01D5\u01D7\u01D9\u01DB\u01DE\u01E0\u01E2\u01E4\u01E6\u01E8\u01EA\u01EC\u01EE\u01F1\u01F4\u01F6-\u01F8\u01FA\u01FC\u01FE\u0200\u0202\u0204\u0206\u0208\u020A\u020C\u020E\u0210\u0212\u0214\u0216\u0218\u021A\u021C\u021E\u0220\u0222\u0224\u0226\u0228\u022A\u022C\u022E\u0230\u0232\u023A-\u023B\u023D-\u023E\u0241\u0243-\u0246\u0248\u024A\u024C\u024E\u0370\u0372\u0376\u037F\u0386\u0388-\u038A\u038C\u038E-\u038F\u0391-\u03A1\u03A3-\u03AB\u03CF\u03D2-\u03D4\u03D8\u03DA\u03DC\u03DE\u03E0\u03E2\u03E4\u03E6\u03E8\u03EA\u03EC\u03EE\u03F4\u03F7\u03F9-\u03FA\u03FD-\u042F\u0460\u0462\u0464\u0466\u0468\u046A\u046C\u046E\u0470\u0472\u0474\u0476\u0478\u047A\u047C\u047E\u0480\u048A\u048C\u048E\u0490\u0492\u0494\u0496\u0498\u049A\u049C\u049E\u04A0\u04A2\u04A4\u04A6\u04A8\u04AA\u04AC\u04AE\u04B0\u04B2\u04B4\u04B6\u04B8\u04BA\u04BC\u04BE\u04C0-\u04C1\u04C3\u04C5\u04C7\u04C9\u04CB\u04CD\u04D0\u04D2\u04D4\u04D6\u04D8\u04DA\u04DC\u04DE\u04E0\u04E2\u04E4\u04E6\u04E8\u04EA\u04EC\u04EE\u04F0\u04F2\u04F4\u04F6\u04F8\u04FA\u04FC\u04FE\u0500\u0502\u0504\u0506\u0508\u050A\u050C\u050E\u0510\u0512\u0514\u0516\u0518\u051A\u051C\u051E\u0520\u0522\u0524\u0526\u0528\u052A\u052C\u052E\u0531-\u0556\u10A0-\u10C5\u10C7\u10CD\u13A0-\u13F5\u1E00\u1E02\u1E04\u1E06\u1E08\u1E0A\u1E0C\u1E0E\u1E10\u1E12\u1E14\u1E16\u1E18\u1E1A\u1E1C\u1E1E\u1E20\u1E22\u1E24\u1E26\u1E28\u1E2A\u1E2C\u1E2E\u1E30\u1E32\u1E34\u1E36\u1E38\u1E3A\u1E3C\u1E3E\u1E40\u1E42\u1E44\u1E46\u1E48\u1E4A\u1E4C\u1E4E\u1E50\u1E52\u1E54\u1E56\u1E58\u1E5A\u1E5C\u1E5E\u1E60\u1E62\u1E64\u1E66\u1E68\u1E6A\u1E6C\u1E6E\u1E70\u1E72\u1E74\u1E76\u1E78\u1E7A\u1E7C\u1E7E\u1E80\u1E82\u1E84\u1E86\u1E88\u1E8A\u1E8C\u1E8E\u1E90\u1E92\u1E94\u1E9E\u1EA0\u1EA2\u1EA4\u1EA6\u1EA8\u1EAA\u1EAC\u1EAE\u1EB0\u1EB2\u1EB4\u1EB6\u1EB8\u1EBA\u1EBC\u1EBE\u1EC0\u1EC2\u1EC4\u1EC6\u1EC8\u1ECA\u1ECC\u1ECE\u1ED0\u1ED2\u1ED4\u1ED6\u1ED8\u1EDA\u1EDC\u1EDE\u1EE0\u1EE2\u1EE4\u1EE6\u1EE8\u1EEA\u1EEC\u1EEE\u1EF0\u1EF2\u1EF4\u1EF6\u1EF8\u1EFA\u1EFC\u1EFE\u1F08-\u1F0F\u1F18-\u1F1D\u1F28-\u1F2F\u1F38-\u1F3F\u1F48-\u1F4D\u1F59\u1F5B\u1F5D\u1F5F\u1F68-\u1F6F\u1FB8-\u1FBB\u1FC8-\u1FCB\u1FD8-\u1FDB\u1FE8-\u1FEC\u1FF8-\u1FFB\u2102\u2107\u210B-\u210D\u2110-\u2112\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u2130-\u2133\u213E-\u213F\u2145\u2183\u2C00-\u2C2E\u2C60\u2C62-\u2C64\u2C67\u2C69\u2C6B\u2C6D-\u2C70\u2C72\u2C75\u2C7E-\u2C80\u2C82\u2C84\u2C86\u2C88\u2C8A\u2C8C\u2C8E\u2C90\u2C92\u2C94\u2C96\u2C98\u2C9A\u2C9C\u2C9E\u2CA0\u2CA2\u2CA4\u2CA6\u2CA8\u2CAA\u2CAC\u2CAE\u2CB0\u2CB2\u2CB4\u2CB6\u2CB8\u2CBA\u2CBC\u2CBE\u2CC0\u2CC2\u2CC4\u2CC6\u2CC8\u2CCA\u2CCC\u2CCE\u2CD0\u2CD2\u2CD4\u2CD6\u2CD8\u2CDA\u2CDC\u2CDE\u2CE0\u2CE2\u2CEB\u2CED\u2CF2\uA640\uA642\uA644\uA646\uA648\uA64A\uA64C\uA64E\uA650\uA652\uA654\uA656\uA658\uA65A\uA65C\uA65E\uA660\uA662\uA664\uA666\uA668\uA66A\uA66C\uA680\uA682\uA684\uA686\uA688\uA68A\uA68C\uA68E\uA690\uA692\uA694\uA696\uA698\uA69A\uA722\uA724\uA726\uA728\uA72A\uA72C\uA72E\uA732\uA734\uA736\uA738\uA73A\uA73C\uA73E\uA740\uA742\uA744\uA746\uA748\uA74A\uA74C\uA74E\uA750\uA752\uA754\uA756\uA758\uA75A\uA75C\uA75E\uA760\uA762\uA764\uA766\uA768\uA76A\uA76C\uA76E\uA779\uA77B\uA77D-\uA77E\uA780\uA782\uA784\uA786\uA78B\uA78D\uA790\uA792\uA796\uA798\uA79A\uA79C\uA79E\uA7A0\uA7A2\uA7A4\uA7A6\uA7A8\uA7AA-\uA7AD\uA7B0-\uA7B4\uA7B6\uFF21-\uFF3A] - // These parts could surely benefit from expansion but STACK probably will not support all possibilities. IdentifierStart = [a-zA-Z] @@ -403,7 +396,6 @@ StringCharsB / "\\\\" { return "\\"; } / "\\'" { return "'"; } - _ "whitespace allowed" = [ \t\n\r]* diff --git a/stack/cas/castext2/blocks/iframe.block.php b/stack/cas/castext2/blocks/iframe.block.php index 982c4f053d723ee25c32ed581f4eb8df18ae0158..ca3ce3312128faa7fa7ae631bfc2cc7560e3be19 100644 --- a/stack/cas/castext2/blocks/iframe.block.php +++ b/stack/cas/castext2/blocks/iframe.block.php @@ -33,7 +33,14 @@ class stack_cas_castext2_iframe extends stack_cas_castext2_block { // All frames need unique (at request level) identifiers, // we use running numbering. - private static $countframes = 1; + private static $counters = ['///IFRAME_COUNT///' => 1]; + + // Add separate running numbering for different block types to + // ease debugging, so that one does not need to know which all affect + // the numbers. This numbering applies only to the titles. + public static function register_counter(string $name): void { + self::$counters[$name] = 1; + } public function compile($format, $options): ?MP_Node { $r = new MP_List([ @@ -79,8 +86,8 @@ class stack_cas_castext2_iframe extends stack_cas_castext2_block { return ''; } - $divid = 'stack-iframe-holder-' . self::$countframes; - $frameid = 'stack-iframe-' . self::$countframes; + $divid = 'stack-iframe-holder-' . self::$counters['///IFRAME_COUNT///']; + $frameid = 'stack-iframe-' . self::$counters['///IFRAME_COUNT///']; $parameters = json_decode($params[1], true); $content = ''; @@ -135,9 +142,16 @@ class stack_cas_castext2_iframe extends stack_cas_castext2_block { } // Some form of title for debug and accessibility. - $title = 'STACK IFRAME ' . self::$countframes; + $title = 'STACK IFRAME ' . self::$counters['///IFRAME_COUNT///']; if (isset($parameters['title'])) { $title = $parameters['title']; + // Counter updates. + foreach (self::$counters as $key => $value) { + if (strpos($title, $key) !== false) { + $title = str_replace($key, '' . $value, $title); + self::$counters[$key] = $value + 1; + } + } } $scrolling = true; if (isset($parameters['scrolling'])) { @@ -170,7 +184,7 @@ class stack_cas_castext2_iframe extends stack_cas_castext2_block { 'require(["qtype_stack/stackjsvle"], ' . 'function(stackjsvle,){stackjsvle.create_iframe(' . implode(',', $args). ');});'); - self::$countframes = self::$countframes + 1; + self::$counters['///IFRAME_COUNT///'] = self::$counters['///IFRAME_COUNT///'] + 1; // Output the placeholder for this frame. return html_writer::tag('div', '', $attributes); diff --git a/stack/cas/castext2/blocks/javascript.block.php b/stack/cas/castext2/blocks/javascript.block.php index 12759f142df78089cf7538f07f3cf4e74e18f488..9a3f85190ec49fd672cd06e9c32808539a038232 100644 --- a/stack/cas/castext2/blocks/javascript.block.php +++ b/stack/cas/castext2/blocks/javascript.block.php @@ -23,6 +23,9 @@ require_once(__DIR__ . '/root.specialblock.php'); require_once(__DIR__ . '/stack_translate.specialblock.php'); require_once(__DIR__ . '/../../../../vle_specific.php'); +require_once(__DIR__ . '/iframe.block.php'); +stack_cas_castext2_iframe::register_counter('///JAVASCRIPT_COUNT///'); + /** * A convenience block for creation of iframes with input-references and * stack_js pre-loaded. Use when you want to build some logic connected @@ -35,9 +38,6 @@ require_once(__DIR__ . '/../../../../vle_specific.php'); */ class stack_cas_castext2_javascript extends stack_cas_castext2_block { - /* We still count the scripts. */ - public static $countscripts = 1; - public function compile($format, $options): ? MP_Node { $r = new MP_List([new MP_String('iframe')]); @@ -52,8 +52,7 @@ class stack_cas_castext2_javascript extends stack_cas_castext2_block { // These will be hidden. $pars = ['hidden' => true]; // Set a title. - $pars['title'] = 'STACK javascript ' . self::$countscripts; - self::$countscripts = self::$countscripts + 1; + $pars['title'] = 'STACK javascript ///JAVASCRIPT_COUNT///'; $r->items[] = new MP_String(json_encode($pars)); diff --git a/stack/cas/castext2/blocks/jsxgraph.block.php b/stack/cas/castext2/blocks/jsxgraph.block.php index 86c545a97f2cc987faab5b6ecd0a8925d51262a9..96cf91bf08bcd088b98ea443355cba28949fc392 100644 --- a/stack/cas/castext2/blocks/jsxgraph.block.php +++ b/stack/cas/castext2/blocks/jsxgraph.block.php @@ -22,6 +22,9 @@ require_once(__DIR__ . '/root.specialblock.php'); require_once(__DIR__ . '/stack_translate.specialblock.php'); require_once(__DIR__ . '/../../../../vle_specific.php'); +require_once(__DIR__ . '/iframe.block.php'); +stack_cas_castext2_iframe::register_counter('///JSXGRAPH_COUNT///'); + class stack_cas_castext2_jsxgraph extends stack_cas_castext2_block { /* This is not something we want people to edit in general. */ @@ -38,9 +41,6 @@ class stack_cas_castext2_jsxgraph extends stack_cas_castext2_block { ] ]; - /* We still count the graphs. */ - public static $countgraphs = 1; - public function compile($format, $options): ? MP_Node { $r = new MP_List([new MP_String('iframe')]); @@ -70,8 +70,7 @@ class stack_cas_castext2_jsxgraph extends stack_cas_castext2_block { // Disable scrolling for this. $xpars['scrolling'] = false; // Set a title. - $xpars['title'] = 'STACK JSXGraph ' . self::$countgraphs; - self::$countgraphs = self::$countgraphs + 1; + $xpars['title'] = 'STACK JSXGraph ///JSXGRAPH_COUNT///'; // Figure out what scripts we serve. $css = self::$namedversions['local']['css']; diff --git a/stack/cas/castext2/blocks/reveal.block.php b/stack/cas/castext2/blocks/reveal.block.php index 803b87927644dd475eedc29dee4666ce7dd4de74..32064c86145c8bd8dc971854896ac31480072860 100644 --- a/stack/cas/castext2/blocks/reveal.block.php +++ b/stack/cas/castext2/blocks/reveal.block.php @@ -18,6 +18,10 @@ defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../block.interface.php'); require_once(__DIR__ . '/../../../utils.class.php'); +// Register a counter. +require_once(__DIR__ . '/iframe.block.php'); +stack_cas_castext2_iframe::register_counter('///REVEAL_COUNT///'); + /** * A dynamic JavaScript backed that toggles the visibility of its contents. * Based on the value of a given input. Will only do singular direct string @@ -25,11 +29,8 @@ require_once(__DIR__ . '/../../../utils.class.php'); */ class stack_cas_castext2_reveal extends stack_cas_castext2_block { - // All reveals need unique (at request level) identifiers, - // we use running numbering. - private static $countreveals = 1; - public function compile($format, $options): ?MP_Node { + static $count = 0; /* * This block compiles into multiple things. * 1. There is the default hidden div containing the contents. @@ -41,8 +42,12 @@ class stack_cas_castext2_reveal extends stack_cas_castext2_block { */ $body = new MP_List([new MP_String('%root')]); + // This should have enough randomness to avoid collisions. + $uid = '' . rand(100, 999) . time() . '_' . $count; + $count = $count + 1; + // Name and hide the contents. - $body->items[] = new MP_String('<div style="display:none;" id="stack-reveal-' . self::$countreveals . '">'); + $body->items[] = new MP_String('<div style="display:none;" id="stack-reveal-' . $uid . '">'); foreach ($this->children as $item) { $c = $item->compile($format, $options); @@ -58,20 +63,20 @@ class stack_cas_castext2_reveal extends stack_cas_castext2_block { // Once we get the access immediately bind a listener to it. $code .= 'const input = document.getElementById(id);'; $code .= 'input.addEventListener("change",(e)=>{'; - $code .= 'stack_js.toggle_visibility("stack-reveal-' . self::$countreveals . '",input.value===' . + $code .= 'stack_js.toggle_visibility("stack-reveal-' . $uid . '",input.value===' . json_encode($this->params['value']) . ');});'; // Finally check whether the value was already matching, or // if it changed during the previous steps. - $code .= 'stack_js.toggle_visibility("stack-reveal-' . self::$countreveals . '",input.value===' . + $code .= 'stack_js.toggle_visibility("stack-reveal-' . $uid . '",input.value===' . json_encode($this->params['value']) . ');'; $code .= '});'; // Now add a hidden [[iframe]] with suitable scripts. $body->items[] = new MP_List([ new MP_String('iframe'), - new MP_String(json_encode(['hidden' => true, 'title' => 'Logic container for a revealing portion ' . - self::$countreveals . '.'])), + new MP_String(json_encode(['hidden' => true, + 'title' => 'Logic container for a revealing portion ///REVEAL_COUNT///.'])), new MP_List([ new MP_String('script'), new MP_String(json_encode(['type' => 'module'])), @@ -79,9 +84,6 @@ class stack_cas_castext2_reveal extends stack_cas_castext2_block { ]) ]); - // Update count. - self::$countreveals = self::$countreveals + 1; - return $body; } diff --git a/stack/cas/castext2/blocks/todo.block.php b/stack/cas/castext2/blocks/todo.block.php new file mode 100644 index 0000000000000000000000000000000000000000..8283985f1bcb58a87cc76da75f3bea7f9b5bc920 --- /dev/null +++ b/stack/cas/castext2/blocks/todo.block.php @@ -0,0 +1,37 @@ +<?php +// This file is part of Stateful +// +// Stateful 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. +// +// Stateful 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 Stateful. If not, see <http://www.gnu.org/licenses/>. +defined('MOODLE_INTERNAL') || die(); + + +require_once(__DIR__ . '/../block.interface.php'); + + +class stack_cas_castext2_todo extends stack_cas_castext2_block { + + public function compile($format, $options): ?MP_Node { + $body = new MP_List([new MP_String('%root')]); + $body->items[] = new MP_String('<!--- stack_todo --->'); + return $body; + } + + public function is_flat(): bool { + return true; + } + + public function validate_extract_attributes(): array { + return array(); + } +} diff --git a/stack/cas/connector.healthcheck.class.php b/stack/cas/connector.healthcheck.class.php new file mode 100644 index 0000000000000000000000000000000000000000..1f206e3a9cce8fbc900ae274f6dd2964721dc463 --- /dev/null +++ b/stack/cas/connector.healthcheck.class.php @@ -0,0 +1,242 @@ +<?php +// This file is part of Stack - http://stack.maths.ed.ac.uk/ +// +// Stack 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. +// +// Stack 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 Stack. If not, see <http://www.gnu.org/licenses/>. + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../../locallib.php'); +require_once(__DIR__ . '/../utils.class.php'); +require_once(__DIR__ . '/../options.class.php'); +require_once(__DIR__ . '/connectorhelper.class.php'); +require_once(__DIR__ . '/cassession2.class.php'); +require_once(__DIR__ . '/castext2/castext2_evaluatable.class.php'); +require_once(__DIR__ . '/connector.dbcache.class.php'); +require_once(__DIR__ . '/installhelper.class.php'); + +/** + * This class supports the healthcheck functions.. + * + * @copyright 2023 The University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require_once(__DIR__ . '/ast.container.class.php'); +require_once(__DIR__ . '/connectorhelper.class.php'); +require_once(__DIR__ . '/cassession2.class.php'); + + +class stack_cas_healthcheck { + /* This variable holds the state of the healthcheck. */ + protected $ishealthy = true; + + protected $config = null; + + protected $tests = array(); + + public function __construct($config) { + global $CFG; + $this->config = $config; + + // Record the platform in the summary. + $test = array(); + $test['tag'] = 'platform'; + $test['result'] = null; + $test['summary'] = $config->platform; + $test['details'] = null; + $this->tests[] = $test; + + // List the requested maxima packages in ths summary. + $test = array(); + $test['tag'] = 'settingmaximalibraries'; + $test['result'] = null; + $test['summary'] = $config->maximalibraries; + $test['details'] = null; + $this->tests[] = $test; + + // Check if the current options for library packages are permitted (maximalibraries). + list($result, $message) = stack_cas_configuration::validate_maximalibraries(); + if (!$result) { + $this->ishealthy = false; + $test = array(); + $test['tag'] = 'settingmaximalibraries'; + $test['result'] = $result; + $test['summary'] = $message; + $test['details'] = html_writer::tag('p', $message); + $test['details'] .= html_writer::tag('p', stack_string('settingmaximalibraries_failed')); + $test['details'] .= html_writer::tag('p', stack_string('settingmaximalibraries_desc')); + $this->tests[] = $test; + } + + // Try to connect to create maxima local. + stack_cas_configuration::create_maximalocal(); + + // Make sure we are in a position to call maxima. + switch ($config->platform) { + case 'win': + $maximalocation = stack_cas_configuration::confirm_maxima_win_location(); + if ('' != $maximalocation) { + $test = array(); + $test['tag'] = 'stackmaximalibraries'; + $test['result'] = null; + $test['summary'] = null; + $test['details'] = html_writer::tag('p', stack_string('healthcheckconfigintro1').' '. + html_writer::tag('tt', $maximalocation)); + $this->tests[] = $test; + } else { + $this->ishealthy = false; + $test = array(); + $test['result'] = false; + $test['summary'] = "Could not confirm the location of Maxima"; + $this->tests[] = $test; + } + + stack_cas_configuration::copy_maxima_bat(); + + if (!is_readable($CFG->dataroot . '/stack/maxima.bat')) { + $this->ishealthy = false; + $test = array(); + $test['tag'] = 'healthcheckmaximabat'; + $test['result'] = false; + $test['summary'] = stack_string('healthcheckmaximabatinfo', $CFG->dataroot); + $test['details'] = html_writer::tag('p', stack_string('healthcheckmaximabatinfo', $CFG->dataroot)); + $this->tests[] = $test; + } + + break; + case 'linux': + // On a raw linux server list the versions of Maxima available. + $connection = stack_connection_helper::make(); + $test = array(); + $test['tag'] = 'healthcheckmaximaavailable'; + $test['result'] = null; + $test['summary'] = null; + $test['details'] = html_writer::tag('pre', $connection->get_maxima_available()); + $this->tests[] = $test; + break; + default: + // Server/optimised. + // TODO: add in any specific tests for these setups? + break; + } + + // Record the contents of the maximalocal file. + if ($this->ishealthy) { + $test = array(); + $test['tag'] = 'healthcheckmaximalocal'; + $test['result'] = null; + $test['summary'] = null; + $test['details'] = html_writer::tag('textarea', stack_cas_configuration::generate_maximalocal_contents(), + array('readonly' => 'readonly', 'wrap' => 'virtual', 'rows' => '32', 'cols' => '100')); + $this->tests[] = $test; + } + + // Test an *uncached* call to the CAS. I.e. a genuine call to the process. + if ($this->ishealthy) { + list($message, $genuinedebug, $result) = stack_connection_helper::stackmaxima_genuine_connect(); + $this->ishealthy = $result; + + $test = array(); + $test['tag'] = 'healthuncached'; + $test['result'] = $result; + $test['summary'] = $message; + $test['details'] = html_writer::tag('p', stack_string('healthuncachedintro')) . $message; + $test['details'] .= $genuinedebug; + $this->tests[] = $test; + } + + // Test Maxima connection. + if ($this->ishealthy) { + // Intentionally use get_string for the sample CAS and plots, so we don't render + // the maths too soon. + $this->output_cas_text('healthcheckconnect', + stack_string('healthcheckconnectintro'), get_string('healthchecksamplecas', 'qtype_stack')); + $this->output_cas_text('healthcheckconnectunicode', + stack_string('healthcheckconnectintro'), get_string('healthchecksamplecasunicode', 'qtype_stack')); + $this->output_cas_text('healthcheckplots', + stack_string('healthcheckplotsintro'), get_string('healthchecksampleplots', 'qtype_stack')); + } + + // If we have a linux machine, and we are testing the raw connection then we should + // attempt to automatically create an optimized maxima image on the system. + if ($this->ishealthy && $config->platform === 'linux') { + list($message, $debug, $result, $commandline, $rawcommand) + = stack_connection_helper::stackmaxima_auto_maxima_optimise($genuinedebug); + $test = array(); + $test['tag'] = 'healthautomaxopt'; + $test['result'] = $result; + $test['summary'] = $message; + $test['details'] = html_writer::tag('p', stack_string('healthautomaxoptintro')); + $test['details'] .= html_writer::tag('pre', $debug); + $this->tests[] = $test; + } + + if ($this->ishealthy) { + list($message, $details, $result) = stack_connection_helper::stackmaxima_version_healthcheck(); + $test = array(); + $test['tag'] = 'healthchecksstackmaximaversion'; + $test['result'] = $result; + $test['summary'] = stack_string($message, $details); + $test['details'] = stack_string($message, $details); + $this->tests[] = $test; + } + + // Record whether caching is taking place in the summary. + $test = array(); + $test['tag'] = 'settingcasresultscache'; + $test['result'] = null; + $test['summary'] = stack_string('healthcheckcache_' . $config->casresultscache); + $test['details'] = null; + $this->tests[] = $test; + } + + /* + * Try and evaluate the raw castext and build a result entry. + */ + private function output_cas_text($title, $intro, $castext) { + $ct = castext2_evaluatable::make_from_source($castext, 'healthcheck'); + $session = new stack_cas_session2([$ct]); + $session->instantiate(); + + $test = array(); + $test['tag'] = $title; + $test['result'] = null; + $test['summary'] = null; + $test['details'] = html_writer::tag('p', $intro) . html_writer::tag('pre', s($castext)); + + if ($session->get_errors()) { + $this->ishealthy = false; + $test['result'] = false; + $test['summary'] = stack_string('errors') . $ct->get_errors(); + $test['details'] .= stack_string('errors') . $ct->get_errors(); + $test['details'] .= stack_string('debuginfo') . $session->get_debuginfo(); + } else { + $test['details'] .= html_writer::tag('p', stack_ouput_castext($ct->get_rendered())); + } + $this->tests[] = $test; + } + + /* + * This function returns a summary of the status of the healthcheck. + */ + public function get_test_results() { + return $this->tests; + } + + /* + * Return overall results. + */ + public function get_overall_result() { + return $this->ishealthy; + } +} diff --git a/stack/cas/installhelper.class.php b/stack/cas/installhelper.class.php index afe54e3e4102729c0a262cf4268bee9c247aad1c..4328722e047208c63700349555e17ac3833253aa 100644 --- a/stack/cas/installhelper.class.php +++ b/stack/cas/installhelper.class.php @@ -183,10 +183,6 @@ class stack_cas_configuration { public function copy_maxima_bat() { global $CFG; - if ($this->settings->platform != 'win') { - return true; - } - $batchfilename = $this->maxima_win_location() . 'bin/maxima.bat'; if (substr_count($batchfilename, ' ') === 0) { $batchfilecontents = "rem Auto-generated Maxima batch file. \n\n"; @@ -205,16 +201,6 @@ class stack_cas_configuration { return true; } - public function maxima_bat_is_ok() { - global $CFG; - - if ($this->settings->platform != 'win') { - return true; - } - - return is_readable($CFG->dataroot . '/stack/maxima.bat'); - } - public function get_maximalocal_contents() { $contents = <<<END /* ***********************************************************************/ @@ -324,8 +310,6 @@ END; make_upload_directory('stack/plots'); make_upload_directory('stack/tmp'); - self::get_instance()->copy_maxima_bat(); - if (!file_put_contents(self::maximalocal_location(), self::generate_maximalocal_contents())) { throw new stack_exception('Failed to write Maxima configuration file.'); } @@ -339,14 +323,6 @@ END; return self::get_instance()->get_maximalocal_contents(); } - /** - * Generate the contents for the maximalocal configuration file. - * @return string the contents that the maximalocal.mac file should have. - */ - public static function maxima_bat_is_missing() { - return !self::get_instance()->maxima_bat_is_ok(); - } - /** * Generate the directoryname * @return string the contents that the maximalocal.mac file should have. diff --git a/stack/cas/parsingrules/003_no_dot_dot.filter.php b/stack/cas/parsingrules/003_no_dot_dot.filter.php index 8531678875aa298b0865db793e41bae278246d4d..b1e03c71c2c98644e9a32f7ece14865d427b6a60 100644 --- a/stack/cas/parsingrules/003_no_dot_dot.filter.php +++ b/stack/cas/parsingrules/003_no_dot_dot.filter.php @@ -84,8 +84,7 @@ class stack_ast_filter_003_no_dot_dot implements stack_cas_astfilter { $node->position['invalid'] = true; // No need to warn about this if we are already invalid due to whatever reason. $answernotes[] = 'MatrixMultWithFloat'; - $errors[] = 'Due to syntactical reasons matrix multiplication "." with scalar floats is ' . - 'forbidden, use normal multiplication "*" instead for the same result. ' . $node->toString(); + $errors[] = stack_string('stackCas_baddotdot') . $node->toString(); } } } diff --git a/stack/cas/security-map.json b/stack/cas/security-map.json index 948a130283b69dae5cd2dca58cf254db1bfdd100..13e2f2ee4b0ca60b9f696e3cdaf7ca3aa0db5aaf 100644 --- a/stack/cas/security-map.json +++ b/stack/cas/security-map.json @@ -39,6 +39,10 @@ "function": "f", "globalyforbiddenvariable": true }, + "%_ce_expedite": { + "function": "f", + "globalyforbiddenvariable": true + }, "%ERR": { "variable": "f" }, @@ -7314,6 +7318,9 @@ "stackunits_make": { "function": "s" }, + "stack_seed": { + "constant": "t" + }, "stack_unit_si_declare": { "function": "s", "contextvariable": "true" diff --git a/stack/input/algebraic/algebraic.class.php b/stack/input/algebraic/algebraic.class.php index 3e0e07e2f0bfe6918c95fc3884a0a5616cae44f8..02b94f09119373fea8a4414bce4a2d0023c45ab6 100644 --- a/stack/input/algebraic/algebraic.class.php +++ b/stack/input/algebraic/algebraic.class.php @@ -24,9 +24,9 @@ class stack_algebraic_input extends stack_input { protected $extraoptions = array( 'hideanswer' => false, + 'allowempty' => false, 'simp' => false, 'rationalized' => false, - 'allowempty' => false, 'nounits' => false, 'align' => 'left', 'consolidatesubscripts' => false, diff --git a/stack/input/boolean/boolean.class.php b/stack/input/boolean/boolean.class.php index 5cbf61daf611e86695ed0a3992f2c2c17199488f..4a23bdc350509bc3d0ce3cc5aa6428179ed45f47 100644 --- a/stack/input/boolean/boolean.class.php +++ b/stack/input/boolean/boolean.class.php @@ -33,11 +33,6 @@ class stack_boolean_input extends stack_input { ); } - protected $extraoptions = array( - 'hideanswer' => false, - 'allowempty' => false - ); - protected function extra_validation($contents) { $validation = $contents[0]; if ($validation === 'EMPTYANSWER') { diff --git a/stack/input/dropdown/dropdown.class.php b/stack/input/dropdown/dropdown.class.php index 269a090448f5c806b1049f31b3437e1a14704270..7811b8ca5b4e7c2fb662c132ea02f01e15ff0b59 100644 --- a/stack/input/dropdown/dropdown.class.php +++ b/stack/input/dropdown/dropdown.class.php @@ -67,13 +67,24 @@ class stack_dropdown_input extends stack_input { */ protected $teacheranswerdisplay = ''; - protected function internal_contruct() { + protected function internal_construct() { $options = $this->get_parameter('options'); if ($options != null && trim($options) != '') { $options = explode(',', $options); foreach ($options as $option) { $option = strtolower(trim($option)); + list($opt, $arg) = stack_utils::parse_option($option); + if (array_key_exists($opt, $this->extraoptions)) { + if ($arg === '') { + // Extra options with no argument set a Boolean flag. + $this->extraoptions[$opt] = true; + } else { + $this->extraoptions[$opt] = $arg; + } + break; + } + switch($option) { // Does a student see LaTeX or cassting values? case 'latex': @@ -529,6 +540,9 @@ class stack_dropdown_input extends stack_input { * @return string the teacher's answer, displayed to the student in the general feedback. */ public function get_teacher_answer_display($value, $display) { + if ($this->get_extra_option('hideanswer')) { + return ''; + } // Can we really ignore the $value and $display inputs here and rely on the internal state? return stack_string('teacheranswershow_mcq', array('display' => $this->teacheranswerdisplay)); } diff --git a/stack/input/equiv/equiv.class.php b/stack/input/equiv/equiv.class.php index 44531bf41bb27b7e03c06fc36cd4233e2a5dcc8c..5b02c14699253c746558fac46f94d0e7c2e271f2 100644 --- a/stack/input/equiv/equiv.class.php +++ b/stack/input/equiv/equiv.class.php @@ -35,6 +35,8 @@ class stack_equiv_input extends stack_input { * @var array */ protected $extraoptions = array( + 'hideanswer' => false, + 'allowempty' => false, 'nounits' => false, // Does a student see the equivalence signs at validation time? 'hideequiv' => false, @@ -136,6 +138,9 @@ class stack_equiv_input extends stack_input { $contents = array(); if (array_key_exists($this->name, $response)) { $sans = $response[$this->name]; + if (trim($sans) == '' && $this->get_extra_option('allowempty')) { + return array('EMPTYANSWER'); + } $rowsin = explode("\n", $sans); $rowsout = array(); foreach ($rowsin as $key => $row) { @@ -161,8 +166,9 @@ class stack_equiv_input extends stack_input { 'nontuples' => false ); foreach ($caslines as $line) { - if ($line->get_valid()) { - $vals[] = $line->ast_to_string(null, $params); + $str = $line->ast_to_string(null, $params); + if ($line->get_valid() || $str === 'EMPTYANSWER') { + $vals[] = $str; } else { // This is an empty place holder for an invalid expression. $vals[] = 'EMPTYCHAR'; @@ -435,6 +441,9 @@ class stack_equiv_input extends stack_input { * @return string the teacher's answer, displayed to the student in the general feedback. */ public function get_teacher_answer_display($value, $display) { + if ($this->get_extra_option('hideanswer')) { + return ''; + } $values = stack_utils::list_to_array($value, false); foreach ($values as $key => $val) { if (trim($val) !== '' ) { @@ -462,6 +471,9 @@ class stack_equiv_input extends stack_input { if (self::BLANK == $state->status) { return ''; } + if ($this->get_extra_option('allowempty') && $this->is_blank_response($state->contents)) { + return ''; + } if ($this->get_parameter('showValidation', 1) == 0 && self::INVALID != $state->status) { return ''; diff --git a/stack/input/inputbase.class.php b/stack/input/inputbase.class.php index af7c338f23baf3a10cddaa623ab2e11461e4d6fb..1f5057b03969c7688173b008934f06c7d052e110 100644 --- a/stack/input/inputbase.class.php +++ b/stack/input/inputbase.class.php @@ -88,7 +88,10 @@ abstract class stack_input { * For examples see the numerical input. * @var array */ - protected $extraoptions = array(); + protected $extraoptions = array( + 'hideanswer' => false, + 'allowempty' => false + ); /** * The question level options for CAS sessions. @@ -162,14 +165,14 @@ abstract class stack_input { } } - $this->internal_contruct(); + $this->internal_construct(); } /** * This allows each input type to adapt to the values of parameters. For example, the dropdown and units * use this to sort out options. */ - protected function internal_contruct() { + protected function internal_construct() { $options = $this->get_parameter('options'); if (trim($options ?? '') != '') { $options = explode(',', $options); @@ -454,7 +457,7 @@ abstract class stack_input { if ($parameter == 'insertStars') { $this->parameters['grammarAutofixes'] = stack_input_factory::convert_legacy_insert_stars($value); } - $this->internal_contruct(); + $this->internal_construct(); } /** @@ -1449,7 +1452,8 @@ abstract class stack_input { $class .= ' empty'; } - $feedback = html_writer::tag($divspan, $feedback, array('class' => $class, 'id' => $fieldname.'_val')); + $feedback = html_writer::tag($divspan, $feedback, + ['class' => $class, 'id' => $fieldname.'_val', 'aria-live' => 'assertive']); $response = str_replace("[[validation:{$name}]]", $feedback, $questiontext); return $response; diff --git a/stack/input/matrix/matrix.class.php b/stack/input/matrix/matrix.class.php index ea9991788021285d39b707b08efb55de74251a89..75ea79ecdd7da3a6c295f7a8cad9385c738fc9f4 100644 --- a/stack/input/matrix/matrix.class.php +++ b/stack/input/matrix/matrix.class.php @@ -25,9 +25,10 @@ class stack_matrix_input extends stack_input { protected $height; protected $extraoptions = array( + 'hideanswer' => false, + 'allowempty' => false, 'nounits' => false, 'simp' => false, - 'allowempty' => false, 'consolidatesubscripts' => false, 'checkvars' => 0, 'validator' => false diff --git a/stack/input/notes/notes.class.php b/stack/input/notes/notes.class.php index d6ac9404da2be1047601a9421d788130fb950d0d..8ee324991eb2f9d0a699b35801e44e37734befae 100644 --- a/stack/input/notes/notes.class.php +++ b/stack/input/notes/notes.class.php @@ -29,6 +29,7 @@ class stack_notes_input extends stack_input { protected $extraoptions = array( 'hideanswer' => false, + 'allowempty' => false, 'manualgraded' => false, ); diff --git a/stack/input/numerical/numerical.class.php b/stack/input/numerical/numerical.class.php index 666242bba85c00d720d2ebb7e456911ac875d3a1..683d811eb78f32316494b0758c5c1a1a965b116d 100644 --- a/stack/input/numerical/numerical.class.php +++ b/stack/input/numerical/numerical.class.php @@ -29,6 +29,7 @@ class stack_numerical_input extends stack_input { */ protected $extraoptions = array( 'hideanswer' => false, + 'allowempty' => false, 'nounits' => false, 'simp' => false, // Forbid variables. Always true for numerical inputs. @@ -46,7 +47,6 @@ class stack_numerical_input extends stack_input { // Require min/max number of significant figures? 'minsf' => false, 'maxsf' => false, - 'allowempty' => false, 'align' => 'left', 'validator' => false ); diff --git a/stack/input/singlechar/singlechar.class.php b/stack/input/singlechar/singlechar.class.php index 76abe31794e07116e7d10dbd7a1705e07eab6355..60318b9b3ef8357a9e8c39f03425d60a0577af1c 100644 --- a/stack/input/singlechar/singlechar.class.php +++ b/stack/input/singlechar/singlechar.class.php @@ -23,8 +23,9 @@ class stack_singlechar_input extends stack_input { protected $extraoptions = array( - 'nounits' => true, + 'hideanswer' => false, 'allowempty' => false, + 'nounits' => true, 'validator' => false ); diff --git a/stack/input/textarea/textarea.class.php b/stack/input/textarea/textarea.class.php index 95643264014a2481a4186e1863ee09bbc021c0fb..2c1ce9535b186648b40752fd0e514a5711cbe659 100644 --- a/stack/input/textarea/textarea.class.php +++ b/stack/input/textarea/textarea.class.php @@ -29,6 +29,8 @@ require_once(__DIR__ . '/../../utils.class.php'); class stack_textarea_input extends stack_input { protected $extraoptions = array( + 'hideanswer' => false, + 'allowempty' => false, 'nounits' => true, 'simp' => false, 'consolidatesubscripts' => false @@ -94,6 +96,9 @@ class stack_textarea_input extends stack_input { $contents = array(); if (array_key_exists($this->name, $response)) { $sans = $response[$this->name]; + if (trim($sans) == '' && $this->get_extra_option('allowempty')) { + return array('EMPTYANSWER'); + } $rowsin = explode("\n", $sans); $rowsout = array(); foreach ($rowsin as $key => $row) { @@ -119,8 +124,9 @@ class stack_textarea_input extends stack_input { 'nontuples' => false ); foreach ($caslines as $line) { - if ($line->get_valid()) { - $vals[] = $line->ast_to_string(null, $params); + $str = $line->ast_to_string(null, $params); + if ($line->get_valid() || $str === 'EMPTYANSWER') { + $vals[] = $str; } else { // This is an empty place holder for an invalid expression. $vals[] = 'EMPTYCHAR'; @@ -287,6 +293,9 @@ class stack_textarea_input extends stack_input { * @return string the teacher's answer, displayed to the student in the general feedback. */ public function get_teacher_answer_display($value, $display) { + if ($this->get_extra_option('hideanswer')) { + return ''; + } $values = stack_utils::list_to_array($value, false); foreach ($values as $key => $val) { if (trim($val) !== '' ) { diff --git a/stack/input/units/units.class.php b/stack/input/units/units.class.php index 247ea1ae2ea7c9e045036756083874711c1676e4..f37e4a8dccb27896d349d866b6b25d2c10120ee8 100644 --- a/stack/input/units/units.class.php +++ b/stack/input/units/units.class.php @@ -28,6 +28,8 @@ class stack_units_input extends stack_input { * @var array */ protected $extraoptions = array( + 'hideanswer' => false, + 'allowempty' => false, 'simp' => false, 'negpow' => false, // Require min/max number of decimal places? @@ -36,7 +38,6 @@ class stack_units_input extends stack_input { // Require min/max number of significant figures? 'minsf' => false, 'maxsf' => false, - 'allowempty' => false, 'align' => 'left', 'consolidatesubscripts' => false, 'validator' => false @@ -151,6 +152,9 @@ class stack_units_input extends stack_input { * @return string the teacher's answer, displayed to the student in the general feedback. */ public function get_teacher_answer_display($value, $display) { + if ($this->get_extra_option('hideanswer')) { + return ''; + } if (trim($value) == 'EMPTYANSWER') { return stack_string('teacheranswerempty'); } diff --git a/stack/input/varmatrix/varmatrix.class.php b/stack/input/varmatrix/varmatrix.class.php index 2608766062c5a4557398c293f98a3972939bf992..bc2b60605c0eeb9a9c72fcc03b46192258465b99 100644 --- a/stack/input/varmatrix/varmatrix.class.php +++ b/stack/input/varmatrix/varmatrix.class.php @@ -33,9 +33,10 @@ class stack_varmatrix_input extends stack_input { ); protected $extraoptions = array( + 'hideanswer' => false, + 'allowempty' => false, 'simp' => false, 'rationalized' => false, - 'allowempty' => false, 'consolidatesubscripts' => false, 'checkvars' => 0, 'validator' => false @@ -162,7 +163,7 @@ class stack_varmatrix_input extends stack_input { $vals = array(); foreach ($caslines as $line) { if ($line->get_valid()) { - $vals[] = $line->get_evaluationform(); + $vals[] = $line->get_inputform(); } else { // This is an empty place holder for an invalid expression. $vals[] = 'EMPTYCHAR'; diff --git a/stack/maxima/assessment.mac b/stack/maxima/assessment.mac index 309a267fa33d3df6c0c1bc165ae1df2ccc0a386b..7d635ac5c073aea11941ee04b9e672e015d49725 100644 --- a/stack/maxima/assessment.mac +++ b/stack/maxima/assessment.mac @@ -427,6 +427,20 @@ real_numberp(ex):= block([keepfloat, trigexpand, logexpand], if floatnump(ex) then return(true) else return(false) )$ +/* Do we have a complex number? */ +simp_complex_number_p(ex):= block([keepfloat, trigexpand, logexpand], + trigexpand:true, + logexpand:super, + keepfloat:true, + /* Using full ratsimp here makes this function unacceptably slow. */ + ex:errcatch(ev(ex, lg=logbasesimp, simp)), + if ex=[] then return(false), + ex:ev(float(ex[1]),simp), + if listofvars(ex)#[] then return(false), + if floatnump(ex) then return(true), + if complex_number_p(ex) then return(true) else return(false) +)$ + /* Do we have a real number, inf or -inf? */ extended_real_numberp(ex) := block( if (ex=inf or ex=-inf or ex=minf or ex=-minf) then return(true), diff --git a/stack/maxima/noun_simp.mac b/stack/maxima/noun_simp.mac index 19137d3e08f3f4ae69897fd80a94605b4ea1fd6e..326374cb1219562943ee12c2ec893207c52fab1c 100644 --- a/stack/maxima/noun_simp.mac +++ b/stack/maxima/noun_simp.mac @@ -156,11 +156,15 @@ flatten_pow_minus_one(ex):= block( ex )$ -/* Recursive rule which takes UNARY_MINUS nounmul n, where n is an integer to -n */ -unary_minus_remove(ex):= block( +/* Recursive rule which takes UNARY_MINUS nounmul n, where n is an integer/float to -n */ +unary_minus_remove(ex):= block([exl], if atom(ex) then return(ex), - if safe_op(ex)="nounmul" and is(first(args(ex))=UNARY_MINUS) and integerp(second(args(ex))) then return(-second(args(ex))), - apply(op(ex), maplist(unary_minus_remove, args(ex))) + if not(safe_op(ex)="nounmul") or not(is(first(args(ex))=UNARY_MINUS)) then return(apply(op(ex), maplist(unary_minus_remove, args(ex)))), + /* The sort moves any numbers to the front of the list of arguments for *. */ + exl:sort(rest(args(ex))), + if is(length(exl)=1) then return(-first(exl)), + exl[1]:-first(exl), + apply("nounmul", exl) )$ equals_commute_prepare(ex):=block([ex1n], diff --git a/stack/maxima/stackmaxima.mac b/stack/maxima/stackmaxima.mac index b73b9a024f578e22eb4e2151e4cf35d0849a14c5..8d8d4e8f4a634c3bc4774bc9353c448457358fdc 100644 --- a/stack/maxima/stackmaxima.mac +++ b/stack/maxima/stackmaxima.mac @@ -1075,6 +1075,7 @@ set output ", afn), set_plot_option([gnuplot_out_file, tfnp1]), if plotdebug then print(plot_options), /* Create and execute the actual plot commands. */ + ex:%_ce_expedite(ex), pltargs: append([ex], ral), if plotdebug then print(pltargs), plotfunmake: funmake(plot2d, pltargs), @@ -3246,7 +3247,7 @@ table_difference(T1, T2) := table_zip_with(lambda([ex1,ex2], if ex1=ex2 then ex1 /* Trees */ /* */ /* ****************************************************** */ -disptree(e) := sconcat("<ul class='tree'><li>", tree_rec(e), "</li></ul>"); +disptree(e) := sconcat("<ul class='algebratree'><li>", tree_rec(e), "</li></ul>"); /* A list of functions which should use the TeX representation. (Defined as a list to be user-editable) */ @@ -3300,4 +3301,4 @@ is_lang(code):=ev(is(%_STACK_LANG=code),simp=true)$ /* Stack expects some output with the version number the output happens at */ /* maximalocal.mac after additional library loading */ -stackmaximaversion:2023060600$ +stackmaximaversion:2023072102$ diff --git a/stack/maxima/stackunits.mac b/stack/maxima/stackunits.mac index b80e7f3f7b5246449c5712417e0be78408f0b0ea..a63abfac2365996aaf83c9bdfc87fdaddde3ec85 100644 --- a/stack/maxima/stackunits.mac +++ b/stack/maxima/stackunits.mac @@ -229,13 +229,13 @@ stackunits_make(ex) := block([oldsimp, exn, exu, exl], exn:maplist(unary_minus_remove, exn), exn:stack_units_rational_number(exn), if (debug) then (print("stackunits_make: (2) reformulated numbers as "), print(exn)), - if is(first(exn) = UNARY_MINUS) then - ( + if is(first(exn) = UNARY_MINUS) then block( exn:rest(exn), exn[1]:ev(-1*exn[1],simp) - ), + ), if length(exn)=1 then exn:first(exn) else exn:apply("nounmul", exn), if length(exu)=1 then exu:first(exu) else exu:apply("nounmul", exu), + if (debug) then (print("stackunits_make: (3) reformulated numbers as "), print(exn)), if (debug) then (print("stackunits_make: (3) reformulated units as "), print(exu)), verb_arith(stackunits(exn, exu)) )$ @@ -260,7 +260,7 @@ stackunits_make_p(ex) := block( return(true), if emptyp(listofvars(ex)) then return(true), - if simp_numberp(ev(float(verb_arith(ex)), simp)) then + if simp_complex_number_p(ev(float(verb_arith(ex)), simp)) then return(true), return(false) )$ diff --git a/stack/maxima/utils.mac b/stack/maxima/utils.mac index da3a4b0e81a6e97b9ab30fca7b82d94330345a41..e471af0d9cd261df27bec7de90529c24a4754ab7 100644 --- a/stack/maxima/utils.mac +++ b/stack/maxima/utils.mac @@ -305,3 +305,16 @@ all_ops(%_expr) := block([%_edge, %_next_edge, %_tmp, %_op, %_result], /* We need to compile %_CE_rem so that it is available to lisp as a lisp function. */ compile(%_ce_rem)$ +/* Remove %C_ and %E from expessions, but evaluate them now. I.e. expedite the checks. */ +%_ce_expedite(ex) := block([ex2,simp], + /* We need to assume simp:false, so unevaluated/simplified expressions don't potentially throw errors here. */ + simp:false, + /* The case below is atoms and things like m[k], which should not be processed further. */ + if safe_op(ex) = "" then return(ex), + if safe_op(ex) = "(" and safe_op(first(args(ex))) = "%_C" then (ev(first(args(ex))), return(%_ce_rem(second(args(ex))))), + if safe_op(ex) = "(" and safe_op(first(args(ex))) = "%_E" then (ev(first(args(ex))), return(%_ce_rem(second(args(ex))))), + /* Rather subtle order of evaluation issue. */ + ex2:args(ex), + ex2:map(%_ce_expedite, ex2), + substpart(op(ex), ex2, 0) +)$ diff --git a/stack/options.class.php b/stack/options.class.php index c3d2387ad440b40010df96b4bd0be5b40ff9ba6e..727f392ad618b70ae0d1559caf3f07cc6ce88b5e 100644 --- a/stack/options.class.php +++ b/stack/options.class.php @@ -69,14 +69,6 @@ class stack_options { 'caskey' => 'make_logic', 'castype' => 'fun', ), - 'floats' => array( - 'type' => 'boolean', - 'value' => 1, - 'strict' => true, - 'values' => array(), - 'caskey' => 'OPT_NoFloats', - 'castype' => 'ex', - ), 'sqrtsign' => array( 'type' => 'boolean', 'value' => true, @@ -141,7 +133,6 @@ class stack_options { $this->set_option('inversetrig', $stackconfig->inversetrig); $this->set_option('logicsymbol', $stackconfig->logicsymbol); $this->set_option('matrixparens', $stackconfig->matrixparens); - $this->set_option('floats', (bool) $stackconfig->inputforbidfloat); $this->set_option('sqrtsign', (bool) $stackconfig->sqrtsign); $this->set_option('simplify', (bool) $stackconfig->questionsimplify); $this->set_option('assumepos', (bool) $stackconfig->assumepositive); diff --git a/stack/questiontestresult.php b/stack/questiontestresult.php index 7274b0ada55754f0c65280a39e1554d1a9dec247..930678a4b0e946063ea3de50a539a200cbc3c040 100644 --- a/stack/questiontestresult.php +++ b/stack/questiontestresult.php @@ -205,7 +205,7 @@ class stack_question_test_result { * @return bool whether the answer notes match sufficiently. */ protected function test_answer_note($expected, $actual) { - $lastactual = array_pop($actual); + $lastactual = array_pop($actual) ?? ''; if ('NULL' == $expected) { return '' == trim($lastactual); } diff --git a/styles.css b/styles.css index 8036082016c75c8be80131c954d75e4e5cf5ad2d..a6261146c37d1f70abd4b299da440890b1c195d5 100644 --- a/styles.css +++ b/styles.css @@ -1115,33 +1115,33 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* It's supposed to look like a tree diagram. */ -.tree, -.tree ul, -.tree li { +.algebratree, +.algebratree ul, +.algebratree li { list-style: none; margin: 0; padding: 0; position: relative; } -.tree { +.algebratree { margin: 0 0 1em; text-align: center; } -.tree, -.tree ul { +.algebratree, +.algebratree ul { display: table; } -.tree ul { +.algebratree ul { width: 100%; } -.tree li { +.algebratree li { display: table-cell; padding: .5em 0; vertical-align: top; } /* _________ */ -.tree li:before { +.algebratree li:before { outline: solid 1px #666; content: ""; left: 0; @@ -1149,16 +1149,16 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. right: 0; top: 0; } -.tree li:first-child:before { +.algebratree li:first-child:before { left: 50%; } -.tree li:last-child:before { +.algebratree li:last-child:before { right: 50%; } -.tree span.atom, -.tree span.op, -.tree span.cell, -.tree code { +.algebratree span.atom, +.algebratree span.op, +.algebratree span.cell, +.algebratree code { border: solid .1em #666; border-radius: .2em; display: inline-block; @@ -1167,47 +1167,47 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. position: relative; } /* If the tree represents DOM structure. */ -.tree span.atom { +.algebratree span.atom { border-radius: .5em; color: DarkRed; background-color: LightCyan; } -.tree code, -.tree span.op { +.algebratree code, +.algebratree span.op { border-radius: .2em; color: Red; background-color: LemonChiffon; } /* | */ -.tree ul:before, -.tree span.atom:before, -.tree span.op:before, -.tree span.cell:before, -.tree code:before { +.algebratree ul:before, +.algebratree span.atom:before, +.algebratree span.op:before, +.algebratree span.cell:before, +.algebratree code:before { outline: solid 1px #666; content: ""; height: .5em; left: 50%; position: absolute; } -.tree ul:before { +.algebratree ul:before { top: -.5em; } -.tree span.atom:before, -.tree span.op:before, -.tree span.cell:before, -.tree code:before { +.algebratree span.atom:before, +.algebratree span.op:before, +.algebratree span.cell:before, +.algebratree code:before { top: -.55em; } /* The root node doesn't connect upwards. */ -.tree > li { +.algebratree > li { margin-top: 0; } -.tree > li:before, -.tree > li:after, -.tree > li > span.atom:before, -.tree > li > span.op:before, -.tree > li > span.cell:before, -.tree > li > code:before { +.algebratree > li:before, +.algebratree > li:after, +.algebratree > li > span.atom:before, +.algebratree > li > span.op:before, +.algebratree > li > span.cell:before, +.algebratree > li > code:before { outline: none; } diff --git a/tests/caskeyval_test.php b/tests/caskeyval_test.php index 2563831cbdfbba010bd6d809c9bdd94e27178281..c8ef55a50afd7430b09b9bce69254a2aa9b690f3 100644 --- a/tests/caskeyval_test.php +++ b/tests/caskeyval_test.php @@ -307,4 +307,13 @@ class caskeyval_test extends qtype_stack_testcase { "ta1:expand(df_simp);"; $this->assertEquals($expected, $s->get_keyval_representation()); } + + public function test_stack_seed_redef() { + $tests = 'v:2;stack_seed:2'; + $kv = new stack_cas_keyval($tests); + $this->assertFalse($kv->get_valid()); + $expected = array('Redefinition of key constants is forbidden: ' . + '<span class="stacksyntaxexample">stack_seed</span>.'); + $this->assertEquals($expected, $kv->get_errors()); + } } diff --git a/tests/castext2_test.php b/tests/castext2_test.php index d761b011835766b127f60ebae688ca80a1c7b46a..db0f1fdf5c3fcf7af3d38206bb0e6aba8a3725ed 100644 --- a/tests/castext2_test.php +++ b/tests/castext2_test.php @@ -474,6 +474,19 @@ class castext2_test extends qtype_stack_testcase { $this->assertEquals($output, $this->evaluate($input)); } + /** + * Block-system "todo"-block, functional requirements: + * 1. Comments out itself and contents. + * 2. Even if contents are invalid or incomplete. + * + * @covers \qtype_stack\stack_cas_castext2_comment + */ + public function test_blocks_todo() { + $input = '1[[ todo]] [[ foreach bar="foo"]] {#y@} [[/todo]]2'; + $output = '1<!--- stack_todo --->2'; + $this->assertEquals($output, $this->evaluate($input)); + } + /** * Block-system "escape"-block, functional requirements: * 1. Escapes the contents so that they will not be processed. @@ -542,8 +555,11 @@ class castext2_test extends qtype_stack_testcase { */ public function test_stackfltfmt() { $input = '{@a@}, {@(stackfltfmt:"~f",a)@}'; - $preamble = array('stackfltfmt:"~e"', 'a:0.000012'); - $output = '\({1.2e-5}\), \({0.000012}\)'; + // Note that 0.000012 has rounding in clisp which is not the point of this test. + // And 0.000013 has rounding in SBCL/GCL. + // And 0.000016 has rounding in SBCL! + $preamble = array('stackfltfmt:"~e"', 'a:0.000025'); + $output = '\({2.5e-5}\), \({0.000025}\)'; $this->assertEquals($output, strtolower($this->evaluate($input, $preamble))); } @@ -555,6 +571,9 @@ class castext2_test extends qtype_stack_testcase { $input = '{@(stackintfmt:"~:r",a)@}, {@(stackintfmt:"~@R",a)@}'; $preamble = array('a:1998'); $output = '\({\mbox{one thousand nine hundred ninety-eighth}}\), \({MCMXCVIII}\)'; + if ($this->adapt_to_new_maxima('5.46.0')) { + $output = '\({\mbox{one thousand, nine hundred ninety-eighth}}\), \({MCMXCVIII}\)'; + } $this->assertEquals($output, $this->evaluate($input, $preamble)); } @@ -657,8 +676,14 @@ class castext2_test extends qtype_stack_testcase { } public function test_plot_if() { + // This test case caused an error in Maxima 5.45.0. + // The fix to this error is the use of ex:%_ce_expedite(ex) in the plot function to remove %_C. + // However, we need to actively evaluate the %_C functions at the point we remove them. + // When expressions occur within the "then" clause they are not actually evaluated and in Maxima 5.45.0 + // this happens _after_ the list of variables has been created. So at that point, %_C(sin) contributes an extra + // variable "sin" to the picture, and so plot2d throws a (needless) error. Hence, the fix is to + // expedite the security checks before we send the cleaned-up expression to plot2d. $input = '{@plot(if x<=0 then x^2+1 else sin(x)/x, [x,-4,20], [y,-1,6])@}'; - $output = ''; $this->assertTrue(strpos($this->evaluate($input), '!ploturl!stackplot') > 0); } @@ -694,4 +719,10 @@ class castext2_test extends qtype_stack_testcase { $output = 'Xoverride'; $this->assertEquals($output, $this->evaluate($input)); } + + public function test_maplist_labda() { + $input = '{@maplist(lambda([ex], x^ex), [1,2,3,4])@}'; + $output = '\({\left[ x , x^2 , x^3 , x^4 \right]}\)'; + $this->assertEquals($output, $this->evaluate($input)); + } } diff --git a/tests/castext_test.php b/tests/castext_test.php index ad80b4f8b42af27e9787b9f78878facdf78b5d28..4eae0b4f17bbf1f36945787d34dc153e1012a1a1 100644 --- a/tests/castext_test.php +++ b/tests/castext_test.php @@ -1990,10 +1990,10 @@ class castext_test extends qtype_stack_testcase { $this->assertTrue($at2->get_valid()); $cs2->add_statement($at2); $cs2->instantiate(); - $this->assertEquals("\({x=a\,{\mbox{ or }}\, b}\): <ul class='tree'><li><span class='op'>\(\,{\mbox{ or }}\, \)" . + $this->assertEquals("\({x=a\,{\mbox{ or }}\, b}\): <ul class='algebratree'><li><span class='op'>\(\,{\mbox{ or }}\, \)" . "</span><ul><li><code>=</code><ul><li><span class='atom'>\(x\)</span></li><li><span class='atom'>\(a\)</span>" . "</li></ul></li><li><span class='atom'>\(b\)</span></li></ul></li></ul> <br/> " . - "\({x=\left(a\,{\mbox{ or }}\, b\\right)}\): <ul class='tree'><li><code>=</code><ul><li><span class='atom'>" . + "\({x=\left(a\,{\mbox{ or }}\, b\\right)}\): <ul class='algebratree'><li><code>=</code><ul><li><span class='atom'>" . "\(x\)</span></li><li><span class='op'>\(\,{\mbox{ or }}\, \)</span><ul><li><span class='atom'>\(a\)</span>" . "</li><li><span class='atom'>\(b\)</span></li></ul></li></ul></li></ul>", $at2->get_rendered()); @@ -2007,7 +2007,7 @@ class castext_test extends qtype_stack_testcase { $cs2->add_statement($at2); $cs2->instantiate(); $this->assertEquals("\({1+\left(\\frac{\mathrm{d}^2}{\mathrm{d} x^2} \sin \left( x\cdot y \\right)\\right)}\): " . - "<ul class='tree'><li><code>+</code><ul><li><span class='atom'>\(1\)</span></li><li><span class='op'>" . + "<ul class='algebratree'><li><code>+</code><ul><li><span class='atom'>\(1\)</span></li><li><span class='op'>" . "\(\\frac{\mathrm{d}^2 }{\mathrm{d} x^2} \)</span><ul><li><code>sin</code><ul><li><code>*</code><ul><li>" . "<span class='atom'>\(x\)</span></li><li><span class='atom'>\(y\)</span></li></ul></li></ul></li></ul></li>" . "</ul></li></ul>", $at2->get_rendered()); @@ -2016,7 +2016,7 @@ class castext_test extends qtype_stack_testcase { $this->assertTrue($at2->get_valid()); $cs2->add_statement($at2); $cs2->instantiate(); - $this->assertEquals("<ul class='tree'><li><span class='op'>\({ \pm }\)</span><ul><li><span class='atom'>\(a\)" . + $this->assertEquals("<ul class='algebratree'><li><span class='op'>\({ \pm }\)</span><ul><li><span class='atom'>\(a\)" . "</span></li><li><span class='op'>\(\Gamma\)</span><ul><li><span class='atom'>\(x\)</span></li></ul></li>" . "</ul></li></ul>", $at2->get_rendered()); @@ -2024,14 +2024,14 @@ class castext_test extends qtype_stack_testcase { $this->assertTrue($at2->get_valid()); $cs2->add_statement($at2); $cs2->instantiate(); - $this->assertEquals("<ul class='tree'><li><span class='op'>\(\sqrt{}\)</span><ul><li>" . + $this->assertEquals("<ul class='algebratree'><li><span class='op'>\(\sqrt{}\)</span><ul><li>" . "<span class='atom'>\(x\)</span></li></ul></li></ul>", $at2->get_rendered()); $at2 = castext2_evaluatable::make_from_source("{@disptree('limit(1/(x+1),x,0))@}", 'test-case'); $this->assertTrue($at2->get_valid()); $cs2->add_statement($at2); $cs2->instantiate(); - $this->assertEquals("<ul class='tree'><li><span class='op'>\(\lim_{x\\rightarrow{0}} \cdots \)</span><ul>" . + $this->assertEquals("<ul class='algebratree'><li><span class='op'>\(\lim_{x\\rightarrow{0}} \cdots \)</span><ul>" . "<li><code>/</code><ul><li><span class='atom'>\(1\)</span></li><li><code>+</code><ul>" . "<li><span class='atom'>\(x\)</span></li><li><span class='atom'>\(1\)</span></li></ul></li></ul>" . "</li></ul></li></ul>", $at2->get_rendered()); @@ -2040,7 +2040,7 @@ class castext_test extends qtype_stack_testcase { $this->assertTrue($at2->get_valid()); $cs2->add_statement($at2); $cs2->instantiate(); - $this->assertEquals("<ul class='tree'><li><code>*</code><ul><li><span class='atom'>\(2\)</span></li>" . + $this->assertEquals("<ul class='algebratree'><li><code>*</code><ul><li><span class='atom'>\(2\)</span></li>" . "<li><span class='atom'>\(\left[\begin{array}{cc} a & b \\\\ c & d \\end{array}\\right]\)</span></li>" . "</ul></li></ul>", $at2->get_rendered()); } @@ -2064,7 +2064,7 @@ class castext_test extends qtype_stack_testcase { $cs2->add_statement($at2); $cs2->instantiate(); - $this->assertEquals("<ul class='tree'><li><span class='op'>\(\diamond\)</span><ul><li>" . + $this->assertEquals("<ul class='algebratree'><li><span class='op'>\(\diamond\)</span><ul><li>" . "<span class='atom'>\(a\)</span></li><li><span class='atom'>\(b\)</span></li></ul></li></ul>", $at2->get_rendered()); } @@ -2171,4 +2171,29 @@ class castext_test extends qtype_stack_testcase { $cs2->instantiate(); $this->assertEquals("Hello world", $at2->get_rendered()); } + + /** + * @covers \qtype_stack\castext2_evaluatable::make_from_source + * @covers \qtype_stack\stack_cas_keyval + */ + public function test_stack_pick_seed() { + $a2 = array(); + $s2 = array(); + foreach ($a2 as $s) { + $cs = stack_ast_container::make_from_teacher_source($s, '', new stack_cas_security(), array()); + $this->assertTrue($cs->get_valid()); + $s2[] = $cs; + } + $options = new stack_options(); + $options->set_option('simplify', false); + $cs2 = new stack_cas_session2($s2, $options, 123456); + + $textinput = "{@stack_seed@}"; + $at1 = castext2_evaluatable::make_from_source($textinput, 'test-case'); + $this->assertTrue($at1->get_valid()); + $cs2->add_statement($at1); + $cs2->instantiate(); + + $this->assertEquals('\({123456}\)', $at1->get_rendered()); + } } diff --git a/tests/fixtures/answertestfixtures.class.php b/tests/fixtures/answertestfixtures.class.php index 0dacf5e1a31680d2668ed9ff459381076227b5dd..9d8d05f2a3d8dfd6c59f3ff23e5f49f8c2e7f7ea 100644 --- a/tests/fixtures/answertestfixtures.class.php +++ b/tests/fixtures/answertestfixtures.class.php @@ -1617,6 +1617,9 @@ class stack_answertest_test_data { 'ATNumerical_wrongentries: TA/SA=[3.14159], SA/TA=[3.1].', ''), array('NumRelative', '0.1', '{1.414,3.1}', '{pi,sqrt(2)}', 1, '', ''), array('NumRelative', '0.1', '{0,1,2}', '{0,1,2}', 1, '', ''), + // What happens with floating point complex numbers? + // This is rejected as not a real number. + array('NumRelative', '0.1', '0.99*%i', '%i', 0, 'ATNumerical_SA_not_number.', 'Complex numbers'), array('NumAbsolute', '', '1/0', '0', -1, 'ATNumAbsolute_STACKERROR_SAns.', 'Basic tests'), array('NumAbsolute', '', '0', '1/0', -1, 'ATNumAbsolute_STACKERROR_TAns.', ''), diff --git a/tests/fixtures/inputfixtures.class.php b/tests/fixtures/inputfixtures.class.php index 9e1b296cd132ebfecd1290dac67a0c4ebebcb300..173950b73dc6506753be255ddb70394ef8d294c3 100644 --- a/tests/fixtures/inputfixtures.class.php +++ b/tests/fixtures/inputfixtures.class.php @@ -368,8 +368,6 @@ class stack_inputvalidation_test_data { array('root(x)', 'php_true', 'root(x)', 'cas_true', '\sqrt{x}', '', ''), array('root(x,3)', 'php_true', 'root(x,3)', 'cas_true', 'x^{\frac{1}{3}}', '', ''), array('root(2,-3)', 'php_true', 'root(2,-3)', 'cas_true', '2^{\frac{1}{-3}}', '', ''), - array('conjugate(x)', 'php_true', 'conjugate(x)', 'cas_true', 'x^\star', '', ''), - array('conjugate(x)^2', 'php_true', 'conjugate(x)^2', 'cas_true', '{x^\star}^2', '', ''), // Parser rules in 4.3, identify cases where known functions (cf) are prefixed with single letter variables. array('bsin(t)', 'php_true', 'b*sin(t)', 'cas_true', 'b\cdot \sin \left( t \right)', 'missing_stars', ""), // So we have added gcf as a function so it is not g*cf... diff --git a/tests/input_algebraic_test.php b/tests/input_algebraic_test.php index 475a40a905b58707a4f77847689cbee2c6312503..7ae91ca447429529f7e1aeda3931925618aad09a 100644 --- a/tests/input_algebraic_test.php +++ b/tests/input_algebraic_test.php @@ -151,7 +151,8 @@ class input_algebraic_test extends qtype_stack_testcase { . '<code>x^2/(1+x^2)</code>', $el->get_teacher_answer_display('x^2/(1+x^2)', '\frac{x^2}{1+x^2}')); $el->set_parameter('showValidation', 1); - $vr = '<div class="stackinputfeedback standard" id="sans1_val"><p>Your last answer was interpreted as follows: ' . + $vr = '<div class="stackinputfeedback standard" id="sans1_val" aria-live="assertive">' . + '<p>Your last answer was interpreted as follows: ' . '<span class="filter_mathjaxloader_equation"><span class="nolink">\[ x^2 \]</span></span></p>' . '<input type="hidden" name="sans1_val" value="x^2" />The variables found in your answer were: ' . '<span class="filter_mathjaxloader_equation"><span class="nolink">\( \left[ x \right]\)</span></span> ' . @@ -159,7 +160,8 @@ class input_algebraic_test extends qtype_stack_testcase { $this->assertEquals($vr, $el->replace_validation_tags($state, 'sans1', '[[validation:sans1]]')); $el->set_parameter('showValidation', 2); - $vr = '<div class="stackinputfeedback standard" id="sans1_val"><p>Your last answer was interpreted as follows: ' . + $vr = '<div class="stackinputfeedback standard" id="sans1_val" aria-live="assertive">' . + '<p>Your last answer was interpreted as follows: ' . '<span class="filter_mathjaxloader_equation"><span class="nolink">\[ x^2 \]</span></span></p>' . '<input type="hidden" name="sans1_val" value="x^2" /></div>'; $this->assertEquals($vr, $el->replace_validation_tags($state, 'sans1', '[[validation:sans1]]')); @@ -167,7 +169,8 @@ class input_algebraic_test extends qtype_stack_testcase { $el->set_parameter('showValidation', 3); // We re-generate the state to get inline displayed equations. $state = $el->validate_student_response(array('sans1' => 'x^2'), $options, 'x^2/(1+x^2)', new stack_cas_security()); - $vr = '<span class="stackinputfeedback compact" id="sans1_val"><span class="filter_mathjaxloader_equation">' . + $vr = '<span class="stackinputfeedback compact" id="sans1_val" aria-live="assertive">' . + '<span class="filter_mathjaxloader_equation">' . '<span class="nolink">\( x^2 \)</span></span><input type="hidden" name="sans1_val" value="x^2" /></span>'; $this->assertEquals($vr, $el->replace_validation_tags($state, 'sans1', '[[validation:sans1]]')); } @@ -179,7 +182,8 @@ class input_algebraic_test extends qtype_stack_testcase { $this->assertEquals(stack_input::INVALID, $state->status); $el->set_parameter('showValidation', 1); - $vr = '<div class="stackinputfeedback standard" id="sans1_val"><p>Your last answer was interpreted as follows: ' . + $vr = '<div class="stackinputfeedback standard" id="sans1_val" aria-live="assertive">' . + '<p>Your last answer was interpreted as follows: ' . '<span class="stacksyntaxexample">2x(1+x^2)</span></p>' . '<input type="hidden" name="sans1_val" value="2x(1+x^2)" /><div class="alert alert-danger stackinputerror">' . 'This answer is invalid. You seem to be missing * characters. ' . @@ -295,7 +299,8 @@ class input_algebraic_test extends qtype_stack_testcase { $state->contentsdisplayed); $el->set_parameter('showValidation', 1); - $vr = '<div class="stackinputfeedback standard" id="sans1_val"><p>Your last answer was interpreted as ' . + $vr = '<div class="stackinputfeedback standard" id="sans1_val" aria-live="assertive">' . + '<p>Your last answer was interpreted as ' . 'follows: <span class="filter_mathjaxloader_equation"><span class="nolink">' . '\[ 3\cdot {\it ex}+2\cdot {\it ey}+5\cdot {\it ez} \]</span></span></p>' . '<input type="hidden" name="sans1_val" value="3*ex+2*ey+5*ez" />The variables found in your answer ' . @@ -1271,7 +1276,8 @@ class input_algebraic_test extends qtype_stack_testcase { $this->assertEquals($state->contentsdisplayed, '\[ \left( 1,\, 2\right] \]'); $el->set_parameter('showValidation', 1); - $vr = '<div class="stackinputfeedback standard" id="sans1_val"><p>Your last answer was interpreted as follows: ' . + $vr = '<div class="stackinputfeedback standard" id="sans1_val" aria-live="assertive">' . + '<p>Your last answer was interpreted as follows: ' . '<span class="filter_mathjaxloader_equation"><span class="nolink">\[ \left( 1,\, 2\right] \]</span></span>' . '</p><input type="hidden" name="sans1_val" value="oc(1,2,3)" />' . '<div class="alert alert-danger stackinputerror">This answer is invalid. Interval construction must have ' . @@ -1592,6 +1598,21 @@ class input_algebraic_test extends qtype_stack_testcase { $this->assertEquals('Coordinates are not permitted in this input.', $state->errors); } + public function test_validate_student_response_no_dot_dot() { + $options = new stack_options(); + $el = stack_input_factory::make('algebraic', 'sans1', '3.14*2.78'); + $el->set_parameter('forbidFloats', false); + $state = $el->validate_student_response(array('sans1' => '3.14.2.78'), $options, '3.14*2.78', + new stack_cas_security(false, '', '', array('ta'))); + $this->assertEquals($state->status, stack_input::INVALID); + $this->assertEquals('3.14 . 2.78', $state->contentsmodified); + $this->assertEquals('<span class="stacksyntaxexample">3.14.2.78</span>', + $state->contentsdisplayed); + $this->assertEquals('MatrixMultWithFloat', $state->note); + $this->assertEquals('Using matrix multiplication "." with scalar floats is forbidden, ' . + 'use normal multiplication "*" instead for the same result. 3.14 . 2.78', $state->errors); + } + public function test_validate_consolidatesubscripts() { $options = new stack_options(); $el = stack_input_factory::make('algebraic', 'state', 'M_1'); @@ -1662,4 +1683,35 @@ class input_algebraic_test extends qtype_stack_testcase { $this->assertEquals('The optional validator threw internal Maxima errors.', $state->errors); } + + public function test_validate_student_response_conjugate() { + $options = new stack_options(); + $el = stack_input_factory::make('algebraic', 'sans1', '2*conjugate(x)'); + $state = $el->validate_student_response(array('sans1' => 'conjugate(x)'), $options, 'conjugate(x)', + new stack_cas_security(false, '', '', array('ta'))); + $this->assertEquals(stack_input::VALID, $state->status); + $this->assertEquals('', $state->note); + $this->assertEquals('', $state->errors); + $this->assertEquals('conjugate(x)', $state->contentsmodified); + $displayed = '\[ x^\star \]'; + if ($this->adapt_to_new_maxima('5.47.0')) { + $displayed = '\[ x^{\ast} \]'; + } + $this->assertEquals($displayed, $state->contentsdisplayed); + $this->assertEquals('\( \left[ x \right]\) ', $state->lvars); + + $state = $el->validate_student_response(array('sans1' => 'conjugate(x)^2'), $options, 'conjugate(x)^2', + new stack_cas_security(false, '', '', array('ta'))); + $this->assertEquals(stack_input::VALID, $state->status); + $this->assertEquals('', $state->note); + $this->assertEquals('', $state->errors); + $this->assertEquals('conjugate(x)^2', $state->contentsmodified); + $displayed = '\[ {x^\star}^2 \]'; + if ($this->adapt_to_new_maxima('5.47.0')) { + // Personally I (CJS) prefer these brackets, so I'm going to keep them. + $displayed = '\[ {\left(x^{\ast}\right)}^2 \]'; + } + $this->assertEquals($displayed, $state->contentsdisplayed); + $this->assertEquals('\( \left[ x \right]\) ', $state->lvars); + } } diff --git a/tests/input_dropdown_test.php b/tests/input_dropdown_test.php index 7e93ab4a1bdeb930208f0b04f00c54f85d8af2bc..ac16aaefc8679be477ea68941c00f99c76ea8416 100644 --- a/tests/input_dropdown_test.php +++ b/tests/input_dropdown_test.php @@ -271,6 +271,16 @@ class input_dropdown_test extends qtype_stack_walkthrough_test_base { $el->get_teacher_answer_display(null, null)); } + public function test_teacher_answer_display_hideanswer() { + $el = stack_input_factory::make('dropdown', 'ans1', '[[1+x,false],[2+x^2,false],[{},true,"None of these"]]', null, array()); + $el->adapt_to_model_answer('[[1+x,false],[2+x^2,false],[{},true,"None of these"]]'); + $el->set_parameter('options', 'hideanswer'); + + $correctresponse = ''; + $this->assertEquals($correctresponse, + $el->get_teacher_answer_display(null, null)); + } + public function test_teacher_answer_html_entities() { $options = new stack_options(); $ta = '[[A,false,"n/a"],[B,true,"≥"],[C,false,"≤"],[D,false,"="],[E,false,"?"]]'; diff --git a/tests/input_equiv_test.php b/tests/input_equiv_test.php index 372d6c9d9a19e6a0791e4ae86ff238afeb6b73d0..ba0af11aca39530905095e877dc545f7670153bb 100644 --- a/tests/input_equiv_test.php +++ b/tests/input_equiv_test.php @@ -689,6 +689,18 @@ class input_equiv_test extends qtype_stack_testcase { $this->assertEquals('', $state->note); } + public function test_validate_student_response_valid_empty() { + $options = new stack_options(); + $el = stack_input_factory::make('equiv', 'sans1', '[x^2-5*x+6,stackeq((x-2)*(x-3))]'); + $el->set_parameter('options', 'allowempty'); + $state = $el->validate_student_response(array('sans1' => ""), $options, + '[x^2-5*x+6,stackeq((x-2)*(x-3))]', new stack_cas_security()); + $this->assertEquals(stack_input::SCORE, $state->status); + $this->assertEquals('[EMPTYANSWER]', $state->contentsmodified); + $this->assertEquals('', $state->errors); + $this->assertEquals('', $state->note); + } + public function test_validate_student_response_surds() { $options = new stack_options(); $options->set_option('multiplicationsign', 'none'); @@ -766,6 +778,10 @@ class input_equiv_test extends qtype_stack_testcase { '<span class="nolink">\( ### \)</span></span>, which can be typed in as follows: <br/>' . '<code>(x-a)^2 = 4</code><br/><code>x-a = +-2</code><br/><code>x = a+-2</code>', $el->get_teacher_answer_display($val, '###')); + + $el->set_parameter('options', 'hideanswer'); + $this->assertEquals('', $el->get_teacher_answer_display($val, '###')); + } public function test_validate_student_response_forbidwords_lists() { diff --git a/tests/input_notes_test.php b/tests/input_notes_test.php index d590c864fb16795f5cf02511a77cc5ae8446cbe4..f9771e47d1fe8fc915cf37038df198f576adf728 100644 --- a/tests/input_notes_test.php +++ b/tests/input_notes_test.php @@ -59,18 +59,20 @@ class input_notes_test extends qtype_stack_testcase { $this->assertEquals('', $state->contentsmodified); $el->set_parameter('showValidation', 0); - $vr = '<div class="stackinputfeedback standard empty" id="sans1_val"></div>'; + $vr = '<div class="stackinputfeedback standard empty" id="sans1_val" aria-live="assertive"></div>'; $this->assertEquals($vr, $el->replace_validation_tags($state, 'sans1', '[[validation:sans1]]')); $el->set_parameter('showValidation', 1); - $vr = '<div class="stackinputfeedback standard" id="sans1_val"><span class="filter_mathjaxloader_equation">' . + $vr = '<div class="stackinputfeedback standard" id="sans1_val" aria-live="assertive">' . + '<span class="filter_mathjaxloader_equation">' . '<div class="text_to_html"><p>This input gives an instant rendering of LaTeX e.g. ' . '<span class="nolink">\[ \sum_{n=1}^\infty \frac{1}{n^2}=\frac{\pi^2}{6}.\]</span></p>' . '<p class="stackinputnotice">(This input is not assessed automatically by STACK.)</p></div></span></div>'; $this->assertEquals($vr, $el->replace_validation_tags($state, 'sans1', '[[validation:sans1]]')); $el->set_parameter('showValidation', 2); - $vr = '<div class="stackinputfeedback standard" id="sans1_val"><span class="filter_mathjaxloader_equation">' . + $vr = '<div class="stackinputfeedback standard" id="sans1_val" aria-live="assertive">' . + '<span class="filter_mathjaxloader_equation">' . '<div class="text_to_html"><p>This input gives an instant rendering of LaTeX e.g. ' . '<span class="nolink">\[ \sum_{n=1}^\infty \frac{1}{n^2}=\frac{\pi^2}{6}.\]</span></p>' . '<p class="stackinputnotice">(This input is not assessed automatically by STACK.)</p></div></span></div>'; @@ -87,7 +89,8 @@ class input_notes_test extends qtype_stack_testcase { 'hDrk?autoplay=1&loop=1;controls=0"<https://www.youtube.com/embed/IB3d1UthDrk?autoplay' . '=1&loop=1;controls=0> allow="accelerometer; autoplay; encrypted-media; gyroscope; ' . 'picture-in-picture" allowfullscreen="" width="0" height="0" frameborder="0"></iframe>}$$'; - $ta = '<div class="stackinputfeedback standard" id="sans1_val"><span class="filter_mathjaxloader_' . + $ta = '<div class="stackinputfeedback standard" id="sans1_val" aria-live="assertive">' . + '<span class="filter_mathjaxloader_' . 'equation"><div class="text_to_html"><p><span class="nolink">$$ \unicode{ allow="accelerometer; ' . 'autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" width="0" height="0' . '" frameborder="0">}$$</span></p><p class="stackinputnotice">(This input is not assessed autom' . diff --git a/tests/input_units_test.php b/tests/input_units_test.php index 64a2e7e325be3013246be344830ff99645fde154..2385108e6c4c3fef0bccbfa667ce0faa5c765834 100644 --- a/tests/input_units_test.php +++ b/tests/input_units_test.php @@ -288,6 +288,17 @@ class input_units_test extends qtype_stack_testcase { $this->assertEquals('\[ 1\, \]', $state->contentsdisplayed); } + public function test_validate_student_response_student_calculation() { + $options = new stack_options(); + $el = stack_input_factory::make('units', 'sans1', '9.81*m'); + $el->set_parameter('insertStars', 0); + $state = $el->validate_student_response(array('sans1' => '9.4*m-53*cm'), $options, '9.81*m', + new stack_cas_security(true, '', '', array('tans'))); + $this->assertEquals(stack_input::INVALID, $state->status); + $this->assertEquals('9.4*m-53*cm', $state->contentsmodified); + $this->assertEquals('\[ 9.4\, \mathrm{m}-53\, \mathrm{c}\mathrm{m}\, \]', $state->contentsdisplayed); + } + public function test_validate_student_response_student_powers_ten() { $options = new stack_options(); $el = stack_input_factory::make('units', 'sans1', '9.81*m/s^2'); @@ -1116,4 +1127,26 @@ class input_units_test extends qtype_stack_testcase { '\[ \[ 0.0\, \mathrm{M}\mathrm{Pa} \]</span></span> \), which can be typed in as follows: <code>0.0*MPa</code>', $el->get_teacher_answer_display($state->contentsmodified, $state->contentsdisplayed)); } + + public function test_validate_student_response_complex_1() { + $options = new stack_options(); + $el = stack_input_factory::make('units', 'sans1', '570.37298*ohm'); + $el->set_parameter('insertStars', 1); + $el->set_parameter('forbidFloats', false); + $state = $el->validate_student_response(array('sans1' => '(72.0*%i-570.37298)*ohm'), $options, '570.37298*ohm', + new stack_cas_security(true)); + $this->assertEquals(stack_input::VALID, $state->status); + $this->assertEquals('', $state->note); + $this->assertEquals('(72.0*%i-570.37298)*ohm', $state->contentsmodified); + $this->assertEquals('\[ \left( 72.0\, \mathrm{i}-570.37298\right)\, \Omega \]', $state->contentsdisplayed); + $this->assertEquals('', $state->errors); + + $state = $el->validate_student_response(array('sans1' => '(72.0-%i*570.37298)*ohm'), $options, '570.37298*ohm', + new stack_cas_security(true)); + $this->assertEquals(stack_input::VALID, $state->status); + $this->assertEquals('', $state->note); + $this->assertEquals('(72.0-%i*570.37298)*ohm', $state->contentsmodified); + $this->assertEquals('\[ \left( 72.0-570.37298\, \mathrm{i}\right)\, \Omega \]', $state->contentsdisplayed); + $this->assertEquals('', $state->errors); + } } diff --git a/tests/input_varmatrix_test.php b/tests/input_varmatrix_test.php index 71fe2bd9ebf21904d4d5b5bcc799193636b04d8e..9a44ed09ff49fd79a08ee86260e48a5738f39748 100644 --- a/tests/input_varmatrix_test.php +++ b/tests/input_varmatrix_test.php @@ -241,4 +241,22 @@ class input_varmatrix_test extends qtype_stack_testcase { $state->contentsdisplayed); $this->assertEquals('\( \left[ x \right]\) ', $state->lvars); } + + public function test_validate_student_response_valid_logs() { + $options = new stack_options(); + $el = stack_input_factory::make('varmatrix', 'ans1', 'M'); + $inputvals = array( + 'ans1' => "log(9)^2*y^2*9^(x*y) log(9)^2*x*y*9^(x*y)+log(9)*9^(x*y)\n" . + "log(9)^2*x*y*9^(x*y)+log(9)*9^(x*y) log(9)^2*x^2*9^(x*y)", + ); + $state = $el->validate_student_response($inputvals, $options, 'matrix([a,b],[c,d])', new stack_cas_security()); + $this->assertEquals(stack_input::VALID, $state->status); + $this->assertEquals('matrix([log(9)^2*y^2*9^(x*y),log(9)^2*x*y*9^(x*y)+log(9)*9^(x*y)],'. + '[log(9)^2*x*y*9^(x*y)+log(9)*9^(x*y),log(9)^2*x^2*9^(x*y)])', $state->contentsmodified); + $this->assertEquals('\[ \left[\begin{array}{cc} \ln ^2\left(9\right)\cdot y^2\cdot 9^{x\cdot y} & ' . + '\ln ^2\left(9\right)\cdot x\cdot y\cdot 9^{x\cdot y}+\ln \left( 9 \right)\cdot 9^{x\cdot y} \\\\ ' . + '\ln ^2\left(9\right)\cdot x\cdot y\cdot 9^{x\cdot y}+\ln \left( 9 \right)\cdot 9^{x\cdot y} & ' . + '\ln ^2\left(9\right)\cdot x^2\cdot 9^{x\cdot y} \end{array}\right] \]', + $state->contentsdisplayed); + } } diff --git a/version.php b/version.php index 907653473f1c563db48c3234912d7e4114417fbb..14b84fef3d527908006f77a8d52a54d581c79766 100644 --- a/version.php +++ b/version.php @@ -24,12 +24,12 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2023060600; +$plugin->version = 2023072102; $plugin->requires = 2020061500; $plugin->cron = 0; $plugin->component = 'qtype_stack'; $plugin->maturity = MATURITY_ALPHA; -$plugin->release = '4.4.5 for Moodle 3.9+'; +$plugin->release = '4.4.6 for Moodle 3.9+'; $plugin->dependencies = array( 'qbehaviour_adaptivemultipart' => 2020103000, diff --git a/vle_specific.php b/vle_specific.php index a54c5e831b4d703cba1cdc9983a9ab83528eb3ad..1f200af90118948716178bbfb2c8c6e60ba17072 100644 --- a/vle_specific.php +++ b/vle_specific.php @@ -187,3 +187,10 @@ function stack_get_mathjax_url(): string { // TODO: figure out how to support VLE local with CORS. return 'https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'; } + +/* + * Give the VLE a chance to clear any question cache. + */ +function stack_clear_vle_question_cache(int $questionid) { + question_bank::notify_question_edited($questionid); +}