From ba23a42edd9afa2683fd924839236085947a1c93 Mon Sep 17 00:00:00 2001
From: Tim Hunt
Date: Sat, 29 Apr 2023 09:58:06 +0100
Subject: [PATCH] MDL-79863 qtype_ordering: Ordering question type. Upgrade to
work with Moodle 4.0, 4.1 and 4.2 (#72)
* Add moodle-plugin-ci github actions configuration
* Rebuild JavaScript
* Fix behat test failing in Moodle 4.0
* Ordering: better define 'rows' for items in horizontal list #401861 #55
* Ordering: Option to show number of correct choices/highlight correct and incorrect placement.
* Ordering: Improper alignment of feedback with Horizontal layout of items
* Ordering: make M4-compatible (including behat etc.) #606639
* M4: Behat: question/type/ordering tests failing #598659
* Fix grunt errors
* Fix unreliability in the preview Behat tests
* Fix Moodle 4.0-style regrading
Also, correctly initialise all parts of the question object in initialise_question_instance
* Update CI config
---------
Co-authored-by: sangnguyen
Co-authored-by: Thong Bui
Co-authored-by: Anupama Sarjoshi
---
.../type/ordering/.github/workflows/ci.yml | 124 ++++++++
question/type/ordering/CHANGES.txt | 5 +
.../type/ordering/amd/build/autoscroll.min.js | 22 +-
.../ordering/amd/build/autoscroll.min.js.map | 1 +
.../ordering/amd/build/drag_reorder.min.js | 15 +-
.../amd/build/drag_reorder.min.js.map | 2 +-
.../type/ordering/amd/build/dragdrop.min.js | 21 +-
.../ordering/amd/build/dragdrop.min.js.map | 1 +
.../type/ordering/amd/build/key_codes.min.js | 17 +-
.../ordering/amd/build/key_codes.min.js.map | 1 +
.../type/ordering/amd/build/reorder.min.js | 5 +-
.../ordering/amd/build/reorder.min.js.map | 2 +-
.../type/ordering/amd/src/drag_reorder.js | 77 +++--
question/type/ordering/amd/src/key_codes.js | 2 +-
question/type/ordering/amd/src/reorder.js | 24 +-
question/type/ordering/backup/moodle1/lib.php | 2 -
.../backup_qtype_ordering_plugin.class.php | 4 +-
.../restore_qtype_ordering_plugin.class.php | 5 +-
.../ordering/classes/privacy/provider.php | 4 +-
.../classes/question_hint_ordering.php | 87 +++++
question/type/ordering/db/install.xml | 1 +
question/type/ordering/db/upgrade.php | 15 +-
question/type/ordering/edit_ordering_form.php | 75 ++++-
.../type/ordering/lang/en/qtype_ordering.php | 26 +-
question/type/ordering/lib.php | 2 -
question/type/ordering/question.php | 301 +++++++++++++-----
question/type/ordering/questiontype.php | 229 +++++++++++--
question/type/ordering/readme.txt | 6 +-
question/type/ordering/renderer.php | 126 +++++++-
question/type/ordering/styles.css | 94 +++---
.../type/ordering/tests/behat/add.feature | 7 +-
.../tests/behat/backup_and_restore.feature | 17 +-
.../tests/behat/behat_qtype_ordering.php | 11 +
.../type/ordering/tests/behat/edit.feature | 27 +-
.../type/ordering/tests/behat/export.feature | 7 +-
.../type/ordering/tests/behat/import.feature | 27 +-
.../type/ordering/tests/behat/preview.feature | 52 ++-
.../tests/fixtures/testoldquestion.moodle.xml | 69 ++++
.../tests/fixtures/testquestion.moodle.xml | 9 +
question/type/ordering/tests/helper.php | 5 +-
.../type/ordering/tests/question_test.php | 154 ++++++++-
.../type/ordering/tests/questiontype_test.php | 13 +-
.../type/ordering/tests/walkthrough_test.php | 52 ++-
question/type/ordering/version.php | 6 +-
44 files changed, 1449 insertions(+), 303 deletions(-)
create mode 100644 question/type/ordering/.github/workflows/ci.yml
create mode 100644 question/type/ordering/amd/build/autoscroll.min.js.map
create mode 100644 question/type/ordering/amd/build/dragdrop.min.js.map
create mode 100644 question/type/ordering/amd/build/key_codes.min.js.map
create mode 100644 question/type/ordering/classes/question_hint_ordering.php
create mode 100644 question/type/ordering/tests/fixtures/testoldquestion.moodle.xml
diff --git a/question/type/ordering/.github/workflows/ci.yml b/question/type/ordering/.github/workflows/ci.yml
new file mode 100644
index 00000000000..3430e6529b6
--- /dev/null
+++ b/question/type/ordering/.github/workflows/ci.yml
@@ -0,0 +1,124 @@
+name: Moodle plugin CI
+on: [push, pull_request]
+
+jobs:
+ test:
+ runs-on: 'ubuntu-latest'
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - php: '8.0'
+ moodle-branch: 'master'
+ database: 'pgsql'
+ - php: '8.1'
+ moodle-branch: 'MOODLE_402_STABLE'
+ database: 'mariadb'
+ - php: '8.0'
+ moodle-branch: 'MOODLE_401_STABLE'
+ database: 'mariadb'
+ - php: '7.4'
+ moodle-branch: 'MOODLE_400_STABLE'
+ database: 'mariadb'
+ - php: '7.3'
+ moodle-branch: 'MOODLE_311_STABLE'
+ database: 'pgsql'
+
+ services:
+ postgres:
+ image: postgres:13
+ env:
+ POSTGRES_USER: 'postgres'
+ POSTGRES_HOST_AUTH_METHOD: 'trust'
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 3
+ ports:
+ - 5432:5432
+
+ mariadb:
+ image: mariadb:10
+ env:
+ MYSQL_USER: 'root'
+ MYSQL_ALLOW_EMPTY_PASSWORD: "true"
+ ports:
+ - 3306:3306
+ options: --health-cmd="mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 3
+
+ steps:
+ - name: Check out out repository code
+ uses: actions/checkout@v2
+ with:
+ path: plugin
+
+ - name: Setup PHP ${{ matrix.php }}
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: ${{ matrix.extensions }}
+ ini-values: max_input_vars=5000
+ coverage: none
+
+ - name: Initialise moodle-plugin-ci
+ run: |
+ composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^3
+ echo $(cd ci/bin; pwd) >> $GITHUB_PATH
+ echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH
+ sudo locale-gen en_AU.UTF-8
+ echo "NVM_DIR=$HOME/.nvm" >> $GITHUB_ENV
+
+ - name: Install Moodle
+ run: |
+ moodle-plugin-ci install --plugin ./plugin --db-host=127.0.0.1
+ env:
+ DB: ${{ matrix.database }}
+ MOODLE_BRANCH: ${{ matrix.moodle-branch }}
+
+ - name: PHP Lint
+ if: ${{ always() }}
+ run: moodle-plugin-ci phplint
+
+ - name: PHP Copy/Paste Detector
+ continue-on-error: true # This step will show errors but will not fail.
+ if: ${{ always() }}
+ run: moodle-plugin-ci phpcpd
+
+ - name: PHP Mess Detector
+ if: ${{ always() }}
+ run: moodle-plugin-ci phpmd
+
+ - name: Moodle Code Checker
+ if: ${{ always() }}
+ run: moodle-plugin-ci codechecker --max-warnings 0
+
+ - name: Moodle PHPDoc Checker
+ continue-on-error: true # This step will show errors but will not fail.
+ if: ${{ always() }}
+ run: moodle-plugin-ci phpdoc
+
+ - name: Validating
+ if: ${{ always() }}
+ run: moodle-plugin-ci validate
+
+ - name: Check upgrade savepoints
+ if: ${{ always() }}
+ run: moodle-plugin-ci savepoints
+
+ - name: Mustache Lint
+ if: ${{ always() }}
+ run: moodle-plugin-ci mustache
+
+ - name: Grunt
+ if: ${{ matrix.moodle-branch == 'MOODLE_400_STABLE' }}
+ run: moodle-plugin-ci grunt --max-lint-warnings 0
+
+ - name: PHPUnit tests
+ if: ${{ always() }}
+ run: moodle-plugin-ci phpunit --fail-on-warning
+
+ - name: Behat features
+ if: ${{ always() }}
+ run: moodle-plugin-ci behat --profile chrome
diff --git a/question/type/ordering/CHANGES.txt b/question/type/ordering/CHANGES.txt
index 2ecd6ed5705..92433fcd0d0 100644
--- a/question/type/ordering/CHANGES.txt
+++ b/question/type/ordering/CHANGES.txt
@@ -2,6 +2,11 @@
Change log for qtype_ordering
========================================
+2022-09-27 (07)
+ - This version works with Moodle 4.0, 4.1 and 4.2.
+ - New option to tell students how many choices were right, as part of the automatic feedback.
+ - Fixed some bugs with the layout of horizontal ordering questions.
+
2022-07-08 (06)
- optimize code to ensure that recorded responses are no longer than 100 bytes.
diff --git a/question/type/ordering/amd/build/autoscroll.min.js b/question/type/ordering/amd/build/autoscroll.min.js
index 32eefc32b18..9ee152f113d 100644
--- a/question/type/ordering/amd/build/autoscroll.min.js
+++ b/question/type/ordering/amd/build/autoscroll.min.js
@@ -1 +1,21 @@
-define(["jquery"],function(a){var b={SCROLL_THRESHOLD:30,SCROLL_FREQUENCY:1e3/60,SCROLL_SPEED:.5,scrollingId:null,scrollAmount:0,callback:null,start:function(c){a(window).on("mousemove",b.mouseMove),a(window).on("touchmove",b.touchMove),b.callback=c},stop:function(){a(window).off("mousemove",b.mouseMove),a(window).off("touchmove",b.touchMove),null!==b.scrollingId&&b.stopScrolling()},touchMove:function(a){for(var c=0;ca(window).height()-b.SCROLL_THRESHOLD?b.scrollAmount=Math.min(d-(a(window).height()-b.SCROLL_THRESHOLD),b.SCROLL_THRESHOLD):b.scrollAmount=0,b.scrollAmount&&null===b.scrollingId?b.startScrolling():b.scrollAmount||null===b.scrollingId||b.stopScrolling()},startScrolling:function(){var c=a(document).height()-a(window).height();b.scrollingId=window.setInterval(function(){var d=a(window).scrollTop(),e=Math.round(b.scrollAmount*b.SCROLL_SPEED);if(d+e<0&&(e=-d),d+e>c&&(e=c-d),0!==e){a(window).scrollTop(d+e);var f=a(window).scrollTop()-d;0!==f&&b.callback&&b.callback(f)}},b.SCROLL_FREQUENCY)},stopScrolling:function(){window.clearInterval(b.scrollingId),b.scrollingId=null}};return{start:b.start,stop:b.stop}});
\ No newline at end of file
+/*
+ * JavaScript to provide automatic scrolling, e.g. during a drag operation.
+ *
+ * This is a copy of a library that was added to Moodle core in Moodle 3.6,
+ * so we can support older Moodle versions.
+ *
+ * Note: this module is defined statically. It is a singleton. You
+ * can only have one use of it active at any time. However, since this
+ * is usually used in relation to drag-drop, and since you only ever
+ * drag one thing at a time, this is not a problem in practice.
+ *
+ * @module qtype_ordering/autoscroll
+ * @class autoscroll
+ * @package qtype_ordering
+ * @copyright 2016 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since 3.6
+ */
+define("qtype_ordering/autoscroll",["jquery"],(function($){var autoscroll={SCROLL_THRESHOLD:30,SCROLL_FREQUENCY:1e3/60,SCROLL_SPEED:.5,scrollingId:null,scrollAmount:0,callback:null,start:function(callback){$(window).on("mousemove",autoscroll.mouseMove),$(window).on("touchmove",autoscroll.touchMove),autoscroll.callback=callback},stop:function(){$(window).off("mousemove",autoscroll.mouseMove),$(window).off("touchmove",autoscroll.touchMove),null!==autoscroll.scrollingId&&autoscroll.stopScrolling()},touchMove:function(e){for(var i=0;i$(window).height()-autoscroll.SCROLL_THRESHOLD?autoscroll.scrollAmount=Math.min(clientY-($(window).height()-autoscroll.SCROLL_THRESHOLD),autoscroll.SCROLL_THRESHOLD):autoscroll.scrollAmount=0,autoscroll.scrollAmount&&null===autoscroll.scrollingId?autoscroll.startScrolling():autoscroll.scrollAmount||null===autoscroll.scrollingId||autoscroll.stopScrolling()},startScrolling:function(){var maxScroll=$(document).height()-$(window).height();autoscroll.scrollingId=window.setInterval((function(){var y=$(window).scrollTop(),offset=Math.round(autoscroll.scrollAmount*autoscroll.SCROLL_SPEED);if(y+offset<0&&(offset=-y),y+offset>maxScroll&&(offset=maxScroll-y),0!==offset){$(window).scrollTop(y+offset);var realOffset=$(window).scrollTop()-y;0!==realOffset&&autoscroll.callback&&autoscroll.callback(realOffset)}}),autoscroll.SCROLL_FREQUENCY)},stopScrolling:function(){window.clearInterval(autoscroll.scrollingId),autoscroll.scrollingId=null}};return{start:autoscroll.start,stop:autoscroll.stop}}));
+
+//# sourceMappingURL=autoscroll.min.js.map
\ No newline at end of file
diff --git a/question/type/ordering/amd/build/autoscroll.min.js.map b/question/type/ordering/amd/build/autoscroll.min.js.map
new file mode 100644
index 00000000000..bf97197e3b7
--- /dev/null
+++ b/question/type/ordering/amd/build/autoscroll.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"autoscroll.min.js","sources":["../src/autoscroll.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/*\n * JavaScript to provide automatic scrolling, e.g. during a drag operation.\n *\n * This is a copy of a library that was added to Moodle core in Moodle 3.6,\n * so we can support older Moodle versions.\n *\n * Note: this module is defined statically. It is a singleton. You\n * can only have one use of it active at any time. However, since this\n * is usually used in relation to drag-drop, and since you only ever\n * drag one thing at a time, this is not a problem in practice.\n *\n * @module qtype_ordering/autoscroll\n * @class autoscroll\n * @package qtype_ordering\n * @copyright 2016 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.6\n */\ndefine(['jquery'], function($) {\n /**\n * @alias module:qtype_ordering/autoscroll\n */\n var autoscroll = {\n /**\n * Size of area near edge of screen that triggers scrolling.\n * @private\n */\n SCROLL_THRESHOLD: 30,\n\n /**\n * How frequently to scroll window.\n * @private\n */\n SCROLL_FREQUENCY: 1000 / 60,\n\n /**\n * How many pixels to scroll per unit (1 = max scroll 30).\n * @private\n */\n SCROLL_SPEED: 0.5,\n\n /**\n * Set if currently scrolling up/down.\n * @private\n */\n scrollingId: null,\n\n /**\n * Speed we are supposed to scroll (range 1 to SCROLL_THRESHOLD).\n * @private\n */\n scrollAmount: 0,\n\n /**\n * Optional callback called when it scrolls\n * @private\n */\n callback: null,\n\n /**\n * Starts automatically scrolling if user moves near edge of window.\n * This should be called in response to mouse down or touch start.\n *\n * @public\n * @param {Function} callback Optional callback that is called every time it scrolls\n */\n start: function(callback) {\n $(window).on('mousemove', autoscroll.mouseMove);\n $(window).on('touchmove', autoscroll.touchMove);\n autoscroll.callback = callback;\n },\n\n /**\n * Stops automatically scrolling. This should be called in response to mouse up or touch end.\n *\n * @public\n */\n stop: function() {\n $(window).off('mousemove', autoscroll.mouseMove);\n $(window).off('touchmove', autoscroll.touchMove);\n if (autoscroll.scrollingId !== null) {\n autoscroll.stopScrolling();\n }\n },\n\n /**\n * Event handler for touch move.\n *\n * @private\n * @param {Object} e Event\n */\n touchMove: function(e) {\n for (var i = 0; i < e.changedTouches.length; i++) {\n autoscroll.handleMove(e.changedTouches[i].clientX, e.changedTouches[i].clientY);\n }\n },\n\n /**\n * Event handler for mouse move.\n *\n * @private\n * @param {Object} e Event\n */\n mouseMove: function(e) {\n autoscroll.handleMove(e.clientX, e.clientY);\n },\n\n /**\n * Handles user moving.\n *\n * @private\n * @param {number} clientX X\n * @param {number} clientY Y\n */\n handleMove: function(clientX, clientY) {\n // If near the bottom or top, start auto-scrolling.\n if (clientY < autoscroll.SCROLL_THRESHOLD) {\n autoscroll.scrollAmount = -Math.min(autoscroll.SCROLL_THRESHOLD - clientY, autoscroll.SCROLL_THRESHOLD);\n } else if (clientY > $(window).height() - autoscroll.SCROLL_THRESHOLD) {\n autoscroll.scrollAmount = Math.min(clientY - ($(window).height() - autoscroll.SCROLL_THRESHOLD),\n autoscroll.SCROLL_THRESHOLD);\n } else {\n autoscroll.scrollAmount = 0;\n }\n if (autoscroll.scrollAmount && autoscroll.scrollingId === null) {\n autoscroll.startScrolling();\n } else if (!autoscroll.scrollAmount && autoscroll.scrollingId !== null) {\n autoscroll.stopScrolling();\n }\n },\n\n /**\n * Starts automatic scrolling.\n *\n * @private\n */\n startScrolling: function() {\n var maxScroll = $(document).height() - $(window).height();\n autoscroll.scrollingId = window.setInterval(function() {\n // Work out how much to scroll.\n var y = $(window).scrollTop();\n var offset = Math.round(autoscroll.scrollAmount * autoscroll.SCROLL_SPEED);\n if (y + offset < 0) {\n offset = -y;\n }\n if (y + offset > maxScroll) {\n offset = maxScroll - y;\n }\n if (offset === 0) {\n return;\n }\n\n // Scroll.\n $(window).scrollTop(y + offset);\n var realOffset = $(window).scrollTop() - y;\n if (realOffset === 0) {\n return;\n }\n\n // Inform callback\n if (autoscroll.callback) {\n autoscroll.callback(realOffset);\n }\n\n }, autoscroll.SCROLL_FREQUENCY);\n },\n\n /**\n * Stops the automatic scrolling.\n *\n * @private\n */\n stopScrolling: function() {\n window.clearInterval(autoscroll.scrollingId);\n autoscroll.scrollingId = null;\n }\n };\n\n return {\n /**\n * Starts automatic scrolling if user moves near edge of window.\n * This should be called in response to mouse down or touch start.\n *\n * @public\n * @param {Function} callback Optional callback that is called every time it scrolls\n */\n start: autoscroll.start,\n\n /**\n * Stops automatic scrolling. This should be called in response to mouse up or touch end.\n *\n * @public\n */\n stop: autoscroll.stop\n };\n\n});\n"],"names":["define","$","autoscroll","SCROLL_THRESHOLD","SCROLL_FREQUENCY","SCROLL_SPEED","scrollingId","scrollAmount","callback","start","window","on","mouseMove","touchMove","stop","off","stopScrolling","e","i","changedTouches","length","handleMove","clientX","clientY","Math","min","height","startScrolling","maxScroll","document","setInterval","y","scrollTop","offset","round","realOffset","clearInterval"],"mappings":";;;;;;;;;;;;;;;;;;AAiCAA,mCAAO,CAAC,WAAW,SAASC,OAIpBC,WAAa,CAKbC,iBAAkB,GAMlBC,iBAAkB,IAAO,GAMzBC,aAAc,GAMdC,YAAa,KAMbC,aAAc,EAMdC,SAAU,KASVC,MAAO,SAASD,UACZP,EAAES,QAAQC,GAAG,YAAaT,WAAWU,WACrCX,EAAES,QAAQC,GAAG,YAAaT,WAAWW,WACrCX,WAAWM,SAAWA,UAQ1BM,KAAM,WACFb,EAAES,QAAQK,IAAI,YAAab,WAAWU,WACtCX,EAAES,QAAQK,IAAI,YAAab,WAAWW,WACP,OAA3BX,WAAWI,aACXJ,WAAWc,iBAUnBH,UAAW,SAASI,OACX,IAAIC,EAAI,EAAGA,EAAID,EAAEE,eAAeC,OAAQF,IACzChB,WAAWmB,WAAWJ,EAAEE,eAAeD,GAAGI,QAASL,EAAEE,eAAeD,GAAGK,UAU/EX,UAAW,SAASK,GAChBf,WAAWmB,WAAWJ,EAAEK,QAASL,EAAEM,UAUvCF,WAAY,SAASC,QAASC,SAEtBA,QAAUrB,WAAWC,iBACrBD,WAAWK,cAAgBiB,KAAKC,IAAIvB,WAAWC,iBAAmBoB,QAASrB,WAAWC,kBAC/EoB,QAAUtB,EAAES,QAAQgB,SAAWxB,WAAWC,iBACjDD,WAAWK,aAAeiB,KAAKC,IAAIF,SAAWtB,EAAES,QAAQgB,SAAWxB,WAAWC,kBAC1ED,WAAWC,kBAEfD,WAAWK,aAAe,EAE1BL,WAAWK,cAA2C,OAA3BL,WAAWI,YACtCJ,WAAWyB,iBACHzB,WAAWK,cAA2C,OAA3BL,WAAWI,aAC9CJ,WAAWc,iBASnBW,eAAgB,eACRC,UAAY3B,EAAE4B,UAAUH,SAAWzB,EAAES,QAAQgB,SACjDxB,WAAWI,YAAcI,OAAOoB,aAAY,eAEpCC,EAAI9B,EAAES,QAAQsB,YACdC,OAAST,KAAKU,MAAMhC,WAAWK,aAAeL,WAAWG,iBACzD0B,EAAIE,OAAS,IACbA,QAAUF,GAEVA,EAAIE,OAASL,YACbK,OAASL,UAAYG,GAEV,IAAXE,QAKJhC,EAAES,QAAQsB,UAAUD,EAAIE,YACpBE,WAAalC,EAAES,QAAQsB,YAAcD,EACtB,IAAfI,YAKAjC,WAAWM,UACXN,WAAWM,SAAS2B,eAGzBjC,WAAWE,mBAQlBY,cAAe,WACXN,OAAO0B,cAAclC,WAAWI,aAChCJ,WAAWI,YAAc,aAI1B,CAQHG,MAAOP,WAAWO,MAOlBK,KAAMZ,WAAWY"}
\ No newline at end of file
diff --git a/question/type/ordering/amd/build/drag_reorder.min.js b/question/type/ordering/amd/build/drag_reorder.min.js
index 7515c910637..e31dcac97a8 100644
--- a/question/type/ordering/amd/build/drag_reorder.min.js
+++ b/question/type/ordering/amd/build/drag_reorder.min.js
@@ -1,2 +1,13 @@
-define ("qtype_ordering/drag_reorder",["jquery",require.specified("core/dragdrop")?"core/dragdrop":"qtype_ordering/dragdrop",require.specified("core/key_codes")?"core/key_codes":"qtype_ordering/key_codes"],function(a,b,c){return function(d){var e=null,f=null,g=null,h=null,j=null,k=null,l=function(c,h){k=a(d.list);e={time:new Date().getTime(),x:h.x,y:h.y};g=a(c.currentTarget).closest(d.itemInPage);if("undefined"!=typeof d.reorderStart){d.reorderStart(g.closest(d.list),g)}f=t();j=a(d.proxyHtml.replace("%%ITEM_HTML%%",g.html()).replace("%%ITEM_CLASS_NAME%%",g.attr("class")).replace("%%LIST_CLASS_NAME%%",k.attr("class")));a(document.body).append(j);j.css("position","absolute");j.css(g.offset());j.width(g.outerWidth());j.height(g.outerHeight());g.addClass(d.itemMovingClass);n(g);b.start(c,j,m,o)},m=function(){var b=g.closest(d.list),c=null,e=null;b.find(d.item).each(function(b,d){var f=s(d,j);if(null===c||fnew Date().getTime()-e.time&&10>Math.abs(e.x-a)&&10>Math.abs(e.y-b)){g[0].focus()}j.remove();j=null;g.removeClass(d.itemMovingClass);g=null;e=null},p=function(a,b){switch(a.keyCode){case c.space:case c.arrowRight:case c.arrowDown:a.preventDefault();a.stopPropagation();var d=b.next();if(d.length){d.insertBefore(b)}break;case c.arrowLeft:case c.arrowUp:a.preventDefault();a.stopPropagation();var e=b.prev();if(e.length){e.insertAfter(b)}break;}},q=function(a){return a.offset().left+a.outerWidth()/2},r=function(a){return a.offset().top+a.outerHeight()/2},s=function(b,c){var d=a(b),e=a(c),f=q(d)-q(e),g=r(d)-r(e);return Math.sqrt(f*f+g*g)},t=function(){return(g||h).closest(d.list).find(d.item).map(function(a,b){return d.idGetter(b)}).get()},u=function(a,b){return a.length===b.length&&a.every(function(a,c){return a===b[c]})};d.itemInPage=function combineSelectors(a,b){var c=[];a.split(",").forEach(function(a){b.split(",").forEach(function(b){c.push(a.trim()+" "+b.trim())})});return c.join(", ")}(d.list,d.item);a(d.list).on("mousedown touchstart",d.item,function(a){var c=b.prepare(a);if(c.start){l(a,c)}});a(d.list).on("keydown",d.item,function(b){h=a(b.currentTarget).closest(d.itemInPage);f=t();p(b,h);var c=t();if(!u(f,c)){d.reorderDone(h.closest(d.list),h,c)}});a(d.itemInPage).attr("tabindex","0")}});
-//# sourceMappingURL=drag_reorder.min.js.map
+/*
+ * Generic library to allow things in a vertical list to be re-ordered using drag and drop.
+ *
+ * To make a set of things draggable, create a new instance of this object passing the
+ * necessary config, as explained in the comment on the constructor.
+ *
+ * @package qtype_ordering
+ * @copyright 2018 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define("qtype_ordering/drag_reorder",["jquery",require.specified("core/dragdrop")?"core/dragdrop":"qtype_ordering/dragdrop",require.specified("core/key_codes")?"core/key_codes":"qtype_ordering/key_codes"],(function($,drag,keys){return function(config){var outer,inner,combined,dragStart=null,originalOrder=null,itemDragging=null,itemMoving=null,proxy=null,orderList=null,dragMove=function(){var list=itemDragging.closest(config.list),closestItem=null,closestDistance=null;if(list.find(config.item).each((function(index,element){var distance=distanceBetweenElements(element,proxy);(null===closestItem||distance.\n\n/**\n * Generic library to allow things in a vertical list to be re-ordered using drag and drop.\n *\n * To make a set of things draggable, create a new instance of this object passing the\n * necessary config, as explained in the comment on the constructor.\n *\n * @package qtype_ordering\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * @module qtype_ordering/drag_reorder\n */\ndefine([\n 'jquery',\n require.specified('core/dragdrop') ? 'core/dragdrop' : 'qtype_ordering/dragdrop',\n require.specified('core/key_codes') ? 'core/key_codes' : 'qtype_ordering/key_codes'\n], function(\n $,\n drag,\n keys\n) {\n\n /**\n * Constructor.\n *\n * To make a list draggable, create a new instance of this object, passing the necessary config.\n * For example:\n * {\n * // Selector for the list (or lists) to be reordered.\n * list: 'ul.my-list',\n *\n * // Selector, relative to the list selector, for the items that can be moved.\n * item: '> li',\n *\n * // The user actually drags a proxy object, which is constructed from this string,\n * // and then added directly as a child of . The token %%ITEM_HTML%% is\n * // replaced with the innerHtml of the item being dragged. The token %%ITEM_CLASS_NAME%%\n * // is replaced with the class attribute of the item being dragged. Because of this,\n * // the styling of the contents of your list item needs to work for the proxy, as well as\n * // for items in place in the context of the list. Your CSS also needs to ensure\n * // that this proxy has position: absolute. You probably want other styles, like a\n * // drop shadow. Using class osep-itemmoving might be all you need to do.\n * proxyHtml: '%%ITEM_HTML%%
,\n *\n * // While the proxy is being dragged, this class is added to the item being moved.\n * // You can probably use \"osep-itemmoving\" here.\n * itemMovingClass: \"osep-itemmoving\",\n *\n * // This is a callback which, when called with the DOM node for an item,\n * // returns the string that uniquely identifies each item.\n * // Therefore, the result of the drag action will be represented by the array\n * // obtained by calling this method on each item in the list in order.\n * idGetter: function(item) { return $(node).data('id'); },\n *\n * // This is a callback which, when called with the DOM node for an item,\n * // returns a string that is the name of the item.\n * nameGetter: function(item) { return $(node).text(); },\n *\n * // Function that will be called when a re-order starts (optional, can be not set).\n * // Useful if you need to save information about the initial state.\n * // This function should have two parameters. The first will be a\n * // jQuery object for the list that was reordered, the second will\n * // be the jQuery object for the item moved - which will not yet have been moved.\n * // Note, it is quite possible for reorderStart to be called with no\n * // subsequent call to reorderDone.\n * reorderStart: function($list, $item) { ... }\n *\n * // Function that will be called when a drag has finished, and the list\n * // has been reordered. This function should have three parameters. The first will be\n * // a jQuery object for the list that was reordered, the second will be the jQuery\n * // object for the item moved, and the third will be the new order, which is\n * // an array of ids obtained by calling idGetter on each item in the list in order.\n * // This callback will only be called in the new order is actually different from the old order.\n * reorderDone: function($list, $item, newOrder) { ... }\n *\n * // Function that is alwasy called when a re-order ends (optional, can be not set)\n * // whether or not the order has changed. Useful if you need to undo changes made\n * // in reorderStart, since reorderDone is only called if the new order is different\n * // from the original order.\n * reorderEnd: function($list, $item) { ... }\n * }\n *\n * There is a subtlety ( === hack?) that you can use. If you have items in your list that do not\n * have a drag handle, they they are considered to be placeholders in otherwise empty containers.\n * See how block_userlinks does it, if this seems like it might be useful. nameGetter should return\n * the container name for these items.\n *\n * @param config As above.\n */\n return function(config) {\n var dragStart = null, // Information about when and where the drag started.\n originalOrder = null, // Array of ids.\n itemDragging = null, // Item being moved by dragging (jQuery object).\n itemMoving = null, // Item being moved using the accessible modal (jQuery object).\n proxy = null, // Drag proxy (jQuery object).\n orderList = null; // Order list (jQuery object).\n\n var startDrag = function(event, details) {\n orderList = $(config.list);\n\n dragStart = {\n time: new Date().getTime(),\n x: details.x,\n y: details.y\n };\n\n itemDragging = $(event.currentTarget).closest(config.itemInPage);\n\n if (typeof config.reorderStart !== 'undefined') {\n config.reorderStart(itemDragging.closest(config.list), itemDragging);\n }\n\n originalOrder = getCurrentOrder();\n proxy = $(config.proxyHtml.replace('%%ITEM_HTML%%', itemDragging.html())\n .replace('%%ITEM_CLASS_NAME%%', itemDragging.attr('class'))\n .replace('%%LIST_CLASS_NAME%%', orderList.attr('class')));\n\n $(document.body).append(proxy);\n proxy.css('position', 'absolute');\n proxy.css(itemDragging.offset());\n proxy.width(itemDragging.outerWidth());\n proxy.height(itemDragging.outerHeight());\n itemDragging.addClass(config.itemMovingClass);\n updateProxy(itemDragging);\n\n // Start drag.\n drag.start(event, proxy, dragMove, dragEnd);\n\n };\n\n var dragMove = function() {\n var list = itemDragging.closest(config.list);\n var closestItem = null;\n var closestDistance = null;\n list.find(config.item).each(function (index, element) {\n var distance = distanceBetweenElements(element, proxy);\n if (closestItem === null || distance < closestDistance) {\n closestItem = $(element);\n closestDistance = distance;\n }\n });\n\n if (closestItem[0] === itemDragging[0]) {\n return;\n }\n if (midY(proxy) < midY(closestItem)) {\n itemDragging.insertBefore(closestItem);\n } else {\n itemDragging.insertAfter(closestItem);\n }\n updateProxy(itemDragging);\n };\n\n /**\n * Update proxy's position.\n * @param itemDragging\n */\n var updateProxy = function(itemDragging) {\n var list = itemDragging.closest('ol, ul');\n var items = list.find('li');\n var count = items.length;\n for (var i = 0; i < count; ++i) {\n //proxy.css('margin-left', '20p');\n if (itemDragging[0] === items[i]) {\n proxy.find('li').attr('value', i + 1);\n break;\n }\n }\n };\n\n /**\n * It outer and inner are two CSS selectors, which may contain commas,\n * then combine them safely. So combineSelectors('a, b', 'c, d')\n * gives 'a c, a d, b c, b d'.\n * @param outer\n * @param inner\n * @returns {string}\n */\n var combineSelectors = function(outer, inner) {\n var combined = [];\n outer.split(',').forEach(function(firstSelector) {\n inner.split(',').forEach(function(secondSelector) {\n combined.push(firstSelector.trim() + ' ' + secondSelector.trim());\n });\n });\n return combined.join(', ');\n };\n\n var dragEnd = function(x, y) {\n if (typeof config.reorderEnd !== 'undefined') {\n config.reorderEnd(itemDragging.closest(config.list), itemDragging);\n }\n\n var newOrder = getCurrentOrder();\n if (!arrayEquals(originalOrder, newOrder)) {\n // Order has changed, call the callback.\n config.reorderDone(itemDragging.closest(config.list), itemDragging, newOrder);\n\n } else if (new Date().getTime() - dragStart.time < 500 &&\n Math.abs(dragStart.x - x) < 10 && Math.abs(dragStart.y - y) < 10) {\n // This was really a click. Set the focus on the current item.\n itemDragging[0].focus();\n }\n proxy.remove();\n proxy = null;\n itemDragging.removeClass(config.itemMovingClass);\n itemDragging = null;\n dragStart = null;\n };\n\n /**\n * Items can be moved and placed using certain keys.\n * Tab for tabbing though and choose the item to be moved\n * space, arrow-right arrow-down for moving current element forewards.\n * arrow-right arrow-down for moving the current element backwards.\n * @param e, the event\n * @param item, the current moving item\n */\n var itemMovedByKeyboard = function (e, current) {\n switch (e.keyCode) {\n case keys.space:\n case keys.arrowRight:\n case keys.arrowDown:\n e.preventDefault();\n e.stopPropagation();\n var next = current.next();\n if (next.length) {\n next.insertBefore(current);\n }\n break;\n\n case keys.arrowLeft:\n case keys.arrowUp:\n e.preventDefault();\n e.stopPropagation();\n var prev = current.prev();\n if (prev.length) {\n prev.insertAfter(current);\n }\n break;\n }\n };\n\n /**\n * Get the x-position of the middle of the DOM node represented by the given jQuery object.\n * @param jQuery wrapping a DOM node.\n * @returns Number the x-coordinate of the middle (left plus half outerWidth).\n */\n var midX = function(jQuery) {\n return jQuery.offset().left + jQuery.outerWidth() / 2;\n };\n\n /**\n * Get the y-position of the middle of the DOM node represented by the given jQuery object.\n * @param jQuery wrapping a DOM node.\n * @returns Number the y-coordinate of the middle (top plus half outerHeight).\n */\n var midY = function(jQuery) {\n return jQuery.offset().top + jQuery.outerHeight() / 2;\n };\n\n /**\n * Calculate the distance between the centres of two elements.\n * @param element1 selector, element or jQuery.\n * @param element2 selector, element or jQuery.\n * @return number the distance in pixels.\n */\n var distanceBetweenElements = function(element1, element2) {\n var e1 = $(element1), e2 = $(element2);\n var dx = midX(e1) - midX(e2);\n var dy = midY(e1) - midY(e2);\n return Math.sqrt(dx * dx + dy * dy);\n };\n\n /**\n * Get the current order of the list containing itemDragging.\n * @returns Array of strings, the id of each element in order.\n */\n var getCurrentOrder = function() {\n return (itemDragging || itemMoving).closest(config.list).find(config.item).map(\n function(index, item) { return config.idGetter(item); }).get();\n };\n\n /**\n * Compare two arrays, which just contain simple values like ints or strings,\n * to see if they are equal.\n * @param a1 first array.\n * @param a2 second array.\n * @return boolean true if they both contain the same elements in the same order, else false.\n */\n var arrayEquals = function(a1, a2) {\n return a1.length === a2.length &&\n a1.every(function(v, i) { return v === a2[i]; });\n };\n config.itemInPage = combineSelectors(config.list, config.item);\n\n // AJAX for section drag and click-to-move.\n $(config.list).on('mousedown touchstart', config.item, function(event) {\n var details = drag.prepare(event);\n if (details.start) {\n startDrag(event, details);\n }\n });\n\n $(config.list).on('keydown', config.item, function(event) {\n itemMoving = $(event.currentTarget).closest(config.itemInPage);\n originalOrder = getCurrentOrder();\n itemMovedByKeyboard(event, itemMoving);\n var newOrder = getCurrentOrder();\n if (!arrayEquals(originalOrder, newOrder)) {\n // Order has changed, call the callback.\n config.reorderDone(itemMoving.closest(config.list), itemMoving, newOrder);\n }\n });\n\n // Make the items tabbable.\n $(config.itemInPage).attr('tabindex', '0');\n };\n});\n"],"file":"drag_reorder.min.js"}
\ No newline at end of file
+{"version":3,"file":"drag_reorder.min.js","sources":["../src/drag_reorder.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/*\n * Generic library to allow things in a vertical list to be re-ordered using drag and drop.\n *\n * To make a set of things draggable, create a new instance of this object passing the\n * necessary config, as explained in the comment on the constructor.\n *\n * @package qtype_ordering\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * @module qtype_ordering/drag_reorder\n */\ndefine([\n 'jquery',\n require.specified('core/dragdrop') ? 'core/dragdrop' : 'qtype_ordering/dragdrop',\n require.specified('core/key_codes') ? 'core/key_codes' : 'qtype_ordering/key_codes'\n], function(\n $,\n drag,\n keys\n) {\n\n /**\n * Constructor.\n *\n * To make a list draggable, create a new instance of this object, passing the necessary config.\n * For example:\n * {\n * // Selector for the list (or lists) to be reordered.\n * list: 'ul.my-list',\n *\n * // Selector, relative to the list selector, for the items that can be moved.\n * item: '> li',\n *\n * // The user actually drags a proxy object, which is constructed from this string,\n * // and then added directly as a child of . The token %%ITEM_HTML%% is\n * // replaced with the innerHtml of the item being dragged. The token %%ITEM_CLASS_NAME%%\n * // is replaced with the class attribute of the item being dragged. Because of this,\n * // the styling of the contents of your list item needs to work for the proxy, as well as\n * // for items in place in the context of the list. Your CSS also needs to ensure\n * // that this proxy has position: absolute. You probably want other styles, like a\n * // drop shadow. Using class osep-itemmoving might be all you need to do.\n * proxyHtml: '%%ITEM_HTML%%
,\n *\n * // While the proxy is being dragged, this class is added to the item being moved.\n * // You can probably use \"osep-itemmoving\" here.\n * itemMovingClass: \"osep-itemmoving\",\n *\n * // This is a callback which, when called with the DOM node for an item,\n * // returns the string that uniquely identifies each item.\n * // Therefore, the result of the drag action will be represented by the array\n * // obtained by calling this method on each item in the list in order.\n * idGetter: function(item) { return $(node).data('id'); },\n *\n * // This is a callback which, when called with the DOM node for an item,\n * // returns a string that is the name of the item.\n * nameGetter: function(item) { return $(node).text(); },\n *\n * // Function that will be called when a re-order starts (optional, can be not set).\n * // Useful if you need to save information about the initial state.\n * // This function should have two parameters. The first will be a\n * // jQuery object for the list that was reordered, the second will\n * // be the jQuery object for the item moved - which will not yet have been moved.\n * // Note, it is quite possible for reorderStart to be called with no\n * // subsequent call to reorderDone.\n * reorderStart: function($list, $item) { ... }\n *\n * // Function that will be called when a drag has finished, and the list\n * // has been reordered. This function should have three parameters. The first will be\n * // a jQuery object for the list that was reordered, the second will be the jQuery\n * // object for the item moved, and the third will be the new order, which is\n * // an array of ids obtained by calling idGetter on each item in the list in order.\n * // This callback will only be called in the new order is actually different from the old order.\n * reorderDone: function($list, $item, newOrder) { ... }\n *\n * // Function that is alwasy called when a re-order ends (optional, can be not set)\n * // whether or not the order has changed. Useful if you need to undo changes made\n * // in reorderStart, since reorderDone is only called if the new order is different\n * // from the original order.\n * reorderEnd: function($list, $item) { ... }\n * }\n *\n * There is a subtlety ( === hack?) that you can use. If you have items in your list that do not\n * have a drag handle, they they are considered to be placeholders in otherwise empty containers.\n * See how block_userlinks does it, if this seems like it might be useful. nameGetter should return\n * the container name for these items.\n *\n * @param {Object} config As above.\n */\n return function(config) {\n var dragStart = null, // Information about when and where the drag started.\n originalOrder = null, // Array of ids.\n itemDragging = null, // Item being moved by dragging (jQuery object).\n itemMoving = null, // Item being moved using the accessible modal (jQuery object).\n proxy = null, // Drag proxy (jQuery object).\n orderList = null; // Order list (jQuery object).\n\n var startDrag = function(event, details) {\n orderList = $(config.list);\n\n dragStart = {\n time: new Date().getTime(),\n x: details.x,\n y: details.y\n };\n\n itemDragging = $(event.currentTarget).closest(config.itemInPage);\n\n if (typeof config.reorderStart !== 'undefined') {\n config.reorderStart(itemDragging.closest(config.list), itemDragging);\n }\n\n originalOrder = getCurrentOrder();\n proxy = $(config.proxyHtml.replace('%%ITEM_HTML%%', itemDragging.html())\n .replace('%%ITEM_CLASS_NAME%%', itemDragging.attr('class'))\n .replace('%%LIST_CLASS_NAME%%', orderList.attr('class')));\n\n $(document.body).append(proxy);\n proxy.css('position', 'absolute');\n proxy.css(itemDragging.offset());\n proxy.width(itemDragging.outerWidth());\n proxy.height(itemDragging.outerHeight());\n itemDragging.addClass(config.itemMovingClass);\n updateProxy(itemDragging);\n\n // Start drag.\n drag.start(event, proxy, dragMove, dragEnd);\n\n };\n\n var dragMove = function() {\n var list = itemDragging.closest(config.list);\n var closestItem = null;\n var closestDistance = null;\n list.find(config.item).each(function(index, element) {\n var distance = distanceBetweenElements(element, proxy);\n if (closestItem === null || distance < closestDistance) {\n closestItem = $(element);\n closestDistance = distance;\n }\n });\n\n if (closestItem[0] === itemDragging[0]) {\n return;\n }\n var offsetValue = 0;\n // Set offset depending on if item is being dragged downwards/upwards.\n if (midY(proxy) < midY(closestItem)) {\n offsetValue = 20;\n window.console.log(\"For midY(proxy) < midY(closestItem) offset is: \" + offsetValue);\n } else {\n offsetValue = -20;\n window.console.log(\"For midY(proxy) < midY(closestItem) offset is: \" + offsetValue);\n }\n if (midY(proxy) + offsetValue < midY(closestItem)) {\n itemDragging.insertBefore(closestItem);\n } else {\n itemDragging.insertAfter(closestItem);\n }\n updateProxy(itemDragging);\n };\n\n /**\n * Update proxy's position.\n * @param {jQuery} itemDragging\n */\n var updateProxy = function(itemDragging) {\n var list = itemDragging.closest('ol, ul');\n var items = list.find('li');\n var count = items.length;\n for (var i = 0; i < count; ++i) {\n if (itemDragging[0] === items[i]) {\n proxy.find('li').attr('value', i + 1);\n break;\n }\n }\n };\n\n /**\n * It outer and inner are two CSS selectors, which may contain commas,\n * then combine them safely. So combineSelectors('a, b', 'c, d')\n * gives 'a c, a d, b c, b d'.\n * @param {Selector} outer\n * @param {Selector} inner\n * @returns {string}\n */\n var combineSelectors = function(outer, inner) {\n var combined = [];\n outer.split(',').forEach(function(firstSelector) {\n inner.split(',').forEach(function(secondSelector) {\n combined.push(firstSelector.trim() + ' ' + secondSelector.trim());\n });\n });\n return combined.join(', ');\n };\n\n var dragEnd = function(x, y) {\n if (typeof config.reorderEnd !== 'undefined') {\n config.reorderEnd(itemDragging.closest(config.list), itemDragging);\n }\n\n var newOrder = getCurrentOrder();\n if (!arrayEquals(originalOrder, newOrder)) {\n // Order has changed, call the callback.\n config.reorderDone(itemDragging.closest(config.list), itemDragging, newOrder);\n\n } else if (new Date().getTime() - dragStart.time < 500 &&\n Math.abs(dragStart.x - x) < 10 && Math.abs(dragStart.y - y) < 10) {\n // This was really a click. Set the focus on the current item.\n itemDragging[0].focus();\n }\n proxy.remove();\n proxy = null;\n itemDragging.removeClass(config.itemMovingClass);\n itemDragging = null;\n dragStart = null;\n };\n\n /**\n * Items can be moved and placed using certain keys.\n * Tab for tabbing though and choose the item to be moved\n * space, arrow-right arrow-down for moving current element forewards.\n * arrow-right arrow-down for moving the current element backwards.\n * @param {Object} e the event\n * @param {jQuery} current the current moving item\n */\n var itemMovedByKeyboard = function(e, current) {\n switch (e.keyCode) {\n case keys.space:\n case keys.arrowRight:\n case keys.arrowDown:\n e.preventDefault();\n e.stopPropagation();\n var next = current.next();\n if (next.length) {\n next.insertBefore(current);\n }\n break;\n\n case keys.arrowLeft:\n case keys.arrowUp:\n e.preventDefault();\n e.stopPropagation();\n var prev = current.prev();\n if (prev.length) {\n prev.insertAfter(current);\n }\n break;\n }\n };\n\n /**\n * Get the x-position of the middle of the DOM node represented by the given jQuery object.\n * @param {jQuery} jQuery wrapping a DOM node.\n * @returns {number} Number the x-coordinate of the middle (left plus half outerWidth).\n */\n var midX = function(jQuery) {\n return jQuery.offset().left + jQuery.outerWidth() / 2;\n };\n\n /**\n * Get the y-position of the middle of the DOM node represented by the given jQuery object.\n * @param {jQuery} jQuery wrapping a DOM node.\n * @returns {number} Number the y-coordinate of the middle (top plus half outerHeight).\n */\n var midY = function(jQuery) {\n return jQuery.offset().top + jQuery.outerHeight() / 2;\n };\n\n /**\n * Calculate the distance between the centres of two elements.\n * @param {Selector|Element|jQuery} element1 selector, element or jQuery.\n * @param {Selector|Element|jQuery} element2 selector, element or jQuery.\n * @return {number} number the distance in pixels.\n */\n var distanceBetweenElements = function(element1, element2) {\n var e1 = $(element1);\n var e2 = $(element2);\n var dx = midX(e1) - midX(e2);\n var dy = midY(e1) - midY(e2);\n return Math.sqrt(dx * dx + dy * dy);\n };\n\n /**\n * Get the current order of the list containing itemDragging.\n * @returns {Array} Array of strings, the id of each element in order.\n */\n var getCurrentOrder = function() {\n return (itemDragging || itemMoving).closest(config.list).find(config.item).map(\n function(index, item) {\n return config.idGetter(item);\n }).get();\n };\n\n /**\n * Compare two arrays, which just contain simple values like ints or strings,\n * to see if they are equal.\n * @param {Array} a1 first array.\n * @param {Array} a2 second array.\n * @return {Boolean} boolean true if they both contain the same elements in the same order, else false.\n */\n var arrayEquals = function(a1, a2) {\n return a1.length === a2.length &&\n a1.every(function(v, i) {\n return v === a2[i];\n });\n };\n config.itemInPage = combineSelectors(config.list, config.item);\n\n // AJAX for section drag and click-to-move.\n $(config.list).on('mousedown touchstart', config.item, function(event) {\n var details = drag.prepare(event);\n if (details.start) {\n startDrag(event, details);\n }\n });\n\n $(config.list).on('keydown', config.item, function(event) {\n itemMoving = $(event.currentTarget).closest(config.itemInPage);\n originalOrder = getCurrentOrder();\n itemMovedByKeyboard(event, itemMoving);\n var newOrder = getCurrentOrder();\n if (!arrayEquals(originalOrder, newOrder)) {\n // Order has changed, call the callback.\n config.reorderDone(itemMoving.closest(config.list), itemMoving, newOrder);\n }\n });\n\n // Make the items tabbable.\n $(config.itemInPage).attr('tabindex', '0');\n };\n});\n"],"names":["define","require","specified","$","drag","keys","config","outer","inner","combined","dragStart","originalOrder","itemDragging","itemMoving","proxy","orderList","dragMove","list","closest","closestItem","closestDistance","find","item","each","index","element","distance","distanceBetweenElements","offsetValue","midY","window","console","log","insertBefore","insertAfter","updateProxy","items","count","length","i","attr","dragEnd","x","y","reorderEnd","newOrder","getCurrentOrder","arrayEquals","Date","getTime","time","Math","abs","focus","reorderDone","remove","removeClass","itemMovingClass","midX","jQuery","offset","left","outerWidth","top","outerHeight","element1","element2","e1","e2","dx","dy","sqrt","map","idGetter","get","a1","a2","every","v","itemInPage","split","forEach","firstSelector","secondSelector","push","trim","join","on","event","details","prepare","start","currentTarget","reorderStart","proxyHtml","replace","html","document","body","append","css","width","height","addClass","startDrag","e","current","keyCode","space","arrowRight","arrowDown","preventDefault","stopPropagation","next","arrowLeft","arrowUp","prev","itemMovedByKeyboard"],"mappings":";;;;;;;;;;AA6BAA,qCAAO,CACH,SACAC,QAAQC,UAAU,iBAAmB,gBAAkB,0BACvDD,QAAQC,UAAU,kBAAoB,iBAAmB,6BAC1D,SACCC,EACAC,KACAC,aAsEO,SAASC,YAiGoBC,MAAOC,MAC/BC,SAjGJC,UAAY,KACZC,cAAgB,KAChBC,aAAe,KACfC,WAAa,KACbC,MAAQ,KACRC,UAAY,KAmCZC,SAAW,eACPC,KAAOL,aAAaM,QAAQZ,OAAOW,MACnCE,YAAc,KACdC,gBAAkB,QACtBH,KAAKI,KAAKf,OAAOgB,MAAMC,MAAK,SAASC,MAAOC,aACpCC,SAAWC,wBAAwBF,QAASX,QAC5B,OAAhBK,aAAwBO,SAAWN,mBACnCD,YAAchB,EAAEsB,SAChBL,gBAAkBM,aAItBP,YAAY,KAAOP,aAAa,QAGhCgB,YAAc,EAEdC,KAAKf,OAASe,KAAKV,cACnBS,YAAc,GACdE,OAAOC,QAAQC,IAAI,kDAAoDJ,eAEvEA,aAAe,GACfE,OAAOC,QAAQC,IAAI,kDAAoDJ,cAExEC,KAAKf,OAASc,YAAcC,KAAKV,aAChCP,aAAaqB,aAAad,aAE1BP,aAAasB,YAAYf,aAE7BgB,YAAYvB,gBAOZuB,YAAc,SAASvB,sBAEnBwB,MADOxB,aAAaM,QAAQ,UACfG,KAAK,MAClBgB,MAAQD,MAAME,OACTC,EAAI,EAAGA,EAAIF,QAASE,KACrB3B,aAAa,KAAOwB,MAAMG,GAAI,CAC9BzB,MAAMO,KAAK,MAAMmB,KAAK,QAASD,EAAI,WAwB3CE,QAAU,SAASC,EAAGC,QACW,IAAtBrC,OAAOsC,YACdtC,OAAOsC,WAAWhC,aAAaM,QAAQZ,OAAOW,MAAOL,kBAGrDiC,SAAWC,kBACVC,YAAYpC,cAAekC,WAIrB,IAAIG,MAAOC,UAAYvC,UAAUwC,KAAO,KAC/CC,KAAKC,IAAI1C,UAAUgC,EAAIA,GAAK,IAAMS,KAAKC,IAAI1C,UAAUiC,EAAIA,GAAK,IAE9D/B,aAAa,GAAGyC,QALhB/C,OAAOgD,YAAY1C,aAAaM,QAAQZ,OAAOW,MAAOL,aAAciC,UAOxE/B,MAAMyC,SACNzC,MAAQ,KACRF,aAAa4C,YAAYlD,OAAOmD,iBAChC7C,aAAe,KACfF,UAAY,MAyCZgD,KAAO,SAASC,eACTA,OAAOC,SAASC,KAAOF,OAAOG,aAAe,GAQpDjC,KAAO,SAAS8B,eACTA,OAAOC,SAASG,IAAMJ,OAAOK,cAAgB,GASpDrC,wBAA0B,SAASsC,SAAUC,cACzCC,GAAKhE,EAAE8D,UACPG,GAAKjE,EAAE+D,UACPG,GAAKX,KAAKS,IAAMT,KAAKU,IACrBE,GAAKzC,KAAKsC,IAAMtC,KAAKuC,WAClBjB,KAAKoB,KAAKF,GAAKA,GAAKC,GAAKA,KAOhCxB,gBAAkB,kBACVlC,cAAgBC,YAAYK,QAAQZ,OAAOW,MAAMI,KAAKf,OAAOgB,MAAMkD,KACnE,SAAShD,MAAOF,aACLhB,OAAOmE,SAASnD,SACxBoD,OAUX3B,YAAc,SAAS4B,GAAIC,WACpBD,GAAGrC,SAAWsC,GAAGtC,QACpBqC,GAAGE,OAAM,SAASC,EAAGvC,UACVuC,IAAMF,GAAGrC,OAG5BjC,OAAOyE,YAzHyBxE,MAyHKD,OAAOW,KAzHLT,MAyHWF,OAAOgB,KAxHjDb,SAAW,GACfF,MAAMyE,MAAM,KAAKC,SAAQ,SAASC,eAC9B1E,MAAMwE,MAAM,KAAKC,SAAQ,SAASE,gBAC9B1E,SAAS2E,KAAKF,cAAcG,OAAS,IAAMF,eAAeE,cAG3D5E,SAAS6E,KAAK,OAqHzBnF,EAAEG,OAAOW,MAAMsE,GAAG,uBAAwBjF,OAAOgB,MAAM,SAASkE,WACxDC,QAAUrF,KAAKsF,QAAQF,OACvBC,QAAQE,OAvNA,SAASH,MAAOC,SAC5B1E,UAAYZ,EAAEG,OAAOW,MAErBP,UAAY,CACRwC,MAAM,IAAIF,MAAOC,UACjBP,EAAG+C,QAAQ/C,EACXC,EAAG8C,QAAQ9C,GAGf/B,aAAeT,EAAEqF,MAAMI,eAAe1E,QAAQZ,OAAOyE,iBAElB,IAAxBzE,OAAOuF,cACdvF,OAAOuF,aAAajF,aAAaM,QAAQZ,OAAOW,MAAOL,cAG3DD,cAAgBmC,kBAChBhC,MAAQX,EAAEG,OAAOwF,UAAUC,QAAQ,gBAAiBnF,aAAaoF,QAC5DD,QAAQ,sBAAuBnF,aAAa4B,KAAK,UACjDuD,QAAQ,sBAAuBhF,UAAUyB,KAAK,WAEnDrC,EAAE8F,SAASC,MAAMC,OAAOrF,OACxBA,MAAMsF,IAAI,WAAY,YACtBtF,MAAMsF,IAAIxF,aAAagD,UACvB9C,MAAMuF,MAAMzF,aAAakD,cACzBhD,MAAMwF,OAAO1F,aAAaoD,eAC1BpD,aAAa2F,SAASjG,OAAOmD,iBAC7BtB,YAAYvB,cAGZR,KAAKuF,MAAMH,MAAO1E,MAAOE,SAAUyB,SA2L/B+D,CAAUhB,MAAOC,YAIzBtF,EAAEG,OAAOW,MAAMsE,GAAG,UAAWjF,OAAOgB,MAAM,SAASkE,OAC/C3E,WAAaV,EAAEqF,MAAMI,eAAe1E,QAAQZ,OAAOyE,YACnDpE,cAAgBmC,kBA7FM,SAAS2D,EAAGC,gBAC1BD,EAAEE,cACDtG,KAAKuG,WACLvG,KAAKwG,gBACLxG,KAAKyG,UACNL,EAAEM,iBACFN,EAAEO,sBACEC,KAAOP,QAAQO,OACfA,KAAK3E,QACL2E,KAAKhF,aAAayE,oBAIrBrG,KAAK6G,eACL7G,KAAK8G,QACNV,EAAEM,iBACFN,EAAEO,sBACEI,KAAOV,QAAQU,OACfA,KAAK9E,QACL8E,KAAKlF,YAAYwE,UA2E7BW,CAAoB7B,MAAO3E,gBACvBgC,SAAWC,kBACVC,YAAYpC,cAAekC,WAE5BvC,OAAOgD,YAAYzC,WAAWK,QAAQZ,OAAOW,MAAOJ,WAAYgC,aAKxE1C,EAAEG,OAAOyE,YAAYvC,KAAK,WAAY"}
\ No newline at end of file
diff --git a/question/type/ordering/amd/build/dragdrop.min.js b/question/type/ordering/amd/build/dragdrop.min.js
index cab8193afd6..42137936ab1 100644
--- a/question/type/ordering/amd/build/dragdrop.min.js
+++ b/question/type/ordering/amd/build/dragdrop.min.js
@@ -1 +1,20 @@
-define(["jquery","qtype_ordering/autoscroll"],function(a,b){var c={eventCaptureOptions:{passive:!1,capture:!0},dragProxy:null,onMove:null,onDrop:null,initialPosition:null,initialX:null,initialY:null,touching:null,prepare:function(a){a.preventDefault();var b;if(b="touchstart"===a.type?null===c.touching&&a.changedTouches.length>0:1===a.which){var d=c.getEventXY(a);return d.start=!0,d}return{start:!1}},start:function(a,d,e,f){var g=c.getEventXY(a);switch(c.initialX=g.x,c.initialY=g.y,c.initialPosition=d.offset(),c.dragProxy=d,c.onMove=e,c.onDrop=f,a.type){case"mousedown":c.addEventSpecial("mousemove",c.mouseMove),c.addEventSpecial("mouseup",c.mouseUp);break;case"touchstart":c.addEventSpecial("touchend",c.touchEnd),c.addEventSpecial("touchcancel",c.touchEnd),c.addEventSpecial("touchmove",c.touchMove),c.touching=a.changedTouches[0].identifier;break;default:throw new Error("Unexpected event type: "+a.type)}b.start(c.scroll)},addEventSpecial:function(a,b){try{window.addEventListener(a,b,c.eventCaptureOptions)}catch(d){c.eventCaptureOptions=!0,window.addEventListener(a,b,c.eventCaptureOptions)}},getEventXY:function(a){switch(a.type){case"touchstart":return{x:a.changedTouches[0].pageX,y:a.changedTouches[0].pageY};case"mousedown":return{x:a.pageX,y:a.pageY};default:throw new Error("Unexpected event type: "+a.type)}},touchMove:function(a){a.preventDefault();for(var b=0;b0:1===event.which){var details=dragdrop.getEventXY(event);return details.start=!0,details}return{start:!1}},start:function(event,dragProxy,onMove,onDrop){var xy=dragdrop.getEventXY(event);switch(dragdrop.initialX=xy.x,dragdrop.initialY=xy.y,dragdrop.initialPosition=dragProxy.offset(),dragdrop.dragProxy=dragProxy,dragdrop.onMove=onMove,dragdrop.onDrop=onDrop,event.type){case"mousedown":dragdrop.addEventSpecial("mousemove",dragdrop.mouseMove),dragdrop.addEventSpecial("mouseup",dragdrop.mouseUp);break;case"touchstart":dragdrop.addEventSpecial("touchend",dragdrop.touchEnd),dragdrop.addEventSpecial("touchcancel",dragdrop.touchEnd),dragdrop.addEventSpecial("touchmove",dragdrop.touchMove),dragdrop.touching=event.changedTouches[0].identifier;break;default:throw new Error("Unexpected event type: "+event.type)}autoScroll.start(dragdrop.scroll)},addEventSpecial:function(event,handler){try{window.addEventListener(event,handler,dragdrop.eventCaptureOptions)}catch(ex){dragdrop.eventCaptureOptions=!0,window.addEventListener(event,handler,dragdrop.eventCaptureOptions)}},getEventXY:function(event){switch(event.type){case"touchstart":return{x:event.changedTouches[0].pageX,y:event.changedTouches[0].pageY};case"mousedown":return{x:event.pageX,y:event.pageY};default:throw new Error("Unexpected event type: "+event.type)}},touchMove:function(e){e.preventDefault();for(var i=0;i.\n\n/*\n * JavaScript to handle drag operations, including automatic scrolling.\n *\n * This is a copy of a library that was added to Moodle core in Moodle 3.6,\n * so we can support older Moodle versions.\n *\n * Note: this module is defined statically. It is a singleton. You\n * can only have one use of it active at any time. However, you\n * can only drag one thing at a time, this is not a problem in practice.\n *\n * @module qtype_ordering/dragdrop\n * @class dragdrop\n * @package qtype_ordering\n * @copyright 2016 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.6\n */\ndefine(['jquery', 'qtype_ordering/autoscroll'], function($, autoScroll) {\n /**\n * @alias module:qtype_ordering/dragdrop\n */\n var dragdrop = {\n /**\n * A boolean or options argument depending on whether browser supports passive events.\n * @private\n */\n eventCaptureOptions: {passive: false, capture: true},\n\n /**\n * Drag proxy if any.\n * @private\n */\n dragProxy: null,\n\n /**\n * Function called on move.\n * @private\n */\n onMove: null,\n\n /**\n * Function called on drop.\n * @private\n */\n onDrop: null,\n\n /**\n * Initial position of proxy at drag start.\n */\n initialPosition: null,\n\n /**\n * Initial page X of cursor at drag start.\n */\n initialX: null,\n\n /**\n * Initial page Y of cursor at drag start.\n */\n initialY: null,\n\n /**\n * If touch event is in progress, this will be the id, otherwise null\n */\n touching: null,\n\n /**\n * Prepares to begin a drag operation - call with a mousedown or touchstart event.\n *\n * If the returned object has 'start' true, then you can set up a drag proxy, and call\n * start. This function will call preventDefault automatically regardless of whether\n * starting or not.\n *\n * @public\n * @param {Object} event Event (should be either mousedown or touchstart)\n * @return {Object} Object with start (boolean flag) and x, y (only if flag true) values\n */\n prepare: function(event) {\n event.preventDefault();\n var start;\n if (event.type === 'touchstart') {\n // For touch, start if there's at least one touch and we are not currently doing\n // a touch event.\n start = (dragdrop.touching === null) && event.changedTouches.length > 0;\n } else {\n // For mousedown, start if it's the left button.\n start = event.which === 1;\n }\n if (start) {\n var details = dragdrop.getEventXY(event);\n details.start = true;\n return details;\n } else {\n return {start: false};\n }\n },\n\n /**\n * Call to start a drag operation, in response to a mouse down or touch start event.\n * Normally call this after calling prepare and receiving start true (you can probably\n * skip prepare if only supporting drag not touch).\n *\n * Note: The caller is responsible for creating a 'drag proxy' which is the\n * thing that actually gets dragged. At present, this doesn't really work\n * properly unless it is added directly within the body tag.\n *\n * You also need to ensure that there is CSS so the proxy is absolutely positioned,\n * and styled to look like it is floating.\n *\n * You also need to absolutely position the proxy where you want it to start.\n *\n * @public\n * @param {Object} event Event (should be either mousedown or touchstart)\n * @param {jQuery} dragProxy An absolute-positioned element for dragging\n * @param {Object} onMove Function that receives X and Y page locations for a move\n * @param {Object} onDrop Function that receives X and Y page locations when dropped\n */\n start: function(event, dragProxy, onMove, onDrop) {\n var xy = dragdrop.getEventXY(event);\n dragdrop.initialX = xy.x;\n dragdrop.initialY = xy.y;\n dragdrop.initialPosition = dragProxy.offset();\n dragdrop.dragProxy = dragProxy;\n dragdrop.onMove = onMove;\n dragdrop.onDrop = onDrop;\n\n switch (event.type) {\n case 'mousedown':\n // Cannot use jQuery 'on' because events need to not be passive.\n dragdrop.addEventSpecial('mousemove', dragdrop.mouseMove);\n dragdrop.addEventSpecial('mouseup', dragdrop.mouseUp);\n break;\n case 'touchstart':\n dragdrop.addEventSpecial('touchend', dragdrop.touchEnd);\n dragdrop.addEventSpecial('touchcancel', dragdrop.touchEnd);\n dragdrop.addEventSpecial('touchmove', dragdrop.touchMove);\n dragdrop.touching = event.changedTouches[0].identifier;\n break;\n default:\n throw new Error('Unexpected event type: ' + event.type);\n }\n autoScroll.start(dragdrop.scroll);\n },\n\n /**\n * Adds an event listener with special event capture options (capture, not passive). If the\n * browser does not support passive events, it will fall back to the boolean for capture.\n *\n * @private\n * @param {Object} event Event type string\n * @param {Object} handler Handler function\n */\n addEventSpecial: function(event, handler) {\n try {\n window.addEventListener(event, handler, dragdrop.eventCaptureOptions);\n } catch (ex) {\n dragdrop.eventCaptureOptions = true;\n window.addEventListener(event, handler, dragdrop.eventCaptureOptions);\n }\n },\n\n /**\n * Gets X/Y co-ordinates of an event, which can be either touchstart or mousedown.\n *\n * @private\n * @param {Object} event Event (should be either mousedown or touchstart)\n * @return {Object} X/Y co-ordinates\n */\n getEventXY: function(event) {\n switch (event.type) {\n case 'touchstart':\n return {x: event.changedTouches[0].pageX,\n y: event.changedTouches[0].pageY};\n case 'mousedown':\n return {x: event.pageX, y: event.pageY};\n default:\n throw new Error('Unexpected event type: ' + event.type);\n }\n },\n\n /**\n * Event handler for touch move.\n *\n * @private\n * @param {Object} e Event\n */\n touchMove: function(e) {\n e.preventDefault();\n for (var i = 0; i < e.changedTouches.length; i++) {\n if (e.changedTouches[i].identifier === dragdrop.touching) {\n dragdrop.handleMove(e.changedTouches[i].pageX, e.changedTouches[i].pageY);\n }\n }\n },\n\n /**\n * Event handler for mouse move.\n *\n * @private\n * @param {Object} e Event\n */\n mouseMove: function(e) {\n dragdrop.handleMove(e.pageX, e.pageY);\n },\n\n /**\n * Shared handler for move event (mouse or touch).\n *\n * @private\n * @param {number} pageX X co-ordinate\n * @param {number} pageY Y co-ordinate\n */\n handleMove: function(pageX, pageY) {\n // Move the drag proxy, not letting you move it out of screen or window bounds.\n var current = dragdrop.dragProxy.offset();\n var topOffset = current.top - parseInt(dragdrop.dragProxy.css('top'));\n var leftOffset = current.left - parseInt(dragdrop.dragProxy.css('left'));\n var maxY = $(document).height() - dragdrop.dragProxy.outerHeight() - topOffset;\n var maxX = $(document).width() - dragdrop.dragProxy.outerWidth() - leftOffset;\n var minY = -topOffset;\n var minX = -leftOffset;\n var initial = dragdrop.initialPosition;\n var position = {\n top: Math.max(minY, Math.min(maxY, initial.top + (pageY - dragdrop.initialY) - topOffset)),\n left: Math.max(minX, Math.min(maxX, initial.left + (pageX - dragdrop.initialX) - leftOffset))\n };\n dragdrop.dragProxy.css(position);\n\n // Trigger move handler.\n dragdrop.onMove(pageX, pageY, dragdrop.dragProxy);\n },\n\n /**\n * Event handler for touch end.\n *\n * @private\n * @param {Object} e Event\n */\n touchEnd: function(e) {\n e.preventDefault();\n for (var i = 0; i < e.changedTouches.length; i++) {\n if (e.changedTouches[i].identifier === dragdrop.touching) {\n dragdrop.handleEnd(e.changedTouches[i].pageX, e.changedTouches[i].pageY);\n }\n }\n },\n\n /**\n * Event handler for mouse up.\n *\n * @private\n * @param {Object} e Event\n */\n mouseUp: function(e) {\n dragdrop.handleEnd(e.pageX, e.pageY);\n },\n\n /**\n * Shared handler for end drag (mouse or touch).\n *\n * @private\n * @param {number} pageX X\n * @param {number} pageY Y\n */\n handleEnd: function(pageX, pageY) {\n if (dragdrop.touching !== null) {\n window.removeEventListener('touchend', dragdrop.touchEnd, dragdrop.eventCaptureOptions);\n window.removeEventListener('touchcancel', dragdrop.touchEnd, dragdrop.eventCaptureOptions);\n window.removeEventListener('touchmove', dragdrop.touchMove, dragdrop.eventCaptureOptions);\n dragdrop.touching = null;\n } else {\n window.removeEventListener('mousemove', dragdrop.mouseMove, dragdrop.eventCaptureOptions);\n window.removeEventListener('mouseup', dragdrop.mouseUp, dragdrop.eventCaptureOptions);\n }\n autoScroll.stop();\n dragdrop.onDrop(pageX, pageY, dragdrop.dragProxy);\n },\n\n /**\n * Called when the page scrolls.\n *\n * @private\n * @param {number} offset Amount of scroll\n */\n scroll: function(offset) {\n // Move the proxy to match.\n var maxY = $(document).height() - dragdrop.dragProxy.outerHeight();\n var currentPosition = dragdrop.dragProxy.offset();\n currentPosition.top = Math.min(maxY, currentPosition.top + offset);\n dragdrop.dragProxy.css(currentPosition);\n }\n };\n\n return {\n /**\n * Prepares to begin a drag operation - call with a mousedown or touchstart event.\n *\n * If the returned object has 'start' true, then you can set up a drag proxy, and call\n * start. This function will call preventDefault automatically regardless of whether\n * starting or not.\n *\n * @param {Object} event Event (should be either mousedown or touchstart)\n * @return {Object} Object with start (boolean flag) and x, y (only if flag true) values\n */\n prepare: dragdrop.prepare,\n\n /**\n * Call to start a drag operation, in response to a mouse down or touch start event.\n * Normally call this after calling prepare and receiving start true (you can probably\n * skip prepare if only supporting drag not touch).\n *\n * Note: The caller is responsible for creating a 'drag proxy' which is the\n * thing that actually gets dragged. At present, this doesn't really work\n * properly unless it is added directly within the body tag.\n *\n * You also need to ensure that there is CSS so the proxy is absolutely positioned,\n * and styled to look like it is floating.\n *\n * You also need to absolutely position the proxy where you want it to start.\n *\n * @param {Object} event Event (should be either mousedown or touchstart)\n * @param {jQuery} dragProxy An absolute-positioned element for dragging\n * @param {Object} onMove Function that receives X and Y page locations for a move\n * @param {Object} onDrop Function that receives X and Y page locations when dropped\n */\n start: dragdrop.start\n };\n});\n"],"names":["define","$","autoScroll","dragdrop","eventCaptureOptions","passive","capture","dragProxy","onMove","onDrop","initialPosition","initialX","initialY","touching","prepare","event","preventDefault","type","changedTouches","length","which","details","getEventXY","start","xy","x","y","offset","addEventSpecial","mouseMove","mouseUp","touchEnd","touchMove","identifier","Error","scroll","handler","window","addEventListener","ex","pageX","pageY","e","i","handleMove","current","topOffset","top","parseInt","css","leftOffset","left","maxY","document","height","outerHeight","maxX","width","outerWidth","minY","minX","initial","position","Math","max","min","handleEnd","removeEventListener","stop","currentPosition"],"mappings":";;;;;;;;;;;;;;;;;AAgCAA,iCAAO,CAAC,SAAU,8BAA8B,SAASC,EAAGC,gBAIpDC,SAAW,CAKXC,oBAAqB,CAACC,SAAS,EAAOC,SAAS,GAM/CC,UAAW,KAMXC,OAAQ,KAMRC,OAAQ,KAKRC,gBAAiB,KAKjBC,SAAU,KAKVC,SAAU,KAKVC,SAAU,KAaVC,QAAS,SAASC,UACdA,MAAMC,iBAEa,eAAfD,MAAME,KAGyB,OAAtBd,SAASU,UAAsBE,MAAMG,eAAeC,OAAS,EAG9C,IAAhBJ,MAAMK,MAEP,KACHC,QAAUlB,SAASmB,WAAWP,cAClCM,QAAQE,OAAQ,EACTF,cAEA,CAACE,OAAO,IAwBvBA,MAAO,SAASR,MAAOR,UAAWC,OAAQC,YAClCe,GAAKrB,SAASmB,WAAWP,cAC7BZ,SAASQ,SAAWa,GAAGC,EACvBtB,SAASS,SAAWY,GAAGE,EACvBvB,SAASO,gBAAkBH,UAAUoB,SACrCxB,SAASI,UAAYA,UACrBJ,SAASK,OAASA,OAClBL,SAASM,OAASA,OAEVM,MAAME,UACL,YAEDd,SAASyB,gBAAgB,YAAazB,SAAS0B,WAC/C1B,SAASyB,gBAAgB,UAAWzB,SAAS2B,mBAE5C,aACD3B,SAASyB,gBAAgB,WAAYzB,SAAS4B,UAC9C5B,SAASyB,gBAAgB,cAAezB,SAAS4B,UACjD5B,SAASyB,gBAAgB,YAAazB,SAAS6B,WAC/C7B,SAASU,SAAWE,MAAMG,eAAe,GAAGe,+BAGtC,IAAIC,MAAM,0BAA4BnB,MAAME,MAE1Df,WAAWqB,MAAMpB,SAASgC,SAW9BP,gBAAiB,SAASb,MAAOqB,aAEzBC,OAAOC,iBAAiBvB,MAAOqB,QAASjC,SAASC,qBACnD,MAAOmC,IACLpC,SAASC,qBAAsB,EAC/BiC,OAAOC,iBAAiBvB,MAAOqB,QAASjC,SAASC,uBAWzDkB,WAAY,SAASP,cACTA,MAAME,UACL,mBACM,CAACQ,EAAGV,MAAMG,eAAe,GAAGsB,MAC3Bd,EAAGX,MAAMG,eAAe,GAAGuB,WAClC,kBACM,CAAChB,EAAGV,MAAMyB,MAAOd,EAAGX,MAAM0B,qBAE3B,IAAIP,MAAM,0BAA4BnB,MAAME,QAU9De,UAAW,SAASU,GAChBA,EAAE1B,qBACG,IAAI2B,EAAI,EAAGA,EAAID,EAAExB,eAAeC,OAAQwB,IACrCD,EAAExB,eAAeyB,GAAGV,aAAe9B,SAASU,UAC5CV,SAASyC,WAAWF,EAAExB,eAAeyB,GAAGH,MAAOE,EAAExB,eAAeyB,GAAGF,QAW/EZ,UAAW,SAASa,GAChBvC,SAASyC,WAAWF,EAAEF,MAAOE,EAAED,QAUnCG,WAAY,SAASJ,MAAOC,WAEpBI,QAAU1C,SAASI,UAAUoB,SAC7BmB,UAAYD,QAAQE,IAAMC,SAAS7C,SAASI,UAAU0C,IAAI,QAC1DC,WAAaL,QAAQM,KAAOH,SAAS7C,SAASI,UAAU0C,IAAI,SAC5DG,KAAOnD,EAAEoD,UAAUC,SAAWnD,SAASI,UAAUgD,cAAgBT,UACjEU,KAAOvD,EAAEoD,UAAUI,QAAUtD,SAASI,UAAUmD,aAAeR,WAC/DS,MAAQb,UACRc,MAAQV,WACRW,QAAU1D,SAASO,gBACnBoD,SAAW,CACXf,IAAKgB,KAAKC,IAAIL,KAAMI,KAAKE,IAAIb,KAAMS,QAAQd,KAAON,MAAQtC,SAASS,UAAYkC,YAC/EK,KAAMY,KAAKC,IAAIJ,KAAMG,KAAKE,IAAIT,KAAMK,QAAQV,MAAQX,MAAQrC,SAASQ,UAAYuC,cAErF/C,SAASI,UAAU0C,IAAIa,UAGvB3D,SAASK,OAAOgC,MAAOC,MAAOtC,SAASI,YAS3CwB,SAAU,SAASW,GACfA,EAAE1B,qBACG,IAAI2B,EAAI,EAAGA,EAAID,EAAExB,eAAeC,OAAQwB,IACrCD,EAAExB,eAAeyB,GAAGV,aAAe9B,SAASU,UAC5CV,SAAS+D,UAAUxB,EAAExB,eAAeyB,GAAGH,MAAOE,EAAExB,eAAeyB,GAAGF,QAW9EX,QAAS,SAASY,GACdvC,SAAS+D,UAAUxB,EAAEF,MAAOE,EAAED,QAUlCyB,UAAW,SAAS1B,MAAOC,OACG,OAAtBtC,SAASU,UACTwB,OAAO8B,oBAAoB,WAAYhE,SAAS4B,SAAU5B,SAASC,qBACnEiC,OAAO8B,oBAAoB,cAAehE,SAAS4B,SAAU5B,SAASC,qBACtEiC,OAAO8B,oBAAoB,YAAahE,SAAS6B,UAAW7B,SAASC,qBACrED,SAASU,SAAW,OAEpBwB,OAAO8B,oBAAoB,YAAahE,SAAS0B,UAAW1B,SAASC,qBACrEiC,OAAO8B,oBAAoB,UAAWhE,SAAS2B,QAAS3B,SAASC,sBAErEF,WAAWkE,OACXjE,SAASM,OAAO+B,MAAOC,MAAOtC,SAASI,YAS3C4B,OAAQ,SAASR,YAETyB,KAAOnD,EAAEoD,UAAUC,SAAWnD,SAASI,UAAUgD,cACjDc,gBAAkBlE,SAASI,UAAUoB,SACzC0C,gBAAgBtB,IAAMgB,KAAKE,IAAIb,KAAMiB,gBAAgBtB,IAAMpB,QAC3DxB,SAASI,UAAU0C,IAAIoB,yBAIxB,CAWHvD,QAASX,SAASW,QAqBlBS,MAAOpB,SAASoB"}
\ No newline at end of file
diff --git a/question/type/ordering/amd/build/key_codes.min.js b/question/type/ordering/amd/build/key_codes.min.js
index 88f103ec657..cee0113e44b 100644
--- a/question/type/ordering/amd/build/key_codes.min.js
+++ b/question/type/ordering/amd/build/key_codes.min.js
@@ -1 +1,16 @@
-define([],function(){return{tab:9,enter:13,escape:27,space:32,end:35,home:36,arrowLeft:37,arrowUp:38,arrowRight:39,arrowDown:40,8:56,asterix:106,pageUp:33,pageDown:34}});
\ No newline at end of file
+/*
+ * A list of human readable names for the keycodes.
+ *
+ * This is a copy of a library that was added to Moodle core in Moodle 3.2,
+ * so we can support older Moodle versions.
+ *
+ * @module qtype_ordering/key_codes
+ * @class key_codes
+ * @package qtype_ordering
+ * @copyright 2016 Ryan Wyllie
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since 3.2
+ */
+define("qtype_ordering/key_codes",[],(function(){return{tab:9,enter:13,escape:27,space:32,end:35,home:36,arrowLeft:37,arrowUp:38,arrowRight:39,arrowDown:40,8:56,asterix:106,pageUp:33,pageDown:34}}));
+
+//# sourceMappingURL=key_codes.min.js.map
\ No newline at end of file
diff --git a/question/type/ordering/amd/build/key_codes.min.js.map b/question/type/ordering/amd/build/key_codes.min.js.map
new file mode 100644
index 00000000000..ae7ef7fbf64
--- /dev/null
+++ b/question/type/ordering/amd/build/key_codes.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"key_codes.min.js","sources":["../src/key_codes.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/*\n * A list of human readable names for the keycodes.\n *\n * This is a copy of a library that was added to Moodle core in Moodle 3.2,\n * so we can support older Moodle versions.\n *\n * @module qtype_ordering/key_codes\n * @class key_codes\n * @package qtype_ordering\n * @copyright 2016 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.2\n */\ndefine([], function() {\n\n return /** @alias module:qtype_ordering/key_codes */ {\n 'tab': 9,\n 'enter': 13,\n 'escape': 27,\n 'space': 32,\n 'end': 35,\n 'home': 36,\n 'arrowLeft': 37,\n 'arrowUp': 38,\n 'arrowRight': 39,\n 'arrowDown': 40,\n '8': 56,\n 'asterix': 106,\n 'pageUp': 33,\n 'pageDown': 34,\n };\n});\n"],"names":["define"],"mappings":";;;;;;;;;;;;;AA4BAA,kCAAO,IAAI,iBAE8C,KAC1C,QACE,UACC,SACD,OACF,QACC,aACK,WACF,cACG,aACD,KACR,WACM,WACD,YACE"}
\ No newline at end of file
diff --git a/question/type/ordering/amd/build/reorder.min.js b/question/type/ordering/amd/build/reorder.min.js
index c9539a7d642..653a73011c8 100644
--- a/question/type/ordering/amd/build/reorder.min.js
+++ b/question/type/ordering/amd/build/reorder.min.js
@@ -1,2 +1,3 @@
-define ("qtype_ordering/reorder",["jquery","qtype_ordering/drag_reorder"],function(a,b){return{init:function init(c,d){new b({list:"ul#"+c,item:"li.sortableitem",proxyHtml:"",itemMovingClass:"current-drop",idGetter:function idGetter(b){return a(b).attr("id")},nameGetter:function nameGetter(b){return a(b).text},reorderStart:function reorderStart(){},reorderEnd:function reorderEnd(){},reorderDone:function reorderDone(b,c,e){a("input#"+d)[0].value=e.join(",")}})}}});
-//# sourceMappingURL=reorder.min.js.map
+define("qtype_ordering/reorder",["jquery","qtype_ordering/drag_reorder"],(function($,DragReorder){return{init:function(sortableid,responseid){new DragReorder({list:"ul#"+sortableid,item:"li.sortableitem",proxyHtml:'',itemMovingClass:"current-drop",idGetter:function(item){return $(item).attr("id")},nameGetter:function(item){return $(item).text},reorderStart:function(){},reorderEnd:function(){},reorderDone:function(list,item,newOrder){$("input#"+responseid)[0].value=newOrder.join(",")}})}}}));
+
+//# sourceMappingURL=reorder.min.js.map
\ No newline at end of file
diff --git a/question/type/ordering/amd/build/reorder.min.js.map b/question/type/ordering/amd/build/reorder.min.js.map
index 162006f6cb4..85d556564ad 100644
--- a/question/type/ordering/amd/build/reorder.min.js.map
+++ b/question/type/ordering/amd/build/reorder.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../src/reorder.js"],"names":["define","$","dragReorder","init","sortableid","responseid","list","item","proxyHtml","itemMovingClass","idGetter","attr","nameGetter","text","reorderStart","reorderEnd","reorderDone","newOrder","value","join"],"mappings":"AAAAA,OAAM,0BAAC,CAAC,QAAD,CAAW,6BAAX,CAAD,CAA4C,SAASC,CAAT,CAAYC,CAAZ,CAAyB,CACvE,MAAO,CAOHC,IAAI,CAAE,cAAUC,CAAV,CAAsBC,CAAtB,CAAkC,CACpC,GAAIH,CAAAA,CAAJ,CAAgB,CACZI,IAAI,CAAE,MAAQF,CADF,CAEZG,IAAI,CAAE,iBAFM,CAGZC,SAAS,sJAHG,CAMZC,eAAe,CAAE,cANL,CAOZC,QAAQ,CAAE,kBAAUH,CAAV,CAAgB,CAAE,MAAON,CAAAA,CAAC,CAACM,CAAD,CAAD,CAAQI,IAAR,CAAa,IAAb,CAAqB,CAP5C,CAQZC,UAAU,CAAE,oBAAUL,CAAV,CAAgB,CAAE,MAAON,CAAAA,CAAC,CAACM,CAAD,CAAD,CAAQM,IAAO,CARxC,CASZC,YAAY,CAAE,uBAAW,CAAE,CATf,CAUZC,UAAU,CAAE,qBAAW,CAAE,CAVb,CAWZC,WAAW,CAAE,qBAASV,CAAT,CAAeC,CAAf,CAAqBU,CAArB,CAA+B,CACxChB,CAAC,CAAC,SAAWI,CAAZ,CAAD,CAAyB,CAAzB,EAA4Ba,KAA5B,CAAoCD,CAAQ,CAACE,IAAT,CAAc,GAAd,CACvC,CAbW,CAAhB,CAeH,CAvBE,CAyBV,CA1BK,CAAN","sourcesContent":["define(['jquery', 'qtype_ordering/drag_reorder'], function($, dragReorder) {\n return {\n /**\n * Initialise one ordering question.\n *\n * @param {String} sortableid id of ul for this question.\n * @param {String} responseid id of hidden field for this question.\n */\n init: function (sortableid, responseid) {\n new dragReorder({\n list: 'ul#' + sortableid,\n item: 'li.sortableitem',\n proxyHtml: '',\n itemMovingClass: \"current-drop\",\n idGetter: function (item) { return $(item).attr('id'); },\n nameGetter: function (item) { return $(item).text; },\n reorderStart: function() {},\n reorderEnd: function() {},\n reorderDone: function(list, item, newOrder) {\n $('input#' + responseid)[0].value = newOrder.join(',');\n }\n });\n }\n };\n});\n"],"file":"reorder.min.js"}
\ No newline at end of file
+{"version":3,"file":"reorder.min.js","sources":["../src/reorder.js"],"sourcesContent":["define(['jquery', 'qtype_ordering/drag_reorder'], function($, DragReorder) {\n return {\n /**\n * Initialise one ordering question.\n *\n * @param {String} sortableid id of ul for this question.\n * @param {String} responseid id of hidden field for this question.\n */\n init: function(sortableid, responseid) {\n new DragReorder({\n list: 'ul#' + sortableid,\n item: 'li.sortableitem',\n proxyHtml: '',\n itemMovingClass: \"current-drop\",\n idGetter: function(item) {\n return $(item).attr('id');\n },\n nameGetter: function(item) {\n return $(item).text;\n },\n reorderStart: function() {\n // Do nothing.\n },\n reorderEnd: function() {\n // Do nothing.\n },\n reorderDone: function(list, item, newOrder) {\n $('input#' + responseid)[0].value = newOrder.join(',');\n }\n });\n }\n };\n});\n"],"names":["define","$","DragReorder","init","sortableid","responseid","list","item","proxyHtml","itemMovingClass","idGetter","attr","nameGetter","text","reorderStart","reorderEnd","reorderDone","newOrder","value","join"],"mappings":"AAAAA,gCAAO,CAAC,SAAU,gCAAgC,SAASC,EAAGC,mBACnD,CAOHC,KAAM,SAASC,WAAYC,gBACnBH,YAAY,CACZI,KAAM,MAAQF,WACdG,KAAM,kBACNC,UAAW,gJAGXC,gBAAiB,eACjBC,SAAU,SAASH,aACJN,EAAEM,MAAMI,KAAK,OAE5BC,WAAY,SAASL,aACNN,EAAEM,MAAMM,MAEvBC,aAAc,aAGdC,WAAY,aAGZC,YAAa,SAASV,KAAMC,KAAMU,UAC9BhB,EAAE,SAAWI,YAAY,GAAGa,MAAQD,SAASE,KAAK"}
\ No newline at end of file
diff --git a/question/type/ordering/amd/src/drag_reorder.js b/question/type/ordering/amd/src/drag_reorder.js
index cb3116015d7..eafa42fd6ec 100644
--- a/question/type/ordering/amd/src/drag_reorder.js
+++ b/question/type/ordering/amd/src/drag_reorder.js
@@ -13,15 +13,15 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see .
-/**
+/*
* Generic library to allow things in a vertical list to be re-ordered using drag and drop.
*
* To make a set of things draggable, create a new instance of this object passing the
* necessary config, as explained in the comment on the constructor.
*
- * @package qtype_ordering
+ * @package qtype_ordering
* @copyright 2018 The Open University
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
@@ -102,15 +102,15 @@ define([
* See how block_userlinks does it, if this seems like it might be useful. nameGetter should return
* the container name for these items.
*
- * @param config As above.
+ * @param {Object} config As above.
*/
return function(config) {
- var dragStart = null, // Information about when and where the drag started.
- originalOrder = null, // Array of ids.
- itemDragging = null, // Item being moved by dragging (jQuery object).
- itemMoving = null, // Item being moved using the accessible modal (jQuery object).
- proxy = null, // Drag proxy (jQuery object).
- orderList = null; // Order list (jQuery object).
+ var dragStart = null, // Information about when and where the drag started.
+ originalOrder = null, // Array of ids.
+ itemDragging = null, // Item being moved by dragging (jQuery object).
+ itemMoving = null, // Item being moved using the accessible modal (jQuery object).
+ proxy = null, // Drag proxy (jQuery object).
+ orderList = null; // Order list (jQuery object).
var startDrag = function(event, details) {
orderList = $(config.list);
@@ -149,7 +149,7 @@ define([
var list = itemDragging.closest(config.list);
var closestItem = null;
var closestDistance = null;
- list.find(config.item).each(function (index, element) {
+ list.find(config.item).each(function(index, element) {
var distance = distanceBetweenElements(element, proxy);
if (closestItem === null || distance < closestDistance) {
closestItem = $(element);
@@ -160,7 +160,16 @@ define([
if (closestItem[0] === itemDragging[0]) {
return;
}
+ var offsetValue = 0;
+ // Set offset depending on if item is being dragged downwards/upwards.
if (midY(proxy) < midY(closestItem)) {
+ offsetValue = 20;
+ window.console.log("For midY(proxy) < midY(closestItem) offset is: " + offsetValue);
+ } else {
+ offsetValue = -20;
+ window.console.log("For midY(proxy) < midY(closestItem) offset is: " + offsetValue);
+ }
+ if (midY(proxy) + offsetValue < midY(closestItem)) {
itemDragging.insertBefore(closestItem);
} else {
itemDragging.insertAfter(closestItem);
@@ -170,14 +179,13 @@ define([
/**
* Update proxy's position.
- * @param itemDragging
+ * @param {jQuery} itemDragging
*/
var updateProxy = function(itemDragging) {
var list = itemDragging.closest('ol, ul');
var items = list.find('li');
var count = items.length;
for (var i = 0; i < count; ++i) {
- //proxy.css('margin-left', '20p');
if (itemDragging[0] === items[i]) {
proxy.find('li').attr('value', i + 1);
break;
@@ -189,8 +197,8 @@ define([
* It outer and inner are two CSS selectors, which may contain commas,
* then combine them safely. So combineSelectors('a, b', 'c, d')
* gives 'a c, a d, b c, b d'.
- * @param outer
- * @param inner
+ * @param {Selector} outer
+ * @param {Selector} inner
* @returns {string}
*/
var combineSelectors = function(outer, inner) {
@@ -230,10 +238,10 @@ define([
* Tab for tabbing though and choose the item to be moved
* space, arrow-right arrow-down for moving current element forewards.
* arrow-right arrow-down for moving the current element backwards.
- * @param e, the event
- * @param item, the current moving item
+ * @param {Object} e the event
+ * @param {jQuery} current the current moving item
*/
- var itemMovedByKeyboard = function (e, current) {
+ var itemMovedByKeyboard = function(e, current) {
switch (e.keyCode) {
case keys.space:
case keys.arrowRight:
@@ -260,8 +268,8 @@ define([
/**
* Get the x-position of the middle of the DOM node represented by the given jQuery object.
- * @param jQuery wrapping a DOM node.
- * @returns Number the x-coordinate of the middle (left plus half outerWidth).
+ * @param {jQuery} jQuery wrapping a DOM node.
+ * @returns {number} Number the x-coordinate of the middle (left plus half outerWidth).
*/
var midX = function(jQuery) {
return jQuery.offset().left + jQuery.outerWidth() / 2;
@@ -269,8 +277,8 @@ define([
/**
* Get the y-position of the middle of the DOM node represented by the given jQuery object.
- * @param jQuery wrapping a DOM node.
- * @returns Number the y-coordinate of the middle (top plus half outerHeight).
+ * @param {jQuery} jQuery wrapping a DOM node.
+ * @returns {number} Number the y-coordinate of the middle (top plus half outerHeight).
*/
var midY = function(jQuery) {
return jQuery.offset().top + jQuery.outerHeight() / 2;
@@ -278,12 +286,13 @@ define([
/**
* Calculate the distance between the centres of two elements.
- * @param element1 selector, element or jQuery.
- * @param element2 selector, element or jQuery.
- * @return number the distance in pixels.
+ * @param {Selector|Element|jQuery} element1 selector, element or jQuery.
+ * @param {Selector|Element|jQuery} element2 selector, element or jQuery.
+ * @return {number} number the distance in pixels.
*/
var distanceBetweenElements = function(element1, element2) {
- var e1 = $(element1), e2 = $(element2);
+ var e1 = $(element1);
+ var e2 = $(element2);
var dx = midX(e1) - midX(e2);
var dy = midY(e1) - midY(e2);
return Math.sqrt(dx * dx + dy * dy);
@@ -291,23 +300,27 @@ define([
/**
* Get the current order of the list containing itemDragging.
- * @returns Array of strings, the id of each element in order.
+ * @returns {Array} Array of strings, the id of each element in order.
*/
var getCurrentOrder = function() {
return (itemDragging || itemMoving).closest(config.list).find(config.item).map(
- function(index, item) { return config.idGetter(item); }).get();
+ function(index, item) {
+ return config.idGetter(item);
+ }).get();
};
/**
* Compare two arrays, which just contain simple values like ints or strings,
* to see if they are equal.
- * @param a1 first array.
- * @param a2 second array.
- * @return boolean true if they both contain the same elements in the same order, else false.
+ * @param {Array} a1 first array.
+ * @param {Array} a2 second array.
+ * @return {Boolean} boolean true if they both contain the same elements in the same order, else false.
*/
var arrayEquals = function(a1, a2) {
return a1.length === a2.length &&
- a1.every(function(v, i) { return v === a2[i]; });
+ a1.every(function(v, i) {
+ return v === a2[i];
+ });
};
config.itemInPage = combineSelectors(config.list, config.item);
diff --git a/question/type/ordering/amd/src/key_codes.js b/question/type/ordering/amd/src/key_codes.js
index 2d45fb7043e..dadd56bc51c 100644
--- a/question/type/ordering/amd/src/key_codes.js
+++ b/question/type/ordering/amd/src/key_codes.js
@@ -13,7 +13,7 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see .
-/**
+/*
* A list of human readable names for the keycodes.
*
* This is a copy of a library that was added to Moodle core in Moodle 3.2,
diff --git a/question/type/ordering/amd/src/reorder.js b/question/type/ordering/amd/src/reorder.js
index 1066bb8e289..73cf57dfc4b 100644
--- a/question/type/ordering/amd/src/reorder.js
+++ b/question/type/ordering/amd/src/reorder.js
@@ -1,4 +1,4 @@
-define(['jquery', 'qtype_ordering/drag_reorder'], function($, dragReorder) {
+define(['jquery', 'qtype_ordering/drag_reorder'], function($, DragReorder) {
return {
/**
* Initialise one ordering question.
@@ -6,21 +6,29 @@ define(['jquery', 'qtype_ordering/drag_reorder'], function($, dragReorder) {
* @param {String} sortableid id of ul for this question.
* @param {String} responseid id of hidden field for this question.
*/
- init: function (sortableid, responseid) {
- new dragReorder({
+ init: function(sortableid, responseid) {
+ new DragReorder({
list: 'ul#' + sortableid,
item: 'li.sortableitem',
proxyHtml: '',
itemMovingClass: "current-drop",
- idGetter: function (item) { return $(item).attr('id'); },
- nameGetter: function (item) { return $(item).text; },
- reorderStart: function() {},
- reorderEnd: function() {},
+ idGetter: function(item) {
+ return $(item).attr('id');
+ },
+ nameGetter: function(item) {
+ return $(item).text;
+ },
+ reorderStart: function() {
+ // Do nothing.
+ },
+ reorderEnd: function() {
+ // Do nothing.
+ },
reorderDone: function(list, item, newOrder) {
$('input#' + responseid)[0].value = newOrder.join(',');
- }
+ }
});
}
};
diff --git a/question/type/ordering/backup/moodle1/lib.php b/question/type/ordering/backup/moodle1/lib.php
index 591b4bf0c1c..442ca9334b5 100644
--- a/question/type/ordering/backup/moodle1/lib.php
+++ b/question/type/ordering/backup/moodle1/lib.php
@@ -22,8 +22,6 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-defined('MOODLE_INTERNAL') || die();
-
/**
* Ordering question type conversion handler class
*
diff --git a/question/type/ordering/backup/moodle2/backup_qtype_ordering_plugin.class.php b/question/type/ordering/backup/moodle2/backup_qtype_ordering_plugin.class.php
index 804d342e47d..2ed041a066d 100644
--- a/question/type/ordering/backup/moodle2/backup_qtype_ordering_plugin.class.php
+++ b/question/type/ordering/backup/moodle2/backup_qtype_ordering_plugin.class.php
@@ -22,8 +22,6 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-defined('MOODLE_INTERNAL') || die();
-
/**
* Provides the information to backup ordering questions
*
@@ -55,7 +53,7 @@ class backup_qtype_ordering_plugin extends backup_qtype_plugin {
'gradingtype', 'showgrading', 'numberingstyle',
'correctfeedback', 'correctfeedbackformat',
'incorrectfeedback', 'incorrectfeedbackformat',
- 'partiallycorrectfeedback', 'partiallycorrectfeedbackformat');
+ 'partiallycorrectfeedback', 'partiallycorrectfeedbackformat', 'shownumcorrect');
$ordering = new backup_nested_element('ordering', array('id'), $fields);
// Now the own qtype tree.
diff --git a/question/type/ordering/backup/moodle2/restore_qtype_ordering_plugin.class.php b/question/type/ordering/backup/moodle2/restore_qtype_ordering_plugin.class.php
index ae0fc224913..28dc71ea5fc 100644
--- a/question/type/ordering/backup/moodle2/restore_qtype_ordering_plugin.class.php
+++ b/question/type/ordering/backup/moodle2/restore_qtype_ordering_plugin.class.php
@@ -22,8 +22,6 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-defined('MOODLE_INTERNAL') || die();
-
/**
* Restore plugin class that provides the necessary information needed to restore one ordering qtype plugin
*
@@ -71,6 +69,9 @@ class restore_qtype_ordering_plugin extends restore_qtype_plugin {
// and create a mapping from the $oldid to the $newid.
if ($this->get_mappingid('question_created', $oldquestionid)) {
$data->questionid = $newquestionid;
+ if (!isset($data->shownumcorrect)) {
+ $data->shownumcorrect = 1;
+ }
$newid = $DB->insert_record('qtype_ordering_options', $data);
$this->set_mapping('qtype_ordering_options', $oldid, $newid);
}
diff --git a/question/type/ordering/classes/privacy/provider.php b/question/type/ordering/classes/privacy/provider.php
index 4df7058a10d..496bc94a877 100644
--- a/question/type/ordering/classes/privacy/provider.php
+++ b/question/type/ordering/classes/privacy/provider.php
@@ -25,8 +25,6 @@
namespace qtype_ordering\privacy;
-defined('MOODLE_INTERNAL') || die();
-
/**
* Privacy Subsystem for qtype_numerical implementing null_provider.
*
@@ -47,7 +45,7 @@ class provider implements \core_privacy\local\metadata\null_provider {
*
* @return string
*/
- public static function _get_reason() {
+ public static function _get_reason() { // phpcs:ignore
return 'privacy:metadata';
}
}
diff --git a/question/type/ordering/classes/question_hint_ordering.php b/question/type/ordering/classes/question_hint_ordering.php
new file mode 100644
index 00000000000..fb52441cfbd
--- /dev/null
+++ b/question/type/ordering/classes/question_hint_ordering.php
@@ -0,0 +1,87 @@
+.
+
+/**
+ * Question hint for ordering.
+ *
+ * @package qtype_ordering
+ * @copyright 2021 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace qtype_ordering;
+
+use question_display_options;
+use question_hint_with_parts;
+
+/**
+ * Question hint for ordering.
+ *
+ * An extension of {@link question_hint} for questions like match and multiple
+ * choice with multile answers, where there are options for whether to show the
+ * number of parts right at each stage, and to reset the wrong parts.
+ *
+ * @package qtype_ordering
+ * @copyright 2021 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_hint_ordering extends question_hint_with_parts {
+ /** Highlight response in the hint options. */
+ public $highlightresponse;
+
+ /**
+ * Constructor.
+ *
+ * @param int The hint id from the database.
+ * @param string $hint The hint text.
+ * @param int The corresponding text FORMAT_... type.
+ * @param bool $shownumcorrect Whether the number of right parts should be shown.
+ * @param bool $clearwrong Whether the wrong parts should be reset.
+ */
+ public function __construct($id, $hint, $hintformat, $shownumcorrect, $clearwrong, $highlightresponse) {
+ parent::__construct($id, $hint, $hintformat, $shownumcorrect, $clearwrong);
+ $this->highlightresponse = $highlightresponse;
+ }
+
+ /**
+ * Create a basic hint from a row loaded from the question_hints table in the database.
+ *
+ * @param object $row With property options as well as hint, shownumcorrect and clearwrong set.
+ * @return question_hint_ordering
+ */
+ public static function load_from_record($row) {
+ global $DB;
+
+ // Initialize with the old questions.
+ if (is_null($row->options) || is_null($row->shownumcorrect)) {
+ $row->options = 1;
+ $row->shownumcorrect = 1;
+ $DB->update_record('question_hints', $row);
+ }
+
+ return new question_hint_ordering($row->id, $row->hint, $row->hintformat,
+ $row->shownumcorrect, $row->clearwrong, $row->options);
+ }
+
+ /**
+ * Adjust this display options according to the hint settings.
+ *
+ * @param question_display_options $options
+ */
+ public function adjust_display_options(question_display_options $options) {
+ parent::adjust_display_options($options);
+ $options->highlightresponse = $this->highlightresponse;
+ }
+}
diff --git a/question/type/ordering/db/install.xml b/question/type/ordering/db/install.xml
index 85c26479b5e..929712491d6 100644
--- a/question/type/ordering/db/install.xml
+++ b/question/type/ordering/db/install.xml
@@ -17,6 +17,7 @@
+
diff --git a/question/type/ordering/db/upgrade.php b/question/type/ordering/db/upgrade.php
index 7bad633ddbd..6c9bcd7d6b2 100644
--- a/question/type/ordering/db/upgrade.php
+++ b/question/type/ordering/db/upgrade.php
@@ -22,8 +22,6 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-defined('MOODLE_INTERNAL') || die();
-
/**
* Upgrade code for the ordering question type.
*
@@ -275,6 +273,7 @@ function xmldb_qtype_ordering_upgrade($oldversion) {
case 'III':
$DB->set_field($table, $field, 'IIII', array('id' => $option->id));
break;
+ // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
// Ignore "abc", "iii", and anything else.
}
}
@@ -282,6 +281,18 @@ function xmldb_qtype_ordering_upgrade($oldversion) {
upgrade_plugin_savepoint(true, $newversion, 'qtype', 'ordering');
}
+ $newversion = '2022092000';
+ if ($oldversion < $newversion) {
+ $table = new xmldb_table('qtype_ordering_options');
+ $field = new xmldb_field('shownumcorrect', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, 0);
+ if ($dbman->field_exists($table, $field)) {
+ $dbman->change_field_type($table, $field);
+ } else {
+ $dbman->add_field($table, $field);
+ }
+ upgrade_plugin_savepoint(true, $newversion, 'qtype', 'ordering');
+ }
+
return true;
}
diff --git a/question/type/ordering/edit_ordering_form.php b/question/type/ordering/edit_ordering_form.php
index 36895da0311..aa5e4c1610d 100644
--- a/question/type/ordering/edit_ordering_form.php
+++ b/question/type/ordering/edit_ordering_form.php
@@ -147,10 +147,10 @@ class qtype_ordering_edit_form extends question_edit_form {
$this->adjust_html_editors($mform, $name);
// Adding feedback fields (=Combined feedback).
- $this->add_combined_feedback_fields(false);
+ $this->add_combined_feedback_fields(true);
// Adding interactive settings (=Multiple tries).
- $this->add_interactive_settings(false, false);
+ $this->add_interactive_settings(false, true);
}
/**
@@ -286,6 +286,39 @@ class qtype_ordering_edit_form extends question_edit_form {
}
}
+ /**
+ * Create the form elements required by one hint.
+ *
+ * @param string $withclearwrong Whether this quesiton type uses the 'Clear wrong' option on hints.
+ * @param string $withshownumpartscorrect Whether this quesiton type uses the 'Show num parts correct' option on hints.
+ * @return array Form field elements for one hint.
+ */
+ protected function get_hint_fields($withclearwrong = false, $withshownumpartscorrect = false) {
+ $mform = $this->_form;
+
+ $repeated = [];
+ $repeated[] = $mform->createElement('editor', 'hint', get_string('hintn', 'question'),
+ ['rows' => 5], $this->editoroptions);
+ $repeatedoptions['hint']['type'] = PARAM_RAW;
+
+ $optionelements = [];
+
+ if ($withshownumpartscorrect) {
+ $optionelements[] = $mform->createElement('advcheckbox', 'hintshownumcorrect', '',
+ get_string('shownumpartscorrect', 'question'));
+ }
+
+ $optionelements[] = $mform->createElement('advcheckbox', 'hintoptions', '',
+ get_string('highlightresponse', 'qtype_ordering'));
+
+ if (count($optionelements)) {
+ $repeated[] = $mform->createElement('group', 'hintoptions',
+ get_string('hintnoptions', 'question'), $optionelements, null, false);
+ }
+
+ return [$repeated, $repeatedoptions];
+ }
+
/**
* Perform an preprocessing needed on the data passed to {@link set_data()}
* before it is used to initialise the form.
@@ -298,9 +331,9 @@ class qtype_ordering_edit_form extends question_edit_form {
$question = $this->data_preprocessing_answers($question, true);
// Preprocess feedback.
- $question = $this->data_preprocessing_combined_feedback($question);
+ $question = $this->data_preprocessing_combined_feedback($question, true);
- $question = $this->data_preprocessing_hints($question, false, false);
+ $question = $this->data_preprocessing_hints($question, false, true);
// Preprocess answers and fractions.
$question->answer = array();
@@ -358,6 +391,28 @@ class qtype_ordering_edit_form extends question_edit_form {
return $question;
}
+ /**
+ * Perform the necessary preprocessing for the hint fields.
+ *
+ * @param object $question The data being passed to the form.
+ * @param bool $withclearwrong Clear wrong hints.
+ * @param bool $withshownumpartscorrect Show number correct.
+ * @return object The modified data.
+ */
+ protected function data_preprocessing_hints($question, $withclearwrong = false, $withshownumpartscorrect = false) {
+ if (empty($question->hints)) {
+ return $question;
+ }
+ parent::data_preprocessing_hints($question, $withclearwrong, $withshownumpartscorrect);
+
+ $question->hintoptions = [];
+ foreach ($question->hints as $hint) {
+ $question->hintoptions[] = $hint->options;
+ }
+
+ return $question;
+ }
+
/**
* Form validation
*
@@ -384,7 +439,7 @@ class qtype_ordering_edit_form extends question_edit_form {
$item = str_replace('{no}', $i + 1, $item);
$item = html_writer::link("#id_answerheader_$i", $item);
$a = (object)array('text' => $answer, 'item' => $item);
- $errors["answer[$answercount]"] = get_string('duplicatesnotallowed', $plugin, $a);
+ $errors["answer[$answercount]"] = get_string('duplicatesnotallowed', $plugin, $a);
} else {
$answers[] = $answer;
}
@@ -427,14 +482,14 @@ class qtype_ordering_edit_form extends question_edit_form {
*
* @param string $name Item name
* @param string|mixed|null $value
- * @return boolean (usually TRUE, unless there is an error)
+ * @return boolean (usually TRUE, unless there is an error)
*/
protected function set_my_default_value($name, $value) {
if (method_exists($this, 'set_default_value')) {
- // This method doesn't exist yet, but it might one day ;-)
+ // This method doesn't exist yet, but it might one day ;-).
return $this->set_default_value($name, $value);
} else {
- // Until at least Moodle <= 4.0, we expect to come this way
+ // Until at least Moodle <= 4.0, we expect to come this way.
$name = $this->get_my_preference_name($name);
return set_user_preferences(array($name => $value));
}
@@ -449,10 +504,10 @@ class qtype_ordering_edit_form extends question_edit_form {
*/
protected function get_my_default_value($name, $default) {
if (method_exists($this, 'get_default_value')) {
- // Moodle >= 3.10
+ // Moodle >= 3.10.
return $this->get_default_value($name, $default);
} else {
- // Moodle <= 3.9
+ // Moodle <= 3.9.
$name = $this->get_my_preference_name($name);
return get_user_preferences($name, $default);
}
diff --git a/question/type/ordering/lang/en/qtype_ordering.php b/question/type/ordering/lang/en/qtype_ordering.php
index 50d465b3633..c8aca5055b8 100644
--- a/question/type/ordering/lang/en/qtype_ordering.php
+++ b/question/type/ordering/lang/en/qtype_ordering.php
@@ -41,35 +41,39 @@ $string['gradedetails'] = 'Grade details';
$string['gradingtype'] = 'Grading type';
$string['gradingtype_help'] = 'Choose the type of grading calculation.
-**All or nothing**
+**All or nothing**
If all items are in the correct position, then full marks are awarded. Otherwise, the score is zero.
-**Absolute position**
+**Absolute position**
An item is considered correct if it is in the same position as in the correct answer. The highest possible score for the question is **the same as** the number of items displayed to the student.
-**Relative to correct position**
+**Relative to correct position**
An item is considered correct if it is in the same position as in the correct answer. Correct items receive a score equal to the number of items displayed minus one. Incorrect items receive a score equal to the number of items displayed minus one and minus the distance of the item from its correct position. Thus, if ***n*** items are displayed to the student, the number of marks available for each item is ***(n - 1)***, and the highest mark available for the question is ***n x (n - 1)***, which is the same as ***(n² - n)***.
-**Relative to the next item (excluding last)**
+**Relative to the next item (excluding last)**
An item is considered correct if it is followed by the same item as it is in the correct answer. The item in the last position is not checked. Thus, the highest possible score for the question is **one less than** the number of items displayed to the student.
-**Relative to the next item (including last)**
+**Relative to the next item (including last)**
An item is considered correct if it is followed by the same item as it is in the correct answer. This includes the last item which must have no item following it. Thus, the highest possible score for the question is **the same as** the number of items displayed to the student.
-**Relative to both the previous and next items**
+**Relative to both the previous and next items**
An item is considered correct if both the previous and next items are the same as they are in the correct answer. The first item should have no previous item, and the last item should have no next item. Thus, there are two possible points for each item, and the highest possible score for the question is **twice** the number of items displayed to the student.
-**Relative to ALL previous and next items**
+**Relative to ALL previous and next items**
An item is considered correct if it is preceded by all the same items as it is in the correct answer, and it is followed by all the same items as it is in the correct answer. The order of the previous items does not matter, and nor does the order of the following items. Thus, if ***n*** items are displayed to the student, the number of marks available for each item is ***(n - 1)***, and the highest mark available for the question is ***n x (n - 1)***, which is the same as ***(n² - n)***.
-**Longest ordered subset**
+**Longest ordered subset**
The grade is the number of items in the longest ordered subset of items. The highest possible grade is the same as the number of items displayed. A subset must have at least two items. Subsets do not need to start at the first item (but they can) and they do not need to be contiguous (but they can be). Where there are multiple subsets of equal length, items in the subset that is found first, when searching from left to right, will be displayed as correct. Other items will be marked as incorrect.
-**Longest contiguous subset**
+**Longest contiguous subset**
The grade is the number of items in the longest contiguous subset of items. The highest possible grade is the same as the number of items displayed. A subset must have at least two items. Subsets do not need to start at the first item (but they can) and they MUST BE CONTIGUOUS. Where there are multiple subsets of equal length, items in the subset that is found first, when searching from left to right, will be displayed as correct. Other items will be marked as incorrect.';
+$string['highlightresponse'] = 'Highlight response as correct or incorrect';
$string['horizontal'] = 'Horizontal';
+$string['itemplural'] = 'items';
+$string['itemsingular'] = 'item';
+
$string['layouttype_help'] = 'Choose whether to display the items vertically or horizontally.';
$string['layouttype'] = 'Layout of items';
$string['longestcontiguoussubset'] = 'Longest contiguous subset';
@@ -98,6 +102,7 @@ $string['pluginnameediting'] = 'Editing an Ordering question';
$string['pluginnamesummary'] = 'Put jumbled items into a meaningful order.';
$string['privacy:metadata'] = 'The ordering question type plugin does not store any personal data.';
+$string['regradeissuenumitemschanged'] = 'The number of draggable items has changed.';
$string['relativeallpreviousandnext'] = 'Relative to ALL the previous and next items';
$string['relativenextexcludelast'] = 'Relative to the next item (excluding last)';
$string['relativenextincludelast'] = 'Relative to the next item (including last)';
@@ -118,3 +123,6 @@ $string['showgrading'] = 'Grading details';
$string['showgrading_help'] = 'Choose whether to show or hide details of the score calculation when a student reviews a response to this Ordering question.';
$string['vertical'] = 'Vertical';
+$string['yougotnright'] = 'You have {$a->numright} {$a->numrightplural} correct.';
+$string['yougotnpartial'] = 'You have {$a->numpartial} {$a->numpartialplural} partially correct.';
+$string['yougotnincorrect'] = 'You have {$a->numincorrect} {$a->numincorrectplural} incorrect.';
diff --git a/question/type/ordering/lib.php b/question/type/ordering/lib.php
index d58ac38ee5c..aaadccd462b 100644
--- a/question/type/ordering/lib.php
+++ b/question/type/ordering/lib.php
@@ -22,8 +22,6 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-defined('MOODLE_INTERNAL') || die();
-
/**
* Checks file access for ordering questions.
*
diff --git a/question/type/ordering/question.php b/question/type/ordering/question.php
index 74985578f43..44c68bc7b9e 100644
--- a/question/type/ordering/question.php
+++ b/question/type/ordering/question.php
@@ -24,7 +24,6 @@
*/
// Prevent direct access to this script.
-defined('MOODLE_INTERNAL') || die();
/**
* Represents an ordering question.
@@ -85,7 +84,7 @@ class qtype_ordering_question extends question_graded_automatically {
/** @var array Records from "question_answers" table */
public $answers;
- /** @var array Records from "qtype_ordering_options" table */
+ /** @var stdClass Records from "qtype_ordering_options" table */
public $options;
/** @var array of answerids in correct order */
@@ -111,18 +110,15 @@ class qtype_ordering_question extends question_graded_automatically {
* 1 and {@link get_num_variants()} inclusive.
*/
public function start_attempt(question_attempt_step $step, $variant) {
- $answers = $this->get_ordering_answers();
- $options = $this->get_ordering_options();
-
- $countanswers = count($answers);
+ $countanswers = count($this->answers);
// Sanitize "selecttype".
- $selecttype = $options->selecttype;
+ $selecttype = $this->options->selecttype;
$selecttype = max(0, $selecttype);
$selecttype = min(2, $selecttype);
// Sanitize "selectcount".
- $selectcount = $options->selectcount;
+ $selectcount = $this->options->selectcount;
$selectcount = max(3, $selectcount);
$selectcount = min($countanswers, $selectcount);
@@ -139,15 +135,15 @@ class qtype_ordering_question extends question_graded_automatically {
// Extract answer ids.
switch ($selecttype) {
case self::SELECT_ALL:
- $answerids = array_keys($answers);
+ $answerids = array_keys($this->answers);
break;
case self::SELECT_RANDOM:
- $answerids = array_rand($answers, $selectcount);
+ $answerids = array_rand($this->answers, $selectcount);
break;
case self::SELECT_CONTIGUOUS:
- $answerids = array_keys($answers);
+ $answerids = array_keys($this->answers);
$offset = mt_rand(0, $countanswers - $selectcount);
$answerids = array_slice($answerids, $offset, $selectcount);
break;
@@ -175,12 +171,47 @@ class qtype_ordering_question extends question_graded_automatically {
* being loaded.
*/
public function apply_attempt_state(question_attempt_step $step) {
- $answers = $this->get_ordering_answers();
- $options = $this->get_ordering_options();
$this->currentresponse = array_filter(explode(',', $step->get_qt_var('_currentresponse')));
$this->correctresponse = array_filter(explode(',', $step->get_qt_var('_correctresponse')));
}
+ public function validate_can_regrade_with_other_version(question_definition $otherversion): ?string {
+ $basemessage = parent::validate_can_regrade_with_other_version($otherversion);
+ if ($basemessage) {
+ return $basemessage;
+ }
+
+ if (count($this->answers) != count($otherversion->answers)) {
+ return get_string('regradeissuenumitemschanged', 'qtype_ordering');
+ }
+
+ return null;
+ }
+
+ public function update_attempt_state_data_for_new_version(
+ question_attempt_step $oldstep, question_definition $otherversion) {
+ parent::update_attempt_state_data_for_new_version($oldstep, $otherversion);
+
+ $mapping = array_combine(array_keys($otherversion->answers), array_keys($this->answers));
+
+ $oldorder = explode(',', $oldstep->get_qt_var('_currentresponse'));
+ $neworder = [];
+ foreach ($oldorder as $oldid) {
+ $neworder[] = $mapping[$oldid] ?? $oldid;
+ }
+
+ $oldcorrect = explode(',', $oldstep->get_qt_var('_correctresponse'));
+ $newcorrect = [];
+ foreach ($oldcorrect as $oldid) {
+ $newcorrect[] = $mapping[$oldid] ?? $oldid;
+ }
+
+ return [
+ '_currentresponse' => implode(',', $neworder),
+ '_correctresponse' => implode(',', $newcorrect),
+ ];
+ }
+
/**
* What data may be included in the form submission when a student submits
* this question in its current state?
@@ -280,7 +311,7 @@ class qtype_ordering_question extends question_graded_automatically {
$classifiedresponse[$subqid] = new question_classified_response(
$currentposition + 1,
- get_string('positionx', 'qtype_ordering', $currentposition + 1),
+ get_string('positionx', 'qtype_ordering', $currentposition + 1),
($currentposition == $position) * $fraction
);
}
@@ -355,8 +386,7 @@ class qtype_ordering_question extends question_graded_automatically {
$countcorrect = 0;
$countanswers = 0;
- $options = $this->get_ordering_options();
- $gradingtype = $options->gradingtype;
+ $gradingtype = $this->options->gradingtype;
switch ($gradingtype) {
case self::GRADING_ALL_OR_NOTHING:
@@ -469,10 +499,8 @@ class qtype_ordering_question extends question_graded_automatically {
return parent::check_file_access($qa, $options, $component, $filearea, $args, $forcedownload);
}
- ///////////////////////////////////////////////////////
- // methods from "question_graded_automatically" class
- // see "question/type/questionbase.php"
- ///////////////////////////////////////////////////////
+ // Methods from "question_graded_automatically" class.
+ // See "question/type/questionbase.php".
/**
* Check a request for access to a file belonging to a combined feedback field.
@@ -502,9 +530,7 @@ class qtype_ordering_question extends question_graded_automatically {
}
}
- ///////////////////////////////////////////////////////
- // Custom methods
- ///////////////////////////////////////////////////////
+ // Custom methods.
/**
* Returns response mform field name
@@ -537,70 +563,13 @@ class qtype_ordering_question extends question_graded_automatically {
}
}
- /**
- * Loads from DB and returns options for question instance
- *
- * @return object
- */
- public function get_ordering_options() {
- global $DB;
- if ($this->options === null) {
- $this->options = $DB->get_record('qtype_ordering_options', array('questionid' => $this->id));
- if (empty($this->options)) {
- $this->options = (object)array(
- 'questionid' => $this->id,
- 'layouttype' => self::LAYOUT_VERTICAL,
- 'selecttype' => self::SELECT_ALL,
- 'selectcount' => 0,
- 'gradingtype' => self::GRADING_ABSOLUTE_POSITION,
- 'showgrading' => 1,
- 'numberingstyle' => self::NUMBERING_STYLE_DEFAULT,
- 'correctfeedback' => '',
- 'correctfeedbackformat' => FORMAT_MOODLE,
- 'incorrectfeedback' => '',
- 'incorrectfeedbackformat' => FORMAT_MOODLE,
- 'partiallycorrectfeedback' => '',
- 'partiallycorrectfeedbackformat' => FORMAT_MOODLE
- );
- $this->options->id = $DB->insert_record('qtype_ordering_options', $this->options);
- }
- }
- return $this->options;
- }
-
- /**
- * Loads from DB and returns array of answers objects
- *
- * @return array of objects
- */
- public function get_ordering_answers() {
- global $CFG, $DB;
- if ($this->answers === null) {
- $this->answers = $DB->get_records('question_answers', array('question' => $this->id), 'fraction,id');
- if ($this->answers) {
- if (isset($CFG->passwordsaltmain)) {
- $salt = $CFG->passwordsaltmain;
- } else {
- $salt = '';
- }
- foreach ($this->answers as $answerid => $answer) {
- $this->answers[$answerid]->md5key = 'ordering_item_'.md5($salt.$answer->answer);
- }
- } else {
- $this->answers = array();
- }
- }
- return $this->answers;
- }
-
/**
* Returns layoutclass
*
* @return string
*/
public function get_ordering_layoutclass() {
- $options = $this->get_ordering_options();
- switch ($options->layouttype) {
+ switch ($this->options->layouttype) {
case self::LAYOUT_VERTICAL:
return 'vertical';
case self::LAYOUT_HORIZONTAL:
@@ -781,7 +750,7 @@ class qtype_ordering_question extends question_graded_automatically {
* @param int $type
* @return array|string array if $type is not specified and single string if $type is specified
*/
- static public function get_types($types, $type) {
+ public static function get_types($types, $type) {
if ($type === null) {
return $types; // Return all $types.
}
@@ -797,7 +766,7 @@ class qtype_ordering_question extends question_graded_automatically {
* @param int $type
* @return array|string array if $type is not specified and single string if $type is specified
*/
- static public function get_select_types($type=null) {
+ public static function get_select_types($type=null) {
$plugin = 'qtype_ordering';
$types = array(
self::SELECT_ALL => get_string('selectall', $plugin),
@@ -813,7 +782,7 @@ class qtype_ordering_question extends question_graded_automatically {
* @param int $type
* @return array|string array if $type is not specified and single string if $type is specified
*/
- static public function get_layout_types($type=null) {
+ public static function get_layout_types($type=null) {
$plugin = 'qtype_ordering';
$types = array(
self::LAYOUT_VERTICAL => get_string('vertical', $plugin),
@@ -828,7 +797,7 @@ class qtype_ordering_question extends question_graded_automatically {
* @param int $type
* @return array|string array if $type is not specified and single string if $type is specified
*/
- static public function get_grading_types($type=null) {
+ public static function get_grading_types($type=null) {
$plugin = 'qtype_ordering';
$types = array(
self::GRADING_ALL_OR_NOTHING => get_string('allornothing', $plugin),
@@ -861,4 +830,166 @@ class qtype_ordering_question extends question_graded_automatically {
'IIII' => get_string('numberingstyleIIII', $plugin));
return self::get_types($styles, $style);
}
+
+ /**
+ * Return the number of subparts of this response that are correct|partial|incorrect.
+ *
+ * @param array $response A response.
+ * @return array Array of three elements: the number of correct subparts,
+ * the number of partial correct subparts and the number of incorrect subparts.
+ */
+ public function get_num_parts_right(array $response) {
+ $this->update_current_response($response);
+ $gradingtype = $this->options->gradingtype;
+
+ $numright = 0;
+ $numpartial = 0;
+ $numincorrect = 0;
+ list($correctresponse, $currentresponse) = $this->get_response_depend_on_grading_type($gradingtype);
+
+ foreach ($this->currentresponse as $position => $answerid) {
+ $fraction = $this->get_fraction_of_item($position, $answerid, $correctresponse, $currentresponse);
+ if (is_null($fraction)) {
+ continue;
+ }
+
+ if ($fraction > 0.999999) {
+ $numright++;
+ } else if ($fraction < 0.000001) {
+ $numincorrect++;
+ } else {
+ $numpartial++;
+ }
+ }
+
+ return [$numright, $numpartial, $numincorrect];
+ }
+
+ /**
+ * Returns the grade for one item, base on the fraction scale.
+ *
+ * @param int $position The position of the current response.
+ * @param int $answerid The answerid of the current response.
+ * @param array $correctresponse The correct response list base on grading type.
+ * @param array $currentresponse The current response list base on grading type.
+ * @return float|null Float if the grade, base on the fraction scale and null if the item is not in the correct response.
+ */
+ protected function get_fraction_of_item(int $position, int $answerid, array $correctresponse, array $currentresponse) {
+ $gradingtype = $this->options->gradingtype;
+
+ $score = 0;
+ $maxscore = null;
+
+ switch ($gradingtype) {
+ case self::GRADING_ALL_OR_NOTHING:
+ case self::GRADING_ABSOLUTE_POSITION:
+ if (isset($correctresponse[$position])) {
+ if ($correctresponse[$position] == $answerid) {
+ $score = 1;
+ }
+ $maxscore = 1;
+ }
+ break;
+ case self::GRADING_RELATIVE_NEXT_EXCLUDE_LAST:
+ case self::GRADING_RELATIVE_NEXT_INCLUDE_LAST:
+ if (isset($correctresponse[$answerid])) {
+ if (isset($currentresponse[$answerid]) && $currentresponse[$answerid] == $correctresponse[$answerid]) {
+ $score = 1;
+ }
+ $maxscore = 1;
+ }
+ break;
+
+ case self::GRADING_RELATIVE_ONE_PREVIOUS_AND_NEXT:
+ case self::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT:
+ if (isset($correctresponse[$answerid])) {
+ $maxscore = 0;
+ $prev = $correctresponse[$answerid]->prev;
+ $maxscore += count($prev);
+ $prev = array_intersect($prev, $currentresponse[$answerid]->prev);
+ $score += count($prev);
+ $next = $correctresponse[$answerid]->next;
+ $maxscore += count($next);
+ $next = array_intersect($next, $currentresponse[$answerid]->next);
+ $score += count($next);
+ }
+ break;
+
+ case self::GRADING_LONGEST_ORDERED_SUBSET:
+ case self::GRADING_LONGEST_CONTIGUOUS_SUBSET:
+ if (isset($correctresponse[$position])) {
+ if (isset($currentresponse[$position])) {
+ $score = $currentresponse[$position];
+ }
+ $maxscore = 1;
+ }
+ break;
+
+ case self::GRADING_RELATIVE_TO_CORRECT:
+ if (isset($correctresponse[$position])) {
+ $maxscore = (count($correctresponse) - 1);
+ $answerid = $currentresponse[$position];
+ $correctposition = array_search($answerid, $correctresponse);
+ $score = ($maxscore - abs($correctposition - $position));
+ if ($score < 0) {
+ $score = 0;
+ }
+ }
+ break;
+ }
+ $fraction = $maxscore ? $score / $maxscore : $maxscore;
+
+ return $fraction;
+ }
+
+ /**
+ * Get correcresponse and currentinfo depending on grading type.
+ *
+ * @param string $gradingtype The kind of grading.
+ * @return array Correctresponse and currentresponsescore in one array.
+ */
+ protected function get_response_depend_on_grading_type(string $gradingtype): array {
+
+ $correctresponse = [];
+ $currentresponse = [];
+ switch ($gradingtype) {
+ case self::GRADING_ALL_OR_NOTHING:
+ case self::GRADING_ABSOLUTE_POSITION:
+ case self::GRADING_RELATIVE_TO_CORRECT:
+ $correctresponse = $this->correctresponse;
+ $currentresponse = $this->currentresponse;
+ break;
+
+ case self::GRADING_RELATIVE_NEXT_EXCLUDE_LAST:
+ case self::GRADING_RELATIVE_NEXT_INCLUDE_LAST:
+ $lastitem = ($gradingtype == self::GRADING_RELATIVE_NEXT_INCLUDE_LAST);
+ $correctresponse = $this->get_next_answerids($this->correctresponse, $lastitem);
+ $currentresponse = $this->get_next_answerids($this->currentresponse, $lastitem);
+ break;
+
+ case self::GRADING_RELATIVE_ONE_PREVIOUS_AND_NEXT:
+ case self::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT:
+ $all = ($gradingtype == self::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT);
+ $correctresponse = $this->get_previous_and_next_answerids($this->correctresponse, $all);
+ $currentresponse = $this->get_previous_and_next_answerids($this->currentresponse, $all);
+ break;
+
+ case self::GRADING_LONGEST_ORDERED_SUBSET:
+ case self::GRADING_LONGEST_CONTIGUOUS_SUBSET:
+ $correctresponse = $this->correctresponse;
+ $currentresponse = $this->currentresponse;
+ $contiguous = ($gradingtype == self::GRADING_LONGEST_CONTIGUOUS_SUBSET);
+ $subset = $this->get_ordered_subset($contiguous);
+ foreach ($currentresponse as $position => $answerid) {
+ if (array_search($position, $subset) === false) {
+ $currentresponse[$position] = 0;
+ } else {
+ $currentresponse[$position] = 1;
+ }
+ }
+ break;
+ }
+
+ return [$correctresponse, $currentresponse];
+ }
}
diff --git a/question/type/ordering/questiontype.php b/question/type/ordering/questiontype.php
index 3591b9586cc..04fd2f107fb 100644
--- a/question/type/ordering/questiontype.php
+++ b/question/type/ordering/questiontype.php
@@ -22,7 +22,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-defined('MOODLE_INTERNAL') || die();
+use qtype_ordering\question_hint_ordering;
/**
* The ordering question type.
@@ -32,11 +32,14 @@ defined('MOODLE_INTERNAL') || die();
*/
class qtype_ordering extends question_type {
+ /** @var int Number of hints default. */
+ const DEFAULT_NUM_HINTS = 2;
+
/** @var array Combined feedback fields */
public $feedbackfields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback');
/**
- * @return whether the question_answers.answer field needs to have
+ * @return bool whether the question_answers.answer field needs to have
* restore_decode_content_links_worker called on it.
*/
public function has_html_answers() {
@@ -63,8 +66,20 @@ class qtype_ordering extends question_type {
* @param object $questiondata the question data loaded from the database.
*/
protected function initialise_question_instance(question_definition $question, $questiondata) {
+ global $CFG;
+
parent::initialise_question_instance($question, $questiondata);
- $this->initialise_combined_feedback($question, $questiondata);
+
+ $question->answers = $questiondata->options->answers;
+ foreach ($question->answers as $answerid => $answer) {
+ $question->answers[$answerid]->md5key =
+ 'ordering_item_' . md5(($CFG->passwordsaltmain ?? '') . $answer->answer);
+ }
+
+ $question->options = clone($questiondata->options);
+ unset($question->options->answers);
+
+ $this->initialise_combined_feedback($question, $questiondata, true);
}
/**
@@ -191,7 +206,7 @@ class qtype_ordering extends question_type {
'numberingstyle' => $question->numberingstyle
);
$options = $this->save_combined_feedback_helper($options, $question, $context, true);
- $this->save_hints($question, false);
+ $this->save_hints($question, true);
// Add/update $options for this ordering question.
if ($options->id = $DB->get_field('qtype_ordering_options', 'id', array('questionid' => $question->id))) {
@@ -219,6 +234,106 @@ class qtype_ordering extends question_type {
return true;
}
+ /**
+ * Count number of hints on the form.
+ *
+ * @param object $formdata The data from the form.
+ * @param bool $withparts Whether to take into account clearwrong and shownumcorrect options.
+ * @return int Count of hints on the form.
+ */
+ protected function count_hints_on_form($formdata, $withparts) {
+ if (!empty($formdata->hint)) {
+ $numhints = max(array_keys($formdata->hint)) + 1;
+ } else {
+ $numhints = 0;
+ }
+
+ if ($withparts) {
+ if (!empty($formdata->hintclearwrong)) {
+ $numclears = max(array_keys($formdata->hintclearwrong)) + 1;
+ } else {
+ $numclears = 0;
+ }
+ if (!empty($formdata->hintshownumcorrect)) {
+ $numshows = max(array_keys($formdata->hintshownumcorrect)) + 1;
+ } else {
+ $numshows = 0;
+ }
+ if (!empty($formdata->hintoptions)) {
+ $numhighlights = max(array_keys($formdata->hintoptions)) + 1;
+ } else {
+ $numhighlights = 0;
+ }
+
+ $numhints = max($numhints, $numclears, $numshows, $numhighlights);
+ }
+ return $numhints;
+ }
+
+ /**
+ * Save hints from the form. Overwrite save_hints function to custom hint controls.
+ *
+ * @param object $formdata The data from the form.
+ * @param bool $withparts Whether to take into account clearwrong, shownumcorrect, and highlightresponse options.
+ */
+ public function save_hints($formdata, $withparts = false) {
+ global $DB;
+ $context = $formdata->context;
+
+ $oldhints = $DB->get_records('question_hints',
+ array('questionid' => $formdata->id), 'id ASC');
+
+ $numhints = $this->count_hints_on_form($formdata, $withparts);
+
+ for ($i = 0; $i < $numhints; $i += 1) {
+ if (html_is_blank($formdata->hint[$i]['text'])) {
+ $formdata->hint[$i]['text'] = '';
+ }
+
+ if ($withparts) {
+ $clearwrong = !empty($formdata->hintclearwrong[$i]);
+ $shownumcorrect = !empty($formdata->hintshownumcorrect[$i]);
+ $highlightresponse = !empty($formdata->hintoptions[$i]);
+ }
+
+ // Update an existing hint if possible.
+ $hint = array_shift($oldhints);
+ if (!$hint) {
+ $hint = new stdClass();
+ $hint->questionid = $formdata->id;
+ $hint->hint = '';
+ $hint->id = $DB->insert_record('question_hints', $hint);
+ }
+
+ $hint->hint = $this->import_or_save_files($formdata->hint[$i],
+ $context, 'question', 'hint', $hint->id);
+ $hint->hintformat = $formdata->hint[$i]['format'];
+ if ($withparts) {
+ $hint->clearwrong = $clearwrong;
+ $hint->shownumcorrect = $shownumcorrect;
+ $hint->options = $highlightresponse;
+ }
+ $DB->update_record('question_hints', $hint);
+ }
+
+ // Delete any remaining old hints.
+ $fs = get_file_storage();
+ foreach ($oldhints as $oldhint) {
+ $fs->delete_area_files($context->id, 'question', 'hint', $oldhint->id);
+ $DB->delete_records('question_hints', array('id' => $oldhint->id));
+ }
+ }
+
+ /**
+ * Create a question_hint, or an appropriate subclass for this question, from a row loaded from the database.
+ *
+ * @param object $hint The DB row from the question hints table.
+ * @return question_hint_ordering Hints of question from record.
+ */
+ protected function make_hint($hint) {
+ return question_hint_ordering::load_from_record($hint);
+ }
+
/**
* This method should return all the possible types of response that are
* recognised for this question.
@@ -262,12 +377,12 @@ class qtype_ordering extends question_type {
$subqid = question_utils::to_plain_text($answer->answer, $answer->answerformat);
- // make sure $subqid is no more than 100 bytes
+ // Make sure $subqid is no more than 100 bytes.
$maxbytes = 100;
if (strlen($subqid) > $maxbytes) {
$subqid = substr($subqid, 0, $maxbytes);
if (preg_match('/^(.|\n)*/u', '', $subqid, $match)) {
- $subqid = $match[0]; // incomplete UTF-8 chars will be removed
+ $subqid = $match[0]; // Incomplete UTF-8 chars will be removed.
}
}
@@ -312,13 +427,39 @@ class qtype_ordering extends question_type {
return false;
}
- // Load the answers - "fraction" is used to signify the order of the answers.
+ // Load the answers - "fraction" is used to signify the order of the answers,
+ // with id as a tie-break which should not be required.
if (!$question->options->answers = $DB->get_records('question_answers',
- array('question' => $question->id), 'fraction ASC')) {
+ array('question' => $question->id), 'fraction, id')) {
echo $OUTPUT->notification('Error: Missing question answers for ordering question ' . $question->id . '!');
return false;
}
+ // Initialize the shownumcorrect and highlight options with the old question when restoring.
+ $hints = $DB->get_records('question_hints', ['questionid' => $question->id], 'id ASC');
+ $counthints = count($hints);
+ for ($i = 0; $i < max(self::DEFAULT_NUM_HINTS, $counthints); $i++) {
+ $hint = array_shift($hints);
+ if (!$hint) {
+ $hint = new stdClass();
+ $hint->questionid = $question->id;
+ $hint->hint = '';
+ $hint->hintformat = 1;
+ $hint->clearwrong = 0;
+ $hint->options = 1;
+ $hint->shownumcorrect = 1;
+ $hint->id = $DB->insert_record('question_hints', $hint);
+ }
+
+ if (isset($hint->shownumcorrect) || isset($hint->options)) {
+ continue;
+ }
+
+ $hint->options = 1;
+ $hint->shownumcorrect = 1;
+ $DB->update_record('question_hints', $hint);
+ }
+
parent::get_question_options($question);
return true;
}
@@ -427,10 +568,18 @@ class qtype_ordering extends question_type {
if (is_numeric($pos)) {
$format = substr($text, 0, $pos);
switch ($format) {
- case 'html': $format = FORMAT_HTML; break;
- case 'plain': $format = FORMAT_PLAIN; break;
- case 'markdown': $format = FORMAT_MARKDOWN; break;
- case 'moodle': $format = FORMAT_MOODLE; break;
+ case 'html':
+ $format = FORMAT_HTML;
+ break;
+ case 'plain':
+ $format = FORMAT_PLAIN;
+ break;
+ case 'markdown':
+ $format = FORMAT_MARKDOWN;
+ break;
+ case 'moodle':
+ $format = FORMAT_MOODLE;
+ break;
}
$text = trim(substr($text, $pos + 1)); // Remove name from text.
}
@@ -452,7 +601,8 @@ class qtype_ordering extends question_type {
} else {
$selectcount = min(6, count($answers));
}
- $this->set_options_for_import($question, $layouttype, $selecttype, $selectcount, $gradingtype, $showgrading, $numberingstyle);
+ $this->set_options_for_import($question, $layouttype, $selecttype, $selectcount,
+ $gradingtype, $showgrading, $numberingstyle);
// Remove blank items.
$answers = array_map('trim', $answers);
@@ -604,10 +754,18 @@ class qtype_ordering extends question_type {
}
switch ($question->questiontextformat) {
- case FORMAT_HTML: $output .= '[html]'; break;
- case FORMAT_PLAIN: $output .= '[plain]'; break;
- case FORMAT_MARKDOWN: $output .= '[markdown]'; break;
- case FORMAT_MOODLE: $output .= '[moodle]'; break;
+ case FORMAT_HTML:
+ $output .= '[html]';
+ break;
+ case FORMAT_PLAIN:
+ $output .= '[plain]';
+ break;
+ case FORMAT_MARKDOWN:
+ $output .= '[markdown]';
+ break;
+ case FORMAT_MOODLE:
+ $output .= '[moodle]';
+ break;
}
$output .= $question->questiontext.'{';
@@ -648,6 +806,12 @@ class qtype_ordering extends question_type {
$output .= " $numberingstyle\n";
$output .= $format->write_combined_feedback($question->options, $question->id, $question->contextid);
+ $shownumcorrect = $question->options->shownumcorrect;
+ if (!empty($question->options->shownumcorrect)) {
+ $output = str_replace(" \n", "", $output);
+ }
+ $output .= " $shownumcorrect\n";
+
foreach ($question->options->answers as $answer) {
$output .= ' format($answer->answerformat).">\n";
$output .= $format->writetext($answer->answer, 3);
@@ -707,7 +871,8 @@ class qtype_ordering extends question_type {
$gradingtype = $format->getpath($data, array('#', 'gradingtype', 0, '#'), 'RELATIVE');
$showgrading = $format->getpath($data, array('#', 'showgrading', 0, '#'), '1');
$numberingstyle = $format->getpath($data, array('#', 'numberingstyle', 0, '#'), '1');
- $this->set_options_for_import($newquestion, $layouttype, $selecttype, $selectcount, $gradingtype, $showgrading, $numberingstyle);
+ $this->set_options_for_import($newquestion, $layouttype, $selecttype, $selectcount,
+ $gradingtype, $showgrading, $numberingstyle);
$newquestion->answer = array();
$newquestion->answerformat = array();
@@ -725,10 +890,28 @@ class qtype_ordering extends question_type {
}
$format->import_combined_feedback($newquestion, $data, false);
+ $newquestion->shownumcorrect = $format->getpath($data, ['#', 'shownumcorrect', 0, '#'], null);
// Check that the required feedback fields exist.
$this->check_ordering_combined_feedback($newquestion);
- $format->import_hints($newquestion, $data, false);
+ $format->import_hints($newquestion, $data, true, true);
+
+ if (!isset($newquestion->shownumcorrect)) {
+ $newquestion->shownumcorrect = 1;
+ $counthintshownumcorrect = self::DEFAULT_NUM_HINTS;
+ $counthintoptions = self::DEFAULT_NUM_HINTS;
+
+ if (isset($newquestion->hintshownumcorrect)) {
+ $counthintshownumcorrect = max(self::DEFAULT_NUM_HINTS, count($newquestion->hintshownumcorrect));
+ }
+
+ if (isset($newquestion->hintoptions)) {
+ $counthintoptions = max(self::DEFAULT_NUM_HINTS, count($newquestion->hintoptions));
+ }
+
+ $newquestion->hintshownumcorrect = array_fill(0, $counthintshownumcorrect, 1);
+ $newquestion->hintoptions = array_fill(0, $counthintoptions, 1);
+ }
return $newquestion;
}
@@ -769,10 +952,10 @@ class qtype_ordering extends question_type {
* @param string $grading the grading type
* @param string $show the grading details or not
*/
- public function set_options_for_import(&$question, $layouttype, $selecttype, $selectcount,
+ public function set_options_for_import(&$question, $layouttype, $selecttype, $selectcount,
$gradingtype, $showgrading, $numberingstyle) {
- // set "layouttype" option
+ // Set "layouttype" option.
switch (strtoupper($layouttype)) {
case 'HORIZONTAL':
@@ -819,7 +1002,7 @@ class qtype_ordering extends question_type {
if (is_numeric($selectcount)) {
$question->selectcount = intval($selectcount);
} else {
- $question->selectcount = 3; // default
+ $question->selectcount = 3; // Default!
}
// Set "gradingtype" option.
@@ -886,7 +1069,7 @@ class qtype_ordering extends question_type {
default:
$question->showgrading = 1;
- break;
+ break;
}
// Set "numberingstyle" option.
diff --git a/question/type/ordering/readme.txt b/question/type/ordering/readme.txt
index 8bf6e1e7f37..3c3bd68e42c 100644
--- a/question/type/ordering/readme.txt
+++ b/question/type/ordering/readme.txt
@@ -1,6 +1,6 @@
-============================================
-The Ordering question type for Moodle >= 2.9
-============================================
+=============================================
+The Ordering question type for Moodle >= 3.11
+=============================================
The ordering question type displays several items in a random order
which the user then drags into the correct sequential order.
diff --git a/question/type/ordering/renderer.php b/question/type/ordering/renderer.php
index 84bed87304f..ba2328bf6d1 100644
--- a/question/type/ordering/renderer.php
+++ b/question/type/ordering/renderer.php
@@ -23,7 +23,6 @@
*/
// Prevent direct access to this script.
-defined('MOODLE_INTERNAL') || die();
/**
* Generates the output for ordering questions
@@ -56,7 +55,7 @@ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer {
* @return string HTML fragment.
*/
public function formulation_and_controls(question_attempt $qa, question_display_options $options) {
- global $CFG, $DB, $PAGE;
+ global $CFG, $DB;
// Initialize the return result.
$result = '';
@@ -82,7 +81,7 @@ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer {
// Set CSS classes for sortable list and sortable items.
$sortablelist = 'sortablelist';
if ($class = $question->get_ordering_layoutclass()) {
- $sortablelist .= ' '.$class; // "vertical" or "horizontal"
+ $sortablelist .= ' '.$class; // Vertical or Horizontal.
}
if ($class = $question->options->numberingstyle) {
$sortablelist .= ' numbering'.$class;
@@ -93,14 +92,19 @@ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer {
$sortablelist .= ' notactive';
}
- // Initialise JavaScript if not in readonly mode
+ // In the multi-tries, the highlight response base on the hint highlight option.
+ if (isset($options->highlightresponse) && $options->highlightresponse) {
+ $sortablelist .= ' notactive';
+ }
+
+ // Initialise JavaScript if not in readonly mode.
if ($options->readonly) {
// Items cannot be dragged in readonly mode.
$sortableitem = '';
} else {
$sortableitem = 'sortableitem';
$params = array($sortableid, $responseid);
- $PAGE->requires->js_call_amd('qtype_ordering/reorder', 'init', $params);
+ $this->page->requires->js_call_amd('qtype_ordering/reorder', 'init', $params);
}
$result .= html_writer::tag('div', $question->format_questiontext($qa), array('class' => 'qtext'));
@@ -130,7 +134,7 @@ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer {
}
// Set the CSS class and correctness img for this response.
- // (correctness: HIDDEN=0, VISIBLE=1, EDITABLE=2)
+ // (correctness: HIDDEN=0, VISIBLE=1, EDITABLE=2).
switch ($options->correctness) {
case question_display_options::VISIBLE:
$score = $this->get_ordering_item_score($question, $position, $answerid);
@@ -148,6 +152,12 @@ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer {
break;
}
+ if (isset($options->highlightresponse) && $options->highlightresponse) {
+ $score = $this->get_ordering_item_score($question, $position, $answerid);
+ list($score, $maxscore, $fraction, $percent, $class, $img) = $score;
+ $class = trim("$sortableitem $class");
+ }
+
// Format the answer text.
$answer = $question->answers[$answerid];
$answertext = $question->format_text($answer->answer, $answer->answerformat,
@@ -179,18 +189,63 @@ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer {
}
/**
- * Generate the specific feedback. This is feedback that varies according to
- * the response the student gave.
+ * Generate the display of the outcome part of the question. This is the
+ * area that contains the various forms of feedback. This function generates
+ * the content of this area belonging to the question type.
*
- * @param question_attempt $qa the question attempt to display.
+ * @param question_attempt $qa The question attempt to display.
+ * @param question_display_options $options Controls what should and should not be displayed.
* @return string HTML fragment.
*/
- public function specific_feedback(question_attempt $qa) {
+ public function feedback(question_attempt $qa, question_display_options $options) {
+ $output = '';
+ $hint = null;
- if ($feedback = $this->combined_feedback($qa)) {
- $feedback = html_writer::tag('p', $feedback);
+ $isshownumpartscorrect = true;
+
+ if ($options->feedback) {
+ $output .= html_writer::nonempty_tag('div', $this->specific_feedback($qa),
+ array('class' => 'specificfeedback'));
+
+ if ($options->numpartscorrect) {
+ $output .= html_writer::nonempty_tag('div', $this->num_parts_correct($qa),
+ array('class' => 'numpartscorrect'));
+ $isshownumpartscorrect = false;
+ }
+
+ $output .= $this->specific_grade_detail_feedback($qa);
+ $hint = $qa->get_applicable_hint();
}
+ if ($options->numpartscorrect && $isshownumpartscorrect) {
+ $output .= html_writer::nonempty_tag('div', $this->num_parts_correct($qa),
+ array('class' => 'numpartscorrect'));
+ }
+
+ if ($hint) {
+ $output .= $this->hint($qa, $hint);
+ }
+
+ if ($options->generalfeedback) {
+ $output .= html_writer::nonempty_tag('div', $this->general_feedback($qa),
+ array('class' => 'generalfeedback'));
+ }
+
+ if ($options->rightanswer) {
+ $output .= html_writer::nonempty_tag('div', $this->correct_response($qa),
+ array('class' => 'rightanswer'));
+ }
+
+ return $output;
+ }
+
+ /**
+ * Display the grade detail of the response.
+ *
+ * @param question_attempt $qa The question attempt to display.
+ * @return string Output grade detail of the response.
+ */
+ public function specific_grade_detail_feedback(question_attempt $qa): string {
$gradingtype = '';
$gradedetails = '';
$scoredetails = '';
@@ -270,7 +325,18 @@ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer {
}
}
- return $feedback.$gradingtype.$gradedetails.$scoredetails;
+ return $gradingtype.$gradedetails.$scoredetails;
+ }
+
+ /**
+ * Generate the specific feedback. This is feedback that varies according to
+ * the response the student gave.
+ *
+ * @param question_attempt $qa The question attempt to display.
+ * @return string HTML fragment.
+ */
+ public function specific_feedback(question_attempt $qa) {
+ return $this->combined_feedback($qa);
}
/**
@@ -308,7 +374,7 @@ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer {
if ($showcorrect) {
$sortableitem = $question->get_ordering_layoutclass();
$output .= html_writer::tag('p', get_string('correctorder', 'qtype_ordering'));
- $output .= html_writer::start_tag('ol', array('class' => 'correctorder'));
+ $output .= html_writer::start_tag('ol', array('class' => 'correctorder ' . $sortableitem));
$correctresponse = $question->correctresponse;
foreach ($correctresponse as $position => $answerid) {
$answer = $question->answers[$answerid];
@@ -517,4 +583,36 @@ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer {
}
return $this->allcorrect;
}
+
+ /**
+ * Gereate a brief statement of how many sub-parts of this question the
+ * student got correct|partial|incorrect.
+ *
+ * @param question_attempt $qa The question attempt to display.
+ * @return string HTML fragment.
+ */
+ protected function num_parts_correct(question_attempt $qa) {
+ $a = new stdClass();
+ $output = '';
+
+ list($a->numright, $a->numpartial, $a->numincorrect) = $qa->get_question()->get_num_parts_right(
+ $qa->get_last_qt_data());
+
+ if ($a->numright) {
+ $a->numrightplural = get_string($a->numright > 1 ? 'itemplural' : 'itemsingular', 'qtype_ordering');
+ $output .= html_writer::nonempty_tag('div', get_string('yougotnright', 'qtype_ordering', $a));
+ }
+
+ if ($a->numpartial) {
+ $a->numpartialplural = get_string($a->numpartial > 1 ? 'itemplural' : 'itemsingular', 'qtype_ordering');
+ $output .= html_writer::nonempty_tag('div', get_string('yougotnpartial', 'qtype_ordering', $a));
+ }
+
+ if ($a->numincorrect) {
+ $a->numincorrectplural = get_string($a->numincorrect > 1 ? 'itemplural' : 'itemsingular', 'qtype_ordering');
+ $output .= html_writer::nonempty_tag('div', get_string('yougotnincorrect', 'qtype_ordering', $a));
+ }
+
+ return $output;
+ }
}
diff --git a/question/type/ordering/styles.css b/question/type/ordering/styles.css
index 342d3720263..7c07fd730d7 100644
--- a/question/type/ordering/styles.css
+++ b/question/type/ordering/styles.css
@@ -6,28 +6,28 @@
}
.que.ordering .sortablelist {
- float : left;
- list-style-type : none;
- margin : 0 0 0 8px;
+ float: left;
+ list-style-type: none;
+ margin: 0 0 0 8px;
}
.que.ordering .sortablelist.active {
- border : 1px dotted #333;
- border-radius : 4px;
+ border: 1px dotted #333;
+ border-radius: 4px;
}
.que.ordering .sortablelist li {
- background-color : #ffffff;
- border : 1px solid #000;
- border-radius : 4px;
- list-style-type : none;
- margin : 4px;
- padding : 6px 12px;
+ background-color: #fff;
+ border: 1px solid #000;
+ border-radius: 4px;
+ list-style-type: none;
+ margin: 4px;
+ padding: 6px 12px;
}
.que.ordering .sortablelist li.sortableitem {
- position : relative;
- cursor : move;
- margin-left : 26px; /* The margin is needed for the list-style-type in numberingxxx classes */
+ position: relative;
+ cursor: move;
+ margin-left: 26px; /* The margin is needed for the list-style-type in numberingxxx classes */
}
.que.ordering .sortablelist li.sortableitem:focus {
border-color: #0a0;
@@ -43,30 +43,39 @@
}
.que.ordering .sortablelist.numberingnone li {
- list-style-type : none;
- margin-left: 0px;
+ list-style-type: none;
+ margin-left: 0;
}
.que.ordering .sortablelist.numbering123 li {
- list-style-type : decimal;
+ list-style-type: decimal;
}
.que.ordering .sortablelist.numberingabc li {
- list-style-type : lower-alpha;
+ list-style-type: lower-alpha;
}
.que.ordering .sortablelist.numberingABCD li {
- list-style-type : upper-alpha;
+ list-style-type: upper-alpha;
}
.que.ordering .sortablelist.numberingiii li {
- list-style-type : lower-roman;
+ list-style-type: lower-roman;
}
.que.ordering .sortablelist.numberingIIII li {
- list-style-type : upper-roman;
+ list-style-type: upper-roman;
}
-.que.ordering .sortablelist.horizontal li {
- float : left;
+.que.ordering .sortablelist.horizontal {
+ display: flex;
+ flex-wrap: wrap;
}
+
+/* Better define 'row' of item for horizontal list. */
+.que.ordering .sortablelist.horizontal {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+}
+
.que.ordering .sortablelist.vertical li {
- min-height : 18px;
+ min-height: 18px;
}
/* Styles for when things are being dragged. */
@@ -98,24 +107,24 @@
/* Styles for feedback. */
.que.ordering .sortablelist.notactive li.correct {
- background-color : #dff4d8; /* light green */
- border-color : #99ff66; /* gentle green */
+ background-color: #dff4d8; /* light green */
+ border-color: #9f6; /* gentle green */
}
.que.ordering .sortablelist.notactive li.partial66 {
- background-color : #dff4d8; /* light green */
- border-color : #ff9900; /* dark orange */
+ background-color: #dff4d8; /* light green */
+ border-color: #f90; /* dark orange */
}
.que.ordering .sortablelist.notactive li.partial33 {
- background-color : #ffebcc; /* light orange */
- border-color : #ff9900; /* dark orange */
+ background-color: #ffebcc; /* light orange */
+ border-color: #f90; /* dark orange */
}
.que.ordering .sortablelist.notactive li.partial00 {
- background-color : #ffdddd; /* light red */
- border-color : #ff9900; /* dark orange */
+ background-color: #fdd; /* light red */
+ border-color: #f90; /* dark orange */
}
.que.ordering .sortablelist.notactive li.incorrect {
- background-color : #ffdddd; /* light red */
- border-color : #ff7373; /* gentle red */
+ background-color: #fdd; /* light red */
+ border-color: #ff7373; /* gentle red */
}
/*
Force containing DIV to cover the floating LI elements
@@ -125,15 +134,20 @@
.que.ordering div.rightanswer {
overflow: auto;
}
-.que.ordering div.rightanswer ol.correctorder li.horizontal {
- float : left;
- margin-left : 24px;
- margin-right : 24px;
+.que.ordering div.rightanswer ol.correctorder {
+ padding-inline-start: 16px;
}
-.que.ordering div.rightanswer ol.correctorder li.horizontal:first-child {
- margin-left : 0;
+.que.ordering div.rightanswer ol.correctorder.horizontal {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+}
+.que.ordering div.rightanswer ol.correctorder li.horizontal {
+ margin-left: 24px;
+ margin-right: 24px;
}
.que.ordering div.rightanswer ol.correctorder li.vertical {
+ margin-left: 24px;
}
/* the width restriction can be limited to editors for draggable items
diff --git a/question/type/ordering/tests/behat/add.feature b/question/type/ordering/tests/behat/add.feature
index 17322e03f05..9c7420067cb 100644
--- a/question/type/ordering/tests/behat/add.feature
+++ b/question/type/ordering/tests/behat/add.feature
@@ -14,9 +14,7 @@ Feature: Test creating an Ordering question
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
- And I log in as "teacher1"
- And I am on "Course 1" course homepage
- And I navigate to "Question bank" in current page administration
+ And I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1"
@javascript
Scenario: Create an Ordering question with 6 draggable items
@@ -35,4 +33,7 @@ Feature: Test creating an Ordering question
| For any incorrect response | Your answer is incorrect |
| Hint 1 | This is your first hint |
| Hint 2 | This is your second hint |
+ | hintoptions[0] | 1 |
+ | hintshownumcorrect[1] | 1 |
+ | shownumcorrect | 1 |
Then I should see "Ordering-001"
diff --git a/question/type/ordering/tests/behat/backup_and_restore.feature b/question/type/ordering/tests/behat/backup_and_restore.feature
index 3094b660ed4..59d21ff3190 100644
--- a/question/type/ordering/tests/behat/backup_and_restore.feature
+++ b/question/type/ordering/tests/behat/backup_and_restore.feature
@@ -15,20 +15,20 @@ Feature: Test duplicating a quiz containing a Ordering question
| questioncategory | qtype | name | template |
| Test questions | ordering | Moodle | moodle |
And the following "activities" exist:
- | activity | name | course | idnumber |
- | quiz | Test quiz | C1 | quiz1 |
+ | activity | name | course | idnumber |
+ | quiz | Test quiz | C1 | quiz1 |
And quiz "Test quiz" contains the following questions:
| Moodle | 1 |
- And I log in as "admin"
- And I am on "Course 1" course homepage
+ And I am on the "Course 1" "Course" page logged in as "admin"
@javascript
Scenario: Backup and restore a course containing an Ordering question
When I backup "Course 1" course using this options:
| Confirmation | Filename | test_backup.mbz |
And I restore "test_backup.mbz" backup into a new course using this options:
- | Schema | Course name | Course 2 |
- And I navigate to "Question bank" in current page administration
+ | Schema | Course name | Course 2 |
+ | Schema | Course short name | C2 |
+ And I am on the "Course 2" "core_question > course question bank" page
And I choose "Edit question" action for "Moodle" in the question bank
Then the following fields match these values:
| Question name | Moodle |
@@ -43,3 +43,8 @@ Feature: Test duplicating a quiz containing a Ordering question
| For any correct response | Well done! |
| For any partially correct response | Parts, but only parts, of your response are correct. |
| For any incorrect response | That is not right at all. |
+ | id_shownumcorrect | 1 |
+ | id_hintshownumcorrect_0 | 1 |
+ | id_hintoptions_0 | 1 |
+ | id_hintshownumcorrect_1 | 1 |
+ | id_hintoptions_1 | 1 |
diff --git a/question/type/ordering/tests/behat/behat_qtype_ordering.php b/question/type/ordering/tests/behat/behat_qtype_ordering.php
index 54ed18f0dcd..f8d280c1bd4 100644
--- a/question/type/ordering/tests/behat/behat_qtype_ordering.php
+++ b/question/type/ordering/tests/behat/behat_qtype_ordering.php
@@ -56,6 +56,12 @@ class behat_qtype_ordering extends behat_base {
/**
* Drag the drag item with the given text to the given space.
*
+ * Warning, only works if the question is using a behaviour like Immediate
+ * feedback that has a check button.
+ *
+ * Also, do not use this to drag an item to the last place. Just drag all
+ * the other non-last items to their place.
+ *
* @param string $label the text of the item to drag.
* @param int $position the number of the position to drop it at.
*
@@ -63,6 +69,11 @@ class behat_qtype_ordering extends behat_base {
*/
public function i_drag_to_space_in_the_drag_and_drop_into_text_question($label, $position) {
$generalcontext = behat_context_helper::get('behat_general');
+ // There was a weird issue where drag-drop was not reliable if an item was being
+ // dragged to the same place it already was. So, first drag below the bottom to reliably
+ // move it to the last place.
+ $generalcontext->i_drag_and_i_drop_it_in($this->item_xpath_by_lable($label),
+ 'xpath_element', get_string('check', 'question'), 'button');
$generalcontext->i_drag_and_i_drop_it_in($this->item_xpath_by_lable($label),
'xpath_element', $this->item_xpath_by_position($position), 'xpath_element');
}
diff --git a/question/type/ordering/tests/behat/edit.feature b/question/type/ordering/tests/behat/edit.feature
index c6d5308f726..b911ac07743 100644
--- a/question/type/ordering/tests/behat/edit.feature
+++ b/question/type/ordering/tests/behat/edit.feature
@@ -20,25 +20,34 @@ Feature: Test editing an Ordering question
And the following "questions" exist:
| questioncategory | qtype | name | template |
| Test questions | ordering | Ordering for editing | moodle |
- And I log in as "teacher1"
- And I am on "Course 1" course homepage
- And I navigate to "Question bank" in current page administration
- @javascript @_switch_window
+ @javascript
Scenario: Edit an Ordering question
- When I choose "Edit question" action for "Ordering for editing" in the question bank
+ When I am on the "Ordering for editing" "core_question > edit" page logged in as teacher1
And I set the following fields to these values:
- | Question name ||
+ | Question name | |
And I press "id_submitbutton"
Then I should see "You must supply a value here."
When I set the following fields to these values:
- | Question name | Edited Ordering |
+ | Question name | Edited Ordering |
+ | hintoptions[0] | 1 |
+ | hintoptions[1] | 0 |
+ | hintshownumcorrect[0] | 0 |
+ | hintshownumcorrect[1] | 1 |
+ | shownumcorrect | 1 |
And I press "id_submitbutton"
Then I should see "Edited Ordering"
+ And I choose "Edit question" action for "Edited Ordering" in the question bank
+ And the following fields match these values:
+ | id_shownumcorrect | 1 |
+ | id_hintshownumcorrect_0 | 0 |
+ | id_hintoptions_0 | 1 |
+ | id_hintshownumcorrect_1 | 1 |
+ | id_hintoptions_1 | 0 |
- @javascript @_switch_window
+ @javascript
Scenario: Editing an ordering question and making sure the form does not allow duplication of draggables
- When I choose "Edit question" action for "Ordering for editing" in the question bank
+ When I am on the "Ordering for editing" "core_question > edit" page logged in as teacher1
And I set the following fields to these values:
| Draggable item 4 | Object |
And I press "id_submitbutton"
diff --git a/question/type/ordering/tests/behat/export.feature b/question/type/ordering/tests/behat/export.feature
index 6b2a472f831..5b218a159af 100644
--- a/question/type/ordering/tests/behat/export.feature
+++ b/question/type/ordering/tests/behat/export.feature
@@ -20,15 +20,12 @@ Feature: Test exporting Ordering questions
And the following "questions" exist:
| questioncategory | qtype | name | template |
| Test questions | ordering | Moodle | moodle |
- And I log in as "teacher1"
- And I am on "Course 1" course homepage
- @javascript
Scenario: Export a Matching question
- When I navigate to "Question bank > Export" in current page administration
+ When I am on the "Course 1" "core_question > course question export" page logged in as teacher1
And I set the field "id_format_xml" to "1"
And I press "Export questions to file"
- Then following "click here" should download between "1700" and "1950" bytes
+ Then following "click here" should download between "1700" and "2350" bytes
# If the download step is the last in the scenario then we can sometimes run
# into the situation where the download page causes a http redirect but behat
# has already conducted its reset (generating an error). By putting a logout
diff --git a/question/type/ordering/tests/behat/import.feature b/question/type/ordering/tests/behat/import.feature
index 15fcb594bcf..fe7c5cba342 100644
--- a/question/type/ordering/tests/behat/import.feature
+++ b/question/type/ordering/tests/behat/import.feature
@@ -14,14 +14,35 @@ Feature: Test importing Ordering questions
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
- And I log in as "teacher1"
- And I am on "Course 1" course homepage
@javascript @_file_upload
Scenario: import Matching question.
- When I navigate to "Question bank > Import" in current page administration
+ When I am on the "Course 1" "core_question > course question import" page logged in as teacher1
And I set the field "id_format_xml" to "1"
And I upload "question/type/ordering/tests/fixtures/testquestion.moodle.xml" file to "Import" filemanager
And I press "id_submitbutton"
And I press "Continue"
Then I should see "dd-ordering 1"
+ And I choose "Edit question" action for "dd-ordering 1" in the question bank
+ Then the following fields match these values:
+ | shownumcorrect | 1 |
+ | id_hintshownumcorrect_0 | 1 |
+ | id_hintoptions_0 | 0 |
+ | id_hintshownumcorrect_1 | 0 |
+ | id_hintoptions_1 | 1 |
+
+ @javascript @_file_upload
+ Scenario: Import old question.
+ When I am on the "Course 1" "core_question > course question import" page logged in as teacher1
+ And I set the field "id_format_xml" to "1"
+ And I upload "question/type/ordering/tests/fixtures/testoldquestion.moodle.xml" file to "Import" filemanager
+ And I press "id_submitbutton"
+ And I press "Continue"
+ Then I should see "dd-ordering old question"
+ And I choose "Edit question" action for "dd-ordering old question" in the question bank
+ Then the following fields match these values:
+ | shownumcorrect | 1 |
+ | id_hintshownumcorrect_0 | 1 |
+ | id_hintoptions_0 | 1 |
+ | id_hintshownumcorrect_1 | 1 |
+ | id_hintoptions_1 | 1 |
diff --git a/question/type/ordering/tests/behat/preview.feature b/question/type/ordering/tests/behat/preview.feature
index cabbc799f28..10e29a6e9e9 100644
--- a/question/type/ordering/tests/behat/preview.feature
+++ b/question/type/ordering/tests/behat/preview.feature
@@ -20,29 +20,53 @@ Feature: Preview an Ordering question
And the following "questions" exist:
| questioncategory | qtype | name | template | layouttype |
| Test questions | ordering | ordering-001 | moodle | 0 |
- Given I log in as "teacher1"
- And I am on "Course 1" course homepage
- And I navigate to "Question bank" in current page administration
- @javascript @_switch_window
+ @javascript
Scenario: Preview an Ordering question and submit a correct response.
- When I choose "Preview" action for "ordering-001" in the question bank
- And I switch to "questionpreview" window
+ When I am on the "ordering-001" "core_question > preview" page logged in as teacher1
+ And I expand all fieldsets
And I set the field "How questions behave" to "Immediate feedback"
And I press "Start again with these options"
- # The test was unreliable unless if an item randomly started in the right place.
- # So we first moved each item to the last place, before putting it into the right place.
- And I drag "Modular" to space "6" in the ordering question
And I drag "Modular" to space "1" in the ordering question
- And I drag "Object" to space "6" in the ordering question
And I drag "Object" to space "2" in the ordering question
- And I drag "Oriented" to space "6" in the ordering question
And I drag "Oriented" to space "3" in the ordering question
- And I drag "Dynamic" to space "6" in the ordering question
And I drag "Dynamic" to space "4" in the ordering question
- And I drag "Learning" to space "6" in the ordering question
And I drag "Learning" to space "5" in the ordering question
And I press "Submit and finish"
Then the state of "Put these words in order." question is shown as "Correct"
And I should see "Mark 1.00 out of 1.00"
- And I switch to the main window
+
+ @javascript
+ Scenario: Preview an Ordering question with show number of correct option.
+ When I am on the "ordering-001" "core_question > preview" page logged in as teacher1
+ And I expand all fieldsets
+ And I set the field "How questions behave" to "Immediate feedback"
+ And I press "Start again with these options"
+ And I drag "Modular" to space "1" in the ordering question
+ And I drag "Oriented" to space "4" in the ordering question
+ And I drag "Dynamic" to space "3" in the ordering question
+ And I drag "Learning" to space "5" in the ordering question
+ And I drag "Environment" to space "2" in the ordering question
+ And I press "Submit and finish"
+ And I should see "You have 1 item correct."
+ And I should see "You have 5 items partially correct."
+
+ @javascript
+ Scenario: Preview an Ordering question with no show number of correct option.
+ When I am on the "ordering-001" "core_question > edit" page logged in as teacher1
+ And I set the following fields to these values:
+ | id_shownumcorrect | 0 |
+ | Question name | Renamed ordering-001 |
+ And I press "id_submitbutton"
+ And I am on the "Renamed ordering-001" "core_question > preview" page
+ And I expand all fieldsets
+ And I set the field "How questions behave" to "Immediate feedback"
+ And I press "Start again with these options"
+ And I drag "Modular" to space "1" in the ordering question
+ And I drag "Oriented" to space "4" in the ordering question
+ And I drag "Dynamic" to space "3" in the ordering question
+ And I drag "Learning" to space "5" in the ordering question
+ And I drag "Environment" to space "2" in the ordering question
+ And I press "Submit and finish"
+ And I should not see "You have 1 item correct."
+ And I should not see "You have 5 items partially correct."
diff --git a/question/type/ordering/tests/fixtures/testoldquestion.moodle.xml b/question/type/ordering/tests/fixtures/testoldquestion.moodle.xml
new file mode 100644
index 00000000000..7938e751cb9
--- /dev/null
+++ b/question/type/ordering/tests/fixtures/testoldquestion.moodle.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+ $course$/Test questions
+
+
+
+
+
+
+
+ dd-ordering old question
+
+
+ Put these words in order.
+
+
+ The correct answer is "Modular Object Oriented Dynamic Learning Environment".
]]>
+
+ 1.0000000
+ 0.3333333
+ 0
+ dd1
+ HORIZONTAL
+ ALL
+ 0
+ ABSOLUTE_POSITION
+ SHOW
+
+ Your answer is correct.
+
+
+ Your answer is partially correct.
+
+
+ Your answer is incorrect.
+
+
+ Modular
+
+
+ Object
+
+
+ Oriented
+
+
+ Dynamic
+
+
+ Learning
+
+
+ Environment
+
+
+
+ Hint 1 ]]>
+
+
+
+
+ Hint 2 ]]>
+
+
+
+
diff --git a/question/type/ordering/tests/fixtures/testquestion.moodle.xml b/question/type/ordering/tests/fixtures/testquestion.moodle.xml
index 718b96456a4..7c214923fb8 100644
--- a/question/type/ordering/tests/fixtures/testquestion.moodle.xml
+++ b/question/type/ordering/tests/fixtures/testquestion.moodle.xml
@@ -37,6 +37,7 @@
Your answer is incorrect.
+ 1
Modular
@@ -55,5 +56,13 @@
Environment
+
+
+
+
+
+
+ 1
+
diff --git a/question/type/ordering/tests/helper.php b/question/type/ordering/tests/helper.php
index 162f0342c4c..38851a31a57 100644
--- a/question/type/ordering/tests/helper.php
+++ b/question/type/ordering/tests/helper.php
@@ -70,6 +70,8 @@ class qtype_ordering_test_helper extends question_test_helper {
$q->options->gradingtype = qtype_ordering_question::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT;
$q->options->showgrading = true;
$q->options->numberingstyle = qtype_ordering_question::NUMBERING_STYLE_DEFAULT;
+ $q->options->shownumcorrect = 1;
+
return $q;
}
@@ -83,7 +85,7 @@ class qtype_ordering_test_helper extends question_test_helper {
* @param bool $addmd5 whether to add the md5key property.
* @return stdClass the answer.
*/
- protected function make_answer($id, $text, $textformat, $order, $addmd5 = false) {
+ public function make_answer($id, $text, $textformat, $order, $addmd5 = false) {
global $CFG;
$answer = new stdClass();
@@ -168,7 +170,6 @@ class qtype_ordering_test_helper extends question_test_helper {
$questiondata->options = new stdClass();
test_question_maker::set_standard_combined_feedback_fields($questiondata->options);
- unset($questiondata->options->shownumcorrect);
$questiondata->options->layouttype = qtype_ordering_question::LAYOUT_HORIZONTAL;
$questiondata->options->selecttype = qtype_ordering_question::SELECT_ALL;
$questiondata->options->selectcount = 0;
diff --git a/question/type/ordering/tests/question_test.php b/question/type/ordering/tests/question_test.php
index ecfe25991d1..b36e890a20c 100644
--- a/question/type/ordering/tests/question_test.php
+++ b/question/type/ordering/tests/question_test.php
@@ -23,6 +23,14 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+namespace qtype_ordering;
+
+use test_question_maker;
+use question_attempt_pending_step;
+use question_state;
+use qtype_ordering_question;
+use question_attempt_step;
+use question_classified_response;
defined('MOODLE_INTERNAL') || die();
@@ -34,8 +42,9 @@ require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
*
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @covers \qtype_ordering_question
*/
-class qtype_ordering_question_test extends advanced_testcase {
+class question_test extends \advanced_testcase {
/**
* Array of draggable items in correct order.
*/
@@ -66,6 +75,7 @@ class qtype_ordering_question_test extends advanced_testcase {
public function test_grading_all_or_nothing () {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
// Zero grade on any error (no partial scroe at all, it is either 1 or 0).
$question->options->gradingtype = qtype_ordering_question::GRADING_ALL_OR_NOTHING;
@@ -83,6 +93,7 @@ class qtype_ordering_question_test extends advanced_testcase {
}
public function test_grading_absolute_position () {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
// Counts items, placed into right absolute place.
$question->options->gradingtype = qtype_ordering_question::GRADING_ABSOLUTE_POSITION;
@@ -112,6 +123,7 @@ class qtype_ordering_question_test extends advanced_testcase {
}
public function test_grading_relative_next_exclude_last () {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
// Every sequential pair in right order is graded (last pair is excluded).
$question->options->gradingtype = qtype_ordering_question::GRADING_RELATIVE_NEXT_EXCLUDE_LAST;
@@ -137,6 +149,7 @@ class qtype_ordering_question_test extends advanced_testcase {
}
public function test_grading_relative_next_include_last () {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
// Every sequential pair in right order is graded (last pair is included).
$question->options->gradingtype = qtype_ordering_question::GRADING_RELATIVE_NEXT_INCLUDE_LAST;
@@ -157,6 +170,7 @@ class qtype_ordering_question_test extends advanced_testcase {
}
public function test_grading_relative_one_previous_and_next () {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
// Single answers that are placed before and after each answer is graded if in right order.
$question->options->gradingtype = qtype_ordering_question::GRADING_RELATIVE_ONE_PREVIOUS_AND_NEXT;
@@ -179,6 +193,7 @@ class qtype_ordering_question_test extends advanced_testcase {
}
public function test_grading_relative_all_previous_and_next () {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
// All answers that are placed before and after each answer is graded if in right order.
$question->options->gradingtype = qtype_ordering_question::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT;
@@ -205,6 +220,7 @@ class qtype_ordering_question_test extends advanced_testcase {
}
public function test_grading_longest_ordered_subset () {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
// Only longest ordered subset is graded.
$question->options->gradingtype = qtype_ordering_question::GRADING_LONGEST_ORDERED_SUBSET;
@@ -227,6 +243,7 @@ class qtype_ordering_question_test extends advanced_testcase {
}
public function test_grading_longest_contiguous_subset () {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
// Only longest ordered and contiguous subset is graded.
$question->options->gradingtype = qtype_ordering_question::GRADING_LONGEST_CONTIGUOUS_SUBSET;
@@ -249,6 +266,7 @@ class qtype_ordering_question_test extends advanced_testcase {
}
public function test_grading_relative_to_correct () {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
// Items are graded relative to their position in the correct answer.
$question->options->gradingtype = qtype_ordering_question::GRADING_RELATIVE_TO_CORRECT;
@@ -273,12 +291,14 @@ class qtype_ordering_question_test extends advanced_testcase {
public function test_get_expected_data() {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
$this->assertArrayHasKey('response_' . $question->id, $question->get_expected_data());
}
public function test_get_correct_response() {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
$question->start_attempt(new question_attempt_pending_step(), 1);
@@ -289,6 +309,7 @@ class qtype_ordering_question_test extends advanced_testcase {
}
public function test_is_same_response() {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
$question->start_attempt(new question_attempt_pending_step(), 1);
@@ -302,6 +323,7 @@ class qtype_ordering_question_test extends advanced_testcase {
public function test_summarise_response() {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
$question->start_attempt(new question_attempt_pending_step(), 1);
@@ -316,14 +338,17 @@ class qtype_ordering_question_test extends advanced_testcase {
$this->assertEquals($expected, $actual);
}
- public function test_get_ordering_answers() {
+ public function test_initialise_question_instance() {
// Create an Ordering question.
- $question = test_question_maker::make_question('ordering');
- $this->assertEquals($question->answers, $question->get_ordering_answers());
+ $questiondata = test_question_maker::get_question_data('ordering');
+ /** @var qtype_ordering_question $question */
+ $question = \question_bank::make_question($questiondata);
+ $this->assertStringContainsString('ordering_item_', reset($question->answers)->md5key);
}
public function test_get_ordering_layoutclass() {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
if ($question->options->layouttype === 0) {
@@ -335,6 +360,7 @@ class qtype_ordering_question_test extends advanced_testcase {
public function test_get_next_answerids() {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
$answerids = array_keys($question->answers);
@@ -363,6 +389,7 @@ class qtype_ordering_question_test extends advanced_testcase {
public function test_get_previous_and_next_answerids() {
// Create an Ordering question.
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
$answerids = array_keys($question->answers);
@@ -385,6 +412,7 @@ class qtype_ordering_question_test extends advanced_testcase {
}
public function test_classify_response_correct() {
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
$question->start_attempt(new question_attempt_step(), 1);
@@ -400,10 +428,11 @@ class qtype_ordering_question_test extends advanced_testcase {
'Environment' => new question_classified_response(6, 'Position 6', 0.1666667),
];
- $this->assertEqualsWithDelta($expected, $classifiedresponse, 0.0000005, '');
+ $this->assertEqualsWithDelta($expected, $classifiedresponse, 0.0000005);
}
public function test_classify_response_partially_correct() {
+ /** @var qtype_ordering_question $question */
$question = test_question_maker::make_question('ordering');
$question->start_attempt(new question_attempt_step(), 1);
@@ -419,6 +448,119 @@ class qtype_ordering_question_test extends advanced_testcase {
'Environment' => new question_classified_response(6, 'Position 6', 0.1666667),
];
- $this->assertEqualsWithDelta($expected, $classifiedresponse, 0.0000005, '');
+ $this->assertEqualsWithDelta($expected, $classifiedresponse, 0.0000005);
+ }
+
+ /**
+ * Test get number of correct|partial|incorrect on response.
+ */
+ public function test_get_num_parts_right() {
+ // Create a Ordering question.
+ /** @var qtype_ordering_question $question */
+ $question = test_question_maker::make_question('ordering');
+
+ $question->options->gradingtype = qtype_ordering_question::GRADING_RELATIVE_TO_CORRECT;
+ $question->start_attempt(new question_attempt_pending_step(), 1);
+
+ $response = $this->get_response($question, ['Dynamic', 'Modular', 'Object', 'Oriented', 'Learning', 'Environment']);
+ $numparts = $question->get_num_parts_right($response);
+
+ $this->assertEquals([2, 4, 0], $numparts);
+ }
+
+ public function test_validate_can_regrade_with_other_version_bad() {
+ if (!method_exists('question_definition', 'validate_can_regrade_with_other_version')) {
+ $this->markTestSkipped('This test only applies to Moodle 4.x');
+ }
+
+ /** @var qtype_ordering_question $question */
+ $question = test_question_maker::make_question('ordering');
+
+ $newq = clone($question);
+ $helper = new \qtype_ordering_test_helper();
+ $newq->answers = [
+ 23 => $helper->make_answer(23, 'Modular', FORMAT_HTML, 1, true),
+ 24 => $helper->make_answer(24, 'Object', FORMAT_HTML, 2, true),
+ 25 => $helper->make_answer(25, 'Oriented', FORMAT_HTML, 3, true),
+ 26 => $helper->make_answer(26, 'Dynamic', FORMAT_HTML, 4, true),
+ 27 => $helper->make_answer(27, 'Learning', FORMAT_HTML, 5, true),
+ ];
+
+ $this->assertEquals(get_string('regradeissuenumitemschanged', 'qtype_ordering'),
+ $newq->validate_can_regrade_with_other_version($question));
+ }
+
+ public function test_validate_can_regrade_with_other_version_ok() {
+ if (!method_exists('question_definition', 'validate_can_regrade_with_other_version')) {
+ $this->markTestSkipped('This test only applies to Moodle 4.x');
+ }
+
+ /** @var qtype_ordering_question $question */
+ $question = test_question_maker::make_question('ordering');
+
+ $newq = clone($question);
+ $helper = new \qtype_ordering_test_helper();
+ $newq->answers = [
+ 23 => $helper->make_answer(23, 'Modular', FORMAT_HTML, 1, true),
+ 24 => $helper->make_answer(24, 'Object', FORMAT_HTML, 2, true),
+ 25 => $helper->make_answer(25, 'Oriented', FORMAT_HTML, 3, true),
+ 26 => $helper->make_answer(26, 'Dynamic', FORMAT_HTML, 4, true),
+ 27 => $helper->make_answer(27, 'Learning', FORMAT_HTML, 5, true),
+ 28 => $helper->make_answer(28, 'Environment', FORMAT_HTML, 6, true),
+ ];
+
+ $this->assertNull($newq->validate_can_regrade_with_other_version($question));
+ }
+
+ public function test_update_attempt_state_date_from_old_version_bad() {
+ if (!method_exists('question_definition', 'update_attempt_state_data_for_new_version')) {
+ $this->markTestSkipped('This test only applies to Moodle 4.x');
+ }
+
+ /** @var qtype_ordering_question $question */
+ $question = test_question_maker::make_question('ordering');
+
+ $newq = clone($question);
+ $helper = new \qtype_ordering_test_helper();
+ $newq->answers = [
+ 23 => $helper->make_answer(23, 'Modular', FORMAT_HTML, 1, true),
+ 24 => $helper->make_answer(24, 'Object', FORMAT_HTML, 2, true),
+ 25 => $helper->make_answer(25, 'Oriented', FORMAT_HTML, 3, true),
+ 26 => $helper->make_answer(26, 'Dynamic', FORMAT_HTML, 4, true),
+ 27 => $helper->make_answer(27, 'Learning', FORMAT_HTML, 5, true),
+ ];
+
+ $oldstep = new question_attempt_step();
+ $oldstep->set_qt_var('_currentresponse', '15,13,17,16,18,14');
+ $oldstep->set_qt_var('_correctresponse', '13,14,15,16,17,18');
+ $this->expectExceptionMessage(get_string('regradeissuenumitemschanged', 'qtype_ordering'));
+ $newq->update_attempt_state_data_for_new_version($oldstep, $question);
+ }
+
+ public function test_update_attempt_state_date_from_old_version_ok() {
+ if (!method_exists('question_definition', 'update_attempt_state_data_for_new_version')) {
+ $this->markTestSkipped('This test only applies to Moodle 4.x');
+ }
+
+ /** @var qtype_ordering_question $question */
+ $question = test_question_maker::make_question('ordering');
+
+ $newq = clone($question);
+ $helper = new \qtype_ordering_test_helper();
+ $newq->answers = [
+ 23 => $helper->make_answer(23, 'Modular', FORMAT_HTML, 1, true),
+ 24 => $helper->make_answer(24, 'Object', FORMAT_HTML, 2, true),
+ 25 => $helper->make_answer(25, 'Oriented', FORMAT_HTML, 3, true),
+ 26 => $helper->make_answer(26, 'Dynamic', FORMAT_HTML, 4, true),
+ 27 => $helper->make_answer(27, 'Learning', FORMAT_HTML, 5, true),
+ 28 => $helper->make_answer(28, 'Environment', FORMAT_HTML, 6, true),
+ ];
+
+ $oldstep = new question_attempt_step();
+ $oldstep->set_qt_var('_currentresponse', '15,13,17,16,18,14');
+ $oldstep->set_qt_var('_correctresponse', '13,14,15,16,17,18');
+
+ $this->assertEquals(['_currentresponse' => '25,23,27,26,28,24', '_correctresponse' => '23,24,25,26,27,28'],
+ $newq->update_attempt_state_data_for_new_version($oldstep, $question));
}
}
diff --git a/question/type/ordering/tests/questiontype_test.php b/question/type/ordering/tests/questiontype_test.php
index 437112e0e89..4f7cc8e7b59 100644
--- a/question/type/ordering/tests/questiontype_test.php
+++ b/question/type/ordering/tests/questiontype_test.php
@@ -22,6 +22,16 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+namespace qtype_ordering;
+
+use qtype_ordering;
+use test_question_maker;
+use qtype_ordering_edit_form;
+use qtype_ordering_test_helper;
+use question_bank;
+use question_possible_response;
+use qtype_ordering_question;
+use core_question_generator;
defined('MOODLE_INTERNAL') || die();
@@ -36,8 +46,9 @@ require_once($CFG->dirroot . '/question/type/ordering/edit_ordering_form.php');
*
* @copyright 20018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @covers \qtype_ordering
*/
-class qtype_ordering_test extends advanced_testcase {
+class questiontype_test extends \advanced_testcase {
/** @var qtype_ordering instance of the question type class to test. */
protected $qtype;
diff --git a/question/type/ordering/tests/walkthrough_test.php b/question/type/ordering/tests/walkthrough_test.php
index 5d182ff20ae..bf13588c214 100644
--- a/question/type/ordering/tests/walkthrough_test.php
+++ b/question/type/ordering/tests/walkthrough_test.php
@@ -22,6 +22,13 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+namespace qtype_ordering;
+
+use qtype_ordering\question_hint_ordering;
+use test_question_maker;
+use question_state;
+use question_pattern_expectation;
+use stdClass;
defined('MOODLE_INTERNAL') || die();
global $CFG;
@@ -36,8 +43,9 @@ require_once($CFG->dirroot . '/question/type/ddwtos/tests/helper.php');
*
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @covers \qtype_ordering
*/
-class qtype_ordering_walkthrough_test extends qbehaviour_walkthrough_test_base {
+class walkthrough_test extends \qbehaviour_walkthrough_test_base {
/**
* Get the array of post data that will .
@@ -107,8 +115,8 @@ class qtype_ordering_walkthrough_test extends qbehaviour_walkthrough_test_base {
// Create a drag-and-drop question.
$question = test_question_maker::make_question('ordering');
$question->hints = array(
- new question_hint(13, 'This is the first hint.', FORMAT_HTML),
- new question_hint(14, 'This is the second hint.', FORMAT_HTML),
+ new question_hint_ordering(13, 'This is the first hint.', FORMAT_HTML, true, false, true),
+ new question_hint_ordering(14, 'This is the second hint.', FORMAT_HTML, false, false, false),
);
$this->start_attempt_at_question($question, 'interactive', 3);
@@ -121,6 +129,7 @@ class qtype_ordering_walkthrough_test extends qbehaviour_walkthrough_test_base {
$this->get_contains_submit_button_expectation(true),
$this->get_does_not_contain_feedback_expectation(),
$this->get_tries_remaining_expectation(3),
+ $this->get_does_not_contain_num_parts_correct(),
$this->get_no_hint_visible_expectation());
// Submit the wrong answer.
@@ -134,6 +143,7 @@ class qtype_ordering_walkthrough_test extends qbehaviour_walkthrough_test_base {
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_try_again_button_expectation(true),
$this->get_does_not_contain_correctness_expectation(),
+ $this->get_contains_num_parts_correct_ordering(0, 5, 1),
$this->get_contains_hint_expectation('This is the first hint'));
// Do try again.
@@ -161,6 +171,7 @@ class qtype_ordering_walkthrough_test extends qbehaviour_walkthrough_test_base {
$this->check_current_output(
$this->get_does_not_contain_submit_button_expectation(),
$this->get_contains_correct_expectation(),
+ $this->get_does_not_contain_num_parts_correct(),
$this->get_no_hint_visible_expectation());
// Check regrading does not mess anything up.
@@ -170,4 +181,39 @@ class qtype_ordering_walkthrough_test extends qbehaviour_walkthrough_test_base {
$this->check_current_state(question_state::$gradedright);
$this->check_current_mark(2);
}
+
+ /**
+ * Return question pattern expectation.
+ *
+ * @param int $numright The number of item correct.
+ * @param int $numpartial The number of partial correct item.
+ * @param int $numincorrect The number of incorrect item.
+ * @return question_pattern_expectation
+ */
+ protected function get_contains_num_parts_correct_ordering($numright, $numpartial, $numincorrect) {
+ $a = new stdClass();
+ $a->numright = $numright;
+ $a->numpartial = $numpartial;
+ $a->numincorrect = $numincorrect;
+
+ $output = '';
+ if ($a->numright) {
+ $a->numrightplural = get_string($a->numright > 1 ? 'itemplural' : 'itemsingular', 'qtype_ordering');
+ $output .= '' . get_string('yougotnright', 'qtype_ordering', $a) . '
';
+ }
+
+ if ($a->numpartial) {
+ $a->numpartialplural = get_string($a->numpartial > 1 ? 'itemplural' : 'itemsingular', 'qtype_ordering');
+ $output .= '' . get_string('yougotnpartial', 'qtype_ordering', $a) . '
';
+ }
+
+ if ($a->numincorrect) {
+ $a->numincorrectplural = get_string($a->numincorrect > 1 ? 'itemplural' : 'itemsingular', 'qtype_ordering');
+ $output .= '' . get_string('yougotnincorrect', 'qtype_ordering', $a) . '
';
+ }
+
+ $pattern = '/' . preg_quote($output, '/');
+
+ return new question_pattern_expectation($pattern . '/');
+ }
}
diff --git a/question/type/ordering/version.php b/question/type/ordering/version.php
index 21b4bd503ee..197233c7f07 100644
--- a/question/type/ordering/version.php
+++ b/question/type/ordering/version.php
@@ -28,6 +28,6 @@ defined('MOODLE_INTERNAL') || die();
$plugin->cron = 0;
$plugin->component = 'qtype_ordering';
$plugin->maturity = MATURITY_STABLE;
-$plugin->requires = 2015051100; // Moodle 2.9.
-$plugin->version = 2022070806;
-$plugin->release = '2022-07-08 (06)';
+$plugin->requires = 2021051700; // Moodle 3.11.
+$plugin->version = 2022092700;
+$plugin->release = '2022-09-27 (07)';