diff --git a/.travis.yml b/.travis.yml index 15bba62..1e62e26 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,19 @@ language: php -sudo: true +sudo: false + +addons: + firefox: "47.0.1" + postgresql: "9.3" + apt: + packages: + - oracle-java8-installer + - oracle-java8-set-default cache: directories: - $HOME/.composer/cache + - $HOME/.npm php: - 7.0 @@ -24,7 +33,7 @@ before_install: - nvm use 8.9 - cd ../.. - composer selfupdate - - composer create-project -n --no-dev moodlerooms/moodle-plugin-ci ci ^1 + - composer create-project -n --no-dev --prefer-dist moodlerooms/moodle-plugin-ci ci ^2 - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" install: @@ -35,10 +44,10 @@ script: - moodle-plugin-ci phpcpd - moodle-plugin-ci phpmd - moodle-plugin-ci codechecker - - moodle-plugin-ci csslint - - moodle-plugin-ci shifter - - moodle-plugin-ci jshint - moodle-plugin-ci validate + - moodle-plugin-ci savepoints + - moodle-plugin-ci mustache + - moodle-plugin-ci grunt - moodle-plugin-ci phpunit - moodle-plugin-ci behat diff --git a/amd/build/quizgame.min.js b/amd/build/quizgame.min.js index ad3cd41..0aedf0d 100644 --- a/amd/build/quizgame.min.js +++ b/amd/build/quizgame.min.js @@ -1 +1 @@ -define(["jquery"],function(a){function b(a){if(document.getElementById("mod_quizgame_sound_on").checked){var b=document.getElementById("mod_quizgame_sound_"+a);b.currentTime=0,b.play()}}function c(){T.removeAttribute("width"),T.removeAttribute("height"),T.removeAttribute("style"),ea.width=T.clientWidth,ea.height=T.clientHeight,T.style.width=ea.width,T.style.height=ea.height,f(T)}function d(){la--,la<1&&c()}function e(){ea.width=window.screen.width||T.clientWidth,ea.height=window.screen.height||T.clientHeight,T.requestFullscreen?T.requestFullscreen():T.msRequestFullscreen?T.msRequestFullscreen():T.mozRequestFullScreen?T.mozRequestFullScreen():T.webkitRequestFullscreen&&T.webkitRequestFullscreen(),la=2,T.style.width=screen.width+"px",T.style.height="100%",f(T)}function f(a){a.width=ea.width,a.height=ea.height,Y.imageSmoothingEnabled=!1}function g(){document.onkeydown=null,document.onkeyup=null,document.onmousedown=null,document.onmouseup=null,document.onmousemove=null,document.ontouchstart=null,document.ontouchend=null,document.ontouchmove=null}function h(){g(),document.onkeydown=F,document.onmouseup=G}function i(){Y.clearRect(0,0,ea.width,ea.height),Y.fillStyle="#FFFFFF",Y.font="18px Audiowide",Y.textAlign="center",null!==S&&S.length>0?(Y.fillText(M.util.get_string("spacetostart","mod_quizgame"),ea.width/2,ea.height/2),h()):Y.fillText(M.util.get_string("emptyquiz","mod_quizgame"),ea.width/2,ea.height/2)}function j(){Q(S),ca?m():(aa.forEach(function(a){var b=new Image;b.src=a,b.onload=function(){ba++,ba>=aa.length&&l()}}),ca=!0)}function k(){h()}function l(){clearInterval(W),W=setInterval(function(){p(Y,ea,_,$,fa),q(ea,_,$)},40),m()}function m(){Z=0,_=[],$=[],da=-1,X=.5,ga=!1,ha=!1,U=new t("pix/ship.png",0,0),U.x=ea.width/2,U.y=ea.height/2,_.push(U),V=new u("pix/planet.png",0,0),V.image.width=ea.width,V.image.height=ea.height,V.direction.y=1,V.movespeed.y=.7,$.push(V),n(),document.onkeyup=I,document.onkeydown=H,document.onmouseup=K,document.onmousedown=J,document.onmousemove=L,document.ontouchstart=N,document.ontouchend=O,document.ontouchmove=P}function n(){da++,da>=S.length&&(da=0,X*=1.3),fa=o(S,da,ea)}function o(a,b,c){if(ia=[],ja=0,ka=0,"multichoice"==a[b].type)a[b].answers.forEach(function(a){var b=new w(Math.random()*c.width,-Math.random()*c.height/2,a.text,a.fraction);a.fraction<1&&(ia.push(b),a.fraction>0&&(ka+=a.fraction)),_.push(b)});else if("match"==a[b].type){var d=0,e=1/a[b].stems.length;ka+=1,a[b].stems.forEach(function(a){d++;var b=new x(Math.random()*c.width,-Math.random()*c.height/2,a.question,e,(-d),(!0)),f=new x(Math.random()*c.width,-Math.random()*c.height/2,a.answer,e,d);ia.push(b),ia.push(f),_.push(b),_.push(f)})}return a[b].question}function p(a,b,c,d,e){a.clearRect(0,0,b.width,b.height);for(var f=0;f1?U.Shoot():(ga=!0,P(a)))}function O(a){0===a.touches.length&&(ga=!1),U.direction.x=0,U.direction.y=0}function P(a){U.mouse.x=a.touches[0].clientX,U.mouse.y=a.touches[0].clientY-U.image.height}function Q(a){for(var b,c,d=a.length;0!==d;)c=Math.floor(Math.random()*d),d-=1,b=a[d],a[d]=a[c],a[c]=b;return a}function R(a){S=a,document.addEventListener&&(document.addEventListener("fullscreenchange",d,!1),document.addEventListener("MSFullscreenChange",d,!1),document.addEventListener("mozfullscreenchange",d,!1),document.addEventListener("webkitfullscreenchange",d,!1)),T=document.getElementById("mod_quizgame_game"),Y=T.getContext("2d"),c(),W=setInterval(function(){i()},500)}var S,T,U,V,W,X,Y,Z=0,$=[],_=[],aa=["pix/icon.gif","pix/planet.png","pix/ship.png","pix/enemy.png","pix/enemystem.png","pix/enemychoice.png","pix/enemystemselected.png","pix/enemychoiceselected.png","pix/laser.png","pix/enemylaser.png"],ba=0,ca=!1,da=-1,ea={x:0,y:0,width:0,height:0},fa="",ga=!1,ha=!1,ia=[],ja=0,ka=0,la=0;a("#mod_quizgame_fullscreen_button").on("click",function(){e()}),r.prototype.right=function(){return this.left+this.width},r.prototype.bottom=function(){return this.top+this.height},r.prototype.Contains=function(a){return a.x>this.left&&a.xthis.top&&a.ythis.right()||a.right()this.bottom()||a.bottom()this.mouse.x?U.direction.x=-1:U.direction.x=0,this.ythis.mouse.y?U.direction.y=-1:U.direction.y=0),s.prototype.update.call(this,a),this.xa.width&&(this.x=a.x-this.image.width),this.ya.height-this.image.height&&(this.y=a.height-this.image.height)},t.prototype.Shoot=function(){b("laser"),_.unshift(new y(U.x,U.y,(!0),24)),ma=!1},t.prototype.die=function(){s.prototype.die.call(this),b("explosion"),E(this.x+this.image.width/2,this.y+this.image.height/2,200,"#FFCC00"),this.lastScore=Z,k()},t.prototype.gotShot=function(a){a.alive&&(this.lives<=1?this.die():(this.lives--,E(this.x+this.image.width/2,this.y+this.image.height/2,100,"#FFCC00")))},u.prototype=Object.create(s.prototype),u.prototype.update=function(a){V.image.width=ea.width,V.image.height=ea.height,s.prototype.update.call(this,a)},v.prototype=Object.create(s.prototype),v.prototype.update=function(a){if(this.y9*a.height/10?(this.movespeed.x=1*this.xspeed,this.movespeed.y=5*this.yspeed):(this.movespeed.x=this.xspeed,this.movespeed.y=this.yspeed),s.prototype.update.call(this,a),this.movementClock--,this.movementClock<=0&&(this.direction.x=Math.floor(3*Math.random())-1,this.movementClock=30*(2+Math.random())),this.shotClock-=X,this.shotClock<=0&&this.y<.6*a.height){b("enemylaser");var c=new y(this.x,this.y);c.direction.y=1,c.friendly=!1,_.unshift(c),this.shotClock=(1+Math.random())*this.shotFrequency}this.xa.width&&(this.x=a.x-this.image.width),this.y>a.height+this.image.height&&this.alive&&(this.alive=!1,this.fraction>0&&(ka-=this.fraction,Z-=1e3*this.fraction),ka<=0&&this.level==da&&U.alive&&n())},v.prototype.draw=function(a){s.prototype.draw.call(this,a),a.fillStyle="#FFFFFF",a.font="15px Audiowide",a.textAlign="center",a.fillText(this.text,this.x+this.image.width/2,this.y-5)},v.prototype.die=function(){s.prototype.die.call(this),E(this.x+this.image.width,this.y+this.image.height,50+150*this.fraction,"#FF0000"),Z+=1e3*this.fraction,b("explosion")},v.prototype.gotShot=function(a){a.die(),this.die()},w.prototype=Object.create(v.prototype),w.prototype.die=function(){v.prototype.die.call(this),this.fraction>0&&(ka-=this.fraction),(this.fraction>=1||this.fraction>0&&ka<=0)&&(ia.forEach(function(a){a.alive&&a.die()}),ia=[],n())},w.prototype.gotShot=function(a){this.fraction>0?(a.die(),this.die()):(Z+=600*(this.fraction-.5),a.deflect())},x.prototype=Object.create(v.prototype),x.prototype.die=function(){v.prototype.die.call(this)},x.prototype.gotShot=function(a){if(a.alive&&this.alive)if(ja==-this.pairid){a.die(),this.die();var b=0;ia.forEach(function(a){a.pairid==ja&&a.die(),a.alive&&b++}),b<=0&&n()}else ja==this.pairid?a.deflect():(a.die(),this.hightlight(),ja=this.pairid)},x.prototype.hightlight=function(){ia.forEach(function(a){a.unhightlight()}),this.stem?this.loadImage("pix/enemystemselected.png"):this.loadImage("pix/enemychoiceselected.png"),this.hightlighted=!0},x.prototype.unhightlight=function(){this.hightlighted&&(this.stem?this.loadImage("pix/enemystem.png"):this.loadImage("pix/enemychoice.png")),this.hightlighted=!1},y.prototype=Object.create(s.prototype),y.prototype.update=function(a){s.prototype.update.call(this,a),(this.xa.width||this.ya.height)&&(this.alive=!1),this.velocity.y=this.laserSpeed*this.direction.y},y.prototype.deflect=function(){this.image=this.loadImage("pix/enemylaser.png"),this.direction.y*=-1,this.friendly=!this.friendly,b("deflect")},z.prototype=Object.create(s.prototype),z.prototype.update=function(a){s.prototype.update.call(this,a),(this.xa.width||this.ya.height)&&(this.alive=!1),this.aliveTime++,this.aliveTime>15*Math.random()+5&&(this.alive=!1)},z.prototype.getRect=function(){return new r(this.x,this.y,this.width,this.height)},z.prototype.draw=function(a){a.fillStyle=this.colour,a.fillRect(this.x,this.y,this.width,this.height),a.stroke()},A.prototype=Object.create(s.prototype),A.prototype.update=function(a){s.prototype.update.call(this,a),this.y>a.height&&(this.alive=!1)},A.prototype.draw=function(a){a.fillStyle="#9999AA",a.fillRect(this.x,this.y,this.width,this.height),a.stroke()};var ma=!0;return{init:R}}); \ No newline at end of file +define(["jquery","core/yui","core/notification","core/ajax"],function(a,b,c,d){function e(a){if(document.getElementById("mod_quizgame_sound_on").checked){var b=document.getElementById("mod_quizgame_sound_"+a);b.currentTime=0,b.play()}}function f(){X.removeAttribute("width"),X.removeAttribute("height"),X.removeAttribute("style"),ia.width=X.clientWidth,ia.height=X.clientHeight,X.style.width=ia.width,X.style.height=ia.height,i(X)}function g(){pa--,pa<1&&f()}function h(){ia.width=window.screen.width||X.clientWidth,ia.height=window.screen.height||X.clientHeight,X.requestFullscreen?X.requestFullscreen():X.msRequestFullscreen?X.msRequestFullscreen():X.mozRequestFullScreen?X.mozRequestFullScreen():X.webkitRequestFullscreen&&X.webkitRequestFullscreen(),pa=2,X.style.width=screen.width+"px",X.style.height="100%",i(X)}function i(a){a.width=ia.width,a.height=ia.height,aa.imageSmoothingEnabled=!1}function j(){document.onkeydown=null,document.onkeyup=null,document.onmousedown=null,document.onmouseup=null,document.onmousemove=null,document.ontouchstart=null,document.ontouchend=null,document.ontouchmove=null}function k(){j(),document.onkeydown=I,document.onmouseup=J}function l(){aa.clearRect(0,0,ia.width,ia.height),aa.fillStyle="#FFFFFF",aa.font="18px Audiowide",aa.textAlign="center",null!==V&&V.length>0?(aa.fillText(M.util.get_string("spacetostart","mod_quizgame"),ia.width/2,ia.height/2),k()):aa.fillText(M.util.get_string("emptyquiz","mod_quizgame"),ia.width/2,ia.height/2)}function m(){T(V),ga?p():(ea.forEach(function(a){var b=new Image;b.src=a,b.onload=function(){fa++,fa>=ea.length&&o()}}),ga=!0)}function n(){d.call([{methodname:"mod_quizgame_update_score",args:{quizgameid:W,score:Math.trunc(ba)},fail:c.exception}]),k()}function o(){clearInterval($),$=setInterval(function(){s(aa,ia,da,ca,ja),t(ia,da,ca)},40),p()}function p(){ba=0,da=[],ca=[],ha=-1,_=.5,ka=!1,la=!1,d.call([{methodname:"mod_quizgame_start_game",args:{quizgameid:W},fail:c.exception}]),Y=new w("pix/ship.png",0,0),Y.x=ia.width/2,Y.y=ia.height/2,da.push(Y),Z=new x("pix/planet.png",0,0),Z.image.width=ia.width,Z.image.height=ia.height,Z.direction.y=1,Z.movespeed.y=.7,ca.push(Z),q(),document.onkeyup=L,document.onkeydown=K,document.onmouseup=O,document.onmousedown=N,document.onmousemove=P,document.ontouchstart=Q,document.ontouchend=R,document.ontouchmove=S}function q(){ha++,ha>=V.length&&(ha=0,_*=1.3),ja=r(V,ha,ia)}function r(a,b,c){if(ma=[],na=0,oa=0,"multichoice"==a[b].type)a[b].answers.forEach(function(a){var b=new z(Math.random()*c.width,-Math.random()*c.height/2,a.text,a.fraction);a.fraction<1&&(ma.push(b),a.fraction>0&&(oa+=a.fraction)),da.push(b)});else if("match"==a[b].type){var d=0,e=1/a[b].stems.length;oa+=1,a[b].stems.forEach(function(a){d++;var b=new A(Math.random()*c.width,-Math.random()*c.height/2,a.question,e,(-d),(!0)),f=new A(Math.random()*c.width,-Math.random()*c.height/2,a.answer,e,d);ma.push(b),ma.push(f),da.push(b),da.push(f)})}return a[b].question}function s(a,b,c,d,e){a.clearRect(0,0,b.width,b.height);for(var f=0;f1?Y.Shoot():(ka=!0,S(a)))}function R(a){0===a.touches.length&&(ka=!1),Y.direction.x=0,Y.direction.y=0}function S(a){Y.mouse.x=a.touches[0].clientX,Y.mouse.y=a.touches[0].clientY-Y.image.height}function T(a){for(var b,c,d=a.length;0!==d;)c=Math.floor(Math.random()*d),d-=1,b=a[d],a[d]=a[c],a[c]=b;return a}function U(a,b){V=a,W=b,document.addEventListener&&(document.addEventListener("fullscreenchange",g,!1),document.addEventListener("MSFullscreenChange",g,!1),document.addEventListener("mozfullscreenchange",g,!1),document.addEventListener("webkitfullscreenchange",g,!1)),X=document.getElementById("mod_quizgame_game"),aa=X.getContext("2d"),f(),$=setInterval(function(){l()},500)}var V,W,X,Y,Z,$,_,aa,ba=0,ca=[],da=[],ea=["pix/icon.gif","pix/planet.png","pix/ship.png","pix/enemy.png","pix/enemystem.png","pix/enemychoice.png","pix/enemystemselected.png","pix/enemychoiceselected.png","pix/laser.png","pix/enemylaser.png"],fa=0,ga=!1,ha=-1,ia={x:0,y:0,width:0,height:0},ja="",ka=!1,la=!1,ma=[],na=0,oa=0,pa=0;a("#mod_quizgame_fullscreen_button").on("click",function(){h()}),u.prototype.right=function(){return this.left+this.width},u.prototype.bottom=function(){return this.top+this.height},u.prototype.Contains=function(a){return a.x>this.left&&a.xthis.top&&a.ythis.right()||a.right()this.bottom()||a.bottom()this.mouse.x?Y.direction.x=-1:Y.direction.x=0,this.ythis.mouse.y?Y.direction.y=-1:Y.direction.y=0),v.prototype.update.call(this,a),this.xa.width&&(this.x=a.x-this.image.width),this.ya.height-this.image.height&&(this.y=a.height-this.image.height)},w.prototype.Shoot=function(){e("laser"),da.unshift(new B(Y.x,Y.y,(!0),24)),qa=!1},w.prototype.die=function(){v.prototype.die.call(this),e("explosion"),H(this.x+this.image.width/2,this.y+this.image.height/2,200,"#FFCC00"),this.lastScore=ba,n()},w.prototype.gotShot=function(a){a.alive&&(this.lives<=1?this.die():(this.lives--,H(this.x+this.image.width/2,this.y+this.image.height/2,100,"#FFCC00")))},x.prototype=Object.create(v.prototype),x.prototype.update=function(a){Z.image.width=ia.width,Z.image.height=ia.height,v.prototype.update.call(this,a)},y.prototype=Object.create(v.prototype),y.prototype.update=function(a){if(this.y9*a.height/10?(this.movespeed.x=1*this.xspeed,this.movespeed.y=5*this.yspeed):(this.movespeed.x=this.xspeed,this.movespeed.y=this.yspeed),v.prototype.update.call(this,a),this.movementClock--,this.movementClock<=0&&(this.direction.x=Math.floor(3*Math.random())-1,this.movementClock=30*(2+Math.random())),this.shotClock-=_,this.shotClock<=0&&this.y<.6*a.height){e("enemylaser");var b=new B(this.x,this.y);b.direction.y=1,b.friendly=!1,da.unshift(b),this.shotClock=(1+Math.random())*this.shotFrequency}this.xa.width&&(this.x=a.x-this.image.width),this.y>a.height+this.image.height&&this.alive&&(this.alive=!1,this.fraction>0&&(oa-=this.fraction,ba-=1e3*this.fraction),oa<=0&&this.level==ha&&Y.alive&&q())},y.prototype.draw=function(a){v.prototype.draw.call(this,a),a.fillStyle="#FFFFFF",a.font="15px Audiowide",a.textAlign="center",a.fillText(this.text,this.x+this.image.width/2,this.y-5)},y.prototype.die=function(){v.prototype.die.call(this),H(this.x+this.image.width,this.y+this.image.height,50+150*this.fraction,"#FF0000"),ba+=1e3*this.fraction,e("explosion")},y.prototype.gotShot=function(a){a.die(),this.die()},z.prototype=Object.create(y.prototype),z.prototype.die=function(){y.prototype.die.call(this),this.fraction>0&&(oa-=this.fraction),(this.fraction>=1||this.fraction>0&&oa<=0)&&(ma.forEach(function(a){a.alive&&a.die()}),ma=[],q())},z.prototype.gotShot=function(a){this.fraction>0?(a.die(),this.die()):(ba+=600*(this.fraction-.5),a.deflect())},A.prototype=Object.create(y.prototype),A.prototype.die=function(){y.prototype.die.call(this)},A.prototype.gotShot=function(a){if(a.alive&&this.alive)if(na==-this.pairid){a.die(),this.die();var b=0;ma.forEach(function(a){a.pairid==na&&a.die(),a.alive&&b++}),b<=0&&q()}else na==this.pairid?a.deflect():(a.die(),this.hightlight(),na=this.pairid)},A.prototype.hightlight=function(){ma.forEach(function(a){a.unhightlight()}),this.stem?this.loadImage("pix/enemystemselected.png"):this.loadImage("pix/enemychoiceselected.png"),this.hightlighted=!0},A.prototype.unhightlight=function(){this.hightlighted&&(this.stem?this.loadImage("pix/enemystem.png"):this.loadImage("pix/enemychoice.png")),this.hightlighted=!1},B.prototype=Object.create(v.prototype),B.prototype.update=function(a){v.prototype.update.call(this,a),(this.xa.width||this.ya.height)&&(this.alive=!1),this.velocity.y=this.laserSpeed*this.direction.y},B.prototype.deflect=function(){this.image=this.loadImage("pix/enemylaser.png"),this.direction.y*=-1,this.friendly=!this.friendly,e("deflect")},C.prototype=Object.create(v.prototype),C.prototype.update=function(a){v.prototype.update.call(this,a),(this.xa.width||this.ya.height)&&(this.alive=!1),this.aliveTime++,this.aliveTime>15*Math.random()+5&&(this.alive=!1)},C.prototype.getRect=function(){return new u(this.x,this.y,this.width,this.height)},C.prototype.draw=function(a){a.fillStyle=this.colour,a.fillRect(this.x,this.y,this.width,this.height),a.stroke()},D.prototype=Object.create(v.prototype),D.prototype.update=function(a){v.prototype.update.call(this,a),this.y>a.height&&(this.alive=!1)},D.prototype.draw=function(a){a.fillStyle="#9999AA",a.fillRect(this.x,this.y,this.width,this.height),a.stroke()};var qa=!0;return{init:U}}); \ No newline at end of file diff --git a/amd/src/quizgame.js b/amd/src/quizgame.js index 0e03c06..54d7c5e 100644 --- a/amd/src/quizgame.js +++ b/amd/src/quizgame.js @@ -25,8 +25,9 @@ * @copyright 2016 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define(['jquery'], function($) { +define(['jquery','core/yui', 'core/notification', 'core/ajax'], function($, Y, notification, ajax) { var questions; + var quizgame; var stage; var score = 0; var particles = []; @@ -171,6 +172,11 @@ define(['jquery'], function($) { } function endGame() { + ajax.call([{ + methodname: 'mod_quizgame_update_score', + args: {quizgameid: quizgame, score: Math.trunc(score)}, + fail: notification.exception + }]); menuEvents(); } @@ -196,6 +202,13 @@ define(['jquery'], function($) { touchDown = false; mouseDown = false; + // Queue & trigger the game_started event. + ajax.call([{ + methodname: 'mod_quizgame_start_game', + args: {quizgameid: quizgame}, + fail: notification.exception + }]); + player = new Player("pix/ship.png", 0, 0); player.x = displayRect.width / 2; player.y = displayRect.height / 2; @@ -858,8 +871,9 @@ define(['jquery'], function($) { return array; } - function doInitialize(q) { + function doInitialize(q, qid) { questions = q; + quizgame = qid; if (document.addEventListener) { document.addEventListener('fullscreenchange', fschange, false); document.addEventListener('MSFullscreenChange', fschange, false); diff --git a/backup/moodle2/backup_quizgame_stepslib.php b/backup/moodle2/backup_quizgame_stepslib.php index 684f173..48e65db 100644 --- a/backup/moodle2/backup_quizgame_stepslib.php +++ b/backup/moodle2/backup_quizgame_stepslib.php @@ -36,12 +36,12 @@ protected function define_structure() { // Define each element separated. $quizgame = new backup_nested_element('quizgame', array('id'), array( 'course', 'name', 'intro', 'introformat', 'timecreated', - 'timemodified', 'questioncategory', 'grade')); + 'timemodified', 'questioncategory', 'grade', 'completionscore')); $scores = new backup_nested_element('scores'); $score = new backup_nested_element('score', array('id'), array( - 'quizgameid', 'userid', 'score')); + 'quizgameid', 'userid', 'score', 'timecreated')); // Build the tree. $quizgame->add_child($scores); @@ -65,7 +65,6 @@ protected function define_structure() { // Define id annotations. $score->annotate_ids('user', 'userid'); - // Define file annotations. $quizgame->annotate_files('mod_quizgame', 'intro', null); // This file area hasn't itemid. diff --git a/backup/moodle2/restore_quizgame_activity_task.class.php b/backup/moodle2/restore_quizgame_activity_task.class.php index 2612655..56585c2 100644 --- a/backup/moodle2/restore_quizgame_activity_task.class.php +++ b/backup/moodle2/restore_quizgame_activity_task.class.php @@ -70,7 +70,7 @@ static public function define_decode_rules() { return $rules; } - + /** * Define the restore log rules that will be applied * by the {@link restore_logs_processor} when restoring diff --git a/backup/moodle2/restore_quizgame_stepslib.php b/backup/moodle2/restore_quizgame_stepslib.php index c3930e2..d01d1e6 100644 --- a/backup/moodle2/restore_quizgame_stepslib.php +++ b/backup/moodle2/restore_quizgame_stepslib.php @@ -61,18 +61,18 @@ protected function process_quizgame($data) { $newcontext = $DB->get_field('question_categories', 'contextid', array('id' => $newcat)); // Assemble the field data. if (!empty($newcat)) { - $data->questioncategory = implode(',', array($newcat, $newcontext)); + $data->questioncategory = implode(',', array($newcat, $newcontext)); } else { - if (!$this->task->is_samesite() || $data->course != $oldcourse) { - // We cannot map to the question category. - // They were not included in the backup since they were at a higher context. - // This can happen when we are backing up the activity alone and trying to restore it elsewhere. - $this->log('question category ' . $category[0] . ' was associated with the quizgame ' . - $data->id . ' but cannot actually be used as it is not available in this backup. ' . - 'The category needs to be re-selected.', backup::LOG_INFO); - - // Remove the old data. - $data->questioncategory = ""; + if (!$this->task->is_samesite() || $data->course != $oldcourse) { + // We cannot map to the question category. + // They were not included in the backup since they were at a higher context. + // This can happen when we are backing up the activity alone and trying to restore it elsewhere. + $this->log('question category ' . $category[0] . ' was associated with the quizgame ' . + $data->id . ' but cannot actually be used as it is not available in this backup. ' . + 'The category needs to be re-selected.', backup::LOG_INFO); + + // Remove the old data. + $data->questioncategory = ""; } } diff --git a/classes/event/game_score_added.php b/classes/event/game_score_added.php new file mode 100644 index 0000000..81e4217 --- /dev/null +++ b/classes/event/game_score_added.php @@ -0,0 +1,78 @@ +. + +/** + * The mod_quizgame game_score_added event. + * + * @package mod_quizgame + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_quizgame\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_quizgame game_score_added class. + * + * @package mod_quizgame + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class game_score_added extends \core\event\base { + + /** + * Set basic properties for the event. + */ + protected function init() { + $this->data['objecttable'] = 'quizgame_scores'; + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventgamescoreadded', 'mod_quizgame'); + } + + /** + * Get URL related to the action. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/quizgame/view.php', array('id' => $this->contextinstanceid)); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' scored ".$this->other['score'] . + " in the quizventure with course module id '$this->contextinstanceid'."; + } + + + public static function get_objectid_mapping() { + return array('db' => 'quizgame_scores', 'restore' => 'quizgame_scores'); + } +} diff --git a/classes/event/game_scores_viewed.php b/classes/event/game_scores_viewed.php new file mode 100644 index 0000000..693c7ce --- /dev/null +++ b/classes/event/game_scores_viewed.php @@ -0,0 +1,78 @@ +. + +/** + * The mod_quizgame game_scores_viewed event. + * + * @package mod_quizgame + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_quizgame\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_quizgame game_scores_viewed event class. + * + * @package mod_quizgame + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class game_scores_viewed extends \core\event\base { + + /** + * Set basic properties for the event. + */ + protected function init() { + $this->data['objecttable'] = 'quizgame'; + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventgamescoresviewed', 'mod_quizgame'); + } + + /** + * Get URL related to the action. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/quizgame/scores.php', array('id' => $this->objectid)); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' viewed all of the player scores for the quizventure with" + . " course module id '$this->contextinstanceid'."; + } + + + public static function get_objectid_mapping() { + return array('db' => 'quizgame', 'restore' => 'quizgame'); + } +} diff --git a/classes/event/game_started.php b/classes/event/game_started.php new file mode 100644 index 0000000..6614de2 --- /dev/null +++ b/classes/event/game_started.php @@ -0,0 +1,77 @@ +. + +/** + * The mod_quizgame game_started event. + * + * @package mod_quizgame + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_quizgame\event; +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_quizgame game_started event class. + * + * @package mod_quizgame + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class game_started extends \core\event\base { + + /** + * Set basic properties for the event. + */ + protected function init() { + $this->data['objecttable'] = 'quizgame'; + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * Returns localised general event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventgamestarted', 'mod_quizgame'); + } + + /** + * Get URL related to the action. + * + * @return \moodle_url + */ + public function get_url() { + return new \moodle_url('/mod/quizgame/view.php', array('id' => $this->contextinstanceid)); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' started the quizventure with course module id '$this->contextinstanceid'."; + } + + + public static function get_objectid_mapping() { + return array('db' => 'quizgame', 'restore' => 'quizgame'); + } +} diff --git a/classes/external.php b/classes/external.php new file mode 100644 index 0000000..52014c5 --- /dev/null +++ b/classes/external.php @@ -0,0 +1,152 @@ +. + +/** + * Quizgame external API + * + * @package mod_quizgame + * @category external + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 3.5 + */ + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->libdir . '/externallib.php'); +require_once($CFG->dirroot . '/mod/quizgame/locallib.php'); + +/** + * Quizgame external functions + * + * @package mod_quizgame + * @category external + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 3.5 + */ +class mod_quizgame_external extends external_api { + + + /** + * Returns description of method parameters + * @return external_function_parameters + */ + public static function update_score_parameters() { + // Update_score_parameters() always return an external_function_parameters(). + // The external_function_parameters constructor expects an array of external_description. + return new external_function_parameters( + // An external_description can be: external_value, external_single_structure or external_multiple structure. + array('quizgameid' => new external_value(PARAM_INT, 'quizgame instance ID'), + 'score' => new external_value(PARAM_INT, 'Player final score'), + ) + ); + } + + /** + * The function itself + * @return string welcome message + */ + public static function update_score($quizgameid, $score) { + + global $DB; + $warnings = array(); + $params = self::validate_parameters(self::update_score_parameters(), + array( + 'quizgameid' => $quizgameid, + 'score' => $score + )); + if (!$quizgame = $DB->get_record("quizgame", array("id" => $params['quizgameid']))) { + throw new moodle_exception("invalidcoursemodule", "error"); + } + + $cm = get_coursemodule_from_instance('quizgame', $quizgame->id, 0, false, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + + // Validate the context and check capabilities. + $context = context_module::instance($cm->id); + self::validate_context($context); + + require_capability('mod/quizgame:view', $context); + + // Record the high score. + $id = quizgame_add_highscore($quizgame, $score); + + return $id; + } + + /** + * Returns description of method result value + * @return external_description + */ + public static function update_score_returns() { + return new external_value(PARAM_INT, 'id of score entry'); + } + + + + /** + * Returns description of method parameters + * @return external_function_parameters + */ + public static function start_game_parameters() { + // Update_score_parameters() always return an external_function_parameters(). + // The external_function_parameters constructor expects an array of external_description. + return new external_function_parameters( + // An external_description can be: external_value, external_single_structure or external_multiple structure. + array('quizgameid' => new external_value(PARAM_INT, 'quizgame instance ID')) + ); + } + + /** + * The function itself + * @return string welcome message + */ + public static function start_game($quizgameid) { + + global $DB; + $warnings = array(); + $params = self::validate_parameters(self::start_game_parameters(), + array('quizgameid' => $quizgameid) + ); + if (!$quizgame = $DB->get_record("quizgame", array("id" => $params['quizgameid']))) { + throw new moodle_exception("invalidcoursemodule", "error"); + } + + $cm = get_coursemodule_from_instance('quizgame', $quizgame->id, 0, false, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + + // Validate the context and check capabilities. + $context = context_module::instance($cm->id); + self::validate_context($context); + + require_capability('mod/quizgame:view', $context); + + // Record the game as started. + $result = quizgame_log_game_start($quizgame); + + return $result; + } + + /** + * Returns description of method result value + * @return external_description + */ + public static function start_game_returns() { + return new external_value(PARAM_BOOL, 'Result of logging game start'); + } + +} diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php new file mode 100644 index 0000000..dbc2b11 --- /dev/null +++ b/classes/privacy/provider.php @@ -0,0 +1,215 @@ +. + +/** + * Privacy Subsystem implementation for mod_quizgame. + * + * @package mod_quizgame + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_quizgame\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\deletion_criteria; +use core_privacy\local\request\helper; +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Implementation of the privacy subsystem plugin provider for the quizgame activity module. + * + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + // This plugin stores personal data. + \core_privacy\local\metadata\provider, + + // This plugin is a core_user_data_provider. + \core_privacy\local\request\plugin\provider { + /** + * Return the fields which contain personal data. + * + * @param collection $items a reference to the collection to use to store the metadata. + * @return collection the updated collection of metadata items. + */ + public static function get_metadata(collection $items) : collection { + $items->add_database_table( + 'quizgame_scores', + [ + 'quizgameid' => 'privacy:metadata:quizgame_scores:quizgameid', + 'userid' => 'privacy:metadata:quizgame_scores:userid', + 'score' => 'privacy:metadata:quizgame_scores:score', + 'timecreated' => 'privacy:metadata:quizgame_scores:timecreated', + ], + 'privacy:metadata:quizgame_scores' + ); + + return $items; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid the userid. + * @return contextlist the list of contexts containing user info for the user. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + // Fetch all quizgame scores. + $sql = "SELECT c.id + FROM {context} c + INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname + INNER JOIN {quizgame} qg ON qg.id = cm.instance + INNER JOIN {quizgame_scores} qgs ON qgs.quizgameid = qg.id + WHERE qgs.userid = :userid"; + + $params = [ + 'modname' => 'quizgame', + 'contextlevel' => CONTEXT_MODULE, + 'userid' => $userid, + ]; + $contextlist = new contextlist(); + $contextlist->add_from_sql($sql, $params); + + return $contextlist; + } + + /** + * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist. + * + * @param approved_contextlist $contextlist a list of contexts approved for export. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + + if (empty($contextlist->count())) { + return; + } + + $user = $contextlist->get_user(); + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + + $sql = "SELECT cm.id AS cmid, + qgs.score, + qgs.timecreated + FROM {context} c + INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname + INNER JOIN {quizgame} qg ON qg.id = cm.instance + INNER JOIN {quizgame_scores} qgs ON qgs.quizgameid = qg.id + WHERE c.id {$contextsql} + AND qgs.userid = :userid + ORDER BY cm.id"; + + $params = ['modname' => 'quizgame', 'contextlevel' => CONTEXT_MODULE, 'userid' => $user->id] + $contextparams; + + // Reference to the quizgame activity seen in the last iteration of the loop. By comparing this with the current record, and + // because we know the results are ordered, we know when we've moved to the scores for a new quizgame activity and therefore + // when we can export the complete data for the last activity. + $lastcmid = null; + + $quizgamescores = $DB->get_recordset_sql($sql, $params); + foreach ($quizgamescores as $quizgamescore) { + // If we've moved to a new quizgame, then write the last quizgame data and reinit the quizgame data array. + if ($lastcmid != $quizgamescore->cmid) { + if (!empty($quizgamedata)) { + $context = \context_module::instance($lastcmid); + self::export_quizgame_data_for_user($quizgamedata, $context, $user); + } + $quizgamedata = [ + 'score' => [], + 'timecreated' => [], + ]; + } + $quizgamedata['score'][] = $quizgamescore->score; + $quizgamedata['timecreated'][] = \core_privacy\local\request\transform::datetime($quizgamescore->timecreated); + $lastcmid = $quizgamescore->cmid; + } + $quizgamescores->close(); + + // The data for the last activity won't have been written yet, so make sure to write it now! + if (!empty($quizgamedata)) { + $context = \context_module::instance($lastcmid); + self::export_quizgame_data_for_user($quizgamedata, $context, $user); + } + } + + /** + * Export the supplied personal data for a single quizgame activity, along with any generic data or area files. + * + * @param array $quizgamedata the personal data to export for the quizgame. + * @param \context_module $context the context of the quizgame. + * @param \stdClass $user the user record + */ + protected static function export_quizgame_data_for_user(array $quizgamedata, \context_module $context, \stdClass $user) { + // Fetch the generic module data for the quizgame. + $contextdata = helper::get_context_data($context, $user); + + // Merge with quizgame data and write it. + $contextdata = (object)array_merge((array)$contextdata, $quizgamedata); + writer::with_context($context)->export_data([], $contextdata); + + // Write generic module intro files. + helper::export_context_files($context, $user); + } + + /** + * Delete all data for all users in the specified context. + * + * @param \context $context the context to delete in. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $DB; + + if (!$context instanceof \context_module) { + return; + } + + if ($cm = get_coursemodule_from_id('quizgame', $context->instanceid)) { + $DB->delete_records('quizgame_scores', ['quizgameid' => $cm->instance]); + } + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist a list of contexts approved for deletion. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + if (empty($contextlist->count())) { + return; + } + + $userid = $contextlist->get_user()->id; + foreach ($contextlist->get_contexts() as $context) { + + if (!$context instanceof \context_module) { + continue; + } + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + $DB->delete_records('quizgame_scores', ['quizgameid' => $instanceid, 'userid' => $userid]); + } + } +} diff --git a/classes/table_scores.php b/classes/table_scores.php new file mode 100644 index 0000000..0c788ee --- /dev/null +++ b/classes/table_scores.php @@ -0,0 +1,92 @@ +. + +/** + * table_sql class for viewing and exporting player scores. + * + * @package mod_quizgame + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/tablelib.php'); + +/** + * table_sql class for viewing and exporting player scores. + * + * @package mod_quizgame + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class table_scores extends table_sql { + + /** @var array list of user fullnames shown in report */ + private $userfullnames = array(); + + /** + * Constructor + * @param int $uniqueid all tables have to have a unique id, this is used + * as a key when storing table properties like sort order in the session. + */ + public function __construct($uniqueid) { + parent::__construct($uniqueid); + // Define the list of columns to show. + $columns = array('userid', 'score', 'timecreated'); + $this->define_columns($columns); + + // Define the titles of columns to show in header. + $headers = array(get_string('user'), get_string('scoreheader', 'mod_quizgame'), get_string('date')); + $this->define_headers($headers); + } + + /** + * Generate the time column. + * + * @param stdClass $record data. + * @return string HTML for the time column + */ + public function col_timecreated($record) { + + if (empty($this->download)) { + $dateformat = get_string('strftimerecentfull', 'core_langconfig'); + } else { + $dateformat = get_string('strftimedatetimeshort', 'core_langconfig'); + } + return userdate($record->timecreated, $dateformat); + } + + public function col_userid($record) { + global $DB; + + if (!empty($this->userfullnames[$record->userid])) { + return $this->userfullnames[$record->userid]; + } + + // If we reach that point new users logs have been generated since the last users db query. + list($usql, $uparams) = $DB->get_in_or_equal($record->userid); + $sql = "SELECT id," . get_all_user_name_fields(true) . " FROM {user} WHERE id " . $usql; + if (!$user = $DB->get_records_sql($sql, $uparams)) { + // This should never happen. + return 'UNKNOWN'; + } + + $this->userfullnames[$record->userid] = fullname($user[$record->userid]); + return $this->userfullnames[$record->userid]; + } +} diff --git a/db/access.php b/db/access.php index 6a6e5b2..b2c8499 100644 --- a/db/access.php +++ b/db/access.php @@ -70,5 +70,15 @@ 'manager' => CAP_ALLOW ) ), + + 'mod/quizgame:viewallscores' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + ), ); diff --git a/db/install.xml b/db/install.xml index ef8e146..8ed8946 100755 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -15,6 +15,7 @@ + @@ -29,6 +30,7 @@ + diff --git a/db/services.php b/db/services.php new file mode 100644 index 0000000..5b5f207 --- /dev/null +++ b/db/services.php @@ -0,0 +1,47 @@ +. + +/** + * Quizgame external functions and service definitions. + * + * @package mod_quizgame + * @category external + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 3.5 + */ + +defined('MOODLE_INTERNAL') || die; + +$functions = array( + + 'mod_quizgame_update_score' => array( + 'classname' => 'mod_quizgame_external', + 'methodname' => 'update_score', + 'description' => 'Record the score and write to the database.', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => 'mod/quizgame:view', + ), + 'mod_quizgame_start_game' => array( + 'classname' => 'mod_quizgame_external', + 'methodname' => 'start_game', + 'description' => 'Log the player starting the game', + 'type' => 'write', + 'ajax' => true, + 'capabilities' => 'mod/quizgame:view', + ) +); diff --git a/db/upgrade.php b/db/upgrade.php index 4085265..df3214f 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -87,6 +87,36 @@ function xmldb_quizgame_upgrade($oldversion) { upgrade_mod_savepoint(true, 2017011100, 'quizgame'); } + if ($oldversion < 2018062000) { + + // Define field timecreated to be added to quizgame_scores. + $table = new xmldb_table('quizgame_scores'); + $field = new xmldb_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'score'); + + // Conditionally launch add field timecreated. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Quizgame savepoint reached. + upgrade_mod_savepoint(true, 2018062000, 'quizgame'); + } + + if ($oldversion < 2018062001) { + + // Define field completionscore to be added to quizgame. + $table = new xmldb_table('quizgame'); + $field = new xmldb_field('completionscore', XMLDB_TYPE_INTEGER, '20', null, null, null, null, 'grade'); + + // Conditionally launch add field completionscore. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Quizgame savepoint reached. + upgrade_mod_savepoint(true, 2018062001, 'quizgame'); + } + // Final return of upgrade result (true, all went good) to Moodle. return true; } diff --git a/grade.php b/grade.php index ee04fd5..50d3e04 100644 --- a/grade.php +++ b/grade.php @@ -30,5 +30,9 @@ $itemnumber = optional_param('itemnumber', 0, PARAM_INT); $userid = optional_param('userid', 0, PARAM_INT); // Graded user ID (optional). +$cm = get_coursemodule_from_id('quizgame', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +require_login($course, false, $cm); + // In the simplest case just redirect to the view page. redirect('view.php?id='.$id); diff --git a/lang/en/quizgame.php b/lang/en/quizgame.php index b2ede27..4669b96 100644 --- a/lang/en/quizgame.php +++ b/lang/en/quizgame.php @@ -28,8 +28,20 @@ defined('MOODLE_INTERNAL') || die(); +$string['achievedhighscoreof'] = 'Acheived a high score of {$a}'; +$string['attempt'] = 'Attempt #{$a}'; +$string['completionscore'] = 'Student must achieve a minimum score of:'; +$string['completionscoregroup'] = 'Require score'; +$string['completionscoregroup_help'] = 'If enabled, you can require a minimum score is met before the activity is marked as complete. + +Each question is worth 1000 points when answered correctly on the first try, so you may want to set the default to: + +(Number of questions x 1000)'; $string['endofgame'] = 'Your score was: {$a}. Press space or click to restart.'; $string['emptyquiz'] = 'There are no multiple choice questions in the selected category.'; +$string['eventgamestarted'] = 'Quizventure game started'; +$string['eventgamescoreadded'] = 'Quizventure score recorded'; +$string['eventgamescoresviewed'] = 'Quizventure scores viewed'; $string['fullscreen'] = 'Fullscreen'; $string['modulename_help'] = 'Students procastinating too much? Are they playing games instead of studying? Well now you can motivate them by allowing them to do both at once! @@ -38,8 +50,17 @@ **Note**: Quizventure is designed to promote learning rather than for assessment. Students will have infinite attempts with instant feedback. For this reason, only add questions you want students to learn the answer to, rather than questions you want to assess if they have learned'; $string['modulenameplural'] = 'Quizventure games'; $string['modulename'] = 'Quizventure'; +$string['notyetplayed'] = 'Not yet played'; +$string['achievedhighscoreof'] = 'Acheived a high score of {$a}'; +$string['playedxtimeswithhighscore'] = 'Played {$a->times} times. The last game ended with a high score of {$a->score}'; $string['pluginadministration'] = 'Quizventure administration'; $string['pluginname'] = 'Quizventure'; +$string['playerscores'] = 'Player scores'; +$string['privacy:metadata:quizgame_scores'] = 'Information about the user\'s chosen answer(s) for a given choice activity'; +$string['privacy:metadata:quizgame_scores:quizgameid'] = 'The ID of the quizgame activity the user is providing answer for'; +$string['privacy:metadata:quizgame_scores:score'] = 'The score of the user during that playthrough.'; +$string['privacy:metadata:quizgame_scores:timecreated'] = 'The timestamp indicating when the quizgame was played by the user'; +$string['privacy:metadata:quizgame_scores:userid'] = 'The ID of the user playing this quizgame activity'; $string['questioncategory'] = 'Question category'; $string['questioncategory_help'] = 'Select the category from the question bank to use in the game. @@ -52,6 +73,11 @@ $string['quizgame'] = 'Quizventure'; $string['quizgame:addinstance'] = 'Add a Quizventure instance'; $string['quizgame:view'] = 'View Quizventure'; +$string['quizgame:viewallscores'] = 'View player scores'; +$string['removescores'] = 'Remove all user scores'; $string['score'] = 'Score: {$a->score} Lives: {$a->lives}'; +$string['scoreheader'] = 'Score'; +$string['scoreslink'] = 'View all attempts'; +$string['scoreslinkhelp'] = 'View all player attempts and scores'; $string['spacetostart'] = 'Press space or click to start'; $string['sound'] = 'Sound'; diff --git a/lib.php b/lib.php index ce67f9b..5389b85 100644 --- a/lib.php +++ b/lib.php @@ -39,6 +39,10 @@ */ function quizgame_supports($feature) { switch($feature) { + case FEATURE_COMPLETION_TRACKS_VIEWS: + return true; + case FEATURE_COMPLETION_HAS_RULES: + return true; case FEATURE_MOD_INTRO: return true; case FEATURE_SHOW_DESCRIPTION: @@ -111,9 +115,8 @@ function quizgame_delete_instance($id) { return false; } - // TODO: Delete highscores. - $DB->delete_records('quizgame', array('id' => $quizgame->id)); + $DB->delete_records('quizgame_scores', array('quizgameid' => $quizgame->id)); return true; } @@ -133,10 +136,29 @@ function quizgame_delete_instance($id) { */ function quizgame_user_outline($course, $user, $mod, $quizgame) { - $return = new stdClass(); - $return->time = 0; - $return->info = ''; - return $return; + global $DB; + if ($game = $DB->count_records('quizgame_scores', array('quizgameid' => $quizgame->id, 'userid' => $user->id))) { + $result = new stdClass(); + + if ($game > 0) { + $games = $DB->get_records('quizgame_scores', + array('quizgameid' => $quizgame->id, 'userid' => $user->id), 'timecreated DESC', '*', 0, 1); + foreach ($games as $last) { + $data = new stdClass(); + $data->score = $last->score; + $data->times = $game; + $result->info = get_string("playedxtimeswithhighscore", "quizgame", $data); + $result->time = $last->timecreated; + } + } else { + $result->info = get_string("notyetplayed", "quizgame"); + + } + + return $result; + } + return null; + } /** @@ -147,9 +169,65 @@ function quizgame_user_outline($course, $user, $mod, $quizgame) { * @param stdClass $user the record of the user we are generating report for * @param cm_info $mod course module info * @param stdClass $quizgame the module instance record - * @return void, is supposed to echp directly + * @return void, is supposed to echo directly */ function quizgame_user_complete($course, $user, $mod, $quizgame) { + global $DB; + + if ($games = $DB->get_records('quizgame_scores', + array('quizgameid' => $quizgame->id, 'userid' => $user->id), + 'timecreated ASC')) { + $attempt = 1; + foreach ($games as $game) { + + echo get_string('attempt', 'quizgame', $attempt++) . ': '; + echo get_string('achievedhighscoreof', 'quizgame', $game->score); + echo ' - '.userdate($game->timecreated).'
'; + } + } else { + print_string("notyetplayed", "quizgame"); + } + +} + +/** + * Obtains the automatic completion state for this quizgame based on any conditions + * in quizgame settings. + * + * @global object $DB + * @param object $course Course + * @param object $cm Course-module + * @param int $userid User ID + * @param bool $type Type of comparison (or/and; can be used as return value if no conditions) + * @return bool True if completed, false if not. (If no conditions, then return + * value depends on comparison type) + */ +function quizgame_get_completion_state($course, $cm, $userid, $type) { + global $DB; + + // Get quizgame details. + if (!($quizgame = $DB->get_record('quizgame', array('id' => $cm->instance)))) { + throw new Exception("Can't find quizgame {$cm->instance}"); + } + + // Default return value. + $result = $type; + if ($quizgame->completionscore) { + $where = ' quizgameid = :quizgameid AND userid = :userid AND score >= :score'; + $params = array( + 'quizgameid' => $quizgame->id, + 'userid' => $userid, + 'score' => $quizgame->completionscore, + ); + $value = $DB->count_records_select('quizgame_scores', $where, $params) > 0; + if ($type == COMPLETION_AND) { + $result = $result && $value; + } else { + $result = $result || $value; + } + } + + return $result; } /** @@ -256,7 +334,6 @@ function quizgame_scale_used($quizgameid, $scaleid) { * @return boolean true if the scale is used by any quizgame instance */ function quizgame_scale_used_anywhere($scaleid) { - global $DB; return false; } @@ -293,7 +370,7 @@ function quizgame_grade_item_update(stdClass $quizgame, $grades=null) { * @return void */ function quizgame_update_grades(stdClass $quizgame, $userid = 0) { - global $CFG, $DB; + global $CFG; require_once($CFG->libdir.'/gradelib.php'); $grades = array(); // Populate array of grade objects indexed by userid. @@ -354,7 +431,6 @@ function quizgame_get_file_info($browser, $areas, $course, $cm, $context, $filea * @param array $options additional options affecting the file serving */ function quizgame_pluginfile($course, $cm, $context, $filearea, array $args, $forcedownload, array $options=array()) { - global $DB, $CFG; if ($context->contextlevel != CONTEXT_MODULE) { send_file_not_found(); @@ -392,3 +468,71 @@ function quizgame_extend_navigation(navigation_node $navref, stdclass $course, s */ function quizgame_extend_settings_navigation(settings_navigation $settingsnav, navigation_node $quizgamenode=null) { } + +/** + * Implementation of the function for printing the form elements that control + * whether the course reset functionality affects the quizgame. + * @param stdClass $mform form passed by reference + */ +function quizgame_reset_course_form_definition(&$mform) { + + $mform->addElement('header', 'quizgameheader', get_string('modulenameplural', 'quizgame')); + $mform->addElement('advcheckbox', 'reset_quizgame_scores', get_string('removescores', 'quizgame')); + +} + +/** + * Course reset form defaults. + * @return array + */ +function quizgame_reset_course_form_defaults($course) { + return array('reset_quizgame_scores' => 1); + +} + +/** + * Actual implementation of the rest coures functionality, delete all the + * quizgame responses for course $data->courseid. + * + * @global stdClass + * @param $data the data submitted from the reset course. + * @return array status array + */ +function quizgame_reset_userdata($data) { + global $DB; + $componentstr = get_string('modulenameplural', 'quizgame'); + $status = array(); + + if (!empty($data->reset_quizgame_scores)) { + $scoresql = "SELECT qg.id + FROM {quizgame} qg + WHERE qg.course=?"; + + $DB->delete_records_select('quizgame_scores', "quizgameid IN ($scoresql)", array($data->courseid)); + $status[] = array('component' => $componentstr, 'item' => get_string('removescores', 'quizgame'), 'error' => false); + } + + return $status; +} + +/** + * Removes all grades from gradebook + * + * @global stdClass + * @param int $courseid + * @param string optional type + */ +// TODO: LOOK AT AFTER GRADES ARE IMPLEMENTED! +function quizgame_reset_gradebook($courseid, $type='') { + global $DB; + + $sql = "SELECT g.*, cm.idnumber as cmidnumber, g.course as courseid + FROM {quizgame} g, {course_modules} cm, {modules} m + WHERE m.name='quizgame' AND m.id=cm.module AND cm.instance=g.id AND g.course=?"; + + if ($quizgames = $DB->get_records_sql($sql, array($courseid))) { + foreach ($quizgames as $quizgame) { + quizgame_grade_item_update($quizgame, 'reset'); + } + } +} \ No newline at end of file diff --git a/locallib.php b/locallib.php index 749a4e0..dab083c 100644 --- a/locallib.php +++ b/locallib.php @@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/questionlib.php'); +require_once($CFG->dirroot.'/lib/completionlib.php'); /** * Function to prepare strings to be printed out as JSON. @@ -41,3 +42,72 @@ function quizgame_cleanup($string) { $string = preg_replace('/[\n\r]/', ' ', $string); return $string; } +/** + * Function to add the students score to the DB. + * @global type $USER + * @global type $DB + * @param type $quizgame + * @param type $score + * @return type + */ +function quizgame_add_highscore($quizgame, $score) { + global $USER, $DB; + + $cm = get_coursemodule_from_instance('quizgame', $quizgame->id, 0, false, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + $context = context_module::instance($cm->id); + + // Write the high score to the DB. + $record = new stdClass(); + $record->quizgameid = $quizgame->id; + $record->userid = $USER->id; + $record->score = $score; + $record->timecreated = time(); + $record->id = $DB->insert_record('quizgame_scores', $record); + + // Trigger the game score added event. + $event = \mod_quizgame\event\game_score_added::create(array( + 'objectid' => $record->id, + 'context' => $context, + 'other' => array('score' => $score) + )); + + $event->add_record_snapshot('quizgame', $quizgame); + $event->add_record_snapshot('quizgame_scores', $record); + $event->trigger(); + + // Update completion state. + $completion = new completion_info($course); + if ($completion->is_enabled($cm) == COMPLETION_TRACKING_AUTOMATIC && $quizgame->completionscore) { + $completion->update_state($cm, COMPLETION_COMPLETE, $record->userid); + } + + return $record->id; +} + +/** + * Function to record the player starting the quizgame. + * @global type $USER + * @global type $DB + * @param type $quizgame + * @param type $score + * @return type + */ +function quizgame_log_game_start($quizgame) { + global $DB; + + $cm = get_coursemodule_from_instance('quizgame', $quizgame->id, 0, false, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + $context = context_module::instance($cm->id); + + // Trigger the game score added event. + $event = \mod_quizgame\event\game_started::create(array( + 'objectid' => $quizgame->id, + 'context' => $context, + )); + + $event->add_record_snapshot('quizgame', $quizgame); + $event->trigger(); + + return true; +} diff --git a/mod_form.php b/mod_form.php index 88699ba..c4c08ad 100644 --- a/mod_form.php +++ b/mod_form.php @@ -77,4 +77,70 @@ public function definition() { // Add standard buttons, common to all modules. $this->add_action_buttons(); } + + /** + * Define custom completion rules + * @return array + */ + public function add_completion_rules() { + $mform =& $this->_form; + $group = array(); + $group[] =& $mform->createElement('checkbox', 'completionscoreenabled', '', + get_string('completionscore', 'quizgame')); + $group[] =& $mform->createElement('text', 'completionscore', '', array('size' => 3)); + $mform->setType('completionscore', PARAM_INT); + $mform->addGroup($group, 'completionscoregroup', + get_string('completionscoregroup', 'quizgame'), array(' '), false); + $mform->disabledIf('completionscore', 'completionscoreenabled', 'notchecked'); + $mform->addHelpButton('completionscoregroup', 'completionscoregroup', 'quizgame'); + return array('completionscoregroup'); + } + + /** + * Determines if custom criteria is active. + * @param array $data + * @return bool + */ + public function completion_rule_enabled($data) { + return (!empty($data['completionscoreenabled']) && $data['completionscore'] != 0); + } + + /** + * Loads custom completion data. + * @return boolean + */ + public function get_data() { + $data = parent::get_data(); + if (!$data) { + return false; + } + if (!empty($data->completionunlocked)) { + // Turn off completion settings if the checkboxes aren't ticked. + $autocompletion = !empty($data->completion) && $data->completion == COMPLETION_TRACKING_AUTOMATIC; + if (empty($data->completionscoreenabled) || !$autocompletion) { + $data->completionscore = 0; + } + } + return $data; + } + + /** + * Used to pre-populate mform. + * @param array $defaultvalues + */ + public function data_preprocessing(&$defaultvalues) { + parent::data_preprocessing($defaultvalues); + + // Set up the completion checkboxes which aren't part of standard data. + // We also make the default value (if you turn on the checkbox) for those + // numbers to be 1, this will not apply unless checkbox is ticked. + if (!empty($defaultvalues['completionscore'])) { + $defaultvalues['completionscoreenabled'] = 1; + } else { + $defaultvalues['completionscoreenabled'] = 0; + } + if (empty($defaultvalues['completionscore'])) { + $defaultvalues['completionscore'] = 10000; + } + } } diff --git a/nbproject/private/private.xml b/nbproject/private/private.xml new file mode 100644 index 0000000..4750962 --- /dev/null +++ b/nbproject/private/private.xml @@ -0,0 +1,4 @@ + + + + diff --git a/renderer.php b/renderer.php index c95bb07..385fd18 100644 --- a/renderer.php +++ b/renderer.php @@ -25,7 +25,7 @@ class mod_quizgame_renderer extends plugin_renderer_base { * @return string The HTML code of the game */ public function render_game($quizgame, $context) { - global $DB, $OUTPUT; + global $DB; $categoryid = explode(',', $quizgame->questioncategory)[0]; $questionids = array_keys($DB->get_records('question', array('category' => intval($categoryid)), '', 'id')); @@ -60,7 +60,7 @@ public function render_game($quizgame, $context) { } } - $this->page->requires->js_call_amd('mod_quizgame/quizgame', 'init', array($qjson)); + $this->page->requires->js_call_amd('mod_quizgame/quizgame', 'init', array($qjson, $quizgame->id)); $display = ''; $display .= ''; - $display .= ''; $display .= html_writer::checkbox('sound', '', false, get_string('sound', 'mod_quizgame'), @@ -85,4 +85,14 @@ public function render_game($quizgame, $context) { return $display; } + public function render_score_link($quizgame) { + + $url = new moodle_url('/mod/quizgame/scores.php', array('id' => $quizgame->id)); + $scorestring = get_string('scoreslink', 'quizgame'); + $scorestringhelp = get_string('scoreslinkhelp', 'quizgame'); + $display = html_writer::start_tag('div', array('class' => 'quizgame-scores')); + $display .= html_writer::tag('a', $scorestring, array('title' => $scorestringhelp, 'href' => $url)); + $display .= html_writer::end_tag('div'); + return $display; + } } diff --git a/scores.php b/scores.php new file mode 100644 index 0000000..7ffe80d --- /dev/null +++ b/scores.php @@ -0,0 +1,87 @@ +. + +/** + * Displays the high scores for a quizgame + * + * + * @package mod_quizgame + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(dirname(dirname(__FILE__))).'/config.php'); +require_once($CFG->dirroot . '/mod/quizgame/classes/table_scores.php'); + +$id = optional_param('id', 0, PARAM_INT); // The Quizgame instance. +$download = optional_param('download', '', PARAM_ALPHA); + +if ($id) { + $quizgame = $DB->get_record('quizgame', array('id' => $id), '*', MUST_EXIST); + $course = $DB->get_record('course', array('id' => $quizgame->course), '*', MUST_EXIST); + $cm = get_coursemodule_from_instance('quizgame', $quizgame->id, $course->id, false, MUST_EXIST); +} else { + error('You must specify a course_module ID or an instance ID'); +} + +require_login($course, true, $cm); +$context = context_module::instance($cm->id); +require_capability('mod/quizgame:viewallscores', $context); + + +// Trigger scores viewed event. +$event = \mod_quizgame\event\game_scores_viewed::create(array( + 'objectid' => $quizgame->id, + 'context' => $context, +)); + +$event->add_record_snapshot('quizgame', $quizgame); +$event->trigger(); + +// Print the page header. +$PAGE->set_url('/mod/quizgame/scores.php', array('id' => $quizgame->id)); +$PAGE->set_context($context); + +// Generate the table. +$table = new table_scores('quizgame-scores'); +$table->is_downloading($download, 'scores', 'scores'); +if (!$table->is_downloading()) { + // Only print headers if not asked to download data. + // Print the page header. + $PAGE->set_title(format_string($quizgame->name)); + $PAGE->set_heading(format_string($course->fullname)); + $url = new moodle_url('/mod/quizgame/scores.php', array('id' => $quizgame->id)); + $PAGE->navbar->add(get_string('playerscores', 'mod_quizgame'), $url); + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('modulename', 'mod_quizgame')); +} + +// Work out the sql for the table. +$sqlconditions = 'quizgameid = :quizgameid'; +$sqlparams = array('quizgameid' => $quizgame->id); +$table->set_sql('*', "{quizgame_scores}", $sqlconditions, $sqlparams); + +$table->define_baseurl($PAGE->url); +$columns = array('userid', 'score', 'timecreated'); +$headers = array(get_string('user'), get_string('scoreheader', 'mod_quizgame'), get_string('date')); +$table->define_columns($columns); +$table->define_headers($headers); +$table->sortable(true, 'timecreated', SORT_DESC); +$table->out(20, true); + +if (!$table->is_downloading()) { + echo $OUTPUT->footer(); +} diff --git a/styles.css b/styles.css index 19c88e6..b780921 100644 --- a/styles.css +++ b/styles.css @@ -1,11 +1,12 @@ -.path-mod-quizgame div.fontloader -{ - visibility:hidden; +.path-mod-quizgame div.fontloader { + visibility: hidden; font-family: 'Audiowide', Sans; } -.path-mod-quizgame #mod_quizgame_game -{ +.path-mod-quizgame #mod_quizgame_game { background-color: #000; width: 100%; height: 100%; } +.quizgame-scores { + text-align: center; +} diff --git a/tests/events_test.php b/tests/events_test.php new file mode 100644 index 0000000..a0ccebf --- /dev/null +++ b/tests/events_test.php @@ -0,0 +1,204 @@ +. + +/** + * Unit tests for lib.php + * + * @package mod_quizgame + * @category test + * @copyright 2015 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/quizgame/locallib.php'); + +/** + * Unit tests for quizgame events. + * + * @package mod_quizgame + * @category test + * @copyright 2015 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_quizgame_event_testcase extends advanced_testcase { + + /** + * Test setup. + * @global stdclass $DB + */ + public function setUp() { + $this->resetAfterTest(); + } + + /** + * Test the course_module_viewed event. + * @global stdclass $DB + */ + public function test_course_module_viewed() { + global $DB; + // There is no proper API to call to trigger this event, so what we are + // doing here is simply making sure that the events returns the right information. + + $course = $this->getDataGenerator()->create_course(); + $quizgame = $this->getDataGenerator()->create_module('quizgame', array('course' => $course->id)); + + $dbcourse = $DB->get_record('course', array('id' => $course->id)); + $dbquizgame = $DB->get_record('quizgame', array('id' => $quizgame->id)); + $context = context_module::instance($quizgame->cmid); + + $event = \mod_quizgame\event\course_module_viewed::create(array( + 'objectid' => $dbquizgame->id, + 'context' => $context, + )); + + $event->add_record_snapshot('course', $dbcourse); + $event->add_record_snapshot('quizgame', $dbquizgame); + + // Triggering and capturing the event. + $sink = $this->redirectEvents(); + $event->trigger(); + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\mod_quizgame\event\course_module_viewed', $event); + $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); + $this->assertEquals($quizgame->cmid, $event->contextinstanceid); + $this->assertEquals($quizgame->id, $event->objectid); + $expected = array($course->id, 'quizgame', 'view', 'view.php?id=' . $quizgame->cmid, + $quizgame->id, $quizgame->cmid); + + $this->assertEquals(new moodle_url('/mod/quizgame/view.php', array('id' => $quizgame->cmid)), $event->get_url()); + $this->assertEventContextNotUsed($event); + } + + /** + * Test the course_module_instance_list_viewed event. + * @global stdclass $DB + */ + public function test_course_module_instance_list_viewed() { + // There is no proper API to call to trigger this event, so what we are + // doing here is simply making sure that the events returns the right information. + + $course = $this->getDataGenerator()->create_course(); + + $event = \mod_quizgame\event\course_module_instance_list_viewed::create(array( + 'context' => context_course::instance($course->id) + )); + + // Triggering and capturing the event. + $sink = $this->redirectEvents(); + $event->trigger(); + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\mod_quizgame\event\course_module_instance_list_viewed', $event); + $this->assertEquals(CONTEXT_COURSE, $event->contextlevel); + $this->assertEquals($course->id, $event->contextinstanceid); + $expected = array($course->id, 'quizgame', 'view all', 'index.php?id='.$course->id, ''); + $this->assertEventLegacyLogData($expected, $event); + $this->assertEventContextNotUsed($event); + } + + /** + * Test the score_added event. + * @global stdclass $DB + */ + public function test_score_added() { + + $this->setAdminUser(); + $course = $this->getDataGenerator()->create_course(); + $quizgame = $this->getDataGenerator()->create_module('quizgame', array('course' => $course)); + $context = context_module::instance($quizgame->cmid); + $score = mt_rand (0, 50000); + + $sink = $this->redirectEvents(); + $result = quizgame_add_highscore($quizgame, $score); + + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\mod_quizgame\event\game_score_added', $event); + $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); + $this->assertEquals($quizgame->cmid, $event->contextinstanceid); + $this->assertEquals($score, $event->other['score']); + } + + /** + * Test the game_started event. + * @global stdclass $DB + */ + public function test_game_started() { + + $this->setAdminUser(); + $course = $this->getDataGenerator()->create_course(); + $quizgame = $this->getDataGenerator()->create_module('quizgame', array('course' => $course)); + $context = context_module::instance($quizgame->cmid); + + $sink = $this->redirectEvents(); + $result = quizgame_log_game_start($quizgame); + + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\mod_quizgame\event\game_started', $event); + $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); + $this->assertEquals($quizgame->cmid, $event->contextinstanceid); + } + + /** + * Test the game_scores_viewed event. + * @global stdclass $DB + */ + public function test_game_scores_viewed() { + // There is no proper API to call to trigger this event, so what we are + // doing here is simply making sure that the events returns the right information. + + $this->setAdminUser(); + $course = $this->getDataGenerator()->create_course(); + $quizgame = $this->getDataGenerator()->create_module('quizgame', array('course' => $course)); + $context = context_module::instance($quizgame->cmid); + + $quizgamegenerator = $this->getDataGenerator()->get_plugin_generator('mod_quizgame'); + $scores = $quizgamegenerator->create_content($quizgame); + + $event = \mod_quizgame\event\game_scores_viewed::create(array( + 'objectid' => $quizgame->id, + 'context' => $context + )); + + $sink = $this->redirectEvents(); + $event->trigger(); + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\mod_quizgame\event\game_scores_viewed', $event); + $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); + $this->assertEquals($quizgame->cmid, $event->contextinstanceid); + } +} diff --git a/tests/generator/lib.php b/tests/generator/lib.php new file mode 100644 index 0000000..28b3f78 --- /dev/null +++ b/tests/generator/lib.php @@ -0,0 +1,64 @@ +. + +/** + * mod_quizgame data generator. + * + * @package mod_quizgame + * @category test + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * mod_quizgame data generator class. + * + * @package mod_quizgame + * @category test + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_quizgame_generator extends testing_module_generator { + + public function create_instance($record = null, array $options = null) { + + // Add default values for quizgame. + $record = (array)$record + array( + 'questioncategory' => 0, + 'grade' => 100, + 'completionscore' => 0, + ); + + return parent::create_instance($record, (array)$options); + } + + public function create_content($quizgame, $record = array()) { + global $DB, $USER; + $now = time(); + $record = (array)$record + array( + 'quizgameid' => $quizgame->id, + 'timecreated' => $now, + 'userid' => $USER->id, + 'score' => mt_rand (0, 50000), + ); + + $id = $DB->insert_record('quizgame_scores', $record); + + return $DB->get_record('quizgame_scores', array('id' => $id), '*', MUST_EXIST); + } +} diff --git a/tests/generator_test.php b/tests/generator_test.php new file mode 100644 index 0000000..abc476d --- /dev/null +++ b/tests/generator_test.php @@ -0,0 +1,75 @@ +. + +/** + * mod_quizgame generator tests + * + * @package mod_quizgame + * @category test + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Genarator tests class for mod_quizgame. + * + * @package mod_quizgame + * @category test + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_quizgame_generator_testcase extends advanced_testcase { + + public function test_create_instance() { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + + $this->assertFalse($DB->record_exists('quizgame', array('course' => $course->id))); + $quizgame = $this->getDataGenerator()->create_module('quizgame', array('course' => $course)); + $records = $DB->get_records('quizgame', array('course' => $course->id), 'id'); + $this->assertCount(1, $records); + $this->assertTrue(array_key_exists($quizgame->id, $records)); + + $params = array('course' => $course->id, 'name' => 'Another quizgame'); + $quizgame = $this->getDataGenerator()->create_module('quizgame', $params); + $records = $DB->get_records('quizgame', array('course' => $course->id), 'id'); + $this->assertCount(2, $records); + $this->assertEquals('Another quizgame', $records[$quizgame->id]->name); + } + + public function test_create_content() { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $quizgame = $this->getDataGenerator()->create_module('quizgame', array('course' => $course)); + $quizgamegenerator = $this->getDataGenerator()->get_plugin_generator('mod_quizgame'); + + $playthrough1 = $quizgamegenerator->create_content($quizgame); + $playthrough2 = $quizgamegenerator->create_content($quizgame, array('score' => 3550)); + $records = $DB->get_records('quizgame_scores', array('quizgameid' => $quizgame->id), 'id'); + $this->assertCount(2, $records); + $this->assertEquals($playthrough1->id, $records[$playthrough1->id]->id); + $this->assertEquals($playthrough2->id, $records[$playthrough2->id]->id); + $this->assertEquals(3550, $records[$playthrough2->id]->score); + } +} diff --git a/tests/privacy_provider_test.php b/tests/privacy_provider_test.php new file mode 100644 index 0000000..355b0ad --- /dev/null +++ b/tests/privacy_provider_test.php @@ -0,0 +1,201 @@ +. + +/** + * Privacy provider tests. + * + * @package mod_quizgame + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\deletion_criteria; +use mod_quizgame\privacy\provider; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy provider tests class. + * + * @package mod_quizgame + * @copyright 2018 Stephen Bourget + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_quizgame_privacy_provider_testcase extends \core_privacy\tests\provider_testcase { + /** @var stdClass The student object. */ + protected $student; + + /** @var stdClass The quizgame object. */ + protected $quizgame; + + /** @var stdClass The course object. */ + protected $course; + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->resetAfterTest(); + + global $DB; + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $quizgame = $this->getDataGenerator()->create_module('quizgame', array('course' => $course)); + + // Create a quizgame activity. + $quizgamegenerator = $this->getDataGenerator()->get_plugin_generator('mod_quizgame'); + + // Create a student which will make a quizgame. + $student = $generator->create_user(); + $studentrole = $DB->get_record('role', ['shortname' => 'student']); + $generator->enrol_user($student->id, $course->id, $studentrole->id); + + // Have the student play through the game. + $playthrough = $quizgamegenerator->create_content($quizgame, array('userid' => $student->id, 'score' => '9999')); + + $this->student = $student; + $this->quizgame = $quizgame; + $this->course = $course; + } + + /** + * Test for provider::get_metadata(). + */ + public function test_get_metadata() { + $collection = new collection('mod_quizgame'); + $newcollection = provider::get_metadata($collection); + $itemcollection = $newcollection->get_collection(); + $this->assertCount(1, $itemcollection); + + $table = reset($itemcollection); + $this->assertEquals('quizgame_scores', $table->get_name()); + + $privacyfields = $table->get_privacy_fields(); + $this->assertArrayHasKey('quizgameid', $privacyfields); + $this->assertArrayHasKey('score', $privacyfields); + $this->assertArrayHasKey('userid', $privacyfields); + $this->assertArrayHasKey('timecreated', $privacyfields); + + $this->assertEquals('privacy:metadata:quizgame_scores', $table->get_summary()); + } + + /** + * Test for provider::get_contexts_for_userid(). + */ + public function test_get_contexts_for_userid() { + $cm = get_coursemodule_from_instance('quizgame', $this->quizgame->id); + + $contextlist = provider::get_contexts_for_userid($this->student->id); + $this->assertCount(1, $contextlist); + $contextforuser = $contextlist->current(); + $cmcontext = context_module::instance($cm->id); + $this->assertEquals($cmcontext->id, $contextforuser->id); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_for_context() { + $cm = get_coursemodule_from_instance('quizgame', $this->quizgame->id); + $cmcontext = context_module::instance($cm->id); + + // Export all of the data for the context. + $this->export_context_data_for_user($this->student->id, $cmcontext, 'mod_quizgame'); + $writer = \core_privacy\local\request\writer::with_context($cmcontext); + $this->assertTrue($writer->has_any_data()); + } + + /** + * Test for provider::delete_data_for_all_users_in_context(). + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $quizgame = $this->quizgame; + $generator = $this->getDataGenerator(); + $quizgamegenerator = $this->getDataGenerator()->get_plugin_generator('mod_quizgame'); + $cm = get_coursemodule_from_instance('quizgame', $this->quizgame->id); + + // Create another student who will play the quizgame activity. + $student = $generator->create_user(); + $studentrole = $DB->get_record('role', ['shortname' => 'student']); + $generator->enrol_user($student->id, $this->course->id, $studentrole->id); + $playthrough = $quizgamegenerator->create_content($quizgame, array('userid' => $student->id, 'score' => '100001')); + + // Before deletion, we should have 2 responses. + $count = $DB->count_records('quizgame_scores', ['quizgameid' => $quizgame->id]); + $this->assertEquals(2, $count); + + // Delete data based on context. + $cmcontext = context_module::instance($cm->id); + provider::delete_data_for_all_users_in_context($cmcontext); + + // After deletion, the quizgame answers for that quizgame activity should have been deleted. + $count = $DB->count_records('quizgame_scores', ['quizgameid' => $quizgame->id]); + $this->assertEquals(0, $count); + } + + /** + * Test for provider::delete_data_for_user(). + */ + public function test_delete_data_for_user_() { + global $DB; + + $quizgame = $this->quizgame; + $generator = $this->getDataGenerator(); + $cm1 = get_coursemodule_from_instance('quizgame', $this->quizgame->id); + + // Create a second quizgame activity. + $params = array('course' => $this->course->id, 'name' => 'Another quizgame'); + $plugingenerator = $generator->get_plugin_generator('mod_quizgame'); + $quizgame2 = $plugingenerator->create_instance($params); + $plugingenerator->create_instance($params); + $cm2 = get_coursemodule_from_instance('quizgame', $quizgame2->id); + + // Make a playthrough for the first student in the 2nd quizgame activity. + $playthrough = $plugingenerator->create_content($quizgame2, array('userid' => $this->student->id, 'score' => '100')); + + // Create another student who will play the first quizgame activity. + $otherstudent = $generator->create_user(); + $studentrole = $DB->get_record('role', ['shortname' => 'student']); + $generator->enrol_user($otherstudent->id, $this->course->id, $studentrole->id); + $playthrough2 = $plugingenerator->create_content($quizgame, array('userid' => $otherstudent->id, 'score' => '999')); + + // Before deletion, we should have 2 responses. + $count = $DB->count_records('quizgame_scores', ['quizgameid' => $quizgame->id]); + $this->assertEquals(2, $count); + + // Now delete the user's data. + $context1 = context_module::instance($cm1->id); + $context2 = context_module::instance($cm2->id); + $contextlist = new \core_privacy\local\request\approved_contextlist($this->student, 'quizgame', + [context_system::instance()->id, $context1->id, $context2->id]); + provider::delete_data_for_user($contextlist); + + // After deletion, the quizgame answers for the first student should have been deleted. + $count = $DB->count_records('quizgame_scores', ['quizgameid' => $quizgame->id, 'userid' => $this->student->id]); + $this->assertEquals(0, $count); + + // Confirm that we only have one quizgame answer available. + $quizgamescores = $DB->get_records('quizgame_scores'); + $this->assertCount(1, $quizgamescores); + $lastresponse = reset($quizgamescores); + + // And that it's the other student's response. + $this->assertEquals($otherstudent->id, $lastresponse->userid); + } +} diff --git a/version.php b/version.php index be57e96..d93140c 100644 --- a/version.php +++ b/version.php @@ -28,7 +28,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2018042900; // If version == 0 then module will not be installed. +$plugin->version = 2018062003; // If version == 0 then module will not be installed. $plugin->requires = 2014051200.00; // Requires this Moodle version (2.7) $plugin->cron = 0; // Period for cron to check this module (secs). diff --git a/view.php b/view.php index c963c29..ffa0ff7 100644 --- a/view.php +++ b/view.php @@ -58,6 +58,10 @@ $event->add_record_snapshot('quizgame', $quizgame); $event->trigger(); +// Mark as viewed. +$completion = new completion_info($course); +$completion->set_module_viewed($cm); + // Print the page header. $PAGE->set_url('/mod/quizgame/view.php', array('id' => $cm->id)); @@ -81,5 +85,10 @@ echo $renderer->render_game($quizgame, $context); echo "
Loading game
"; +// Display link to view student scores. +if (has_capability('mod/quizgame:viewallscores', $context)) { + echo $renderer->render_score_link($quizgame); +} + // Finish the page. echo $OUTPUT->footer();