diff --git a/README.md b/README.md index 48bf571..02d809a 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,4 @@ Alternatively you can just link to the individual files themselves like so: + diff --git a/build.js b/build.js index e2507db..8935a4b 100755 --- a/build.js +++ b/build.js @@ -34,6 +34,7 @@ new gear.Queue( ,'src/StatementsResult.js' ,'src/State.js' ,'src/ActivityProfile.js' + ,'src/AgentProfile.js' ] ) .log("Linting") diff --git a/build/tincan-min.js b/build/tincan-min.js index 114b254..6513915 100644 --- a/build/tincan-min.js +++ b/build/tincan-min.js @@ -14,4 +14,4 @@ See the License for the specific language governing permissions and limitations under the License. */ -;var TinCan;(function(){"use strict";var _environment=null,_reservedQSParams={statementId:!0,voidedStatementId:!0,verb:!0,object:!0,registration:!0,context:!0,actor:!0,since:!0,until:!0,limit:!0,authoritative:!0,sparse:!0,instructor:!0,ascending:!0,continueToken:!0,agent:!0,activityId:!0,stateId:!0,profileId:!0,activity_platform:!0,grouping:!0,"Accept-Language":!0};TinCan=function(a){this.log("constructor"),this.environment=null,this.recordStores=[],this.actor=null,this.activity=null,this.registration=null,this.context=null,this.init(a)},TinCan.prototype={LOG_SRC:"TinCan",log:function(a,b){TinCan.DEBUG&&typeof console!="undefined"&&console.log&&(b=b||this.LOG_SRC||"TinCan",console.log("TinCan."+b+": "+a))},init:function(a){this.log("init");var b;a=a||{},a.hasOwnProperty("url")&&a.url!==""&&this._initFromQueryString(a.url);if(a.hasOwnProperty("recordStores")&&a.recordStores!==undefined)for(b=0;b0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatement - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatement - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],c.retrieveStatement(a,{callback:b});d="[warning] getStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},voidStatement:function(a,b,c){this.log("voidStatement");var d=this,e,f,g,h=this.recordStores.length,i,j,k=[],l,m=[];a instanceof TinCan.Statement&&(a=a.id),typeof c.actor!="undefined"?f=c.actor:this.actor!==null&&(f=this.actor),g=new TinCan.Statement({actor:f,verb:{id:"http://adlnet.gov/expapi/verbs/voided"},target:{objectType:"StatementRef",id:a}});if(h>0){typeof b=="function"&&(l=function(a,c){var e;d.log("voidStatement - callbackWrapper: "+h),h>1?(h-=1,m.push({err:a,xhr:c})):h===1?(m.push({err:a,xhr:c}),e=[m,g],b.apply(this,e)):d.log("voidStatement - unexpected record store count: "+h)});for(i=0;i0)return c=this.recordStores[0],c.retrieveVoidedStatement(a,{callback:b});d="[warning] getVoidedStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},sendStatements:function(a,b){this.log("sendStatements");var c=this,d,e=[],f=this.recordStores.length,g,h,i=[],j,k=[];if(a.length===0)typeof b=="function"&&b.apply(this,[null,e]);else{for(g=0;g0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatements - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatements - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],a=a||{},d=a.params||{},a.sendActor&&this.actor!==null&&(c.version==="0.9"||c.version==="0.95"?d.actor=this.actor:d.agent=this.actor),a.sendActivity&&this.activity!==null&&(c.version==="0.9"||c.version==="0.95"?d.target=this.activity:d.activity=this.activity),typeof d.registration=="undefined"&&this.registration!==null&&(d.registration=this.registration),b={params:d},typeof a.callback!="undefined"&&(b.callback=a.callback),c.queryStatements(b);e="[warning] getStatements: No LRSs added yet (statements not read)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getState:function(a,b){this.log("getState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveState(a,c);e="[warning] getState: No LRSs added yet (state not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setState:function(a,b,c){this.log("setState");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={agent:typeof c.agent!="undefined"?c.agent:this.actor,activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.registration!="undefined"?d.registration=c.registration:this.registration!==null&&(d.registration=this.registration),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.callback!="undefined"&&(d.callback=c.callback),e.saveState(a,b,d);f="[warning] setState: No LRSs added yet (state not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteState:function(a,b){this.log("deleteState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropState(a,c);e="[warning] deleteState: No LRSs added yet (state not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getActivityProfile:function(a,b){this.log("getActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveActivityProfile(a,c);e="[warning] getActivityProfile: No LRSs added yet (activity profile not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setActivityProfile:function(a,b,c){this.log("setActivityProfile");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.callback!="undefined"&&(d.callback=c.callback),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),e.saveActivityProfile(a,b,d);f="[warning] setActivityProfile: No LRSs added yet (activity profile not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteActivityProfile:function(a,b){this.log("deleteActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropActivityProfile(a,c);e="[warning] deleteActivityProfile: No LRSs added yet (activity profile not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)}},TinCan.DEBUG=!1,TinCan.enableDebug=function(){TinCan.DEBUG=!0},TinCan.disableDebug=function(){TinCan.DEBUG=!1},TinCan.versions=function(){return["1.0.0","0.95","0.9"]},TinCan.environment=function(){return _environment===null&&(_environment={},typeof window!="undefined"?(_environment.isBrowser=!0,_environment.hasCORS=!1,_environment.useXDR=!1,typeof XMLHttpRequest!="undefined"&&typeof (new XMLHttpRequest).withCredentials!="undefined"?_environment.hasCORS=!0:typeof XDomainRequest!="undefined"&&(_environment.hasCORS=!0,_environment.useXDR=!0)):_environment.isBrowser=!1),_environment},TinCan.environment().isBrowser&&(window.JSON||(window.JSON={parse:function(sJSON){return eval("("+sJSON+")")},stringify:function(a){var b="",c,d;if(a instanceof Object){if(a.constructor===Array){for(c=0;c1)d="0"+d,c/=10;return d}return a.getUTCFullYear()+"-"+b(a.getUTCMonth()+1)+"-"+b(a.getUTCDate())+"T"+b(a.getUTCHours())+":"+b(a.getUTCMinutes())+":"+b(a.getUTCSeconds())+"."+b(a.getUTCMilliseconds(),3)+"Z"},getSHA1String:function(a){return CryptoJS.SHA1(a).toString(CryptoJS.enc.Hex)},getBase64String:function(a){return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Latin1.parse(a))},getLangDictionaryValue:function(a,b){var c=this[a],d;if(typeof b!="undefined"&&typeof c[b]!="undefined")return c[b];if(typeof c.und!="undefined")return c.und;if(typeof c["en-US"]!="undefined")return c["en-US"];for(d in c)if(c.hasOwnProperty(d))return c[d];return""},parseURL:function(a){var b=String(a).split("?"),c,d,e,f={};if(b.length===2){c=b[1].split("&");for(e=0;e=200&&b<400||a))return b>0&&(j={err:b,xhr:d},n.alertOnRequestFailure&&alert("[warning] There was a problem communicating with the Learning Record Store. ("+b+" | "+d.responseText+")"),c.callback&&c.callback(b,d)),j;if(!c.callback)return j={err:null,xhr:d},j;c.callback(null,d)}this.log("sendRequest");var d,e=!1,f=window.location,g=this.endpoint+c.url,h={},i,j,k,l,m=[],n=this;c.url.indexOf("http")===0&&(g=c.url);if(this.extended!==null){c.params=c.params||{};for(l in this.extended)this.extended.hasOwnProperty(l)&&(c.params.hasOwnProperty(l)||this.extended[l]!==null&&(c.params[l]=this.extended[l]))}h["Content-Type"]="application/json",h.Authorization=this.auth,this.version!=="0.9"&&(h["X-Experience-API-Version"]=this.version);for(l in c.headers)c.headers.hasOwnProperty(l)&&(h[l]=c.headers[l]);if(this._requestMode===b){this.log("sendRequest using XMLHttpRequest");for(l in c.params)c.params.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(c.params[l]));m.length>0&&(g+="?"+m.join("&")),this.log("sendRequest using XMLHttpRequest - async: "+(typeof c.callback!="undefined")),typeof XMLHttpRequest!="undefined"?d=new XMLHttpRequest:d=new ActiveXObject("Microsoft.XMLHTTP"),d.open(c.method,g,typeof c.callback!="undefined");for(l in h)h.hasOwnProperty(l)&&d.setRequestHeader(l,h[l]);typeof c.data!="undefined"&&(c.data+=""),i=c.data,d.onreadystatechange=function(){n.log("xhr.onreadystatechange - xhr.readyState: "+d.readyState),d.readyState===4&&o()}}else if(this._requestMode===a){this.log("sendRequest using XDomainRequest"),g+="?method="+c.method;for(l in c.params)c.params.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(c.params[l]));for(l in h)h.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(h[l]));c.data!==null&&m.push("content="+encodeURIComponent(c.data)),i=m.join("&"),d=new XDomainRequest,d.open("POST",g),d.onload=function(){o()},d.onerror=function(){o()}}else this.log("sendRequest unrecognized _requestMode: "+this._requestMode);try{d.send(i)}catch(p){this.log("sendRequest caught send exception: "+p)}if(!c.callback){if(this._requestMode===a){k=1e3+Date.now(),this.log("sendRequest - until: "+k+", finished: "+e);while(Date.now()0&&(a.name=a.firstName[0],a.firstName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.lastName!="undefined"&&a.lastName.length>0&&(a.name+=a.lastName[0],a.lastName.length>1&&(this.degraded=!0));else if(typeof a.familyName!="undefined"||typeof a.givenName!="undefined")a.name="",typeof a.givenName!="undefined"&&a.givenName.length>0&&(a.name=a.givenName[0],a.givenName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.familyName!="undefined"&&a.familyName.length>0&&(a.name+=a.familyName[0],a.familyName.length>1&&(this.degraded=!0));typeof a.name=="object"&&a.name!==null&&(a.name.length>1&&(this.degraded=!0),a.name=a.name[0]),typeof a.mbox=="object"&&a.mbox!==null&&(a.mbox.length>1&&(this.degraded=!0),a.mbox=a.mbox[0]),typeof a.mbox_sha1sum=="object"&&a.mbox_sha1sum!==null&&(a.mbox_sha1sum.length>1&&(this.degraded=!0),a.mbox_sha1sum=a.mbox_sha1sum[0]),typeof a.openid=="object"&&a.openid!==null&&(a.openid.length>1&&(this.degraded=!0),a.openid=a.openid[0]),typeof a.account=="object"&&a.account!==null&&typeof a.account.homePage=="undefined"&&typeof a.account.name=="undefined"&&(a.account.length===0?delete a.account:(a.account.length>1&&(this.degraded=!0),a.account=a.account[0])),a.hasOwnProperty("account")&&(a.account instanceof TinCan.AgentAccount?this.account=a.account:this.account=new TinCan.AgentAccount(a.account));for(b=0;b0){b.member=[];for(c=0;c0)for(c=0;c0)if(a==="0.9"||a==="0.95")this[c[d]].length>1&&this.log("[WARNING] version does not support multiple values in: "+c[d]),b[c[d]]=this[c[d]][0].asVersion(a);else{b[c[d]]=[];for(e=0;e>>2]|=(c[e>>>2]>>>24-8*(e%4)&255)<<24-8*((d+e)%4);else if(65535>>2]=c[e>>>2];else b.push.apply(b,c);return this.sigBytes+=a,this},clamp:function(){var b=this.words,c=this.sigBytes;b[c>>>2]&=4294967295<<32-8*(c%4),b.length=a.ceil(c/4)},clone:function(){var a=e.clone.call(this);return a.words=this.words.slice(0),a},random:function(b){for(var c=[],d=0;d>>2]>>>24-8*(d%4)&255;c.push((e>>>4).toString(16)),c.push((e&15).toString(16))}return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>3]|=parseInt(a.substr(d,2),16)<<24-4*(d%8);return f.create(c,b/2)}},i=g.Latin1={stringify:function(a){for(var b=a.words,a=a.sigBytes,c=[],d=0;d>>2]>>>24-8*(d%4)&255));return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>2]|=(a.charCodeAt(d)&255)<<24-8*(d%4);return f.create(c,b)}},j=g.Utf8={stringify:function(a){try{return decodeURIComponent(escape(i.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data")}},parse:function(a){return i.parse(unescape(encodeURIComponent(a)))}},k=d.BufferedBlockAlgorithm=e.extend({reset:function(){this._data=f.create(),this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a)),this._data.concat(a),this._nDataBytes+=a.sigBytes},_process:function(b){var c=this._data,d=c.words,e=c.sigBytes,g=this.blockSize,h=e/(4*g),h=b?a.ceil(h):a.max((h|0)-this._minBufferSize,0),b=h*g,e=a.min(4*b,e);if(b){for(var i=0;ik;k++){if(16>k)d[k]=a[b+k]|0;else{var l=d[k-3]^d[k-8]^d[k-14]^d[k-16];d[k]=l<<1|l>>>31}l=(e<<5|e>>>27)+j+d[k],l=20>k?l+((f&g|~f&i)+1518500249):40>k?l+((f^g^i)+1859775393):60>k?l+((f&g|f&i|g&i)-1894007588):l+((f^g^i)-899497514),j=i,i=g,g=f<<30|f>>>2,f=e,e=l}c[0]=c[0]+e|0,c[1]=c[1]+f|0,c[2]=c[2]+g|0,c[3]=c[3]+i|0,c[4]=c[4]+j|0},_doFinalize:function(){var a=this._data,b=a.words,c=8*this._nDataBytes,d=8*a.sigBytes;b[d>>>5]|=128<<24-d%32,b[(d+64>>>9<<4)+15]=c,a.sigBytes=4*b.length,this._process()}});a.SHA1=b._createHelper(e),a.HmacSHA1=b._createHmacHelper(e)})(),function(){var a=CryptoJS,b=a.lib,c=b.WordArray,d=a.enc,e=d.Base64={stringify:function(a){var b=a.words,c=a.sigBytes,d=this._map;a.clamp();var e=[];for(var f=0;f>>2]>>>24-f%4*8&255,h=b[f+1>>>2]>>>24-(f+1)%4*8&255,i=b[f+2>>>2]>>>24-(f+2)%4*8&255,j=g<<16|h<<8|i;for(var k=0;k<4&&f+k*.75>>6*(3-k)&63))}var l=d.charAt(64);if(l)while(e.length%4)e.push(l);return e.join("")},parse:function(a){a=a.replace(/\s/g,"");var b=a.length,d=this._map,e=d.charAt(64);if(e){var f=a.indexOf(e);f!=-1&&(b=f)}var g=[],h=0;for(var i=0;i>>6-i%4*2;g[h>>>2]|=(j|k)<<24-h%4*8,h++}return c.create(g,h)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(); +;var TinCan;(function(){"use strict";var _environment=null,_reservedQSParams={statementId:!0,voidedStatementId:!0,verb:!0,object:!0,registration:!0,context:!0,actor:!0,since:!0,until:!0,limit:!0,authoritative:!0,sparse:!0,instructor:!0,ascending:!0,continueToken:!0,agent:!0,activityId:!0,stateId:!0,profileId:!0,activity_platform:!0,grouping:!0,"Accept-Language":!0};TinCan=function(a){this.log("constructor"),this.environment=null,this.recordStores=[],this.actor=null,this.activity=null,this.registration=null,this.context=null,this.init(a)},TinCan.prototype={LOG_SRC:"TinCan",log:function(a,b){TinCan.DEBUG&&typeof console!="undefined"&&console.log&&(b=b||this.LOG_SRC||"TinCan",console.log("TinCan."+b+": "+a))},init:function(a){this.log("init");var b;a=a||{},a.hasOwnProperty("url")&&a.url!==""&&this._initFromQueryString(a.url);if(a.hasOwnProperty("recordStores")&&a.recordStores!==undefined)for(b=0;b0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatement - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatement - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],c.retrieveStatement(a,{callback:b});d="[warning] getStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},voidStatement:function(a,b,c){this.log("voidStatement");var d=this,e,f,g,h=this.recordStores.length,i,j,k=[],l,m=[];a instanceof TinCan.Statement&&(a=a.id),typeof c.actor!="undefined"?f=c.actor:this.actor!==null&&(f=this.actor),g=new TinCan.Statement({actor:f,verb:{id:"http://adlnet.gov/expapi/verbs/voided"},target:{objectType:"StatementRef",id:a}});if(h>0){typeof b=="function"&&(l=function(a,c){var e;d.log("voidStatement - callbackWrapper: "+h),h>1?(h-=1,m.push({err:a,xhr:c})):h===1?(m.push({err:a,xhr:c}),e=[m,g],b.apply(this,e)):d.log("voidStatement - unexpected record store count: "+h)});for(i=0;i0)return c=this.recordStores[0],c.retrieveVoidedStatement(a,{callback:b});d="[warning] getVoidedStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},sendStatements:function(a,b){this.log("sendStatements");var c=this,d,e=[],f=this.recordStores.length,g,h,i=[],j,k=[];if(a.length===0)typeof b=="function"&&b.apply(this,[null,e]);else{for(g=0;g0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatements - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatements - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],a=a||{},d=a.params||{},a.sendActor&&this.actor!==null&&(c.version==="0.9"||c.version==="0.95"?d.actor=this.actor:d.agent=this.actor),a.sendActivity&&this.activity!==null&&(c.version==="0.9"||c.version==="0.95"?d.target=this.activity:d.activity=this.activity),typeof d.registration=="undefined"&&this.registration!==null&&(d.registration=this.registration),b={params:d},typeof a.callback!="undefined"&&(b.callback=a.callback),c.queryStatements(b);e="[warning] getStatements: No LRSs added yet (statements not read)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getState:function(a,b){this.log("getState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveState(a,c);e="[warning] getState: No LRSs added yet (state not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setState:function(a,b,c){this.log("setState");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={agent:typeof c.agent!="undefined"?c.agent:this.actor,activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.registration!="undefined"?d.registration=c.registration:this.registration!==null&&(d.registration=this.registration),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),typeof c.callback!="undefined"&&(d.callback=c.callback),e.saveState(a,b,d);f="[warning] setState: No LRSs added yet (state not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteState:function(a,b){this.log("deleteState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropState(a,c);e="[warning] deleteState: No LRSs added yet (state not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getActivityProfile:function(a,b){this.log("getActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveActivityProfile(a,c);e="[warning] getActivityProfile: No LRSs added yet (activity profile not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setActivityProfile:function(a,b,c){this.log("setActivityProfile");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.callback!="undefined"&&(d.callback=c.callback),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),e.saveActivityProfile(a,b,d);f="[warning] setActivityProfile: No LRSs added yet (activity profile not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteActivityProfile:function(a,b){this.log("deleteActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropActivityProfile(a,c);e="[warning] deleteActivityProfile: No LRSs added yet (activity profile not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getAgentProfile:function(a,b){this.log("getAgentProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor},typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveAgentProfile(a,c);e="[warning] getAgentProfile: No LRSs added yet (agent profile not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setAgentProfile:function(a,b,c){this.log("setAgentProfile");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={agent:typeof c.agent!="undefined"?c.agent:this.actor},typeof c.callback!="undefined"&&(d.callback=c.callback),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),e.saveAgentProfile(a,b,d);f="[warning] setAgentProfile: No LRSs added yet (agent profile not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteAgentProfile:function(a,b){this.log("deleteAgentProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor},typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropAgentProfile(a,c);e="[warning] deleteAgentProfile: No LRSs added yet (agent profile not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)}},TinCan.DEBUG=!1,TinCan.enableDebug=function(){TinCan.DEBUG=!0},TinCan.disableDebug=function(){TinCan.DEBUG=!1},TinCan.versions=function(){return["1.0.0","0.95","0.9"]},TinCan.environment=function(){return _environment===null&&(_environment={},typeof window!="undefined"?(_environment.isBrowser=!0,_environment.hasCORS=!1,_environment.useXDR=!1,typeof XMLHttpRequest!="undefined"&&typeof (new XMLHttpRequest).withCredentials!="undefined"?_environment.hasCORS=!0:typeof XDomainRequest!="undefined"&&(_environment.hasCORS=!0,_environment.useXDR=!0)):_environment.isBrowser=!1),_environment},TinCan.environment().isBrowser&&(window.JSON||(window.JSON={parse:function(sJSON){return eval("("+sJSON+")")},stringify:function(a){var b="",c,d;if(a instanceof Object){if(a.constructor===Array){for(c=0;c1)d="0"+d,c/=10;return d}return a.getUTCFullYear()+"-"+b(a.getUTCMonth()+1)+"-"+b(a.getUTCDate())+"T"+b(a.getUTCHours())+":"+b(a.getUTCMinutes())+":"+b(a.getUTCSeconds())+"."+b(a.getUTCMilliseconds(),3)+"Z"},getSHA1String:function(a){return CryptoJS.SHA1(a).toString(CryptoJS.enc.Hex)},getBase64String:function(a){return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Latin1.parse(a))},getLangDictionaryValue:function(a,b){var c=this[a],d;if(typeof b!="undefined"&&typeof c[b]!="undefined")return c[b];if(typeof c.und!="undefined")return c.und;if(typeof c["en-US"]!="undefined")return c["en-US"];for(d in c)if(c.hasOwnProperty(d))return c[d];return""},parseURL:function(a){var b=String(a).split("?"),c,d,e,f={};if(b.length===2){c=b[1].split("&");for(e=0;e=200&&f<400||b))return j={err:f,xhr:d},o.alertOnRequestFailure&&(f===0?alert("[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint ("+f+")"):alert("[warning] There was a problem communicating with the Learning Record Store. ("+f+" | "+d.responseText+")")),c.callback&&c.callback(f,d),j;if(!c.callback)return j={err:null,xhr:d},j;c.callback(null,d)}this.log("sendRequest");var d,e=!1,f=window.location,g=this.endpoint+c.url,h={},i,j,k,l,m,n=[],o=this;c.url.indexOf("http")===0&&(g=c.url);if(this.extended!==null){c.params=c.params||{};for(m in this.extended)this.extended.hasOwnProperty(m)&&(c.params.hasOwnProperty(m)||this.extended[m]!==null&&(c.params[m]=this.extended[m]))}h.Authorization=this.auth,this.version!=="0.9"&&(h["X-Experience-API-Version"]=this.version);for(m in c.headers)c.headers.hasOwnProperty(m)&&(h[m]=c.headers[m]);if(this._requestMode===b){this.log("sendRequest using XMLHttpRequest");for(m in c.params)c.params.hasOwnProperty(m)&&n.push(m+"="+encodeURIComponent(c.params[m]));n.length>0&&(g+="?"+n.join("&")),this.log("sendRequest using XMLHttpRequest - async: "+(typeof c.callback!="undefined")),typeof XMLHttpRequest!="undefined"?d=new XMLHttpRequest:d=new ActiveXObject("Microsoft.XMLHTTP"),d.open(c.method,g,typeof c.callback!="undefined");for(m in h)h.hasOwnProperty(m)&&d.setRequestHeader(m,h[m]);typeof c.data!="undefined"&&(c.data+=""),i=c.data,d.onreadystatechange=function(){o.log("xhr.onreadystatechange - xhr.readyState: "+d.readyState),d.readyState===4&&p()}}else if(this._requestMode===a){this.log("sendRequest using XDomainRequest"),g+="?method="+c.method;for(m in c.params)c.params.hasOwnProperty(m)&&n.push(m+"="+encodeURIComponent(c.params[m]));for(m in h)h.hasOwnProperty(m)&&n.push(m+"="+encodeURIComponent(h[m]));c.data!==null&&n.push("content="+encodeURIComponent(c.data)),i=n.join("&"),d=new XDomainRequest,d.open("POST",g),c.callback?(d.onload=function(){p(200)},d.onerror=function(){p(400)},d.ontimeout=function(){p(0)}):(d.onload=function(){k=200},d.onerror=function(){k=400},d.ontimeout=function(){k=0}),d.onprogress=function(){},d.timeout=0}else this.log("sendRequest unrecognized _requestMode: "+this._requestMode);try{d.send(i)}catch(q){this.log("sendRequest caught send exception: "+q)}if(!c.callback){if(this._requestMode===a){l=1e4+Date.now(),this.log("sendRequest - until: "+l+", finished: "+e);while(Date.now()0&&(a.name=a.firstName[0],a.firstName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.lastName!="undefined"&&a.lastName.length>0&&(a.name+=a.lastName[0],a.lastName.length>1&&(this.degraded=!0));else if(typeof a.familyName!="undefined"||typeof a.givenName!="undefined")a.name="",typeof a.givenName!="undefined"&&a.givenName.length>0&&(a.name=a.givenName[0],a.givenName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.familyName!="undefined"&&a.familyName.length>0&&(a.name+=a.familyName[0],a.familyName.length>1&&(this.degraded=!0));typeof a.name=="object"&&a.name!==null&&(a.name.length>1&&(this.degraded=!0),a.name=a.name[0]),typeof a.mbox=="object"&&a.mbox!==null&&(a.mbox.length>1&&(this.degraded=!0),a.mbox=a.mbox[0]),typeof a.mbox_sha1sum=="object"&&a.mbox_sha1sum!==null&&(a.mbox_sha1sum.length>1&&(this.degraded=!0),a.mbox_sha1sum=a.mbox_sha1sum[0]),typeof a.openid=="object"&&a.openid!==null&&(a.openid.length>1&&(this.degraded=!0),a.openid=a.openid[0]),typeof a.account=="object"&&a.account!==null&&typeof a.account.homePage=="undefined"&&typeof a.account.name=="undefined"&&(a.account.length===0?delete a.account:(a.account.length>1&&(this.degraded=!0),a.account=a.account[0])),a.hasOwnProperty("account")&&(a.account instanceof TinCan.AgentAccount?this.account=a.account:this.account=new TinCan.AgentAccount(a.account));for(b=0;b0){b.member=[];for(c=0;c0)for(c=0;c0)if(a==="0.9"||a==="0.95")this[c[d]].length>1&&this.log("[WARNING] version does not support multiple values in: "+c[d]),b[c[d]]=this[c[d]][0].asVersion(a);else{b[c[d]]=[];for(e=0;e>>2]|=(c[e>>>2]>>>24-8*(e%4)&255)<<24-8*((d+e)%4);else if(65535>>2]=c[e>>>2];else b.push.apply(b,c);return this.sigBytes+=a,this},clamp:function(){var b=this.words,c=this.sigBytes;b[c>>>2]&=4294967295<<32-8*(c%4),b.length=a.ceil(c/4)},clone:function(){var a=e.clone.call(this);return a.words=this.words.slice(0),a},random:function(b){for(var c=[],d=0;d>>2]>>>24-8*(d%4)&255;c.push((e>>>4).toString(16)),c.push((e&15).toString(16))}return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>3]|=parseInt(a.substr(d,2),16)<<24-4*(d%8);return f.create(c,b/2)}},i=g.Latin1={stringify:function(a){for(var b=a.words,a=a.sigBytes,c=[],d=0;d>>2]>>>24-8*(d%4)&255));return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>2]|=(a.charCodeAt(d)&255)<<24-8*(d%4);return f.create(c,b)}},j=g.Utf8={stringify:function(a){try{return decodeURIComponent(escape(i.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data")}},parse:function(a){return i.parse(unescape(encodeURIComponent(a)))}},k=d.BufferedBlockAlgorithm=e.extend({reset:function(){this._data=f.create(),this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a)),this._data.concat(a),this._nDataBytes+=a.sigBytes},_process:function(b){var c=this._data,d=c.words,e=c.sigBytes,g=this.blockSize,h=e/(4*g),h=b?a.ceil(h):a.max((h|0)-this._minBufferSize,0),b=h*g,e=a.min(4*b,e);if(b){for(var i=0;ik;k++){if(16>k)d[k]=a[b+k]|0;else{var l=d[k-3]^d[k-8]^d[k-14]^d[k-16];d[k]=l<<1|l>>>31}l=(e<<5|e>>>27)+j+d[k],l=20>k?l+((f&g|~f&i)+1518500249):40>k?l+((f^g^i)+1859775393):60>k?l+((f&g|f&i|g&i)-1894007588):l+((f^g^i)-899497514),j=i,i=g,g=f<<30|f>>>2,f=e,e=l}c[0]=c[0]+e|0,c[1]=c[1]+f|0,c[2]=c[2]+g|0,c[3]=c[3]+i|0,c[4]=c[4]+j|0},_doFinalize:function(){var a=this._data,b=a.words,c=8*this._nDataBytes,d=8*a.sigBytes;b[d>>>5]|=128<<24-d%32,b[(d+64>>>9<<4)+15]=c,a.sigBytes=4*b.length,this._process()}});a.SHA1=b._createHelper(e),a.HmacSHA1=b._createHmacHelper(e)})(),function(){var a=CryptoJS,b=a.lib,c=b.WordArray,d=a.enc,e=d.Base64={stringify:function(a){var b=a.words,c=a.sigBytes,d=this._map;a.clamp();var e=[];for(var f=0;f>>2]>>>24-f%4*8&255,h=b[f+1>>>2]>>>24-(f+1)%4*8&255,i=b[f+2>>>2]>>>24-(f+2)%4*8&255,j=g<<16|h<<8|i;for(var k=0;k<4&&f+k*.75>>6*(3-k)&63))}var l=d.charAt(64);if(l)while(e.length%4)e.push(l);return e.join("")},parse:function(a){a=a.replace(/\s/g,"");var b=a.length,d=this._map,e=d.charAt(64);if(e){var f=a.indexOf(e);f!=-1&&(b=f)}var g=[],h=0;for(var i=0;i>>6-i%4*2;g[h>>>2]|=(j|k)<<24-h%4*8,h++}return c.create(g,h)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(); diff --git a/build/tincan.js b/build/tincan.js index bca4628..d84c09f 100644 --- a/build/tincan.js +++ b/build/tincan.js @@ -67,7 +67,10 @@ var TinCan; @param {String} [options.url] URL for determining launch provided configuration options @param {Array} [options.recordStores] list of pre-configured LRSes + @param {Object|TinCan.Agent} [options.actor] default actor @param {Object|TinCan.Activity} [options.activity] default activity + @param {String} [options.registration] default registration + @param {Object|TinCan.Context} [options.context] default context **/ TinCan = function (cfg) { this.log("constructor"); @@ -171,6 +174,25 @@ var TinCan; this.activity = new TinCan.Activity (cfg.activity); } } + if (cfg.hasOwnProperty("actor")) { + if (cfg.actor instanceof TinCan.Agent) { + this.actor = cfg.actor; + } + else { + this.actor = new TinCan.Agent (cfg.actor); + } + } + if (cfg.hasOwnProperty("context")) { + if (cfg.context instanceof TinCan.Context) { + this.context = cfg.context; + } + else { + this.context = new TinCan.Context (cfg.context); + } + } + if (cfg.hasOwnProperty("registration")) { + this.registration = cfg.registration; + } }, /** @@ -887,6 +909,7 @@ var TinCan; @param {Object} [cfg.registration] Registration used in query, defaults to 'registration' property if empty @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing state + @param {String} [cfg.contentType] Content-Type to specify in headers @param {Function} [cfg.callback] Function to run with state */ setState: function (key, val, cfg) { @@ -922,6 +945,9 @@ var TinCan; if (typeof cfg.lastSHA1 !== "undefined") { queryCfg.lastSHA1 = cfg.lastSHA1; } + if (typeof cfg.contentType !== "undefined") { + queryCfg.contentType = cfg.contentType; + } if (typeof cfg.callback !== "undefined") { queryCfg.callback = cfg.callback; } @@ -1051,6 +1077,7 @@ var TinCan; @param {Object} [cfg.activity] Activity used in query, defaults to 'activity' property if empty @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers @param {Function} [cfg.callback] Function to run with activity profile */ setActivityProfile: function (key, val, cfg) { @@ -1082,6 +1109,9 @@ var TinCan; if (typeof cfg.lastSHA1 !== "undefined") { queryCfg.lastSHA1 = cfg.lastSHA1; } + if (typeof cfg.contentType !== "undefined") { + queryCfg.contentType = cfg.contentType; + } return lrs.saveActivityProfile(key, val, queryCfg); } @@ -1140,6 +1170,156 @@ var TinCan; else { this.log(msg); } + }, + + /** + @method getAgentProfile + @param {String} key Key to retrieve from the profile + @param {Object} [cfg] Configuration for request + @param {Object} [cfg.agent] Agent used in query, + defaults to 'actor' property if empty + @param {Function} [cfg.callback] Function to run with agent profile + */ + getAgentProfile: function (key, cfg) { + this.log("getAgentProfile"); + var queryCfg, + lrs, + msg + ; + + if (this.recordStores.length > 0) { + // + // for agent profiles (for now) we are only going to store to the first LRS + // so only get from there too + // + // TODO: make this the first non-allowFail LRS but for now it should + // be good enough to make it the first since we know the LMS provided + // LRS is the first + // + lrs = this.recordStores[0]; + + cfg = cfg || {}; + + queryCfg = { + agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor) + }; + if (typeof cfg.callback !== "undefined") { + queryCfg.callback = cfg.callback; + } + + return lrs.retrieveAgentProfile(key, queryCfg); + } + + msg = "[warning] getAgentProfile: No LRSs added yet (agent profile not retrieved)"; + if (TinCan.environment().isBrowser) { + alert(this.LOG_SRC + ": " + msg); + } + else { + this.log(msg); + } + }, + + /** + @method setAgentProfile + @param {String} key Key to store into the agent profile + @param {String|Object} val Value to store into the agent profile, objects will be stringified to JSON + @param {Object} [cfg] Configuration for request + @param {Object} [cfg.agent] Agent used in query, + defaults to 'actor' property if empty + @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers + @param {Function} [cfg.callback] Function to run with agent profile + */ + setAgentProfile: function (key, val, cfg) { + this.log("setAgentProfile"); + var queryCfg, + lrs, + msg + ; + + if (this.recordStores.length > 0) { + // + // for agent profile (for now) we are only going to store to the first LRS + // so only get from there too + // + // TODO: make this the first non-allowFail LRS but for now it should + // be good enough to make it the first since we know the LMS provided + // LRS is the first + // + lrs = this.recordStores[0]; + + cfg = cfg || {}; + + queryCfg = { + agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor) + }; + if (typeof cfg.callback !== "undefined") { + queryCfg.callback = cfg.callback; + } + if (typeof cfg.lastSHA1 !== "undefined") { + queryCfg.lastSHA1 = cfg.lastSHA1; + } + if (typeof cfg.contentType !== "undefined") { + queryCfg.contentType = cfg.contentType; + } + + return lrs.saveAgentProfile(key, val, queryCfg); + } + + msg = "[warning] setAgentProfile: No LRSs added yet (agent profile not saved)"; + if (TinCan.environment().isBrowser) { + alert(this.LOG_SRC + ": " + msg); + } + else { + this.log(msg); + } + }, + + /** + @method deleteAgentProfile + @param {String|null} key Key to remove from the agent profile, or null to clear all + @param {Object} [cfg] Configuration for request + @param {Object} [cfg.agent] Agent used in query, + defaults to 'actor' property if empty + @param {Function} [cfg.callback] Function to run with agent profile + */ + deleteAgentProfile: function (key, cfg) { + this.log("deleteAgentProfile"); + var queryCfg, + lrs, + msg + ; + + if (this.recordStores.length > 0) { + // + // for agent profile (for now) we are only going to store to the first LRS + // so only get from there too + // + // TODO: make this the first non-allowFail LRS but for now it should + // be good enough to make it the first since we know the LMS provided + // LRS is the first + // + lrs = this.recordStores[0]; + + cfg = cfg || {}; + + queryCfg = { + agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor) + }; + if (typeof cfg.callback !== "undefined") { + queryCfg.callback = cfg.callback; + } + + return lrs.dropAgentProfile(key, queryCfg); + } + + msg = "[warning] deleteAgentProfile: No LRSs added yet (agent profile not deleted)"; + if (TinCan.environment().isBrowser) { + alert(this.LOG_SRC + ": " + msg); + } + else { + this.log(msg); + } } }; @@ -1445,11 +1625,31 @@ TinCan client library getServerRoot: function (absoluteUrl) { var urlParts = absoluteUrl.split("/"); return urlParts[0] + "//" + urlParts[2]; + }, + + /** + @method getContentTypeFromHeader + @static + @param {String} Content-Type header value + @return {String} Primary value from Content-Type + */ + getContentTypeFromHeader: function (header) { + return (String(header).split(";"))[0]; + }, + + /** + @method isApplicationJSON + @static + @param {String} Content-Type header value + @return {Boolean} whether "application/json" was matched + */ + isApplicationJSON: function (header) { + return TinCan.Utils.getContentTypeFromHeader(header).toLowerCase().indexOf("application/json") === 0; } }; }()); /* - Copyright 2012 Rustici Software + Copyright 2012-2013 Rustici Software Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -1713,8 +1913,8 @@ TinCan client library Method used to send a request via browser objects to the LRS @method sendRequest - @param {Object} [cfg] Configuration for request - @param {String} [cfg.url] URL portion to add to endpoint + @param {Object} cfg Configuration for request + @param {String} cfg.url URL portion to add to endpoint @param {String} [cfg.method] GET, PUT, POST, etc. @param {Object} [cfg.params] Parameters to set on the querystring @param {String} [cfg.data] String of body content @@ -1736,6 +1936,7 @@ TinCan client library headers = {}, data, requestCompleteResult, + syncFakeStatus, until, prop, pairs = [], @@ -1743,17 +1944,26 @@ TinCan client library ; // Setup request callback - function requestComplete () { + function requestComplete (fakeStatus) { self.log("requestComplete: " + finished + ", xhr.status: " + xhr.status); var notFoundOk, httpStatus; // - // older versions of IE don't properly handle 204 status codes - // so correct when receiving a 1223 to be 204 locally - // http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request + // XDomainRequest doesn't give us a way to get the status, + // so allow passing in a forged one // - httpStatus = (xhr.status === 1223) ? 204 : xhr.status; + if (typeof xhr.status === "undefined") { + httpStatus = fakeStatus; + } + else { + // + // older versions of IE don't properly handle 204 status codes + // so correct when receiving a 1223 to be 204 locally + // http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request + // + httpStatus = (xhr.status === 1223) ? 204 : xhr.status; + } if (! finished) { // may be in sync or async mode, using XMLHttpRequest or IE XDomainRequest, onreadystatechange or @@ -1762,7 +1972,7 @@ TinCan client library finished = true; notFoundOk = (cfg.ignore404 && httpStatus === 404); - if (httpStatus === undefined || (httpStatus >= 200 && httpStatus < 400) || notFoundOk) { + if ((httpStatus >= 200 && httpStatus < 400) || notFoundOk) { if (cfg.callback) { cfg.callback(null, xhr); } @@ -1775,19 +1985,21 @@ TinCan client library } } else { - // Alert all errors except cancelled XHR requests - if (httpStatus > 0) { - requestCompleteResult = { - err: httpStatus, - xhr: xhr - }; - if (self.alertOnRequestFailure) { - alert("[warning] There was a problem communicating with the Learning Record Store. (" + httpStatus + " | " + xhr.responseText+ ")"); + requestCompleteResult = { + err: httpStatus, + xhr: xhr + }; + if (self.alertOnRequestFailure) { + if (httpStatus === 0) { + alert("[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint (" + httpStatus + ")"); } - if (cfg.callback) { - cfg.callback(httpStatus, xhr); + else { + alert("[warning] There was a problem communicating with the Learning Record Store. (" + httpStatus + " | " + xhr.responseText+ ")"); } } + if (cfg.callback) { + cfg.callback(httpStatus, xhr); + } return requestCompleteResult; } } @@ -1818,7 +2030,6 @@ TinCan client library } // consolidate headers - headers["Content-Type"] = "application/json"; headers.Authorization = this.auth; if (this.version !== "0.9") { headers["X-Experience-API-Version"] = this.version; @@ -1905,12 +2116,37 @@ TinCan client library xhr = new XDomainRequest (); xhr.open("POST", fullUrl); - xhr.onload = function () { - requestComplete(); - }; - xhr.onerror = function () { - requestComplete(); - }; + if (! cfg.callback) { + xhr.onload = function () { + syncFakeStatus = 200; + }; + xhr.onerror = function () { + syncFakeStatus = 400; + }; + xhr.ontimeout = function () { + syncFakeStatus = 0; + }; + } + else { + xhr.onload = function () { + requestComplete(200); + }; + xhr.onerror = function () { + requestComplete(400); + }; + xhr.ontimeout = function () { + requestComplete(0); + }; + } + + // IE likes to randomly abort requests when some handlers + // aren't defined, so define them with no-ops, see: + // + // http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/ + // http://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified + // + xhr.onprogress = function () {}; + xhr.timeout = 0; } else { this.log("sendRequest unrecognized _requestMode: " + this._requestMode); @@ -1933,13 +2169,14 @@ TinCan client library // synchronous if (this._requestMode === XDR) { // synchronous call in IE, with no synchronous mode available - until = 1000 + Date.now(); + until = 10000 + Date.now(); this.log("sendRequest - until: " + until + ", finished: " + finished); - while (Date.now() < until && ! finished) { + while (Date.now() < until && typeof syncFakeStatus === "undefined") { //this.log("calling __delay"); this.__delay(); } + return requestComplete(syncFakeStatus); } return requestComplete(); } @@ -1978,7 +2215,10 @@ TinCan client library requestCfg = { url: "statements", - data: JSON.stringify(stmt.asVersion( this.version )) + data: JSON.stringify(stmt.asVersion( this.version )), + headers: { + "Content-Type": "application/json" + } }; if (stmt.id !== null) { requestCfg.method = "PUT"; @@ -2156,7 +2396,10 @@ TinCan client library requestCfg = { url: "statements", method: "POST", - data: JSON.stringify(versionedStatements) + data: JSON.stringify(versionedStatements), + headers: { + "Content-Type": "application/json" + } }; if (typeof cfg.callback !== "undefined") { requestCfg.callback = cfg.callback; @@ -2552,6 +2795,22 @@ TinCan client library // result.etag = TinCan.Utils.getSHA1String(xhr.responseText); } + + if (typeof xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType, + // for the ones that it supports + result.contentType = xhr.contentType; + } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { + result.contentType = xhr.getResponseHeader("Content-Type"); + } + + if (TinCan.Utils.isApplicationJSON(result.contentType)) { + try { + result.contents = JSON.parse(result.contents); + } catch (ex) { + this.log("retrieveState - failed to deserialize JSON: " + ex); + } + } } } @@ -2580,6 +2839,20 @@ TinCan client library // requestResult.state.etag = TinCan.Utils.getSHA1String(requestResult.xhr.responseText); } + if (typeof requestResult.xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + // for the ones that it supports + requestResult.state.contentType = requestResult.xhr.contentType; + } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { + requestResult.state.contentType = requestResult.xhr.getResponseHeader("Content-Type"); + } + if (TinCan.Utils.isApplicationJSON(requestResult.state.contentType)) { + try { + requestResult.state.contents = JSON.parse(requestResult.state.contents); + } catch (ex) { + this.log("retrieveState - failed to deserialize JSON: " + ex); + } + } } } @@ -2597,6 +2870,7 @@ TinCan client library @param {Object} cfg.agent TinCan.Agent @param {String} [cfg.registration] Registration @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing state + @param {String} [cfg.contentType] Content-Type to specify in headers (defaults to 'application/octet-stream') @param {Function} [cfg.callback] Callback to execute on completion */ saveState: function (key, val, cfg) { @@ -2614,7 +2888,11 @@ TinCan client library return; } - if (typeof val === "object") { + if (typeof cfg.contentType === "undefined") { + cfg.contentType = "application/octet-stream"; + } + + if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) { val = JSON.stringify(val); } @@ -2641,15 +2919,16 @@ TinCan client library url: "activities/state", method: "PUT", params: requestParams, - data: val + data: val, + headers: { + "Content-Type": cfg.contentType + } }; if (typeof cfg.callback !== "undefined") { requestCfg.callback = cfg.callback; } if (typeof cfg.lastSHA1 !== "undefined" && cfg.lastSHA1 !== null) { - requestCfg.headers = { - "If-Match": cfg.lastSHA1 - }; + requestCfg.headers["If-Match"] = cfg.lastSHA1; } return this.sendRequest(requestCfg); @@ -2668,8 +2947,8 @@ TinCan client library */ dropState: function (key, cfg) { this.log("dropState"); - var requestParams = {}, - requestCfg = {} + var requestParams, + requestCfg ; // TODO: it would be better to make a subclass that knows @@ -2773,6 +3052,20 @@ TinCan client library // result.etag = TinCan.Utils.getSHA1String(xhr.responseText); } + if (typeof xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + // for the ones that it supports + result.contentType = xhr.contentType; + } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { + result.contentType = xhr.getResponseHeader("Content-Type"); + } + if (TinCan.Utils.isApplicationJSON(result.contentType)) { + try { + result.contents = JSON.parse(result.contents); + } catch (ex) { + this.log("retrieveActivityProfile - failed to deserialize JSON: " + ex); + } + } } } @@ -2802,6 +3095,20 @@ TinCan client library // requestResult.profile.etag = TinCan.Utils.getSHA1String(requestResult.xhr.responseText); } + if (typeof requestResult.xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + // for the ones that it supports + requestResult.profile.contentType = requestResult.xhr.contentType; + } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { + requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type"); + } + if (TinCan.Utils.isApplicationJSON(requestResult.profile.contentType)) { + try { + requestResult.profile.contents = JSON.parse(requestResult.profile.contents); + } catch (ex) { + this.log("retrieveActivityProfile - failed to deserialize JSON: " + ex); + } + } } } @@ -2816,6 +3123,7 @@ TinCan client library @param {Object} cfg Configuration options @param {Object} cfg.activity TinCan.Activity @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers (defaults to 'application/octet-stream') @param {Function} [cfg.callback] Callback to execute on completion */ saveActivityProfile: function (key, val, cfg) { @@ -2830,7 +3138,11 @@ TinCan client library return; } - if (typeof val === "object") { + if (typeof cfg.contentType === "undefined") { + cfg.contentType = "application/octet-stream"; + } + + if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) { val = JSON.stringify(val); } @@ -2841,20 +3153,19 @@ TinCan client library profileId: key, activityId: cfg.activity.id }, - data: val + data: val, + headers: { + "Content-Type": cfg.contentType + } }; if (typeof cfg.callback !== "undefined") { requestCfg.callback = cfg.callback; } if (typeof cfg.lastSHA1 !== "undefined" && cfg.lastSHA1 !== null) { - requestCfg.headers = { - "If-Match": cfg.lastSHA1 - }; + requestCfg.headers["If-Match"] = cfg.lastSHA1; } else { - requestCfg.headers = { - "If-None-Match": "*" - }; + requestCfg.headers["If-None-Match"] = "*"; } return this.sendRequest(requestCfg); @@ -2871,8 +3182,8 @@ TinCan client library */ dropActivityProfile: function (key, cfg) { this.log("dropActivityProfile"); - var requestParams = {}, - requestCfg = {} + var requestParams, + requestCfg ; // TODO: it would be better to make a subclass that knows @@ -2884,11 +3195,9 @@ TinCan client library } requestParams = { + profileId: key, activityId: cfg.activity.id }; - if (key !== null) { - requestParams.profileId = key; - } requestCfg = { url: "activities/profile", @@ -2902,6 +3211,242 @@ TinCan client library return this.sendRequest(requestCfg); }, + /** + Retrieve an agent profile value, when used from a browser sends to the endpoint using the RESTful interface. + + @method retrieveAgentProfile + @param {String} key Key of agent profile to retrieve + @param {Object} cfg Configuration options + @param {Object} cfg.agent TinCan.Agent + @param {Function} [cfg.callback] Callback to execute on completion + @return {Object} Value retrieved + */ + retrieveAgentProfile: function (key, cfg) { + this.log("retrieveAgentProfile"); + var requestCfg = {}, + requestResult, + callbackWrapper + ; + + // TODO: it would be better to make a subclass that knows + // its own environment and just implements the protocol + // that it needs to + if (! TinCan.environment().isBrowser) { + this.log("error: environment not implemented"); + return; + } + + requestCfg = { + method: "GET", + params: { + profileId: key + }, + ignore404: true + }; + if (this.version === "0.9") { + requestCfg.url = "actors/profile"; + requestCfg.params.actor = JSON.stringify(cfg.agent.asVersion(this.version)); + } + else { + requestCfg.url = "agents/profile"; + requestCfg.params.agent = JSON.stringify(cfg.agent.asVersion(this.version)); + } + if (typeof cfg.callback !== "undefined") { + callbackWrapper = function (err, xhr) { + var result = xhr; + + if (err === null) { + if (xhr.status === 404) { + result = null; + } + else { + result = new TinCan.AgentProfile( + { + id: key, + agent: cfg.agent, + contents: xhr.responseText + } + ); + if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("ETag") !== null && xhr.getResponseHeader("ETag") !== "") { + result.etag = xhr.getResponseHeader("ETag"); + } else { + // + // either XHR didn't have getResponseHeader (probably cause it is an IE + // XDomainRequest object which doesn't) or not populated by LRS so create + // the hash ourselves + // + result.etag = TinCan.Utils.getSHA1String(xhr.responseText); + } + if (typeof xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + // for the ones that it supports + result.contentType = xhr.contentType; + } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { + result.contentType = xhr.getResponseHeader("Content-Type"); + } + if (TinCan.Utils.isApplicationJSON(result.contentType)) { + try { + result.contents = JSON.parse(result.contents); + } catch (ex) { + this.log("retrieveAgentProfile - failed to deserialize JSON: " + ex); + } + } + } + } + + cfg.callback(err, result); + }; + requestCfg.callback = callbackWrapper; + } + + requestResult = this.sendRequest(requestCfg); + if (! callbackWrapper) { + requestResult.profile = null; + if (requestResult.err === null && requestResult.xhr.status !== 404) { + requestResult.profile = new TinCan.AgentProfile( + { + id: key, + agent: cfg.agent, + contents: requestResult.xhr.responseText + } + ); + if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("ETag") !== null && requestResult.xhr.getResponseHeader("ETag") !== "") { + requestResult.profile.etag = requestResult.xhr.getResponseHeader("ETag"); + } else { + // + // either XHR didn't have getResponseHeader (probably cause it is an IE + // XDomainRequest object which doesn't) or not populated by LRS so create + // the hash ourselves + // + requestResult.profile.etag = TinCan.Utils.getSHA1String(requestResult.xhr.responseText); + } + if (typeof requestResult.xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + // for the ones that it supports + requestResult.profile.contentType = requestResult.xhr.contentType; + } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { + requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type"); + } + if (TinCan.Utils.isApplicationJSON(requestResult.profile.contentType)) { + try { + requestResult.profile.contents = JSON.parse(requestResult.profile.contents); + } catch (ex) { + this.log("retrieveAgentProfile - failed to deserialize JSON: " + ex); + } + } + } + } + + return requestResult; + }, + + /** + Save an agent profile value, when used from a browser sends to the endpoint using the RESTful interface. + + @method saveAgentProfile + @param {String} key Key of agent profile to retrieve + @param {Object} cfg Configuration options + @param {Object} cfg.agent TinCan.Agent + @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers (defaults to 'application/octet-stream') + @param {Function} [cfg.callback] Callback to execute on completion + */ + saveAgentProfile: function (key, val, cfg) { + this.log("saveAgentProfile"); + var requestCfg; + + // TODO: it would be better to make a subclass that knows + // its own environment and just implements the protocol + // that it needs to + if (! TinCan.environment().isBrowser) { + this.log("error: environment not implemented"); + return; + } + + if (typeof cfg.contentType === "undefined") { + cfg.contentType = "application/octet-stream"; + } + + if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) { + val = JSON.stringify(val); + } + + requestCfg = { + method: "PUT", + params: { + profileId: key + }, + data: val, + headers: { + "Content-Type": cfg.contentType + } + }; + if (this.version === "0.9") { + requestCfg.url = "actors/profile"; + requestCfg.params.actor = JSON.stringify(cfg.agent.asVersion(this.version)); + } + else { + requestCfg.url = "agents/profile"; + requestCfg.params.agent = JSON.stringify(cfg.agent.asVersion(this.version)); + } + if (typeof cfg.callback !== "undefined") { + requestCfg.callback = cfg.callback; + } + if (typeof cfg.lastSHA1 !== "undefined" && cfg.lastSHA1 !== null) { + requestCfg.headers["If-Match"] = cfg.lastSHA1; + } + else { + requestCfg.headers["If-None-Match"] = "*"; + } + + return this.sendRequest(requestCfg); + }, + + /** + Drop an agent profile value or all of the agent profile, when used from a browser sends to the endpoint using the RESTful interface. + + @method dropAgentProfile + @param {String|null} key Key of agent profile to delete, or null for all + @param {Object} cfg Configuration options + @param {Object} cfg.agent TinCan.Agent + @param {Function} [cfg.callback] Callback to execute on completion + */ + dropAgentProfile: function (key, cfg) { + this.log("dropAgentProfile"); + var requestParams, + requestCfg + ; + + // TODO: it would be better to make a subclass that knows + // its own environment and just implements the protocol + // that it needs to + if (! TinCan.environment().isBrowser) { + this.log("error: environment not implemented"); + return; + } + + requestParams = { + profileId: key + }; + requestCfg = { + method: "DELETE", + params: requestParams + }; + if (this.version === "0.9") { + requestCfg.url = "actors/profile"; + requestParams.actor = JSON.stringify(cfg.agent.asVersion(this.version)); + } + else { + requestCfg.url = "agents/profile"; + requestParams.agent = JSON.stringify(cfg.agent.asVersion(this.version)); + } + if (typeof cfg.callback !== "undefined") { + requestCfg.callback = cfg.callback; + } + + return this.sendRequest(requestCfg); + }, + /** Non-environment safe method used to create a delay to give impression of synchronous response @@ -5954,6 +6499,12 @@ TinCan client library */ this.etag = null; + /** + @property contentType + @type String + */ + this.contentType = null; + this.init(cfg); }; State.prototype = { @@ -5976,7 +6527,9 @@ TinCan client library var i, directProps = [ "id", - "contents" + "contents", + "etag", + "contentType" ], val ; @@ -6070,6 +6623,12 @@ TinCan client library */ this.etag = null; + /** + @property contentType + @type String + */ + this.contentType = null; + this.init(cfg); }; ActivityProfile.prototype = { @@ -6093,7 +6652,8 @@ TinCan client library directProps = [ "id", "contents", - "etag" + "etag", + "contentType" ], val ; @@ -6131,6 +6691,139 @@ TinCan client library return new ActivityProfile(_state); }; }()); +/* + Copyright 2013 Rustici Software + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/** +TinCan client library + +@module TinCan +@submodule TinCan.AgentProfile +**/ +(function () { + "use strict"; + + /** + @class TinCan.AgentProfile + @constructor + */ + var AgentProfile = TinCan.AgentProfile = function (cfg) { + this.log("constructor"); + + /** + @property id + @type String + */ + this.id = null; + + /** + @property agent + @type TinCan.Agent + */ + this.agent = null; + + /** + @property updated + @type String + */ + this.updated = null; + + /** + @property contents + @type String + */ + this.contents = null; + + /** + SHA1 of contents as provided by the server during last fetch, + this should be passed through to saveAgentProfile + + @property etag + @type String + */ + this.etag = null; + + /** + @property contentType + @type String + */ + this.contentType = null; + + this.init(cfg); + }; + AgentProfile.prototype = { + /** + @property LOG_SRC + */ + LOG_SRC: 'AgentProfile', + + /** + @method log + */ + log: TinCan.prototype.log, + + /** + @method init + @param {Object} [options] Configuration used to initialize + */ + init: function (cfg) { + this.log("init"); + var i, + directProps = [ + "id", + "contents", + "etag", + "contentType" + ], + val + ; + + cfg = cfg || {}; + + if (cfg.hasOwnProperty("agent")) { + if (cfg.agent instanceof TinCan.Agent) { + this.agent = cfg.agent; + } + else { + this.agent = new TinCan.Agent (cfg.agent); + } + } + + for (i = 0; i < directProps.length; i += 1) { + if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) { + this[directProps[i]] = cfg[directProps[i]]; + } + } + + this.updated = false; + } + }; + + /** + @method fromJSON + @return {Object} AgentProfile + @static + */ + AgentProfile.fromJSON = function (stateJSON) { + AgentProfile.prototype.log("fromJSON"); + var _state = JSON.parse(stateJSON); + + return new AgentProfile(_state); + }; +}()); /* CryptoJS v3.0.2 code.google.com/p/crypto-js diff --git a/src/ActivityProfile.js b/src/ActivityProfile.js index a59b496..534a5e9 100644 --- a/src/ActivityProfile.js +++ b/src/ActivityProfile.js @@ -63,6 +63,12 @@ TinCan client library */ this.etag = null; + /** + @property contentType + @type String + */ + this.contentType = null; + this.init(cfg); }; ActivityProfile.prototype = { @@ -86,7 +92,8 @@ TinCan client library directProps = [ "id", "contents", - "etag" + "etag", + "contentType" ], val ; diff --git a/src/AgentProfile.js b/src/AgentProfile.js new file mode 100644 index 0000000..93a6f94 --- /dev/null +++ b/src/AgentProfile.js @@ -0,0 +1,133 @@ +/* + Copyright 2013 Rustici Software + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/** +TinCan client library + +@module TinCan +@submodule TinCan.AgentProfile +**/ +(function () { + "use strict"; + + /** + @class TinCan.AgentProfile + @constructor + */ + var AgentProfile = TinCan.AgentProfile = function (cfg) { + this.log("constructor"); + + /** + @property id + @type String + */ + this.id = null; + + /** + @property agent + @type TinCan.Agent + */ + this.agent = null; + + /** + @property updated + @type String + */ + this.updated = null; + + /** + @property contents + @type String + */ + this.contents = null; + + /** + SHA1 of contents as provided by the server during last fetch, + this should be passed through to saveAgentProfile + + @property etag + @type String + */ + this.etag = null; + + /** + @property contentType + @type String + */ + this.contentType = null; + + this.init(cfg); + }; + AgentProfile.prototype = { + /** + @property LOG_SRC + */ + LOG_SRC: 'AgentProfile', + + /** + @method log + */ + log: TinCan.prototype.log, + + /** + @method init + @param {Object} [options] Configuration used to initialize + */ + init: function (cfg) { + this.log("init"); + var i, + directProps = [ + "id", + "contents", + "etag", + "contentType" + ], + val + ; + + cfg = cfg || {}; + + if (cfg.hasOwnProperty("agent")) { + if (cfg.agent instanceof TinCan.Agent) { + this.agent = cfg.agent; + } + else { + this.agent = new TinCan.Agent (cfg.agent); + } + } + + for (i = 0; i < directProps.length; i += 1) { + if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) { + this[directProps[i]] = cfg[directProps[i]]; + } + } + + this.updated = false; + } + }; + + /** + @method fromJSON + @return {Object} AgentProfile + @static + */ + AgentProfile.fromJSON = function (stateJSON) { + AgentProfile.prototype.log("fromJSON"); + var _state = JSON.parse(stateJSON); + + return new AgentProfile(_state); + }; +}()); diff --git a/src/LRS.js b/src/LRS.js index ed2e24f..f77e5fa 100644 --- a/src/LRS.js +++ b/src/LRS.js @@ -1,5 +1,5 @@ /* - Copyright 2012 Rustici Software + Copyright 2012-2013 Rustici Software Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -263,8 +263,8 @@ TinCan client library Method used to send a request via browser objects to the LRS @method sendRequest - @param {Object} [cfg] Configuration for request - @param {String} [cfg.url] URL portion to add to endpoint + @param {Object} cfg Configuration for request + @param {String} cfg.url URL portion to add to endpoint @param {String} [cfg.method] GET, PUT, POST, etc. @param {Object} [cfg.params] Parameters to set on the querystring @param {String} [cfg.data] String of body content @@ -286,6 +286,7 @@ TinCan client library headers = {}, data, requestCompleteResult, + syncFakeStatus, until, prop, pairs = [], @@ -293,17 +294,26 @@ TinCan client library ; // Setup request callback - function requestComplete () { + function requestComplete (fakeStatus) { self.log("requestComplete: " + finished + ", xhr.status: " + xhr.status); var notFoundOk, httpStatus; // - // older versions of IE don't properly handle 204 status codes - // so correct when receiving a 1223 to be 204 locally - // http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request + // XDomainRequest doesn't give us a way to get the status, + // so allow passing in a forged one // - httpStatus = (xhr.status === 1223) ? 204 : xhr.status; + if (typeof xhr.status === "undefined") { + httpStatus = fakeStatus; + } + else { + // + // older versions of IE don't properly handle 204 status codes + // so correct when receiving a 1223 to be 204 locally + // http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request + // + httpStatus = (xhr.status === 1223) ? 204 : xhr.status; + } if (! finished) { // may be in sync or async mode, using XMLHttpRequest or IE XDomainRequest, onreadystatechange or @@ -312,7 +322,7 @@ TinCan client library finished = true; notFoundOk = (cfg.ignore404 && httpStatus === 404); - if (httpStatus === undefined || (httpStatus >= 200 && httpStatus < 400) || notFoundOk) { + if ((httpStatus >= 200 && httpStatus < 400) || notFoundOk) { if (cfg.callback) { cfg.callback(null, xhr); } @@ -325,19 +335,21 @@ TinCan client library } } else { - // Alert all errors except cancelled XHR requests - if (httpStatus > 0) { - requestCompleteResult = { - err: httpStatus, - xhr: xhr - }; - if (self.alertOnRequestFailure) { - alert("[warning] There was a problem communicating with the Learning Record Store. (" + httpStatus + " | " + xhr.responseText+ ")"); + requestCompleteResult = { + err: httpStatus, + xhr: xhr + }; + if (self.alertOnRequestFailure) { + if (httpStatus === 0) { + alert("[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint (" + httpStatus + ")"); } - if (cfg.callback) { - cfg.callback(httpStatus, xhr); + else { + alert("[warning] There was a problem communicating with the Learning Record Store. (" + httpStatus + " | " + xhr.responseText+ ")"); } } + if (cfg.callback) { + cfg.callback(httpStatus, xhr); + } return requestCompleteResult; } } @@ -368,7 +380,6 @@ TinCan client library } // consolidate headers - headers["Content-Type"] = "application/json"; headers.Authorization = this.auth; if (this.version !== "0.9") { headers["X-Experience-API-Version"] = this.version; @@ -455,12 +466,37 @@ TinCan client library xhr = new XDomainRequest (); xhr.open("POST", fullUrl); - xhr.onload = function () { - requestComplete(); - }; - xhr.onerror = function () { - requestComplete(); - }; + if (! cfg.callback) { + xhr.onload = function () { + syncFakeStatus = 200; + }; + xhr.onerror = function () { + syncFakeStatus = 400; + }; + xhr.ontimeout = function () { + syncFakeStatus = 0; + }; + } + else { + xhr.onload = function () { + requestComplete(200); + }; + xhr.onerror = function () { + requestComplete(400); + }; + xhr.ontimeout = function () { + requestComplete(0); + }; + } + + // IE likes to randomly abort requests when some handlers + // aren't defined, so define them with no-ops, see: + // + // http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/ + // http://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified + // + xhr.onprogress = function () {}; + xhr.timeout = 0; } else { this.log("sendRequest unrecognized _requestMode: " + this._requestMode); @@ -483,13 +519,14 @@ TinCan client library // synchronous if (this._requestMode === XDR) { // synchronous call in IE, with no synchronous mode available - until = 1000 + Date.now(); + until = 10000 + Date.now(); this.log("sendRequest - until: " + until + ", finished: " + finished); - while (Date.now() < until && ! finished) { + while (Date.now() < until && typeof syncFakeStatus === "undefined") { //this.log("calling __delay"); this.__delay(); } + return requestComplete(syncFakeStatus); } return requestComplete(); } @@ -528,7 +565,10 @@ TinCan client library requestCfg = { url: "statements", - data: JSON.stringify(stmt.asVersion( this.version )) + data: JSON.stringify(stmt.asVersion( this.version )), + headers: { + "Content-Type": "application/json" + } }; if (stmt.id !== null) { requestCfg.method = "PUT"; @@ -706,7 +746,10 @@ TinCan client library requestCfg = { url: "statements", method: "POST", - data: JSON.stringify(versionedStatements) + data: JSON.stringify(versionedStatements), + headers: { + "Content-Type": "application/json" + } }; if (typeof cfg.callback !== "undefined") { requestCfg.callback = cfg.callback; @@ -1102,6 +1145,22 @@ TinCan client library // result.etag = TinCan.Utils.getSHA1String(xhr.responseText); } + + if (typeof xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType, + // for the ones that it supports + result.contentType = xhr.contentType; + } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { + result.contentType = xhr.getResponseHeader("Content-Type"); + } + + if (TinCan.Utils.isApplicationJSON(result.contentType)) { + try { + result.contents = JSON.parse(result.contents); + } catch (ex) { + this.log("retrieveState - failed to deserialize JSON: " + ex); + } + } } } @@ -1130,6 +1189,20 @@ TinCan client library // requestResult.state.etag = TinCan.Utils.getSHA1String(requestResult.xhr.responseText); } + if (typeof requestResult.xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + // for the ones that it supports + requestResult.state.contentType = requestResult.xhr.contentType; + } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { + requestResult.state.contentType = requestResult.xhr.getResponseHeader("Content-Type"); + } + if (TinCan.Utils.isApplicationJSON(requestResult.state.contentType)) { + try { + requestResult.state.contents = JSON.parse(requestResult.state.contents); + } catch (ex) { + this.log("retrieveState - failed to deserialize JSON: " + ex); + } + } } } @@ -1147,6 +1220,7 @@ TinCan client library @param {Object} cfg.agent TinCan.Agent @param {String} [cfg.registration] Registration @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing state + @param {String} [cfg.contentType] Content-Type to specify in headers (defaults to 'application/octet-stream') @param {Function} [cfg.callback] Callback to execute on completion */ saveState: function (key, val, cfg) { @@ -1164,7 +1238,11 @@ TinCan client library return; } - if (typeof val === "object") { + if (typeof cfg.contentType === "undefined") { + cfg.contentType = "application/octet-stream"; + } + + if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) { val = JSON.stringify(val); } @@ -1191,15 +1269,16 @@ TinCan client library url: "activities/state", method: "PUT", params: requestParams, - data: val + data: val, + headers: { + "Content-Type": cfg.contentType + } }; if (typeof cfg.callback !== "undefined") { requestCfg.callback = cfg.callback; } if (typeof cfg.lastSHA1 !== "undefined" && cfg.lastSHA1 !== null) { - requestCfg.headers = { - "If-Match": cfg.lastSHA1 - }; + requestCfg.headers["If-Match"] = cfg.lastSHA1; } return this.sendRequest(requestCfg); @@ -1218,8 +1297,8 @@ TinCan client library */ dropState: function (key, cfg) { this.log("dropState"); - var requestParams = {}, - requestCfg = {} + var requestParams, + requestCfg ; // TODO: it would be better to make a subclass that knows @@ -1323,6 +1402,20 @@ TinCan client library // result.etag = TinCan.Utils.getSHA1String(xhr.responseText); } + if (typeof xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + // for the ones that it supports + result.contentType = xhr.contentType; + } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { + result.contentType = xhr.getResponseHeader("Content-Type"); + } + if (TinCan.Utils.isApplicationJSON(result.contentType)) { + try { + result.contents = JSON.parse(result.contents); + } catch (ex) { + this.log("retrieveActivityProfile - failed to deserialize JSON: " + ex); + } + } } } @@ -1352,6 +1445,20 @@ TinCan client library // requestResult.profile.etag = TinCan.Utils.getSHA1String(requestResult.xhr.responseText); } + if (typeof requestResult.xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + // for the ones that it supports + requestResult.profile.contentType = requestResult.xhr.contentType; + } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { + requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type"); + } + if (TinCan.Utils.isApplicationJSON(requestResult.profile.contentType)) { + try { + requestResult.profile.contents = JSON.parse(requestResult.profile.contents); + } catch (ex) { + this.log("retrieveActivityProfile - failed to deserialize JSON: " + ex); + } + } } } @@ -1366,6 +1473,7 @@ TinCan client library @param {Object} cfg Configuration options @param {Object} cfg.activity TinCan.Activity @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers (defaults to 'application/octet-stream') @param {Function} [cfg.callback] Callback to execute on completion */ saveActivityProfile: function (key, val, cfg) { @@ -1380,7 +1488,11 @@ TinCan client library return; } - if (typeof val === "object") { + if (typeof cfg.contentType === "undefined") { + cfg.contentType = "application/octet-stream"; + } + + if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) { val = JSON.stringify(val); } @@ -1391,20 +1503,19 @@ TinCan client library profileId: key, activityId: cfg.activity.id }, - data: val + data: val, + headers: { + "Content-Type": cfg.contentType + } }; if (typeof cfg.callback !== "undefined") { requestCfg.callback = cfg.callback; } if (typeof cfg.lastSHA1 !== "undefined" && cfg.lastSHA1 !== null) { - requestCfg.headers = { - "If-Match": cfg.lastSHA1 - }; + requestCfg.headers["If-Match"] = cfg.lastSHA1; } else { - requestCfg.headers = { - "If-None-Match": "*" - }; + requestCfg.headers["If-None-Match"] = "*"; } return this.sendRequest(requestCfg); @@ -1421,8 +1532,8 @@ TinCan client library */ dropActivityProfile: function (key, cfg) { this.log("dropActivityProfile"); - var requestParams = {}, - requestCfg = {} + var requestParams, + requestCfg ; // TODO: it would be better to make a subclass that knows @@ -1434,11 +1545,9 @@ TinCan client library } requestParams = { + profileId: key, activityId: cfg.activity.id }; - if (key !== null) { - requestParams.profileId = key; - } requestCfg = { url: "activities/profile", @@ -1452,6 +1561,242 @@ TinCan client library return this.sendRequest(requestCfg); }, + /** + Retrieve an agent profile value, when used from a browser sends to the endpoint using the RESTful interface. + + @method retrieveAgentProfile + @param {String} key Key of agent profile to retrieve + @param {Object} cfg Configuration options + @param {Object} cfg.agent TinCan.Agent + @param {Function} [cfg.callback] Callback to execute on completion + @return {Object} Value retrieved + */ + retrieveAgentProfile: function (key, cfg) { + this.log("retrieveAgentProfile"); + var requestCfg = {}, + requestResult, + callbackWrapper + ; + + // TODO: it would be better to make a subclass that knows + // its own environment and just implements the protocol + // that it needs to + if (! TinCan.environment().isBrowser) { + this.log("error: environment not implemented"); + return; + } + + requestCfg = { + method: "GET", + params: { + profileId: key + }, + ignore404: true + }; + if (this.version === "0.9") { + requestCfg.url = "actors/profile"; + requestCfg.params.actor = JSON.stringify(cfg.agent.asVersion(this.version)); + } + else { + requestCfg.url = "agents/profile"; + requestCfg.params.agent = JSON.stringify(cfg.agent.asVersion(this.version)); + } + if (typeof cfg.callback !== "undefined") { + callbackWrapper = function (err, xhr) { + var result = xhr; + + if (err === null) { + if (xhr.status === 404) { + result = null; + } + else { + result = new TinCan.AgentProfile( + { + id: key, + agent: cfg.agent, + contents: xhr.responseText + } + ); + if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("ETag") !== null && xhr.getResponseHeader("ETag") !== "") { + result.etag = xhr.getResponseHeader("ETag"); + } else { + // + // either XHR didn't have getResponseHeader (probably cause it is an IE + // XDomainRequest object which doesn't) or not populated by LRS so create + // the hash ourselves + // + result.etag = TinCan.Utils.getSHA1String(xhr.responseText); + } + if (typeof xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + // for the ones that it supports + result.contentType = xhr.contentType; + } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { + result.contentType = xhr.getResponseHeader("Content-Type"); + } + if (TinCan.Utils.isApplicationJSON(result.contentType)) { + try { + result.contents = JSON.parse(result.contents); + } catch (ex) { + this.log("retrieveAgentProfile - failed to deserialize JSON: " + ex); + } + } + } + } + + cfg.callback(err, result); + }; + requestCfg.callback = callbackWrapper; + } + + requestResult = this.sendRequest(requestCfg); + if (! callbackWrapper) { + requestResult.profile = null; + if (requestResult.err === null && requestResult.xhr.status !== 404) { + requestResult.profile = new TinCan.AgentProfile( + { + id: key, + agent: cfg.agent, + contents: requestResult.xhr.responseText + } + ); + if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("ETag") !== null && requestResult.xhr.getResponseHeader("ETag") !== "") { + requestResult.profile.etag = requestResult.xhr.getResponseHeader("ETag"); + } else { + // + // either XHR didn't have getResponseHeader (probably cause it is an IE + // XDomainRequest object which doesn't) or not populated by LRS so create + // the hash ourselves + // + requestResult.profile.etag = TinCan.Utils.getSHA1String(requestResult.xhr.responseText); + } + if (typeof requestResult.xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + // for the ones that it supports + requestResult.profile.contentType = requestResult.xhr.contentType; + } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { + requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type"); + } + if (TinCan.Utils.isApplicationJSON(requestResult.profile.contentType)) { + try { + requestResult.profile.contents = JSON.parse(requestResult.profile.contents); + } catch (ex) { + this.log("retrieveAgentProfile - failed to deserialize JSON: " + ex); + } + } + } + } + + return requestResult; + }, + + /** + Save an agent profile value, when used from a browser sends to the endpoint using the RESTful interface. + + @method saveAgentProfile + @param {String} key Key of agent profile to retrieve + @param {Object} cfg Configuration options + @param {Object} cfg.agent TinCan.Agent + @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers (defaults to 'application/octet-stream') + @param {Function} [cfg.callback] Callback to execute on completion + */ + saveAgentProfile: function (key, val, cfg) { + this.log("saveAgentProfile"); + var requestCfg; + + // TODO: it would be better to make a subclass that knows + // its own environment and just implements the protocol + // that it needs to + if (! TinCan.environment().isBrowser) { + this.log("error: environment not implemented"); + return; + } + + if (typeof cfg.contentType === "undefined") { + cfg.contentType = "application/octet-stream"; + } + + if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) { + val = JSON.stringify(val); + } + + requestCfg = { + method: "PUT", + params: { + profileId: key + }, + data: val, + headers: { + "Content-Type": cfg.contentType + } + }; + if (this.version === "0.9") { + requestCfg.url = "actors/profile"; + requestCfg.params.actor = JSON.stringify(cfg.agent.asVersion(this.version)); + } + else { + requestCfg.url = "agents/profile"; + requestCfg.params.agent = JSON.stringify(cfg.agent.asVersion(this.version)); + } + if (typeof cfg.callback !== "undefined") { + requestCfg.callback = cfg.callback; + } + if (typeof cfg.lastSHA1 !== "undefined" && cfg.lastSHA1 !== null) { + requestCfg.headers["If-Match"] = cfg.lastSHA1; + } + else { + requestCfg.headers["If-None-Match"] = "*"; + } + + return this.sendRequest(requestCfg); + }, + + /** + Drop an agent profile value or all of the agent profile, when used from a browser sends to the endpoint using the RESTful interface. + + @method dropAgentProfile + @param {String|null} key Key of agent profile to delete, or null for all + @param {Object} cfg Configuration options + @param {Object} cfg.agent TinCan.Agent + @param {Function} [cfg.callback] Callback to execute on completion + */ + dropAgentProfile: function (key, cfg) { + this.log("dropAgentProfile"); + var requestParams, + requestCfg + ; + + // TODO: it would be better to make a subclass that knows + // its own environment and just implements the protocol + // that it needs to + if (! TinCan.environment().isBrowser) { + this.log("error: environment not implemented"); + return; + } + + requestParams = { + profileId: key + }; + requestCfg = { + method: "DELETE", + params: requestParams + }; + if (this.version === "0.9") { + requestCfg.url = "actors/profile"; + requestParams.actor = JSON.stringify(cfg.agent.asVersion(this.version)); + } + else { + requestCfg.url = "agents/profile"; + requestParams.agent = JSON.stringify(cfg.agent.asVersion(this.version)); + } + if (typeof cfg.callback !== "undefined") { + requestCfg.callback = cfg.callback; + } + + return this.sendRequest(requestCfg); + }, + /** Non-environment safe method used to create a delay to give impression of synchronous response diff --git a/src/State.js b/src/State.js index eafb354..e602448 100644 --- a/src/State.js +++ b/src/State.js @@ -54,6 +54,12 @@ TinCan client library */ this.etag = null; + /** + @property contentType + @type String + */ + this.contentType = null; + this.init(cfg); }; State.prototype = { @@ -76,7 +82,9 @@ TinCan client library var i, directProps = [ "id", - "contents" + "contents", + "etag", + "contentType" ], val ; diff --git a/src/TinCan.js b/src/TinCan.js index ed31601..29daa8f 100644 --- a/src/TinCan.js +++ b/src/TinCan.js @@ -67,7 +67,10 @@ var TinCan; @param {String} [options.url] URL for determining launch provided configuration options @param {Array} [options.recordStores] list of pre-configured LRSes + @param {Object|TinCan.Agent} [options.actor] default actor @param {Object|TinCan.Activity} [options.activity] default activity + @param {String} [options.registration] default registration + @param {Object|TinCan.Context} [options.context] default context **/ TinCan = function (cfg) { this.log("constructor"); @@ -171,6 +174,25 @@ var TinCan; this.activity = new TinCan.Activity (cfg.activity); } } + if (cfg.hasOwnProperty("actor")) { + if (cfg.actor instanceof TinCan.Agent) { + this.actor = cfg.actor; + } + else { + this.actor = new TinCan.Agent (cfg.actor); + } + } + if (cfg.hasOwnProperty("context")) { + if (cfg.context instanceof TinCan.Context) { + this.context = cfg.context; + } + else { + this.context = new TinCan.Context (cfg.context); + } + } + if (cfg.hasOwnProperty("registration")) { + this.registration = cfg.registration; + } }, /** @@ -887,6 +909,7 @@ var TinCan; @param {Object} [cfg.registration] Registration used in query, defaults to 'registration' property if empty @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing state + @param {String} [cfg.contentType] Content-Type to specify in headers @param {Function} [cfg.callback] Function to run with state */ setState: function (key, val, cfg) { @@ -922,6 +945,9 @@ var TinCan; if (typeof cfg.lastSHA1 !== "undefined") { queryCfg.lastSHA1 = cfg.lastSHA1; } + if (typeof cfg.contentType !== "undefined") { + queryCfg.contentType = cfg.contentType; + } if (typeof cfg.callback !== "undefined") { queryCfg.callback = cfg.callback; } @@ -1051,6 +1077,7 @@ var TinCan; @param {Object} [cfg.activity] Activity used in query, defaults to 'activity' property if empty @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers @param {Function} [cfg.callback] Function to run with activity profile */ setActivityProfile: function (key, val, cfg) { @@ -1082,6 +1109,9 @@ var TinCan; if (typeof cfg.lastSHA1 !== "undefined") { queryCfg.lastSHA1 = cfg.lastSHA1; } + if (typeof cfg.contentType !== "undefined") { + queryCfg.contentType = cfg.contentType; + } return lrs.saveActivityProfile(key, val, queryCfg); } @@ -1140,6 +1170,156 @@ var TinCan; else { this.log(msg); } + }, + + /** + @method getAgentProfile + @param {String} key Key to retrieve from the profile + @param {Object} [cfg] Configuration for request + @param {Object} [cfg.agent] Agent used in query, + defaults to 'actor' property if empty + @param {Function} [cfg.callback] Function to run with agent profile + */ + getAgentProfile: function (key, cfg) { + this.log("getAgentProfile"); + var queryCfg, + lrs, + msg + ; + + if (this.recordStores.length > 0) { + // + // for agent profiles (for now) we are only going to store to the first LRS + // so only get from there too + // + // TODO: make this the first non-allowFail LRS but for now it should + // be good enough to make it the first since we know the LMS provided + // LRS is the first + // + lrs = this.recordStores[0]; + + cfg = cfg || {}; + + queryCfg = { + agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor) + }; + if (typeof cfg.callback !== "undefined") { + queryCfg.callback = cfg.callback; + } + + return lrs.retrieveAgentProfile(key, queryCfg); + } + + msg = "[warning] getAgentProfile: No LRSs added yet (agent profile not retrieved)"; + if (TinCan.environment().isBrowser) { + alert(this.LOG_SRC + ": " + msg); + } + else { + this.log(msg); + } + }, + + /** + @method setAgentProfile + @param {String} key Key to store into the agent profile + @param {String|Object} val Value to store into the agent profile, objects will be stringified to JSON + @param {Object} [cfg] Configuration for request + @param {Object} [cfg.agent] Agent used in query, + defaults to 'actor' property if empty + @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers + @param {Function} [cfg.callback] Function to run with agent profile + */ + setAgentProfile: function (key, val, cfg) { + this.log("setAgentProfile"); + var queryCfg, + lrs, + msg + ; + + if (this.recordStores.length > 0) { + // + // for agent profile (for now) we are only going to store to the first LRS + // so only get from there too + // + // TODO: make this the first non-allowFail LRS but for now it should + // be good enough to make it the first since we know the LMS provided + // LRS is the first + // + lrs = this.recordStores[0]; + + cfg = cfg || {}; + + queryCfg = { + agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor) + }; + if (typeof cfg.callback !== "undefined") { + queryCfg.callback = cfg.callback; + } + if (typeof cfg.lastSHA1 !== "undefined") { + queryCfg.lastSHA1 = cfg.lastSHA1; + } + if (typeof cfg.contentType !== "undefined") { + queryCfg.contentType = cfg.contentType; + } + + return lrs.saveAgentProfile(key, val, queryCfg); + } + + msg = "[warning] setAgentProfile: No LRSs added yet (agent profile not saved)"; + if (TinCan.environment().isBrowser) { + alert(this.LOG_SRC + ": " + msg); + } + else { + this.log(msg); + } + }, + + /** + @method deleteAgentProfile + @param {String|null} key Key to remove from the agent profile, or null to clear all + @param {Object} [cfg] Configuration for request + @param {Object} [cfg.agent] Agent used in query, + defaults to 'actor' property if empty + @param {Function} [cfg.callback] Function to run with agent profile + */ + deleteAgentProfile: function (key, cfg) { + this.log("deleteAgentProfile"); + var queryCfg, + lrs, + msg + ; + + if (this.recordStores.length > 0) { + // + // for agent profile (for now) we are only going to store to the first LRS + // so only get from there too + // + // TODO: make this the first non-allowFail LRS but for now it should + // be good enough to make it the first since we know the LMS provided + // LRS is the first + // + lrs = this.recordStores[0]; + + cfg = cfg || {}; + + queryCfg = { + agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor) + }; + if (typeof cfg.callback !== "undefined") { + queryCfg.callback = cfg.callback; + } + + return lrs.dropAgentProfile(key, queryCfg); + } + + msg = "[warning] deleteAgentProfile: No LRSs added yet (agent profile not deleted)"; + if (TinCan.environment().isBrowser) { + alert(this.LOG_SRC + ": " + msg); + } + else { + this.log(msg); + } } }; diff --git a/src/Utils.js b/src/Utils.js index 29ea883..8cba5aa 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -182,6 +182,26 @@ TinCan client library getServerRoot: function (absoluteUrl) { var urlParts = absoluteUrl.split("/"); return urlParts[0] + "//" + urlParts[2]; + }, + + /** + @method getContentTypeFromHeader + @static + @param {String} Content-Type header value + @return {String} Primary value from Content-Type + */ + getContentTypeFromHeader: function (header) { + return (String(header).split(";"))[0]; + }, + + /** + @method isApplicationJSON + @static + @param {String} Content-Type header value + @return {Boolean} whether "application/json" was matched + */ + isApplicationJSON: function (header) { + return TinCan.Utils.getContentTypeFromHeader(header).toLowerCase().indexOf("application/json") === 0; } }; }()); diff --git a/test/complete.html b/test/complete.html index 034230e..a28ab3f 100644 --- a/test/complete.html +++ b/test/complete.html @@ -49,6 +49,7 @@ + --> @@ -60,9 +61,9 @@

- - + + @@ -78,5 +79,7 @@

+ + diff --git a/test/index.html b/test/index.html index 019edc7..5d2c9c0 100644 --- a/test/index.html +++ b/test/index.html @@ -28,6 +28,8 @@

Single Test Files

  • TinCan
  • LRS
  • State
  • +
  • ActivityProfile
  • +
  • AgentProfile
  • StatementsResult
  • Agent
  • Group
  • @@ -46,5 +48,10 @@

    Single Test Files

  • Utils
  • +

    Special Conditions

    + + diff --git a/test/js/ActivityProfile.js b/test/js/ActivityProfile.js new file mode 100644 index 0000000..2c9f2f0 --- /dev/null +++ b/test/js/ActivityProfile.js @@ -0,0 +1,102 @@ +/*! + Copyright 2013 Rustici Software + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +(function () { + var session = null, + commonId = "testActivityProfile", + commonContentString = "test content", + commonContentStringType = "text/plain"; + + module("ActivityProfile Statics"); + + test( + "ActivityProfile.fromJSON", + function () { + var raw = {}, + string, + result + ; + + result = TinCan.ActivityProfile.fromJSON(JSON.stringify(raw)); + ok(result instanceof TinCan.ActivityProfile, "returns TinCan.ActivityProfile"); + } + ); + + module("ActivityProfile Instance"); + + test( + "activityProfile Object", + function () { + var obj = new TinCan.ActivityProfile (), + nullProps = [ + "id", + "contents", + "contentType", + "etag" + ], + i + ; + + ok(obj instanceof TinCan.ActivityProfile, "object is TinCan.ActivityProfile"); + + for (i = 0; i < nullProps.length; i += 1) { + ok(obj.hasOwnProperty(nullProps[i]), "object has property: " + nullProps[i]); + strictEqual(obj[nullProps[i]], null, "object property initial value: " + nullProps[i]); + } + + ok(obj.hasOwnProperty("updated"), "object has property: updated"); + strictEqual(obj.updated, false, "object property initial value: updated"); + + strictEqual(obj.LOG_SRC, "ActivityProfile", "object property LOG_SRC initial value"); + } + ); + + test( + "activityProfile variants", + function () { + var set = [ + { + name: "basic properties: string content", + instanceConfig: { + id: commonId, + contents: commonContentString, + contentType: commonContentStringType + }, + checkProps: { + id: commonId, + contents: commonContentString, + contentType: commonContentStringType + } + } + ], + i, + obj, + result + ; + + for (i = 0; i < set.length; i += 1) { + row = set[i]; + obj = new TinCan.ActivityProfile (row.instanceConfig); + + ok(obj instanceof TinCan.ActivityProfile, "object is TinCan.ActivityProfile (" + row.name + ")"); + if (typeof row.checkProps !== "undefined") { + for (key in row.checkProps) { + deepEqual(obj[key], row.checkProps[key], "object property initial value: " + key + " (" + row.name + ")"); + } + } + } + } + ); +}()); diff --git a/test/js/AgentProfile.js b/test/js/AgentProfile.js new file mode 100644 index 0000000..971067c --- /dev/null +++ b/test/js/AgentProfile.js @@ -0,0 +1,102 @@ +/*! + Copyright 2013 Rustici Software + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +(function () { + var session = null, + commonId = "testAgentProfile", + commonContentString = "test content", + commonContentStringType = "text/plain"; + + module("AgentProfile Statics"); + + test( + "AgentProfile.fromJSON", + function () { + var raw = {}, + string, + result + ; + + result = TinCan.AgentProfile.fromJSON(JSON.stringify(raw)); + ok(result instanceof TinCan.AgentProfile, "returns TinCan.AgentProfile"); + } + ); + + module("AgentProfile Instance"); + + test( + "agentProfile Object", + function () { + var obj = new TinCan.AgentProfile (), + nullProps = [ + "id", + "contents", + "contentType", + "etag" + ], + i + ; + + ok(obj instanceof TinCan.AgentProfile, "object is TinCan.AgentProfile"); + + for (i = 0; i < nullProps.length; i += 1) { + ok(obj.hasOwnProperty(nullProps[i]), "object has property: " + nullProps[i]); + strictEqual(obj[nullProps[i]], null, "object property initial value: " + nullProps[i]); + } + + ok(obj.hasOwnProperty("updated"), "object has property: updated"); + strictEqual(obj.updated, false, "object property initial value: updated"); + + strictEqual(obj.LOG_SRC, "AgentProfile", "object property LOG_SRC initial value"); + } + ); + + test( + "agentProfile variants", + function () { + var set = [ + { + name: "basic properties: string content", + instanceConfig: { + id: commonId, + contents: commonContentString, + contentType: commonContentStringType + }, + checkProps: { + id: commonId, + contents: commonContentString, + contentType: commonContentStringType + } + } + ], + i, + obj, + result + ; + + for (i = 0; i < set.length; i += 1) { + row = set[i]; + obj = new TinCan.AgentProfile (row.instanceConfig); + + ok(obj instanceof TinCan.AgentProfile, "object is TinCan.AgentProfile (" + row.name + ")"); + if (typeof row.checkProps !== "undefined") { + for (key in row.checkProps) { + deepEqual(obj[key], row.checkProps[key], "object property initial value: " + key + " (" + row.name + ")"); + } + } + } + } + ); +}()); diff --git a/test/js/State.js b/test/js/State.js index adefec1..1ed65ed 100644 --- a/test/js/State.js +++ b/test/js/State.js @@ -14,7 +14,10 @@ limitations under the License. */ (function () { - var session = null; + var session = null, + commonId = "testState", + commonContentString = "test content", + commonContentStringType = "text/plain"; module("State Statics"); @@ -30,4 +33,70 @@ ok(result instanceof TinCan.State, "returns TinCan.State"); } ); + + module("State Instance"); + + test( + "state Object", + function () { + var obj = new TinCan.State (), + nullProps = [ + "id", + "contents", + "contentType", + "etag" + ], + i + ; + + ok(obj instanceof TinCan.State, "object is TinCan.State"); + + for (i = 0; i < nullProps.length; i += 1) { + ok(obj.hasOwnProperty(nullProps[i]), "object has property: " + nullProps[i]); + strictEqual(obj[nullProps[i]], null, "object property initial value: " + nullProps[i]); + } + + ok(obj.hasOwnProperty("updated"), "object has property: updated"); + strictEqual(obj.updated, false, "object property initial value: updated"); + + strictEqual(obj.LOG_SRC, "State", "object property LOG_SRC initial value"); + } + ); + + test( + "state variants", + function () { + var set = [ + { + name: "basic properties: string content", + instanceConfig: { + id: commonId, + contents: commonContentString, + contentType: commonContentStringType + }, + checkProps: { + id: commonId, + contents: commonContentString, + contentType: commonContentStringType + } + } + ], + i, + obj, + result + ; + + for (i = 0; i < set.length; i += 1) { + row = set[i]; + obj = new TinCan.State (row.instanceConfig); + + ok(obj instanceof TinCan.State, "object is TinCan.State (" + row.name + ")"); + if (typeof row.checkProps !== "undefined") { + for (key in row.checkProps) { + deepEqual(obj[key], row.checkProps[key], "object property initial value: " + key + " (" + row.name + ")"); + } + } + } + } + ); }()); diff --git a/test/js/TinCan.js b/test/js/TinCan.js index 73a55c9..16a480f 100644 --- a/test/js/TinCan.js +++ b/test/js/TinCan.js @@ -31,7 +31,9 @@ doSendStatementAsyncTest, doGetStatementAsyncTest, doStateSyncTest, - doActivityProfileSyncTest; + doStateSyncContentTypeJSONTest, + doActivityProfileSyncTest, + doActivityProfileSyncContentTypeJSONTest; module("TinCan Statics"); @@ -101,7 +103,6 @@ mockAlerts = []; alertBuiltin = window.alert; window.alert = alertFunc; - }, teardown: function () { session = null; @@ -262,13 +263,6 @@ ); }; - for (i = 0; i < versions.length; i += 1) { - version = versions[i]; - if (TinCanTestCfg.recordStores[version]) { - doSendStatementSyncTest(version); - } - } - doGetStatementSyncTest = function (v) { test( "tincan.getStatement (sync): " + v, @@ -322,13 +316,6 @@ ); }; - for (i = 0; i < versions.length; i += 1) { - version = versions[i]; - if (TinCanTestCfg.recordStores[version]) { - doGetStatementSyncTest(version); - } - } - doVoidStatementSyncTest = function (v) { test( "tincan.getVoidedStatement (sync): " + v, @@ -428,13 +415,6 @@ ); }; - for (i = 0; i < versions.length; i += 1) { - version = versions[i]; - if (TinCanTestCfg.recordStores[version]) { - doVoidStatementSyncTest(version); - } - } - doSendStatementAsyncTest = function (v) { asyncTest( "sendStatement (prepared, async): " + v, @@ -487,13 +467,6 @@ ); }; - for (i = 0; i < versions.length; i += 1) { - version = versions[i]; - if (TinCanTestCfg.recordStores[version]) { - doSendStatementAsyncTest(version); - } - } - doGetStatementAsyncTest = function (v) { asyncTest( "getStatement (async): " + v, @@ -550,19 +523,12 @@ ); }; - for (i = 0; i < versions.length; i += 1) { - version = versions[i]; - if (TinCanTestCfg.recordStores[version]) { - doGetStatementAsyncTest(version); - } - } - doStateSyncTest = function (v) { test( - "tincan state (prepared, sync): " + v, + "tincan state (sync): " + v, function () { var setResult, - key = "setState (prepared, sync)", + key = "setState (sync)", val = "TinCanJS", mbox ="mailto:tincanjs-test-tincan+" + Date.now() + "@tincanapi.com", options = { @@ -584,6 +550,10 @@ ok(setResult.hasOwnProperty("xhr"), "setResult has property: xhr (" + v + ")"); getResult = session[v].getState(key, options); + ok(getResult.hasOwnProperty("state"), "getResult has property: state (" + v + ")"); + ok(getResult.state instanceof TinCan.State, "getResult state property is TinCan.State (" + v + ")"); + deepEqual(getResult.state.contents, val, "getResult state property contents (" + v + ")"); + deepEqual(TinCan.Utils.getContentTypeFromHeader(getResult.state.contentType), "application/octet-stream", "getResult state property contentType (" + v + ")"); // // reset the state to make sure we test the concurrency handling @@ -597,19 +567,63 @@ ); }; - for (i = 0; i < versions.length; i += 1) { - version = versions[i]; - if (TinCanTestCfg.recordStores[version]) { - doStateSyncTest(version); - } - } + doStateSyncContentTypeJSONTest = function (v) { + test( + "tincan state (sync): " + v, + function () { + var setResult, + key = "setState (sync, json content)", + val = { + testObj: { + key1: "val1" + }, + testBool: true, + testNum: 1 + }, + mbox ="mailto:tincanjs-test-tincan+" + Date.now() + "@tincanapi.com", + options = { + contentType: "application/json", + agent: new TinCan.Agent( + { + mbox: mbox + } + ), + activity: new TinCan.Activity( + { + id: "http://tincanapi.com/TinCanJS/Test/TinCan_setState/syncContentType/" + v + } + ) + }; + + setResult = session[v].setState(key, val, options); + + ok(setResult.hasOwnProperty("err"), "setResult has property: err (" + v + ")"); + ok(setResult.hasOwnProperty("xhr"), "setResult has property: xhr (" + v + ")"); + + getResult = session[v].getState(key, options); + ok(getResult.hasOwnProperty("state"), "getResult has property: state (" + v + ")"); + ok(getResult.state instanceof TinCan.State, "getResult state property is TinCan.State (" + v + ")"); + deepEqual(getResult.state.contents, val, "getResult state property contents (" + v + ")"); + deepEqual(TinCan.Utils.getContentTypeFromHeader(getResult.state.contentType), "application/json", "getResult state property contentType (" + v + ")"); + + // + // reset the state to make sure we test the concurrency handling + // + options.lastSHA1 = getResult.state.etag; + setResult = session[v].setState(key, val + 1, options); + delete options.lastSHA1; + + deleteResult = session[v].deleteState(key, options); + } + ); + }; doActivityProfileSyncTest = function (v) { test( - "tincan activityProfile (prepared, sync): " + v, + "tincan activityProfile (sync): " + v, function () { var setResult, - key = "activityProfile (prepared, sync)", + key = "activityProfile (sync)", val = "TinCanJS", options = { activity: new TinCan.Activity( @@ -625,6 +639,10 @@ ok(setResult.hasOwnProperty("xhr"), "setResult has property: xhr (" + v + ")"); getResult = session[v].getActivityProfile(key, options); + ok(getResult.hasOwnProperty("profile"), "getResult has property: profile (" + v + ")"); + ok(getResult.profile instanceof TinCan.ActivityProfile, "getResult profile property is TinCan.ActivityProfile (" + v + ")"); + deepEqual(getResult.profile.contents, val, "getResult profile property contents (" + v + ")"); + deepEqual(TinCan.Utils.getContentTypeFromHeader(getResult.profile.contentType), "application/octet-stream", "getResult profile property contentType (" + v + ")"); // this should "fail" session[v].recordStores[0].alertOnRequestFailure = false; @@ -632,7 +650,57 @@ session[v].recordStores[0].alertOnRequestFailure = true; // - // reset the state to make sure we test the concurrency handling + // reset the profile to make sure we test the concurrency handling + // + options.lastSHA1 = getResult.profile.etag; + setResult = session[v].setActivityProfile(key, val + 2, options); + delete options.lastSHA1; + + deleteResult = session[v].deleteActivityProfile(key, options); + } + ); + }; + + doActivityProfileSyncContentTypeJSONTest = function (v) { + test( + "tincan activityProfile (sync, JSON content type): " + v, + function () { + var setResult, + key = "activityProfile (sync)", + val = { + testObj: { + key1: "val1" + }, + testBool: true, + testNum: 1 + }, + options = { + activity: new TinCan.Activity( + { + id: "http://tincanapi.com/TinCanJS/Test/TinCan_setActivityProfile/syncJSON/" + v + } + ), + contentType: "application/json" + }; + + setResult = session[v].setActivityProfile(key, val, options); + + ok(setResult.hasOwnProperty("err"), "setResult has property: err (" + v + ")"); + ok(setResult.hasOwnProperty("xhr"), "setResult has property: xhr (" + v + ")"); + + getResult = session[v].getActivityProfile(key, options); + ok(getResult.hasOwnProperty("profile"), "getResult has property: profile (" + v + ")"); + ok(getResult.profile instanceof TinCan.ActivityProfile, "getResult profile property is TinCan.ActivityProfile (" + v + ")"); + deepEqual(getResult.profile.contents, val, "getResult profile property contents (" + v + ")"); + deepEqual(TinCan.Utils.getContentTypeFromHeader(getResult.profile.contentType), "application/json", "getResult profile property contentType (" + v + ")"); + + // this should "fail" + session[v].recordStores[0].alertOnRequestFailure = false; + setResult = session[v].setActivityProfile(key, val + 1, options); + session[v].recordStores[0].alertOnRequestFailure = true; + + // + // reset the profile to make sure we test the concurrency handling // options.lastSHA1 = getResult.profile.etag; setResult = session[v].setActivityProfile(key, val + 2, options); @@ -643,11 +711,115 @@ ); }; + doAgentProfileSyncTest = function (v) { + test( + "tincan agentProfile (sync): " + v, + function () { + var setResult, + key = "agentProfile (sync)", + val = "TinCanJS", + mbox ="mailto:tincanjs-test-tincan+" + Date.now() + "@tincanapi.com", + options = { + agent: new TinCan.Agent( + { + mbox: mbox + } + ) + }; + + setResult = session[v].setAgentProfile(key, val, options); + + ok(setResult.hasOwnProperty("err"), "setResult has property: err (" + v + ")"); + ok(setResult.hasOwnProperty("xhr"), "setResult has property: xhr (" + v + ")"); + + getResult = session[v].getAgentProfile(key, options); + ok(getResult.hasOwnProperty("profile"), "getResult has property: profile (" + v + ")"); + ok(getResult.profile instanceof TinCan.AgentProfile, "getResult profile property is TinCan.AgentProfile (" + v + ")"); + deepEqual(getResult.profile.contents, val, "getResult profile property contents (" + v + ")"); + deepEqual(TinCan.Utils.getContentTypeFromHeader(getResult.profile.contentType), "application/octet-stream", "getResult profile property contentType (" + v + ")"); + + // this should "fail" + session[v].recordStores[0].alertOnRequestFailure = false; + setResult = session[v].setAgentProfile(key, val + 1, options); + session[v].recordStores[0].alertOnRequestFailure = true; + + // + // reset the profile to make sure we test the concurrency handling + // + options.lastSHA1 = getResult.profile.etag; + setResult = session[v].setAgentProfile(key, val + 2, options); + delete options.lastSHA1; + + deleteResult = session[v].deleteAgentProfile(key, options); + } + ); + }; + + doAgentProfileSyncContentTypeJSONTest = function (v) { + test( + "tincan agentProfile (sync, JSON content type): " + v, + function () { + var setResult, + key = "agentProfile (sync)", + val = { + testObj: { + key1: "val1" + }, + testBool: true, + testNum: 1 + }, + mbox ="mailto:tincanjs-test-tincan+" + Date.now() + "@tincanapi.com", + options = { + agent: new TinCan.Agent( + { + mbox: mbox + } + ), + contentType: "application/json" + }; + + setResult = session[v].setAgentProfile(key, val, options); + + ok(setResult.hasOwnProperty("err"), "setResult has property: err (" + v + ")"); + ok(setResult.hasOwnProperty("xhr"), "setResult has property: xhr (" + v + ")"); + + getResult = session[v].getAgentProfile(key, options); + ok(getResult.hasOwnProperty("profile"), "getResult has property: profile (" + v + ")"); + ok(getResult.profile instanceof TinCan.AgentProfile, "getResult profile property is TinCan.AgentProfile (" + v + ")"); + deepEqual(getResult.profile.contents, val, "getResult profile property contents (" + v + ")"); + deepEqual(TinCan.Utils.getContentTypeFromHeader(getResult.profile.contentType), "application/json", "getResult profile property contentType (" + v + ")"); + + // this should "fail" + session[v].recordStores[0].alertOnRequestFailure = false; + setResult = session[v].setAgentProfile(key, val + 1, options); + session[v].recordStores[0].alertOnRequestFailure = true; + + // + // reset the profile to make sure we test the concurrency handling + // + options.lastSHA1 = getResult.profile.etag; + setResult = session[v].setAgentProfile(key, val + 2, options); + delete options.lastSHA1; + + deleteResult = session[v].deleteAgentProfile(key, options); + } + ); + }; + for (i = 0; i < versions.length; i += 1) { version = versions[i]; if (TinCanTestCfg.recordStores[version]) { + doSendStatementSyncTest(version); + doGetStatementSyncTest(version); + doVoidStatementSyncTest(version); + doSendStatementAsyncTest(version); + doGetStatementAsyncTest(version); + doStateSyncTest(version); + doStateSyncContentTypeJSONTest(version); doActivityProfileSyncTest(version); + doActivityProfileSyncContentTypeJSONTest(version); + doAgentProfileSyncTest(version); + doAgentProfileSyncContentTypeJSONTest(version); } } - }()); diff --git a/test/js/Utils.js b/test/js/Utils.js index 9927c4b..f4814b0 100644 --- a/test/js/Utils.js +++ b/test/js/Utils.js @@ -111,4 +111,42 @@ ok(result === "http://tincanapi.com:8080", "return value: with port"); } ); + test( + "getContentTypeFromHeader", + function () { + var appJSON = "application/json", + strings = { + "application/json": appJSON, + "application/json; charset=UTF-8": appJSON, + "text/plain;charset=UTF-8": "text/plain" + }, + prop; + for (prop in strings) { + ok(TinCan.Utils.getContentTypeFromHeader(prop) === strings[prop], "'" + prop + "' matches '" + strings[prop]); + } + } + ); + test( + "isApplicationJSON", + function () { + var okStrings = [ + "application/json", + "application/json; charset=UTF-8", + "application/json ", + "Application/JSON", + "Application/JSON " + ], + notOkStrings = [ + "application/octet-stream", + "text/plain" + ], + i; + for (i = 0; i < okStrings.length; i += 1) { + ok(TinCan.Utils.isApplicationJSON(okStrings[i]), "'" + okStrings[i] + "' matched"); + } + for (i = 0; i < notOkStrings.length; i += 1) { + ok(! TinCan.Utils.isApplicationJSON(notOkStrings[i]), "'" + notOkStrings[i] + "' not matched"); + } + } + ); }()); diff --git a/test/js/offline.js b/test/js/offline.js new file mode 100644 index 0000000..9a00297 --- /dev/null +++ b/test/js/offline.js @@ -0,0 +1,121 @@ +/*! + Copyright 2013 Rustici Software + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +(function () { + var session = null, + mockAlerts = null, + alertFunc = function (msg) { + mockAlerts.push(msg); + }, + alertBuiltin; + + module( + "LRS .sendRequest to CORS denied server or offline", + { + setup: function () { + // + // endpoint here shouldn't really matter much cause + // it should be offline and not get through, alternatively + // we get the same processing for a server that denies + // CORS requests (by not specifying correct headers) which + // is why I chose what I did here, so can also use this + // test online for testing CORS failure + // + session = new TinCan.LRS ( + { + endpoint: "http://tincanapi.com", + auth: "test" + } + ); + mockAlerts = []; + alertBuiltin = window.alert; + window.alert = alertFunc; + }, + teardown: function () { + session = null; + mockAlerts = null; + window.alert = alertBuiltin; + } + } + ); + + test( + "basic (sync)", + function () { + var result = session.sendRequest( + { + url: "test" + } + ); + ok(result.hasOwnProperty("err"), "result has property: err"); + deepEqual(result.err, 0, "result property value: err"); + ok(result.hasOwnProperty("xhr"), "result has property: xhr"); + TinCanTest.assertHttpRequestType(result.xhr, "result property value: xhr"); + deepEqual(mockAlerts, ["[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint (0)"], "caught alert: 0"); + } + ); + test( + "no alert (sync)", + function () { + session.alertOnRequestFailure = false; + + var result = session.sendRequest( + { + url: "test" + } + ); + ok(result.hasOwnProperty("err"), "result has property: err"); + deepEqual(result.err, 0, "result property value: err"); + ok(result.hasOwnProperty("xhr"), "result has property: xhr"); + TinCanTest.assertHttpRequestType(result.xhr, "result property value: xhr"); + deepEqual(mockAlerts, [], "no alerts since turned off"); + } + ); + + asyncTest( + "basic (async)", + function () { + var result = session.sendRequest( + { + url: "test", + callback: function (err, xhr) { + start(); + deepEqual(err, 0, "err arg value"); + TinCanTest.assertHttpRequestType(xhr, "xhr arg value"); + deepEqual(mockAlerts, ["[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint (0)"], "caught alert: 0"); + } + } + ); + } + ); + asyncTest( + "no alert (async)", + function () { + session.alertOnRequestFailure = false; + + var result = session.sendRequest( + { + url: "test", + callback: function (err, xhr) { + start(); + deepEqual(err, 0, "err arg value"); + TinCanTest.assertHttpRequestType(xhr, "xhr arg value"); + deepEqual(mockAlerts, [], "no alerts since turned off"); + } + } + ); + } + ); +}()); diff --git a/test/single/ActivityProfile.html b/test/single/ActivityProfile.html new file mode 100644 index 0000000..c6caf2c --- /dev/null +++ b/test/single/ActivityProfile.html @@ -0,0 +1,36 @@ + + + + + TinCanJS Tests (ActivityProfile) + + + + + + +

    TinCanJS Test Suite (ActivityProfile)

    +

    +
    +

    +
      + + + + + + diff --git a/test/single/AgentProfile.html b/test/single/AgentProfile.html new file mode 100644 index 0000000..f0b3e5b --- /dev/null +++ b/test/single/AgentProfile.html @@ -0,0 +1,36 @@ + + + + + TinCanJS Tests (AgentProfile) + + + + + + +

      TinCanJS Test Suite (AgentProfile)

      +

      +
      +

      +
        + + + + + + diff --git a/test/single/offline.html b/test/single/offline.html new file mode 100644 index 0000000..2f33879 --- /dev/null +++ b/test/single/offline.html @@ -0,0 +1,36 @@ + + + + + TinCanJS Tests (offline) + + + + + + +

        TinCanJS Test Suite (offline)

        +

        +
        +

        +
          + + + + + + diff --git a/yuidoc.json b/yuidoc.json index 677a173..eb21c48 100644 --- a/yuidoc.json +++ b/yuidoc.json @@ -1,8 +1,8 @@ { - "version": "0.7.2", + "version": "0.8.2", "name": "TinCanJS", - "description": "Client library for working with TinCan API in JavaScript", - "url": "https://github.com/RusticiSoftware/TinCanJS", + "description": "Library for working with Tin Can API in JavaScript", + "url": "http://rusticisoftware.github.com/TinCanJS/", "options": { "outdir": "doc/api" },