From 5d997cab2107e4f9dc592e794166411ea4727e3c Mon Sep 17 00:00:00 2001 From: Stephen Bourget Date: Sat, 17 Feb 2018 23:34:09 -0500 Subject: [PATCH 01/13] mod_quizgame: Implement user scores This commit includes: 1. An AJAX service to write player scores to the DB 2. The ability to clear user scores upon activity deletion & course reset. 3. User scores now display on the user activity reports (complete & outline) 4. Improved logging for user scores. TODO: 1. Activity completion based on score 2. Logging based on gameplay start 3. Unit Tests 4. Getting travis to pass. --- amd/build/quizgame.min.js | 2 +- amd/src/quizgame.js | 11 ++- classes/event/game_score_added.php | 78 ++++++++++++++++++++ classes/event/game_started.php | 77 ++++++++++++++++++++ classes/external.php | 99 +++++++++++++++++++++++++ db/install.xml | 3 +- db/services.php | 39 ++++++++++ db/upgrade.php | 15 ++++ lang/en/quizgame.php | 8 +++ lib.php | 112 +++++++++++++++++++++++++++-- locallib.php | 35 +++++++++ nbproject/private/private.xml | 4 ++ renderer.php | 4 +- version.php | 2 +- 14 files changed, 475 insertions(+), 14 deletions(-) create mode 100644 classes/event/game_score_added.php create mode 100644 classes/event/game_started.php create mode 100644 classes/external.php create mode 100644 db/services.php create mode 100644 nbproject/private/private.xml diff --git a/amd/build/quizgame.min.js b/amd/build/quizgame.min.js index ad3cd41..f7e7e54 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: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,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..e52d694 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: score}, + fail: notification.exception + }]); menuEvents(); } @@ -858,8 +864,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/classes/event/game_score_added.php b/classes/event/game_score_added.php new file mode 100644 index 0000000..6709226 --- /dev/null +++ b/classes/event/game_score_added.php @@ -0,0 +1,78 @@ +. + +/** + * The mod_quizgame instance list 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 instance list 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_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_started.php b/classes/event/game_started.php new file mode 100644 index 0000000..f00e254 --- /dev/null +++ b/classes/event/game_started.php @@ -0,0 +1,77 @@ +. + +/** + * The mod_quizgame instance list 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 instance list 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_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..4e0cf51 --- /dev/null +++ b/classes/external.php @@ -0,0 +1,99 @@ +. + +/** + * 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'); + } + +} diff --git a/db/install.xml b/db/install.xml index ef8e146..7bdc694 100755 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -29,6 +29,7 @@ + diff --git a/db/services.php b/db/services.php new file mode 100644 index 0000000..6e2f235 --- /dev/null +++ b/db/services.php @@ -0,0 +1,39 @@ +. + +/** + * 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', + ) +); diff --git a/db/upgrade.php b/db/upgrade.php index 4085265..644b77a 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -87,6 +87,21 @@ 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'); + } + // Final return of upgrade result (true, all went good) to Moodle. return true; } diff --git a/lang/en/quizgame.php b/lang/en/quizgame.php index b2ede27..49cad95 100644 --- a/lang/en/quizgame.php +++ b/lang/en/quizgame.php @@ -28,8 +28,12 @@ defined('MOODLE_INTERNAL') || die(); +$string['achievedhighscoreof'] = 'Acheived a high score of {$a}'; +$string['attempt'] = 'Attempt #{$a}'; $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['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,6 +42,9 @@ **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'] = 'Note 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['questioncategory'] = 'Question category'; @@ -52,6 +59,7 @@ $string['quizgame'] = 'Quizventure'; $string['quizgame:addinstance'] = 'Add a Quizventure instance'; $string['quizgame:view'] = 'View Quizventure'; +$string['removescores'] = 'Remove all user scores'; $string['score'] = 'Score: {$a->score} Lives: {$a->lives}'; $string['spacetostart'] = 'Press space or click to start'; $string['sound'] = 'Sound'; diff --git a/lib.php b/lib.php index ce67f9b..f78a000 100644 --- a/lib.php +++ b/lib.php @@ -111,9 +111,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 +132,28 @@ 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 +164,23 @@ 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"); + } + } /** @@ -392,3 +423,70 @@ 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'); + + 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..961ec75 100644 --- a/locallib.php +++ b/locallib.php @@ -41,3 +41,38 @@ 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); + $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(); + + return $record->id; +} 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..158f66c 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 46b3ae4..b780921 100644 --- a/styles.css +++ b/styles.css @@ -7,3 +7,6 @@ width: 100%; height: 100%; } +.quizgame-scores { + text-align: center; +} diff --git a/tests/events_test.php b/tests/events_test.php index f04e967..a0ccebf 100644 --- a/tests/events_test.php +++ b/tests/events_test.php @@ -38,11 +38,18 @@ */ 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 @@ -82,6 +89,10 @@ public function test_course_module_viewed() { $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. @@ -108,17 +119,21 @@ public function test_course_module_instance_list_viewed() { $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); + $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); @@ -130,6 +145,10 @@ public function test_score_added() { $this->assertEquals($score, $event->other['score']); } + /** + * Test the game_started event. + * @global stdclass $DB + */ public function test_game_started() { $this->setAdminUser(); @@ -139,7 +158,7 @@ public function test_game_started() { $sink = $this->redirectEvents(); $result = quizgame_log_game_start($quizgame); - + $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); @@ -150,4 +169,36 @@ public function test_game_started() { $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 index 57d15b7..28b3f78 100644 --- a/tests/generator/lib.php +++ b/tests/generator/lib.php @@ -54,7 +54,7 @@ public function create_content($quizgame, $record = array()) { 'quizgameid' => $quizgame->id, 'timecreated' => $now, 'userid' => $USER->id, - 'score' => mt_rand (0,50000), + 'score' => mt_rand (0, 50000), ); $id = $DB->insert_record('quizgame_scores', $record); diff --git a/version.php b/version.php index af62bf0..d93140c 100644 --- a/version.php +++ b/version.php @@ -28,7 +28,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2018062002; // 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 07c3d0f..ffa0ff7 100644 --- a/view.php +++ b/view.php @@ -85,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(); From 4ca1d202c5cb989e9c1f754cf2afe220e67b69e6 Mon Sep 17 00:00:00 2001 From: Stephen Bourget Date: Fri, 2 Mar 2018 10:45:56 -0500 Subject: [PATCH 10/13] mod_quizgame: Truncate scores before calling web service. With matching questions it is possible to get a score of 666.66667 with will result in an exception since the database is expecting an integer. --- amd/build/quizgame.min.js | 2 +- amd/src/quizgame.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/amd/build/quizgame.min.js b/amd/build/quizgame.min.js index b10d9ef..0aedf0d 100644 --- a/amd/build/quizgame.min.js +++ b/amd/build/quizgame.min.js @@ -1 +1 @@ -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: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 +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 91d1786..54d7c5e 100644 --- a/amd/src/quizgame.js +++ b/amd/src/quizgame.js @@ -174,7 +174,7 @@ define(['jquery','core/yui', 'core/notification', 'core/ajax'], function($, Y, n function endGame() { ajax.call([{ methodname: 'mod_quizgame_update_score', - args: {quizgameid: quizgame, score: score}, + args: {quizgameid: quizgame, score: Math.trunc(score)}, fail: notification.exception }]); menuEvents(); From 9307db85f8d0cf7d1865625cb371eb35416087a9 Mon Sep 17 00:00:00 2001 From: Stephen Bourget Date: Fri, 2 Mar 2018 10:54:06 -0500 Subject: [PATCH 11/13] mod_quizgame: fix typo in language string. --- lang/en/quizgame.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/en/quizgame.php b/lang/en/quizgame.php index 559dbdc..f216130 100644 --- a/lang/en/quizgame.php +++ b/lang/en/quizgame.php @@ -50,7 +50,7 @@ **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'] = 'Note yet played'; +$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'; From 007d0b759d24fd2bb9bee9cd829e188afc8f0d21 Mon Sep 17 00:00:00 2001 From: Stephen Bourget Date: Thu, 8 Mar 2018 10:00:18 -0500 Subject: [PATCH 12/13] mod_quizgame: fix debugging notice when using course reset. --- lib.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib.php b/lib.php index 12c94a9..5389b85 100644 --- a/lib.php +++ b/lib.php @@ -501,6 +501,7 @@ function quizgame_reset_course_form_defaults($course) { 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 58b23177fbc2ab31ac45b855c9961b76d17d6036 Mon Sep 17 00:00:00 2001 From: Stephen Bourget Date: Wed, 20 Jun 2018 12:04:56 -0400 Subject: [PATCH 13/13] mod_quizgame: Add GDPR support --- classes/privacy/provider.php | 215 ++++++++++++++++++++++++++++++++ lang/en/quizgame.php | 5 + tests/privacy_provider_test.php | 201 +++++++++++++++++++++++++++++ 3 files changed, 421 insertions(+) create mode 100644 classes/privacy/provider.php create mode 100644 tests/privacy_provider_test.php 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/lang/en/quizgame.php b/lang/en/quizgame.php index f216130..4669b96 100644 --- a/lang/en/quizgame.php +++ b/lang/en/quizgame.php @@ -56,6 +56,11 @@ $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. 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); + } +}