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 <sna67@open.ac.uk>
Co-authored-by: Thong Bui <qktc1422@gmail.com>
Co-authored-by: Anupama Sarjoshi <anupama.sarjoshi@open.ac.uk>
This commit is contained in:
Tim Hunt 2023-04-29 09:58:06 +01:00 committed by Mathew May
parent fc4adc6003
commit ba23a42edd
44 changed files with 1449 additions and 303 deletions

View File

@ -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

View File

@ -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.

View File

@ -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;c<a.changedTouches.length;c++)b.handleMove(a.changedTouches[c].clientX,a.changedTouches[c].clientY)},mouseMove:function(a){b.handleMove(a.clientX,a.clientY)},handleMove:function(c,d){d<b.SCROLL_THRESHOLD?b.scrollAmount=-Math.min(b.SCROLL_THRESHOLD-d,b.SCROLL_THRESHOLD):d>a(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}});
/*
* 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<e.changedTouches.length;i++)autoscroll.handleMove(e.changedTouches[i].clientX,e.changedTouches[i].clientY)},mouseMove:function(e){autoscroll.handleMove(e.clientX,e.clientY)},handleMove:function(clientX,clientY){clientY<autoscroll.SCROLL_THRESHOLD?autoscroll.scrollAmount=-Math.min(autoscroll.SCROLL_THRESHOLD-clientY,autoscroll.SCROLL_THRESHOLD):clientY>$(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

File diff suppressed because one or more lines are too long

View File

@ -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||f<e){c=a(d);e=f}});if(c[0]===g[0]){return}if(r(j)<r(c)){g.insertBefore(c)}else{g.insertAfter(c)}n(g)},n=function(a){for(var b=a.closest("ol, ul"),c=b.find("li"),d=c.length,e=0;e<d;++e){if(a[0]===c[e]){j.find("li").attr("value",e+1);break}}},o=function(a,b){if("undefined"!=typeof d.reorderEnd){d.reorderEnd(g.closest(d.list),g)}var c=t();if(!u(f,c)){d.reorderDone(g.closest(d.list),g,c)}else if(500>new 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<closestDistance)&&(closestItem=$(element),closestDistance=distance)})),closestItem[0]!==itemDragging[0]){var offsetValue=0;midY(proxy)<midY(closestItem)?(offsetValue=20,window.console.log("For midY(proxy) < midY(closestItem) offset is: "+offsetValue)):(offsetValue=-20,window.console.log("For midY(proxy) < midY(closestItem) offset is: "+offsetValue)),midY(proxy)+offsetValue<midY(closestItem)?itemDragging.insertBefore(closestItem):itemDragging.insertAfter(closestItem),updateProxy(itemDragging)}},updateProxy=function(itemDragging){for(var items=itemDragging.closest("ol, ul").find("li"),count=items.length,i=0;i<count;++i)if(itemDragging[0]===items[i]){proxy.find("li").attr("value",i+1);break}},dragEnd=function(x,y){void 0!==config.reorderEnd&&config.reorderEnd(itemDragging.closest(config.list),itemDragging);var newOrder=getCurrentOrder();arrayEquals(originalOrder,newOrder)?(new Date).getTime()-dragStart.time<500&&Math.abs(dragStart.x-x)<10&&Math.abs(dragStart.y-y)<10&&itemDragging[0].focus():config.reorderDone(itemDragging.closest(config.list),itemDragging,newOrder),proxy.remove(),proxy=null,itemDragging.removeClass(config.itemMovingClass),itemDragging=null,dragStart=null},midX=function(jQuery){return jQuery.offset().left+jQuery.outerWidth()/2},midY=function(jQuery){return jQuery.offset().top+jQuery.outerHeight()/2},distanceBetweenElements=function(element1,element2){var e1=$(element1),e2=$(element2),dx=midX(e1)-midX(e2),dy=midY(e1)-midY(e2);return Math.sqrt(dx*dx+dy*dy)},getCurrentOrder=function(){return(itemDragging||itemMoving).closest(config.list).find(config.item).map((function(index,item){return config.idGetter(item)})).get()},arrayEquals=function(a1,a2){return a1.length===a2.length&&a1.every((function(v,i){return v===a2[i]}))};config.itemInPage=(outer=config.list,inner=config.item,combined=[],outer.split(",").forEach((function(firstSelector){inner.split(",").forEach((function(secondSelector){combined.push(firstSelector.trim()+" "+secondSelector.trim())}))})),combined.join(", ")),$(config.list).on("mousedown touchstart",config.item,(function(event){var details=drag.prepare(event);details.start&&function(event,details){orderList=$(config.list),dragStart={time:(new Date).getTime(),x:details.x,y:details.y},itemDragging=$(event.currentTarget).closest(config.itemInPage),void 0!==config.reorderStart&&config.reorderStart(itemDragging.closest(config.list),itemDragging),originalOrder=getCurrentOrder(),proxy=$(config.proxyHtml.replace("%%ITEM_HTML%%",itemDragging.html()).replace("%%ITEM_CLASS_NAME%%",itemDragging.attr("class")).replace("%%LIST_CLASS_NAME%%",orderList.attr("class"))),$(document.body).append(proxy),proxy.css("position","absolute"),proxy.css(itemDragging.offset()),proxy.width(itemDragging.outerWidth()),proxy.height(itemDragging.outerHeight()),itemDragging.addClass(config.itemMovingClass),updateProxy(itemDragging),drag.start(event,proxy,dragMove,dragEnd)}(event,details)})),$(config.list).on("keydown",config.item,(function(event){itemMoving=$(event.currentTarget).closest(config.itemInPage),originalOrder=getCurrentOrder(),function(e,current){switch(e.keyCode){case keys.space:case keys.arrowRight:case keys.arrowDown:e.preventDefault(),e.stopPropagation();var next=current.next();next.length&&next.insertBefore(current);break;case keys.arrowLeft:case keys.arrowUp:e.preventDefault(),e.stopPropagation();var prev=current.prev();prev.length&&prev.insertAfter(current)}}(event,itemMoving);var newOrder=getCurrentOrder();arrayEquals(originalOrder,newOrder)||config.reorderDone(itemMoving.closest(config.list),itemMoving,newOrder)})),$(config.itemInPage).attr("tabindex","0")}}));
//# sourceMappingURL=drag_reorder.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -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;b<a.changedTouches.length;b++)a.changedTouches[b].identifier===c.touching&&c.handleMove(a.changedTouches[b].pageX,a.changedTouches[b].pageY)},mouseMove:function(a){c.handleMove(a.pageX,a.pageY)},handleMove:function(b,d){var e=c.dragProxy.offset(),f=e.top-parseInt(c.dragProxy.css("top")),g=e.left-parseInt(c.dragProxy.css("left")),h=a(document).height()-c.dragProxy.outerHeight()-f,i=a(document).width()-c.dragProxy.outerWidth()-g,j=-f,k=-g,l=c.initialPosition,m={top:Math.max(j,Math.min(h,l.top+(d-c.initialY)-f)),left:Math.max(k,Math.min(i,l.left+(b-c.initialX)-g))};c.dragProxy.css(m),c.onMove(b,d,c.dragProxy)},touchEnd:function(a){a.preventDefault();for(var b=0;b<a.changedTouches.length;b++)a.changedTouches[b].identifier===c.touching&&c.handleEnd(a.changedTouches[b].pageX,a.changedTouches[b].pageY)},mouseUp:function(a){c.handleEnd(a.pageX,a.pageY)},handleEnd:function(a,d){null!==c.touching?(window.removeEventListener("touchend",c.touchEnd,c.eventCaptureOptions),window.removeEventListener("touchcancel",c.touchEnd,c.eventCaptureOptions),window.removeEventListener("touchmove",c.touchMove,c.eventCaptureOptions),c.touching=null):(window.removeEventListener("mousemove",c.mouseMove,c.eventCaptureOptions),window.removeEventListener("mouseup",c.mouseUp,c.eventCaptureOptions)),b.stop(),c.onDrop(a,d,c.dragProxy)},scroll:function(b){var d=a(document).height()-c.dragProxy.outerHeight(),e=c.dragProxy.offset();e.top=Math.min(d,e.top+b),c.dragProxy.css(e)}};return{prepare:c.prepare,start:c.start}});
/*
* JavaScript to handle drag operations, including automatic scrolling.
*
* 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, you
* can only drag one thing at a time, this is not a problem in practice.
*
* @module qtype_ordering/dragdrop
* @class dragdrop
* @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/dragdrop",["jquery","qtype_ordering/autoscroll"],(function($,autoScroll){var dragdrop={eventCaptureOptions:{passive:!1,capture:!0},dragProxy:null,onMove:null,onDrop:null,initialPosition:null,initialX:null,initialY:null,touching:null,prepare:function(event){if(event.preventDefault(),"touchstart"===event.type?null===dragdrop.touching&&event.changedTouches.length>0: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<e.changedTouches.length;i++)e.changedTouches[i].identifier===dragdrop.touching&&dragdrop.handleMove(e.changedTouches[i].pageX,e.changedTouches[i].pageY)},mouseMove:function(e){dragdrop.handleMove(e.pageX,e.pageY)},handleMove:function(pageX,pageY){var current=dragdrop.dragProxy.offset(),topOffset=current.top-parseInt(dragdrop.dragProxy.css("top")),leftOffset=current.left-parseInt(dragdrop.dragProxy.css("left")),maxY=$(document).height()-dragdrop.dragProxy.outerHeight()-topOffset,maxX=$(document).width()-dragdrop.dragProxy.outerWidth()-leftOffset,minY=-topOffset,minX=-leftOffset,initial=dragdrop.initialPosition,position={top:Math.max(minY,Math.min(maxY,initial.top+(pageY-dragdrop.initialY)-topOffset)),left:Math.max(minX,Math.min(maxX,initial.left+(pageX-dragdrop.initialX)-leftOffset))};dragdrop.dragProxy.css(position),dragdrop.onMove(pageX,pageY,dragdrop.dragProxy)},touchEnd:function(e){e.preventDefault();for(var i=0;i<e.changedTouches.length;i++)e.changedTouches[i].identifier===dragdrop.touching&&dragdrop.handleEnd(e.changedTouches[i].pageX,e.changedTouches[i].pageY)},mouseUp:function(e){dragdrop.handleEnd(e.pageX,e.pageY)},handleEnd:function(pageX,pageY){null!==dragdrop.touching?(window.removeEventListener("touchend",dragdrop.touchEnd,dragdrop.eventCaptureOptions),window.removeEventListener("touchcancel",dragdrop.touchEnd,dragdrop.eventCaptureOptions),window.removeEventListener("touchmove",dragdrop.touchMove,dragdrop.eventCaptureOptions),dragdrop.touching=null):(window.removeEventListener("mousemove",dragdrop.mouseMove,dragdrop.eventCaptureOptions),window.removeEventListener("mouseup",dragdrop.mouseUp,dragdrop.eventCaptureOptions)),autoScroll.stop(),dragdrop.onDrop(pageX,pageY,dragdrop.dragProxy)},scroll:function(offset){var maxY=$(document).height()-dragdrop.dragProxy.outerHeight(),currentPosition=dragdrop.dragProxy.offset();currentPosition.top=Math.min(maxY,currentPosition.top+offset),dragdrop.dragProxy.css(currentPosition)}};return{prepare:dragdrop.prepare,start:dragdrop.start}}));
//# sourceMappingURL=dragdrop.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -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}});
/*
* 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 <ryan@moodle.com>
* @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

View File

@ -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 <http://www.gnu.org/licenses/>.\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 <ryan@moodle.com>\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"}

View File

@ -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:"<div class=\"que ordering dragproxy\"><ul class=\"%%LIST_CLASS_NAME%%\"><li class=\"%%ITEM_CLASS_NAME%% item-moving\">%%ITEM_HTML%%</li></ul></div>",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:'<div class="que ordering dragproxy"><ul class="%%LIST_CLASS_NAME%%"><li class="%%ITEM_CLASS_NAME%% item-moving">%%ITEM_HTML%%</li></ul></div>',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

View File

@ -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: '<div class=\"que ordering dragproxy\">' +\n '<ul class=\"%%LIST_CLASS_NAME%%\"><li class=\"%%ITEM_CLASS_NAME%% item-moving\">' +\n '%%ITEM_HTML%%</li></ul></div>',\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"}
{"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: '<div class=\"que ordering dragproxy\">' +\n '<ul class=\"%%LIST_CLASS_NAME%%\"><li class=\"%%ITEM_CLASS_NAME%% item-moving\">' +\n '%%ITEM_HTML%%</li></ul></div>',\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"}

View File

@ -13,15 +13,15 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
/*
* 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);

View File

@ -13,7 +13,7 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
/*
* 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,

View File

@ -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: '<div class="que ordering dragproxy">' +
'<ul class="%%LIST_CLASS_NAME%%"><li class="%%ITEM_CLASS_NAME%% item-moving">' +
'%%ITEM_HTML%%</li></ul></div>',
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(',');
}
}
});
}
};

View File

@ -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
*

View File

@ -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.

View File

@ -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);
}

View File

@ -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';
}
}

View File

@ -0,0 +1,87 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Question 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;
}
}

View File

@ -17,6 +17,7 @@
<FIELD NAME="incorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="partiallycorrectfeedback" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="partiallycorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="shownumcorrect" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="Primary key for question_ordering"/>

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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**
&nbsp; If all items are in the correct position, then full marks are awarded. Otherwise, the score is zero.
**Absolute position**
**Absolute position**
&nbsp; 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**
&nbsp; 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)***.
**Relative to the next item (excluding last)**
**Relative to the next item (excluding last)**
&nbsp; 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)**
&nbsp; 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**
&nbsp; 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**
&nbsp; 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)***.
**Longest ordered subset**
**Longest ordered subset**
&nbsp; 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**
&nbsp; 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.';

View File

@ -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.
*

View File

@ -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];
}
}

View File

@ -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>$numberingstyle</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(" <shownumcorrect/>\n", "", $output);
}
$output .= " <shownumcorrect>$shownumcorrect</shownumcorrect>\n";
foreach ($question->options->answers as $answer) {
$output .= ' <answer fraction="'.$answer->fraction.'" '.$format->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.

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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

View File

@ -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"

View File

@ -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 |

View File

@ -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');
}

View File

@ -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"

View File

@ -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

View File

@ -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 |

View File

@ -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."

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<quiz>
<!-- question: 0 -->
<question type="category">
<category>
<text>$course$/Test questions</text>
</category>
</question>
<!-- question: 1357 -->
<question type="ordering">
<name>
<text>dd-ordering old question</text>
</name>
<questiontext format="html">
<text>Put these words in order.</text>
</questiontext>
<generalfeedback format="html">
<text><![CDATA[<p>The correct answer is "Modular Object Oriented Dynamic Learning Environment".</p>]]></text>
</generalfeedback>
<defaultgrade>1.0000000</defaultgrade>
<penalty>0.3333333</penalty>
<hidden>0</hidden>
<idnumber>dd1</idnumber>
<layouttype>HORIZONTAL</layouttype>
<selecttype>ALL</selecttype>
<selectcount>0</selectcount>
<gradingtype>ABSOLUTE_POSITION</gradingtype>
<showgrading>SHOW</showgrading>
<correctfeedback format="html">
<text>Your answer is correct.</text>
</correctfeedback>
<partiallycorrectfeedback format="html">
<text>Your answer is partially correct.</text>
</partiallycorrectfeedback>
<incorrectfeedback format="html">
<text>Your answer is incorrect.</text>
</incorrectfeedback>
<answer fraction="1.0000000" format="moodle_auto_format">
<text>Modular</text>
</answer>
<answer fraction="2.0000000" format="moodle_auto_format">
<text>Object</text>
</answer>
<answer fraction="3.0000000" format="moodle_auto_format">
<text>Oriented</text>
</answer>
<answer fraction="4.0000000" format="moodle_auto_format">
<text>Dynamic</text>
</answer>
<answer fraction="5.0000000" format="moodle_auto_format">
<text>Learning</text>
</answer>
<answer fraction="6.0000000" format="moodle_auto_format">
<text>Environment</text>
</answer>
<hint format="html">
<text>
<![CDATA[ <p dir="ltr" style="text-align: left;">Hint 1</p> ]]>
</text>
</hint>
<hint format="html">
<text>
<![CDATA[ <p dir="ltr" style="text-align: left;">Hint 2</p> ]]>
</text>
</hint>
</question>
</quiz>

View File

@ -37,6 +37,7 @@
<incorrectfeedback format="html">
<text>Your answer is incorrect.</text>
</incorrectfeedback>
<shownumcorrect>1</shownumcorrect>
<answer fraction="1.0000000" format="moodle_auto_format">
<text>Modular</text>
</answer>
@ -55,5 +56,13 @@
<answer fraction="6.0000000" format="moodle_auto_format">
<text>Environment</text>
</answer>
<hint format="html">
<text/>
<shownumcorrect/>
</hint>
<hint format="html">
<text/>
<options>1</options>
</hint>
</question>
</quiz>

View File

@ -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;

View File

@ -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));
}
}

View File

@ -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;

View File

@ -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 .= '<div>' . get_string('yougotnright', 'qtype_ordering', $a) . '</div>';
}
if ($a->numpartial) {
$a->numpartialplural = get_string($a->numpartial > 1 ? 'itemplural' : 'itemsingular', 'qtype_ordering');
$output .= '<div>' . get_string('yougotnpartial', 'qtype_ordering', $a) . '</div>';
}
if ($a->numincorrect) {
$a->numincorrectplural = get_string($a->numincorrect > 1 ? 'itemplural' : 'itemsingular', 'qtype_ordering');
$output .= '<div>' . get_string('yougotnincorrect', 'qtype_ordering', $a) . '</div>';
}
$pattern = '/<div class="numpartscorrect">' . preg_quote($output, '/');
return new question_pattern_expectation($pattern . '/');
}
}

View File

@ -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)';