Commit 8bdc0972 by Usman Khalid Committed by muzaffaryousaf

Fixes after rebase to Django 1.8

parent 37929439
...@@ -40,8 +40,6 @@ require.config({ ...@@ -40,8 +40,6 @@ require.config({
"jquery.immediateDescendents": "coffee/src/jquery.immediateDescendents", "jquery.immediateDescendents": "coffee/src/jquery.immediateDescendents",
"datepair": "js/vendor/timepicker/datepair", "datepair": "js/vendor/timepicker/datepair",
"date": "js/vendor/date", "date": "js/vendor/date",
"text": 'js/vendor/requirejs/text',
"moment": "js/vendor/moment-with-locales.min",
"moment": "js/vendor/moment.min", "moment": "js/vendor/moment.min",
"moment-with-locales": "js/vendor/moment-with-locales.min", "moment-with-locales": "js/vendor/moment-with-locales.min",
"text": 'js/vendor/requirejs/text', "text": 'js/vendor/requirejs/text',
......
...@@ -917,6 +917,14 @@ class XMLModuleStore(ModuleStoreReadBase): ...@@ -917,6 +917,14 @@ class XMLModuleStore(ModuleStoreReadBase):
log.warning("get_all_asset_metadata request of XML modulestore - not implemented.") log.warning("get_all_asset_metadata request of XML modulestore - not implemented.")
return [] return []
def fill_in_run(self, course_key):
"""
A no-op.
Added to simplify tests which use the XML-store directly.
"""
return course_key
class LibraryXMLModuleStore(XMLModuleStore): class LibraryXMLModuleStore(XMLModuleStore):
""" """
......
...@@ -55,6 +55,7 @@ class SequenceFields(object): ...@@ -55,6 +55,7 @@ class SequenceFields(object):
scope=Scope.settings, scope=Scope.settings,
) )
class ProctoringFields(object): class ProctoringFields(object):
""" """
Fields that are specific to Proctored or Timed Exams Fields that are specific to Proctored or Timed Exams
...@@ -120,7 +121,7 @@ class ProctoringFields(object): ...@@ -120,7 +121,7 @@ class ProctoringFields(object):
@XBlock.wants('credit') @XBlock.wants('credit')
@XBlock.needs("user") @XBlock.needs("user")
@XBlock.needs("bookmarks") @XBlock.needs("bookmarks")
class SequenceModule(SequenceFields, XModule): class SequenceModule(SequenceFields, ProctoringFields, XModule):
""" """
Layout module which lays out content in a temporal sequence Layout module which lays out content in a temporal sequence
""" """
......
//! moment.js
//! version : 2.10.3
//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
//! license : MIT
//! momentjs.com
!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.moment=b()}(this,function(){"use strict";function a(){return Dc.apply(null,arguments)}function b(a){Dc=a}function c(a){return"[object Array]"===Object.prototype.toString.call(a)}function d(a){return a instanceof Date||"[object Date]"===Object.prototype.toString.call(a)}function e(a,b){var c,d=[];for(c=0;c<a.length;++c)d.push(b(a[c],c));return d}function f(a,b){return Object.prototype.hasOwnProperty.call(a,b)}function g(a,b){for(var c in b)f(b,c)&&(a[c]=b[c]);return f(b,"toString")&&(a.toString=b.toString),f(b,"valueOf")&&(a.valueOf=b.valueOf),a}function h(a,b,c,d){return za(a,b,c,d,!0).utc()}function i(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function j(a){return null==a._pf&&(a._pf=i()),a._pf}function k(a){if(null==a._isValid){var b=j(a);a._isValid=!isNaN(a._d.getTime())&&b.overflow<0&&!b.empty&&!b.invalidMonth&&!b.nullInput&&!b.invalidFormat&&!b.userInvalidated,a._strict&&(a._isValid=a._isValid&&0===b.charsLeftOver&&0===b.unusedTokens.length&&void 0===b.bigHour)}return a._isValid}function l(a){var b=h(0/0);return null!=a?g(j(b),a):j(b).userInvalidated=!0,b}function m(a,b){var c,d,e;if("undefined"!=typeof b._isAMomentObject&&(a._isAMomentObject=b._isAMomentObject),"undefined"!=typeof b._i&&(a._i=b._i),"undefined"!=typeof b._f&&(a._f=b._f),"undefined"!=typeof b._l&&(a._l=b._l),"undefined"!=typeof b._strict&&(a._strict=b._strict),"undefined"!=typeof b._tzm&&(a._tzm=b._tzm),"undefined"!=typeof b._isUTC&&(a._isUTC=b._isUTC),"undefined"!=typeof b._offset&&(a._offset=b._offset),"undefined"!=typeof b._pf&&(a._pf=j(b)),"undefined"!=typeof b._locale&&(a._locale=b._locale),Fc.length>0)for(c in Fc)d=Fc[c],e=b[d],"undefined"!=typeof e&&(a[d]=e);return a}function n(b){m(this,b),this._d=new Date(+b._d),Gc===!1&&(Gc=!0,a.updateOffset(this),Gc=!1)}function o(a){return a instanceof n||null!=a&&null!=a._isAMomentObject}function p(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function q(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;e>d;d++)(c&&a[d]!==b[d]||!c&&p(a[d])!==p(b[d]))&&g++;return g+f}function r(){}function s(a){return a?a.toLowerCase().replace("_","-"):a}function t(a){for(var b,c,d,e,f=0;f<a.length;){for(e=s(a[f]).split("-"),b=e.length,c=s(a[f+1]),c=c?c.split("-"):null;b>0;){if(d=u(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&q(e,c,!0)>=b-1)break;b--}f++}return null}function u(a){var b=null;if(!Hc[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=Ec._abbr,require("./locale/"+a),v(b)}catch(c){}return Hc[a]}function v(a,b){var c;return a&&(c="undefined"==typeof b?x(a):w(a,b),c&&(Ec=c)),Ec._abbr}function w(a,b){return null!==b?(b.abbr=a,Hc[a]||(Hc[a]=new r),Hc[a].set(b),v(a),Hc[a]):(delete Hc[a],null)}function x(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return Ec;if(!c(a)){if(b=u(a))return b;a=[a]}return t(a)}function y(a,b){var c=a.toLowerCase();Ic[c]=Ic[c+"s"]=Ic[b]=a}function z(a){return"string"==typeof a?Ic[a]||Ic[a.toLowerCase()]:void 0}function A(a){var b,c,d={};for(c in a)f(a,c)&&(b=z(c),b&&(d[b]=a[c]));return d}function B(b,c){return function(d){return null!=d?(D(this,b,d),a.updateOffset(this,c),this):C(this,b)}}function C(a,b){return a._d["get"+(a._isUTC?"UTC":"")+b]()}function D(a,b,c){return a._d["set"+(a._isUTC?"UTC":"")+b](c)}function E(a,b){var c;if("object"==typeof a)for(c in a)this.set(c,a[c]);else if(a=z(a),"function"==typeof this[a])return this[a](b);return this}function F(a,b,c){for(var d=""+Math.abs(a),e=a>=0;d.length<b;)d="0"+d;return(e?c?"+":"":"-")+d}function G(a,b,c,d){var e=d;"string"==typeof d&&(e=function(){return this[d]()}),a&&(Mc[a]=e),b&&(Mc[b[0]]=function(){return F(e.apply(this,arguments),b[1],b[2])}),c&&(Mc[c]=function(){return this.localeData().ordinal(e.apply(this,arguments),a)})}function H(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function I(a){var b,c,d=a.match(Jc);for(b=0,c=d.length;c>b;b++)Mc[d[b]]?d[b]=Mc[d[b]]:d[b]=H(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function J(a,b){return a.isValid()?(b=K(b,a.localeData()),Lc[b]||(Lc[b]=I(b)),Lc[b](a)):a.localeData().invalidDate()}function K(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Kc.lastIndex=0;d>=0&&Kc.test(a);)a=a.replace(Kc,c),Kc.lastIndex=0,d-=1;return a}function L(a,b,c){_c[a]="function"==typeof b?b:function(a){return a&&c?c:b}}function M(a,b){return f(_c,a)?_c[a](b._strict,b._locale):new RegExp(N(a))}function N(a){return a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}).replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function O(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),"number"==typeof b&&(d=function(a,c){c[b]=p(a)}),c=0;c<a.length;c++)ad[a[c]]=d}function P(a,b){O(a,function(a,c,d,e){d._w=d._w||{},b(a,d._w,d,e)})}function Q(a,b,c){null!=b&&f(ad,a)&&ad[a](b,c._a,c,a)}function R(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function S(a){return this._months[a.month()]}function T(a){return this._monthsShort[a.month()]}function U(a,b,c){var d,e,f;for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),d=0;12>d;d++){if(e=h([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}}function V(a,b){var c;return"string"==typeof b&&(b=a.localeData().monthsParse(b),"number"!=typeof b)?a:(c=Math.min(a.date(),R(a.year(),b)),a._d["set"+(a._isUTC?"UTC":"")+"Month"](b,c),a)}function W(b){return null!=b?(V(this,b),a.updateOffset(this,!0),this):C(this,"Month")}function X(){return R(this.year(),this.month())}function Y(a){var b,c=a._a;return c&&-2===j(a).overflow&&(b=c[cd]<0||c[cd]>11?cd:c[dd]<1||c[dd]>R(c[bd],c[cd])?dd:c[ed]<0||c[ed]>24||24===c[ed]&&(0!==c[fd]||0!==c[gd]||0!==c[hd])?ed:c[fd]<0||c[fd]>59?fd:c[gd]<0||c[gd]>59?gd:c[hd]<0||c[hd]>999?hd:-1,j(a)._overflowDayOfYear&&(bd>b||b>dd)&&(b=dd),j(a).overflow=b),a}function Z(b){a.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+b)}function $(a,b){var c=!0,d=a+"\n"+(new Error).stack;return g(function(){return c&&(Z(d),c=!1),b.apply(this,arguments)},b)}function _(a,b){kd[a]||(Z(b),kd[a]=!0)}function aa(a){var b,c,d=a._i,e=ld.exec(d);if(e){for(j(a).iso=!0,b=0,c=md.length;c>b;b++)if(md[b][1].exec(d)){a._f=md[b][0]+(e[6]||" ");break}for(b=0,c=nd.length;c>b;b++)if(nd[b][1].exec(d)){a._f+=nd[b][0];break}d.match(Yc)&&(a._f+="Z"),ta(a)}else a._isValid=!1}function ba(b){var c=od.exec(b._i);return null!==c?void(b._d=new Date(+c[1])):(aa(b),void(b._isValid===!1&&(delete b._isValid,a.createFromInputFallback(b))))}function ca(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 1970>a&&h.setFullYear(a),h}function da(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function ea(a){return fa(a)?366:365}function fa(a){return a%4===0&&a%100!==0||a%400===0}function ga(){return fa(this.year())}function ha(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=Aa(a).add(f,"d"),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function ia(a){return ha(a,this._week.dow,this._week.doy).week}function ja(){return this._week.dow}function ka(){return this._week.doy}function la(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function ma(a){var b=ha(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")}function na(a,b,c,d,e){var f,g,h=da(a,0,1).getUTCDay();return h=0===h?7:h,c=null!=c?c:e,f=e-h+(h>d?7:0)-(e>h?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:ea(a-1)+g}}function oa(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function pa(a,b,c){return null!=a?a:null!=b?b:c}function qa(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function ra(a){var b,c,d,e,f=[];if(!a._d){for(d=qa(a),a._w&&null==a._a[dd]&&null==a._a[cd]&&sa(a),a._dayOfYear&&(e=pa(a._a[bd],d[bd]),a._dayOfYear>ea(e)&&(j(a)._overflowDayOfYear=!0),c=da(e,0,a._dayOfYear),a._a[cd]=c.getUTCMonth(),a._a[dd]=c.getUTCDate()),b=0;3>b&&null==a._a[b];++b)a._a[b]=f[b]=d[b];for(;7>b;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b];24===a._a[ed]&&0===a._a[fd]&&0===a._a[gd]&&0===a._a[hd]&&(a._nextDay=!0,a._a[ed]=0),a._d=(a._useUTC?da:ca).apply(null,f),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[ed]=24)}}function sa(a){var b,c,d,e,f,g,h;b=a._w,null!=b.GG||null!=b.W||null!=b.E?(f=1,g=4,c=pa(b.GG,a._a[bd],ha(Aa(),1,4).year),d=pa(b.W,1),e=pa(b.E,1)):(f=a._locale._week.dow,g=a._locale._week.doy,c=pa(b.gg,a._a[bd],ha(Aa(),f,g).year),d=pa(b.w,1),null!=b.d?(e=b.d,f>e&&++d):e=null!=b.e?b.e+f:f),h=na(c,d,e,g,f),a._a[bd]=h.year,a._dayOfYear=h.dayOfYear}function ta(b){if(b._f===a.ISO_8601)return void aa(b);b._a=[],j(b).empty=!0;var c,d,e,f,g,h=""+b._i,i=h.length,k=0;for(e=K(b._f,b._locale).match(Jc)||[],c=0;c<e.length;c++)f=e[c],d=(h.match(M(f,b))||[])[0],d&&(g=h.substr(0,h.indexOf(d)),g.length>0&&j(b).unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),k+=d.length),Mc[f]?(d?j(b).empty=!1:j(b).unusedTokens.push(f),Q(f,d,b)):b._strict&&!d&&j(b).unusedTokens.push(f);j(b).charsLeftOver=i-k,h.length>0&&j(b).unusedInput.push(h),j(b).bigHour===!0&&b._a[ed]<=12&&b._a[ed]>0&&(j(b).bigHour=void 0),b._a[ed]=ua(b._locale,b._a[ed],b._meridiem),ra(b),Y(b)}function ua(a,b,c){var d;return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&12>b&&(b+=12),d||12!==b||(b=0),b):b}function va(a){var b,c,d,e,f;if(0===a._f.length)return j(a).invalidFormat=!0,void(a._d=new Date(0/0));for(e=0;e<a._f.length;e++)f=0,b=m({},a),null!=a._useUTC&&(b._useUTC=a._useUTC),b._f=a._f[e],ta(b),k(b)&&(f+=j(b).charsLeftOver,f+=10*j(b).unusedTokens.length,j(b).score=f,(null==d||d>f)&&(d=f,c=b));g(a,c||b)}function wa(a){if(!a._d){var b=A(a._i);a._a=[b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],ra(a)}}function xa(a){var b,e=a._i,f=a._f;return a._locale=a._locale||x(a._l),null===e||void 0===f&&""===e?l({nullInput:!0}):("string"==typeof e&&(a._i=e=a._locale.preparse(e)),o(e)?new n(Y(e)):(c(f)?va(a):f?ta(a):d(e)?a._d=e:ya(a),b=new n(Y(a)),b._nextDay&&(b.add(1,"d"),b._nextDay=void 0),b))}function ya(b){var f=b._i;void 0===f?b._d=new Date:d(f)?b._d=new Date(+f):"string"==typeof f?ba(b):c(f)?(b._a=e(f.slice(0),function(a){return parseInt(a,10)}),ra(b)):"object"==typeof f?wa(b):"number"==typeof f?b._d=new Date(f):a.createFromInputFallback(b)}function za(a,b,c,d,e){var f={};return"boolean"==typeof c&&(d=c,c=void 0),f._isAMomentObject=!0,f._useUTC=f._isUTC=e,f._l=c,f._i=a,f._f=b,f._strict=d,xa(f)}function Aa(a,b,c,d){return za(a,b,c,d,!1)}function Ba(a,b){var d,e;if(1===b.length&&c(b[0])&&(b=b[0]),!b.length)return Aa();for(d=b[0],e=1;e<b.length;++e)b[e][a](d)&&(d=b[e]);return d}function Ca(){var a=[].slice.call(arguments,0);return Ba("isBefore",a)}function Da(){var a=[].slice.call(arguments,0);return Ba("isAfter",a)}function Ea(a){var b=A(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;this._milliseconds=+k+1e3*j+6e4*i+36e5*h,this._days=+g+7*f,this._months=+e+3*d+12*c,this._data={},this._locale=x(),this._bubble()}function Fa(a){return a instanceof Ea}function Ga(a,b){G(a,0,0,function(){var a=this.utcOffset(),c="+";return 0>a&&(a=-a,c="-"),c+F(~~(a/60),2)+b+F(~~a%60,2)})}function Ha(a){var b=(a||"").match(Yc)||[],c=b[b.length-1]||[],d=(c+"").match(td)||["-",0,0],e=+(60*d[1])+p(d[2]);return"+"===d[0]?e:-e}function Ia(b,c){var e,f;return c._isUTC?(e=c.clone(),f=(o(b)||d(b)?+b:+Aa(b))-+e,e._d.setTime(+e._d+f),a.updateOffset(e,!1),e):Aa(b).local();return c._isUTC?Aa(b).zone(c._offset||0):Aa(b).local()}function Ja(a){return 15*-Math.round(a._d.getTimezoneOffset()/15)}function Ka(b,c){var d,e=this._offset||0;return null!=b?("string"==typeof b&&(b=Ha(b)),Math.abs(b)<16&&(b=60*b),!this._isUTC&&c&&(d=Ja(this)),this._offset=b,this._isUTC=!0,null!=d&&this.add(d,"m"),e!==b&&(!c||this._changeInProgress?$a(this,Va(b-e,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?e:Ja(this)}function La(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}function Ma(a){return this.utcOffset(0,a)}function Na(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(Ja(this),"m")),this}function Oa(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(Ha(this._i)),this}function Pa(a){return a=a?Aa(a).utcOffset():0,(this.utcOffset()-a)%60===0}function Qa(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Ra(){if(this._a){var a=this._isUTC?h(this._a):Aa(this._a);return this.isValid()&&q(this._a,a.toArray())>0}return!1}function Sa(){return!this._isUTC}function Ta(){return this._isUTC}function Ua(){return this._isUTC&&0===this._offset}function Va(a,b){var c,d,e,g=a,h=null;return Fa(a)?g={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(g={},b?g[b]=a:g.milliseconds=a):(h=ud.exec(a))?(c="-"===h[1]?-1:1,g={y:0,d:p(h[dd])*c,h:p(h[ed])*c,m:p(h[fd])*c,s:p(h[gd])*c,ms:p(h[hd])*c}):(h=vd.exec(a))?(c="-"===h[1]?-1:1,g={y:Wa(h[2],c),M:Wa(h[3],c),d:Wa(h[4],c),h:Wa(h[5],c),m:Wa(h[6],c),s:Wa(h[7],c),w:Wa(h[8],c)}):null==g?g={}:"object"==typeof g&&("from"in g||"to"in g)&&(e=Ya(Aa(g.from),Aa(g.to)),g={},g.ms=e.milliseconds,g.M=e.months),d=new Ea(g),Fa(a)&&f(a,"_locale")&&(d._locale=a._locale),d}function Wa(a,b){var c=a&&parseFloat(a.replace(",","."));return(isNaN(c)?0:c)*b}function Xa(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function Ya(a,b){var c;return b=Ia(b,a),a.isBefore(b)?c=Xa(a,b):(c=Xa(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c}function Za(a,b){return function(c,d){var e,f;return null===d||isNaN(+d)||(_(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period)."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=Va(c,d),$a(this,e,a),this}}function $a(b,c,d,e){var f=c._milliseconds,g=c._days,h=c._months;e=null==e?!0:e,f&&b._d.setTime(+b._d+f*d),g&&D(b,"Date",C(b,"Date")+g*d),h&&V(b,C(b,"Month")+h*d),e&&a.updateOffset(b,g||h)}function _a(a){var b=a||Aa(),c=Ia(b,this).startOf("day"),d=this.diff(c,"days",!0),e=-6>d?"sameElse":-1>d?"lastWeek":0>d?"lastDay":1>d?"sameDay":2>d?"nextDay":7>d?"nextWeek":"sameElse";return this.format(this.localeData().calendar(e,this,Aa(b)))}function ab(){return new n(this)}function bb(a,b){var c;return b=z("undefined"!=typeof b?b:"millisecond"),"millisecond"===b?(a=o(a)?a:Aa(a),+this>+a):(c=o(a)?+a:+Aa(a),c<+this.clone().startOf(b))}function cb(a,b){var c;return b=z("undefined"!=typeof b?b:"millisecond"),"millisecond"===b?(a=o(a)?a:Aa(a),+a>+this):(c=o(a)?+a:+Aa(a),+this.clone().endOf(b)<c)}function db(a,b,c){return this.isAfter(a,c)&&this.isBefore(b,c)}function eb(a,b){var c;return b=z(b||"millisecond"),"millisecond"===b?(a=o(a)?a:Aa(a),+this===+a):(c=+Aa(a),+this.clone().startOf(b)<=c&&c<=+this.clone().endOf(b))}function fb(a){return 0>a?Math.ceil(a):Math.floor(a)}function gb(a,b,c){var d,e,f=Ia(a,this),g=6e4*(f.utcOffset()-this.utcOffset());return b=z(b),"year"===b||"month"===b||"quarter"===b?(e=hb(this,f),"quarter"===b?e/=3:"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:fb(e)}function hb(a,b){var c,d,e=12*(b.year()-a.year())+(b.month()-a.month()),f=a.clone().add(e,"months");return 0>b-f?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)}function ib(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function jb(){var a=this.clone().utc();return 0<a.year()&&a.year()<=9999?"function"==typeof Date.prototype.toISOString?this.toDate().toISOString():J(a,"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]"):J(a,"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]")}function kb(b){var c=J(this,b||a.defaultFormat);return this.localeData().postformat(c)}function lb(a,b){return this.isValid()?Va({to:this,from:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function mb(a){return this.from(Aa(),a)}function nb(a,b){return this.isValid()?Va({from:this,to:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function ob(a){return this.to(Aa(),a)}function pb(a){var b;return void 0===a?this._locale._abbr:(b=x(a),null!=b&&(this._locale=b),this)}function qb(){return this._locale}function rb(a){switch(a=z(a)){case"year":this.month(0);case"quarter":case"month":this.date(1);case"week":case"isoWeek":case"day":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===a&&this.weekday(0),"isoWeek"===a&&this.isoWeekday(1),"quarter"===a&&this.month(3*Math.floor(this.month()/3)),this}function sb(a){return a=z(a),void 0===a||"millisecond"===a?this:this.startOf(a).add(1,"isoWeek"===a?"week":a).subtract(1,"ms")}function tb(){return+this._d-6e4*(this._offset||0)}function ub(){return Math.floor(+this/1e3)}function vb(){return this._offset?new Date(+this):this._d}function wb(){var a=this;return[a.year(),a.month(),a.date(),a.hour(),a.minute(),a.second(),a.millisecond()]}function xb(){return k(this)}function yb(){return g({},j(this))}function zb(){return j(this).overflow}function Ab(a,b){G(0,[a,a.length],0,b)}function Bb(a,b,c){return ha(Aa([a,11,31+b-c]),b,c).week}function Cb(a){var b=ha(this,this.localeData()._week.dow,this.localeData()._week.doy).year;return null==a?b:this.add(a-b,"y")}function Db(a){var b=ha(this,1,4).year;return null==a?b:this.add(a-b,"y")}function Eb(){return Bb(this.year(),1,4)}function Fb(){var a=this.localeData()._week;return Bb(this.year(),a.dow,a.doy)}function Gb(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)}function Hb(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function Ib(a){return this._weekdays[a.day()]}function Jb(a){return this._weekdaysShort[a.day()]}function Kb(a){return this._weekdaysMin[a.day()]}function Lb(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=Aa([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b}function Mb(a){var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=Hb(a,this.localeData()),this.add(a-b,"d")):b}function Nb(a){var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function Ob(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)}function Pb(a,b){G(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})}function Qb(a,b){return b._meridiemParse}function Rb(a){return"p"===(a+"").toLowerCase().charAt(0)}function Sb(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function Tb(a){G(0,[a,3],0,"millisecond")}function Ub(){return this._isUTC?"UTC":""}function Vb(){return this._isUTC?"Coordinated Universal Time":""}function Wb(a){return Aa(1e3*a)}function Xb(){return Aa.apply(null,arguments).parseZone()}function Yb(a,b,c){var d=this._calendar[a];return"function"==typeof d?d.call(b,c):d}function Zb(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b}function $b(){return this._invalidDate}function _b(a){return this._ordinal.replace("%d",a)}function ac(a){return a}function bc(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)}function cc(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)}function dc(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function ec(a,b,c,d){var e=x(),f=h().set(d,b);return e[c](f,a)}function fc(a,b,c,d,e){if("number"==typeof a&&(b=a,a=void 0),a=a||"",null!=b)return ec(a,b,c,e);var f,g=[];for(f=0;d>f;f++)g[f]=ec(a,f,c,e);return g}function gc(a,b){return fc(a,b,"months",12,"month")}function hc(a,b){return fc(a,b,"monthsShort",12,"month")}function ic(a,b){return fc(a,b,"weekdays",7,"day")}function jc(a,b){return fc(a,b,"weekdaysShort",7,"day")}function kc(a,b){return fc(a,b,"weekdaysMin",7,"day")}function lc(){var a=this._data;return this._milliseconds=Rd(this._milliseconds),this._days=Rd(this._days),this._months=Rd(this._months),a.milliseconds=Rd(a.milliseconds),a.seconds=Rd(a.seconds),a.minutes=Rd(a.minutes),a.hours=Rd(a.hours),a.months=Rd(a.months),a.years=Rd(a.years),this}function mc(a,b,c,d){var e=Va(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()}function nc(a,b){return mc(this,a,b,1)}function oc(a,b){return mc(this,a,b,-1)}function pc(){var a,b,c,d=this._milliseconds,e=this._days,f=this._months,g=this._data,h=0;return g.milliseconds=d%1e3,a=fb(d/1e3),g.seconds=a%60,b=fb(a/60),g.minutes=b%60,c=fb(b/60),g.hours=c%24,e+=fb(c/24),h=fb(qc(e)),e-=fb(rc(h)),f+=fb(e/30),e%=30,h+=fb(f/12),f%=12,g.days=e,g.months=f,g.years=h,this}function qc(a){return 400*a/146097}function rc(a){return 146097*a/400}function sc(a){var b,c,d=this._milliseconds;if(a=z(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+12*qc(b),"month"===a?c:c/12;switch(b=this._days+Math.round(rc(this._months/12)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 1440*b+d/6e4;case"second":return 86400*b+d/1e3;case"millisecond":return Math.floor(864e5*b)+d;default:throw new Error("Unknown unit "+a)}}function tc(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*p(this._months/12)}function uc(a){return function(){return this.as(a)}}function vc(a){return a=z(a),this[a+"s"]()}function wc(a){return function(){return this._data[a]}}function xc(){return fb(this.days()/7)}function yc(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function zc(a,b,c){var d=Va(a).abs(),e=fe(d.as("s")),f=fe(d.as("m")),g=fe(d.as("h")),h=fe(d.as("d")),i=fe(d.as("M")),j=fe(d.as("y")),k=e<ge.s&&["s",e]||1===f&&["m"]||f<ge.m&&["mm",f]||1===g&&["h"]||g<ge.h&&["hh",g]||1===h&&["d"]||h<ge.d&&["dd",h]||1===i&&["M"]||i<ge.M&&["MM",i]||1===j&&["y"]||["yy",j];return k[2]=b,k[3]=+a>0,k[4]=c,yc.apply(null,k)}function Ac(a,b){return void 0===ge[a]?!1:void 0===b?ge[a]:(ge[a]=b,!0)}function Bc(a){var b=this.localeData(),c=zc(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function Cc(){var a=he(this.years()),b=he(this.months()),c=he(this.days()),d=he(this.hours()),e=he(this.minutes()),f=he(this.seconds()+this.milliseconds()/1e3),g=this.asSeconds();return g?(0>g?"-":"")+"P"+(a?a+"Y":"")+(b?b+"M":"")+(c?c+"D":"")+(d||e||f?"T":"")+(d?d+"H":"")+(e?e+"M":"")+(f?f+"S":""):"P0D"}var Dc,Ec,Fc=a.momentProperties=[],Gc=!1,Hc={},Ic={},Jc=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|x|X|zz?|ZZ?|.)/g,Kc=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Lc={},Mc={},Nc=/\d/,Oc=/\d\d/,Pc=/\d{3}/,Qc=/\d{4}/,Rc=/[+-]?\d{6}/,Sc=/\d\d?/,Tc=/\d{1,3}/,Uc=/\d{1,4}/,Vc=/[+-]?\d{1,6}/,Wc=/\d+/,Xc=/[+-]?\d+/,Yc=/Z|[+-]\d\d:?\d\d/gi,Zc=/[+-]?\d+(\.\d{1,3})?/,$c=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,_c={},ad={},bd=0,cd=1,dd=2,ed=3,fd=4,gd=5,hd=6;G("M",["MM",2],"Mo",function(){return this.month()+1}),G("MMM",0,0,function(a){return this.localeData().monthsShort(this,a)}),G("MMMM",0,0,function(a){return this.localeData().months(this,a)}),y("month","M"),L("M",Sc),L("MM",Sc,Oc),L("MMM",$c),L("MMMM",$c),O(["M","MM"],function(a,b){b[cd]=p(a)-1}),O(["MMM","MMMM"],function(a,b,c,d){var e=c._locale.monthsParse(a,d,c._strict);null!=e?b[cd]=e:j(c).invalidMonth=a});var id="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),jd="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),kd={};a.suppressDeprecationWarnings=!1;var ld=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,md=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],nd=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],od=/^\/?Date\((\-?\d+)/i;a.createFromInputFallback=$("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}),G(0,["YY",2],0,function(){return this.year()%100}),G(0,["YYYY",4],0,"year"),G(0,["YYYYY",5],0,"year"),G(0,["YYYYYY",6,!0],0,"year"),y("year","y"),L("Y",Xc),L("YY",Sc,Oc),L("YYYY",Uc,Qc),L("YYYYY",Vc,Rc),L("YYYYYY",Vc,Rc),O(["YYYY","YYYYY","YYYYYY"],bd),O("YY",function(b,c){c[bd]=a.parseTwoDigitYear(b)}),a.parseTwoDigitYear=function(a){return p(a)+(p(a)>68?1900:2e3)};var pd=B("FullYear",!1);G("w",["ww",2],"wo","week"),G("W",["WW",2],"Wo","isoWeek"),y("week","w"),y("isoWeek","W"),L("w",Sc),L("ww",Sc,Oc),L("W",Sc),L("WW",Sc,Oc),P(["w","ww","W","WW"],function(a,b,c,d){b[d.substr(0,1)]=p(a)});var qd={dow:0,doy:6};G("DDD",["DDDD",3],"DDDo","dayOfYear"),y("dayOfYear","DDD"),L("DDD",Tc),L("DDDD",Pc),O(["DDD","DDDD"],function(a,b,c){c._dayOfYear=p(a)}),a.ISO_8601=function(){};var rd=$("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var a=Aa.apply(null,arguments);return this>a?this:a}),sd=$("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var a=Aa.apply(null,arguments);return a>this?this:a});Ga("Z",":"),Ga("ZZ",""),L("Z",Yc),L("ZZ",Yc),O(["Z","ZZ"],function(a,b,c){c._useUTC=!0,c._tzm=Ha(a)});var td=/([\+\-]|\d\d)/gi;a.updateOffset=function(){};var ud=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,vd=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/;Va.fn=Ea.prototype;var wd=Za(1,"add"),xd=Za(-1,"subtract");a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ";var yd=$("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(a){return void 0===a?this.localeData():this.locale(a)});G(0,["gg",2],0,function(){return this.weekYear()%100}),G(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Ab("gggg","weekYear"),Ab("ggggg","weekYear"),Ab("GGGG","isoWeekYear"),Ab("GGGGG","isoWeekYear"),y("weekYear","gg"),y("isoWeekYear","GG"),L("G",Xc),L("g",Xc),L("GG",Sc,Oc),L("gg",Sc,Oc),L("GGGG",Uc,Qc),L("gggg",Uc,Qc),L("GGGGG",Vc,Rc),L("ggggg",Vc,Rc),P(["gggg","ggggg","GGGG","GGGGG"],function(a,b,c,d){b[d.substr(0,2)]=p(a)}),P(["gg","GG"],function(b,c,d,e){c[e]=a.parseTwoDigitYear(b)}),G("Q",0,0,"quarter"),y("quarter","Q"),L("Q",Nc),O("Q",function(a,b){b[cd]=3*(p(a)-1)}),G("D",["DD",2],"Do","date"),y("date","D"),L("D",Sc),L("DD",Sc,Oc),L("Do",function(a,b){return a?b._ordinalParse:b._ordinalParseLenient}),O(["D","DD"],dd),O("Do",function(a,b){b[dd]=p(a.match(Sc)[0],10)});var zd=B("Date",!0);G("d",0,"do","day"),G("dd",0,0,function(a){return this.localeData().weekdaysMin(this,a)}),G("ddd",0,0,function(a){return this.localeData().weekdaysShort(this,a)}),G("dddd",0,0,function(a){return this.localeData().weekdays(this,a)}),G("e",0,0,"weekday"),G("E",0,0,"isoWeekday"),y("day","d"),y("weekday","e"),y("isoWeekday","E"),L("d",Sc),L("e",Sc),L("E",Sc),L("dd",$c),L("ddd",$c),L("dddd",$c),P(["dd","ddd","dddd"],function(a,b,c){var d=c._locale.weekdaysParse(a);null!=d?b.d=d:j(c).invalidWeekday=a}),P(["d","e","E"],function(a,b,c,d){b[d]=p(a)});var Ad="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Bd="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Cd="Su_Mo_Tu_We_Th_Fr_Sa".split("_");G("H",["HH",2],0,"hour"),G("h",["hh",2],0,function(){return this.hours()%12||12}),Pb("a",!0),Pb("A",!1),y("hour","h"),L("a",Qb),L("A",Qb),L("H",Sc),L("h",Sc),L("HH",Sc,Oc),L("hh",Sc,Oc),O(["H","HH"],ed),O(["a","A"],function(a,b,c){c._isPm=c._locale.isPM(a),c._meridiem=a}),O(["h","hh"],function(a,b,c){b[ed]=p(a),j(c).bigHour=!0});var Dd=/[ap]\.?m?\.?/i,Ed=B("Hours",!0);G("m",["mm",2],0,"minute"),y("minute","m"),L("m",Sc),L("mm",Sc,Oc),O(["m","mm"],fd);var Fd=B("Minutes",!1);G("s",["ss",2],0,"second"),y("second","s"),L("s",Sc),L("ss",Sc,Oc),O(["s","ss"],gd);var Gd=B("Seconds",!1);G("S",0,0,function(){return~~(this.millisecond()/100)}),G(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),Tb("SSS"),Tb("SSSS"),y("millisecond","ms"),L("S",Tc,Nc),L("SS",Tc,Oc),L("SSS",Tc,Pc),L("SSSS",Wc),O(["S","SS","SSS","SSSS"],function(a,b){b[hd]=p(1e3*("0."+a))});var Hd=B("Milliseconds",!1);G("z",0,0,"zoneAbbr"),G("zz",0,0,"zoneName");var Id=n.prototype;Id.add=wd,Id.calendar=_a,Id.clone=ab,Id.diff=gb,Id.endOf=sb,Id.format=kb,Id.from=lb,Id.fromNow=mb,Id.to=nb,Id.toNow=ob,Id.get=E,Id.invalidAt=zb,Id.isAfter=bb,Id.isBefore=cb,Id.isBetween=db,Id.isSame=eb,Id.isValid=xb,Id.lang=yd,Id.locale=pb,Id.localeData=qb,Id.max=sd,Id.min=rd,Id.parsingFlags=yb,Id.set=E,Id.startOf=rb,Id.subtract=xd,Id.toArray=wb,Id.toDate=vb,Id.toISOString=jb,Id.toJSON=jb,Id.toString=ib,Id.unix=ub,Id.valueOf=tb,Id.year=pd,Id.isLeapYear=ga,Id.weekYear=Cb,Id.isoWeekYear=Db,Id.quarter=Id.quarters=Gb,Id.month=W,Id.daysInMonth=X,Id.week=Id.weeks=la,Id.isoWeek=Id.isoWeeks=ma,Id.weeksInYear=Fb,Id.isoWeeksInYear=Eb,Id.date=zd,Id.day=Id.days=Mb,Id.weekday=Nb,Id.isoWeekday=Ob,Id.dayOfYear=oa,Id.hour=Id.hours=Ed,Id.minute=Id.minutes=Fd,Id.second=Id.seconds=Gd,Id.millisecond=Id.milliseconds=Hd,Id.utcOffset=Ka,Id.utc=Ma,Id.local=Na,Id.parseZone=Oa,Id.hasAlignedHourOffset=Pa,Id.isDST=Qa,Id.isDSTShifted=Ra,Id.isLocal=Sa,Id.isUtcOffset=Ta,Id.isUtc=Ua,Id.isUTC=Ua,Id.zoneAbbr=Ub,Id.zoneName=Vb,Id.dates=$("dates accessor is deprecated. Use date instead.",zd),Id.months=$("months accessor is deprecated. Use month instead",W),Id.years=$("years accessor is deprecated. Use year instead",pd),Id.zone=$("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",La);var Jd=Id,Kd={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},Ld={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY LT",LLLL:"dddd, MMMM D, YYYY LT"},Md="Invalid date",Nd="%d",Od=/\d{1,2}/,Pd={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",
hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Qd=r.prototype;Qd._calendar=Kd,Qd.calendar=Yb,Qd._longDateFormat=Ld,Qd.longDateFormat=Zb,Qd._invalidDate=Md,Qd.invalidDate=$b,Qd._ordinal=Nd,Qd.ordinal=_b,Qd._ordinalParse=Od,Qd.preparse=ac,Qd.postformat=ac,Qd._relativeTime=Pd,Qd.relativeTime=bc,Qd.pastFuture=cc,Qd.set=dc,Qd.months=S,Qd._months=id,Qd.monthsShort=T,Qd._monthsShort=jd,Qd.monthsParse=U,Qd.week=ia,Qd._week=qd,Qd.firstDayOfYear=ka,Qd.firstDayOfWeek=ja,Qd.weekdays=Ib,Qd._weekdays=Ad,Qd.weekdaysMin=Kb,Qd._weekdaysMin=Cd,Qd.weekdaysShort=Jb,Qd._weekdaysShort=Bd,Qd.weekdaysParse=Lb,Qd.isPM=Rb,Qd._meridiemParse=Dd,Qd.meridiem=Sb,v("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===p(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),a.lang=$("moment.lang is deprecated. Use moment.locale instead.",v),a.langData=$("moment.langData is deprecated. Use moment.localeData instead.",x);var Rd=Math.abs,Sd=uc("ms"),Td=uc("s"),Ud=uc("m"),Vd=uc("h"),Wd=uc("d"),Xd=uc("w"),Yd=uc("M"),Zd=uc("y"),$d=wc("milliseconds"),_d=wc("seconds"),ae=wc("minutes"),be=wc("hours"),ce=wc("days"),de=wc("months"),ee=wc("years"),fe=Math.round,ge={s:45,m:45,h:22,d:26,M:11},he=Math.abs,ie=Ea.prototype;ie.abs=lc,ie.add=nc,ie.subtract=oc,ie.as=sc,ie.asMilliseconds=Sd,ie.asSeconds=Td,ie.asMinutes=Ud,ie.asHours=Vd,ie.asDays=Wd,ie.asWeeks=Xd,ie.asMonths=Yd,ie.asYears=Zd,ie.valueOf=tc,ie._bubble=pc,ie.get=vc,ie.milliseconds=$d,ie.seconds=_d,ie.minutes=ae,ie.hours=be,ie.days=ce,ie.weeks=xc,ie.months=de,ie.years=ee,ie.humanize=Bc,ie.toISOString=Cc,ie.toString=Cc,ie.toJSON=Cc,ie.locale=pb,ie.localeData=qb,ie.toIsoString=$("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",Cc),ie.lang=yd,G("X",0,0,"unix"),G("x",0,0,"valueOf"),L("x",Xc),L("X",Zc),O("X",function(a,b,c){c._d=new Date(1e3*parseFloat(a,10))}),O("x",function(a,b,c){c._d=new Date(p(a))}),a.version="2.10.3",b(Aa),a.fn=Jd,a.min=Ca,a.max=Da,a.utc=h,a.unix=Wb,a.months=gc,a.isDate=d,a.locale=v,a.invalid=l,a.duration=Va,a.isMoment=o,a.weekdays=ic,a.parseZone=Xb,a.localeData=x,a.isDuration=Fa,a.monthsShort=hc,a.weekdaysMin=kc,a.defineLocale=w,a.weekdaysShort=jc,a.normalizeUnits=z,a.relativeTimeThreshold=Ac;var je=a;return je});
...@@ -12,11 +12,12 @@ class CoursewareSearchPage(CoursePage): ...@@ -12,11 +12,12 @@ class CoursewareSearchPage(CoursePage):
url_path = "courseware/" url_path = "courseware/"
search_bar_selector = '#courseware-search-bar' search_bar_selector = '#courseware-search-bar'
search_results_selector = '.courseware-results'
@property @property
def search_results(self): def search_results(self):
""" search results list showing """ """ search results list showing """
return self.q(css='.courseware-results') return self.q(css=self.search_results_selector)
def is_browser_on_page(self): def is_browser_on_page(self):
""" did we find the search bar in the UI """ """ did we find the search bar in the UI """
...@@ -30,6 +31,7 @@ class CoursewareSearchPage(CoursePage): ...@@ -30,6 +31,7 @@ class CoursewareSearchPage(CoursePage):
""" execute the search """ """ execute the search """
self.q(css=self.search_bar_selector + ' [type="submit"]').click() self.q(css=self.search_bar_selector + ' [type="submit"]').click()
self.wait_for_ajax() self.wait_for_ajax()
self.wait_for_element_visibility(self.search_results_selector, 'Search results are visible')
def search_for_term(self, text): def search_for_term(self, text):
""" """
......
...@@ -207,14 +207,13 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -207,14 +207,13 @@ class BookmarksTest(BookmarksTestMixin):
self.assertEqual(self.bookmarks_page.get_current_page_number(), current_page_number) self.assertEqual(self.bookmarks_page.get_current_page_number(), current_page_number)
self.assertEqual(self.bookmarks_page.get_total_pages, total_pages) self.assertEqual(self.bookmarks_page.get_total_pages, total_pages)
def _navigate_and_verify_bookmarks_list(self, bookmarks_count): def _navigate_to_bookmarks_list(self):
""" """
Navigates and verifies the bookmarks list page. Navigates and verifies the bookmarks list page.
""" """
self.bookmarks_page.click_bookmarks_button() self.bookmarks_page.click_bookmarks_button()
self.assertTrue(self.bookmarks_page.results_present()) self.assertTrue(self.bookmarks_page.results_present())
self.assertEqual(self.bookmarks_page.results_header_text(), 'MY BOOKMARKS') self.assertEqual(self.bookmarks_page.results_header_text(), 'MY BOOKMARKS')
self.assertEqual(self.bookmarks_page.count(), bookmarks_count)
def _verify_breadcrumbs(self, num_units, modified_name=None): def _verify_breadcrumbs(self, num_units, modified_name=None):
""" """
...@@ -310,6 +309,9 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -310,6 +309,9 @@ class BookmarksTest(BookmarksTestMixin):
self._test_setup() self._test_setup()
self._bookmark_units(2) self._bookmark_units(2)
self._navigate_to_bookmarks_list()
self._verify_breadcrumbs(num_units=2)
self._verify_pagination_info( self._verify_pagination_info(
bookmark_count_on_current_page=2, bookmark_count_on_current_page=2,
header_text='Showing 1-2 out of 2 total', header_text='Showing 1-2 out of 2 total',
...@@ -319,9 +321,6 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -319,9 +321,6 @@ class BookmarksTest(BookmarksTestMixin):
total_pages=1 total_pages=1
) )
self._navigate_and_verify_bookmarks_list(bookmarks_count=2)
self._verify_breadcrumbs(num_units=2)
# get usage ids for units # get usage ids for units
xblocks = self.course_fixture.get_nested_xblocks(category="vertical") xblocks = self.course_fixture.get_nested_xblocks(category="vertical")
xblock_usage_ids = [xblock.locator for xblock in xblocks] xblock_usage_ids = [xblock.locator for xblock in xblocks]
...@@ -329,7 +328,7 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -329,7 +328,7 @@ class BookmarksTest(BookmarksTestMixin):
for index in range(2): for index in range(2):
self.bookmarks_page.click_bookmarked_block(index) self.bookmarks_page.click_bookmarked_block(index)
self.courseware_page.wait_for_page() self.courseware_page.wait_for_page()
self.assertTrue(self.courseware_page.active_usage_id() in xblock_usage_ids) self.assertIn(self.courseware_page.active_usage_id(), xblock_usage_ids)
self.courseware_page.visit().wait_for_page() self.courseware_page.visit().wait_for_page()
self.bookmarks_page.click_bookmarks_button() self.bookmarks_page.click_bookmarks_button()
...@@ -352,11 +351,11 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -352,11 +351,11 @@ class BookmarksTest(BookmarksTestMixin):
self._test_setup(num_chapters=1) self._test_setup(num_chapters=1)
self._bookmark_units(num_units=1) self._bookmark_units(num_units=1)
self._navigate_and_verify_bookmarks_list(bookmarks_count=1) self._navigate_to_bookmarks_list()
self._verify_breadcrumbs(num_units=1) self._verify_breadcrumbs(num_units=1)
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
AutoAuthPage( LmsAutoAuthPage(
self.browser, self.browser,
username=self.USERNAME, username=self.USERNAME,
email=self.EMAIL, email=self.EMAIL,
...@@ -368,10 +367,10 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -368,10 +367,10 @@ class BookmarksTest(BookmarksTestMixin):
self.update_and_publish_block_display_name(modified_name) self.update_and_publish_block_display_name(modified_name)
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, course_id=self.course_id).visit() LmsAutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, course_id=self.course_id).visit()
self.courseware_page.visit() self.courseware_page.visit()
self._navigate_and_verify_bookmarks_list(bookmarks_count=1) self._navigate_to_bookmarks_list()
self._verify_breadcrumbs(num_units=1, modified_name=modified_name) self._verify_breadcrumbs(num_units=1, modified_name=modified_name)
def test_unreachable_bookmark(self): def test_unreachable_bookmark(self):
...@@ -387,15 +386,15 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -387,15 +386,15 @@ class BookmarksTest(BookmarksTestMixin):
When I click on deleted bookmark When I click on deleted bookmark
Then I should navigated to 404 page Then I should navigated to 404 page
""" """
self._test_setup() self._test_setup(num_chapters=1)
self._bookmark_units(2) self._bookmark_units(1)
self._delete_section(0) self._delete_section(0)
self._navigate_and_verify_bookmarks_list(bookmarks_count=2) self._navigate_to_bookmarks_list()
self._verify_pagination_info( self._verify_pagination_info(
bookmark_count_on_current_page=2, bookmark_count_on_current_page=1,
header_text='Showing 1-2 out of 2 total', header_text='Showing 1 out of 1 total',
previous_button_enabled=False, previous_button_enabled=False,
next_button_enabled=False, next_button_enabled=False,
current_page_number=1, current_page_number=1,
...@@ -418,7 +417,7 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -418,7 +417,7 @@ class BookmarksTest(BookmarksTestMixin):
""" """
self._test_setup(11) self._test_setup(11)
self._bookmark_units(11) self._bookmark_units(11)
self._navigate_and_verify_bookmarks_list(bookmarks_count=11) self._navigate_to_bookmarks_list()
self._verify_pagination_info( self._verify_pagination_info(
bookmark_count_on_current_page=10, bookmark_count_on_current_page=10,
......
...@@ -68,14 +68,6 @@ class CoursewareTest(UniqueCourseTest): ...@@ -68,14 +68,6 @@ class CoursewareTest(UniqueCourseTest):
self.problem_page = ProblemPage(self.browser) self.problem_page = ProblemPage(self.browser)
self.assertEqual(self.problem_page.problem_name, 'TEST PROBLEM 1') self.assertEqual(self.problem_page.problem_name, 'TEST PROBLEM 1')
def _change_problem_release_date_in_studio(self):
"""
"""
self.course_outline.q(css=".subsection-header-actions .configure-button").first.click()
self.course_outline.q(css="#start_date").fill("01/01/2030")
self.course_outline.q(css=".action-save").first.click()
def _create_breadcrumb(self, index): def _create_breadcrumb(self, index):
""" Create breadcrumb """ """ Create breadcrumb """
return ['Test Section {}'.format(index), 'Test Subsection {}'.format(index), 'Test Problem {}'.format(index)] return ['Test Section {}'.format(index), 'Test Subsection {}'.format(index), 'Test Problem {}'.format(index)]
...@@ -105,9 +97,6 @@ class CoursewareTest(UniqueCourseTest): ...@@ -105,9 +97,6 @@ class CoursewareTest(UniqueCourseTest):
# Set release date for subsection in future. # Set release date for subsection in future.
self.course_outline.change_problem_release_date_in_studio() self.course_outline.change_problem_release_date_in_studio()
# Wait for 2 seconds to save new date.
time.sleep(2)
# Logout and login as a student. # Logout and login as a student.
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
self._auto_auth(self.USERNAME, self.EMAIL, False) self._auto_auth(self.USERNAME, self.EMAIL, False)
...@@ -117,6 +106,23 @@ class CoursewareTest(UniqueCourseTest): ...@@ -117,6 +106,23 @@ class CoursewareTest(UniqueCourseTest):
# Problem name should be "TEST PROBLEM 2". # Problem name should be "TEST PROBLEM 2".
self.assertEqual(self.problem_page.problem_name, 'TEST PROBLEM 2') self.assertEqual(self.problem_page.problem_name, 'TEST PROBLEM 2')
def test_course_tree_breadcrumb(self):
"""
Scenario: Correct course tree breadcrumb is shown.
Given that I am a registered user
And I visit my courseware page
Then I should see correct course tree breadcrumb
"""
self.courseware_page.visit()
xblocks = self.course_fix.get_nested_xblocks(category="problem")
for index in range(1, len(xblocks) + 1):
self.course_nav.go_to_section('Test Section {}'.format(index), 'Test Subsection {}'.format(index))
courseware_page_breadcrumb = self.courseware_page.breadcrumb
expected_breadcrumb = self._create_breadcrumb(index) # pylint: disable=no-member
self.assertEqual(courseware_page_breadcrumb, expected_breadcrumb)
class ProctoredExamTest(UniqueCourseTest): class ProctoredExamTest(UniqueCourseTest):
""" """
...@@ -262,23 +268,6 @@ class ProctoredExamTest(UniqueCourseTest): ...@@ -262,23 +268,6 @@ class ProctoredExamTest(UniqueCourseTest):
self.courseware_page.start_timed_exam() self.courseware_page.start_timed_exam()
self.assertTrue(self.courseware_page.is_timer_bar_present) self.assertTrue(self.courseware_page.is_timer_bar_present)
def test_course_tree_breadcrumb(self):
"""
Scenario: Correct course tree breadcrumb is shown.
Given that I am a registered user
And I visit my courseware page
Then I should see correct course tree breadcrumb
"""
self.courseware_page.visit()
xblocks = self.course_fix.get_nested_xblocks(category="problem")
for index in range(1, len(xblocks) + 1):
self.course_nav.go_to_section('Test Section {}'.format(index), 'Test Subsection {}'.format(index))
courseware_page_breadcrumb = self.courseware_page.breadcrumb
expected_breadcrumb = self._create_breadcrumb(index)
self.assertEqual(courseware_page_breadcrumb, expected_breadcrumb)
def test_time_allotted_field_is_not_visible_with_none_exam(self): def test_time_allotted_field_is_not_visible_with_none_exam(self):
""" """
Given that I am a staff member Given that I am a staff member
......
...@@ -419,6 +419,10 @@ def _index_bulk_op(request, course_key, chapter, section, position): ...@@ -419,6 +419,10 @@ def _index_bulk_op(request, course_key, chapter, section, position):
studio_url = get_studio_url(course, 'course') studio_url = get_studio_url(course, 'course')
language_preference = get_user_preference(request.user, LANGUAGE_KEY)
if not language_preference:
language_preference = settings.LANGUAGE_CODE
context = { context = {
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
'accordion': render_accordion(user, request, course, chapter, section, field_data_cache), 'accordion': render_accordion(user, request, course, chapter, section, field_data_cache),
......
define(['jquery', define(['jquery',
'underscore', 'underscore',
'moment-with-locales',
'teams/js/views/team_card', 'teams/js/views/team_card',
'teams/js/models/team'], 'teams/js/models/team'],
function ($, _, TeamCardView, Team) { function ($, _, moment, TeamCardView, Team) {
'use strict'; 'use strict';
describe('TeamCardView', function () { describe('TeamCardView', function () {
...@@ -35,6 +36,7 @@ define(['jquery', ...@@ -35,6 +36,7 @@ define(['jquery',
}; };
beforeEach(function () { beforeEach(function () {
moment.locale('en');
view = createTeamCardView(); view = createTeamCardView();
view.render(); view.render();
}); });
......
...@@ -1176,7 +1176,6 @@ courseware_js = ( ...@@ -1176,7 +1176,6 @@ courseware_js = (
for pth in ['courseware', 'histogram', 'navigation'] for pth in ['courseware', 'histogram', 'navigation']
] + ] +
['js/' + pth + '.js' for pth in ['ajax-error']] + ['js/' + pth + '.js' for pth in ['ajax-error']] +
['js/bookmarks/main.js'] +
sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/modules/**/*.js')) sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/modules/**/*.js'))
) )
...@@ -1910,7 +1909,6 @@ INSTALLED_APPS = ( ...@@ -1910,7 +1909,6 @@ INSTALLED_APPS = (
# Bookmarks # Bookmarks
'openedx.core.djangoapps.bookmarks', 'openedx.core.djangoapps.bookmarks',
'bookmarks',
# programs support # programs support
'openedx.core.djangoapps.programs', 'openedx.core.djangoapps.programs',
......
;(function (define) {
'use strict';
define([
'js/bookmarks/views/bookmarks_list_button'
],
function(BookmarksListButton) {
return function() {
return new BookmarksListButton();
};
}
);
}).call(this, define || RequireJS.define);
...@@ -8,8 +8,13 @@ ...@@ -8,8 +8,13 @@
PagingCollection.prototype.initialize.call(this); PagingCollection.prototype.initialize.call(this);
this.url = options.url; this.url = options.url;
this.server_api.course_id = function () { return encodeURIComponent(options.course_id); }; this.server_api = _.extend(
this.server_api.fields = function () { return encodeURIComponent('display_name,path'); }; {
course_id: function () { return encodeURIComponent(options.course_id); },
fields : function () { return encodeURIComponent('display_name,path'); }
},
PagingCollection.prototype.server_api
);
delete this.server_api.sort_order; // Sort order is not specified for the Bookmark API delete this.server_api.sort_order; // Sort order is not specified for the Bookmark API
}, },
......
RequireJS.require([
'js/bookmarks/views/bookmarks_list_button'
], function (BookmarksListButton) {
'use strict';
return new BookmarksListButton();
});
;(function (define, undefined) { ;(function (define, undefined) {
'use strict'; 'use strict';
define(['gettext', 'jquery', 'underscore', 'backbone', 'js/views/message'], define(['gettext', 'jquery', 'underscore', 'backbone', 'js/views/message_banner'],
function (gettext, $, _, Backbone, MessageView) { function (gettext, $, _, Backbone, MessageBannerView) {
return Backbone.View.extend({ return Backbone.View.extend({
...@@ -81,9 +81,8 @@ ...@@ -81,9 +81,8 @@
showError: function() { showError: function() {
if (!this.messageView) { if (!this.messageView) {
this.messageView = new MessageView({ this.messageView = new MessageBannerView({
el: $('.coursewide-message-banner'), el: $('.message-banner')
templateId: '#message_banner-tpl'
}); });
} }
this.messageView.showMessage(this.errorMessage, this.errorIcon); this.messageView.showMessage(this.errorMessage, this.errorIcon);
......
;(function (define, undefined) { ;(function (define, undefined) {
'use strict'; 'use strict';
define(['gettext', 'jquery', 'underscore', 'backbone', 'logger', 'moment', define(['gettext', 'jquery', 'underscore', 'backbone', 'logger', 'moment',
'common/js/components/views/paging_header', 'common/js/components/views/paging_footer'], 'common/js/components/views/paging_header', 'common/js/components/views/paging_footer',
function (gettext, $, _, Backbone, Logger, _moment, PagingHeaderView, PagingFooterView) { 'text!templates/bookmarks/bookmarks-list.underscore'
],
function (gettext, $, _, Backbone, Logger, _moment,
PagingHeaderView, PagingFooterView, BookmarksListTemplate) {
var moment = _moment || window.moment; var moment = _moment || window.moment;
...@@ -24,7 +27,7 @@ ...@@ -24,7 +27,7 @@
}, },
initialize: function (options) { initialize: function (options) {
this.template = _.template($('#bookmarks-list-tpl').text()); this.template = _.template(BookmarksListTemplate);
this.loadingMessageView = options.loadingMessageView; this.loadingMessageView = options.loadingMessageView;
this.errorMessageView = options.errorMessageView; this.errorMessageView = options.errorMessageView;
this.langCode = $(this.el).data('langCode'); this.langCode = $(this.el).data('langCode');
......
;(function (define, undefined) { ;(function (define, undefined) {
'use strict'; 'use strict';
define(['gettext', 'jquery', 'underscore', 'backbone', 'js/bookmarks/views/bookmarks_list', define(['gettext', 'jquery', 'underscore', 'backbone', 'js/bookmarks/views/bookmarks_list',
'js/bookmarks/collections/bookmarks', 'js/views/message'], 'js/bookmarks/collections/bookmarks', 'js/views/message_banner'],
function (gettext, $, _, Backbone, BookmarksListView, BookmarksCollection, MessageView) { function (gettext, $, _, Backbone, BookmarksListView, BookmarksCollection, MessageBannerView) {
return Backbone.View.extend({ return Backbone.View.extend({
...@@ -26,8 +26,8 @@ ...@@ -26,8 +26,8 @@
this.bookmarksListView = new BookmarksListView( this.bookmarksListView = new BookmarksListView(
{ {
collection: bookmarksCollection, collection: bookmarksCollection,
loadingMessageView: new MessageView({el: $(this.loadingMessageElement)}), loadingMessageView: new MessageBannerView({el: $(this.loadingMessageElement)}),
errorMessageView: new MessageView({el: $(this.errorMessageElement)}) errorMessageView: new MessageBannerView({el: $(this.errorMessageElement)})
} }
); );
}, },
......
<div class="coursewide-message-banner" aria-live="polite"></div> <div class="message-banner" aria-live="polite"></div>
<div class="xblock xblock-student_view xblock-student_view-vertical xblock-initialized"> <div class="xblock xblock-student_view xblock-student_view-vertical xblock-initialized">
<div class="bookmark-button-wrapper"> <div class="bookmark-button-wrapper">
......
RequireJS.require([
'jquery',
'backbone',
'js/search/course/search_app',
'js/search/base/routers/search_router',
'js/search/course/views/search_form',
'js/search/base/collections/search_collection',
'js/search/course/views/search_results_view'
], function ($, Backbone, SearchApp, SearchRouter, CourseSearchForm, SearchCollection, CourseSearchResultsView) {
'use strict';
var courseId = $('.courseware-results').data('courseId');
var app = new SearchApp(
courseId,
SearchRouter,
CourseSearchForm,
SearchCollection,
CourseSearchResultsView
);
Backbone.history.start();
});
...@@ -68,7 +68,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers ...@@ -68,7 +68,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
}; };
var requests = AjaxHelpers.requests(this); var requests = AjaxHelpers.requests(this);
_.each([[addBookmarkedData, removeBookmarkData], [removeBookmarkData, addBookmarkedData]], function(actionsData) { var bookmarkedData = [[addBookmarkedData, removeBookmarkData], [removeBookmarkData, addBookmarkedData]];
_.each(bookmarkedData, function(actionsData) {
var firstActionData = actionsData[0]; var firstActionData = actionsData[0];
var secondActionData = actionsData[1]; var secondActionData = actionsData[1];
...@@ -110,13 +111,14 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers ...@@ -110,13 +111,14 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
expect(secondActionData.event).toHaveBeenTriggeredOn(bookmarkButtonView.$el); expect(secondActionData.event).toHaveBeenTriggeredOn(bookmarkButtonView.$el);
verifyBookmarkButtonState(bookmarkButtonView, firstActionData.bookmarked); verifyBookmarkButtonState(bookmarkButtonView, firstActionData.bookmarked);
bookmarkButtonView.undelegateEvents();
}); });
}); });
it("shows an error message for HTTP 500", function () { it("shows an error message for HTTP 500", function () {
var requests = AjaxHelpers.requests(this), var requests = AjaxHelpers.requests(this),
$messageBanner = $('.coursewide-message-banner'), $messageBanner = $('.message-banner'),
bookmarkButtonView = createBookmarkButtonView(false); bookmarkButtonView = createBookmarkButtonView(false);
bookmarkButtonView.$el.click(); bookmarkButtonView.$el.click();
......
...@@ -20,7 +20,7 @@ define(['backbone', ...@@ -20,7 +20,7 @@ define(['backbone',
loadFixtures('js/fixtures/bookmarks/bookmarks.html'); loadFixtures('js/fixtures/bookmarks/bookmarks.html');
TemplateHelpers.installTemplates( TemplateHelpers.installTemplates(
[ [
'templates/message_view', 'templates/fields/message_banner',
'templates/bookmarks/bookmarks-list' 'templates/bookmarks/bookmarks-list'
] ]
); );
......
...@@ -66,8 +66,6 @@ ...@@ -66,8 +66,6 @@
'_split': 'js/split', '_split': 'js/split',
'mathjax_delay_renderer': 'coffee/src/mathjax_delay_renderer', 'mathjax_delay_renderer': 'coffee/src/mathjax_delay_renderer',
'MathJaxProcessor': 'coffee/src/customwmd', 'MathJaxProcessor': 'coffee/src/customwmd',
'moment': 'xmodule_js/common_static/js/src/moment',
'moment': 'xmodule_js/common_static/js/vendor/moment-with-locales.min',
// Manually specify LMS files that are not converted to RequireJS // Manually specify LMS files that are not converted to RequireJS
'history': 'js/vendor/history', 'history': 'js/vendor/history',
...@@ -75,7 +73,6 @@ ...@@ -75,7 +73,6 @@
'js/vendor/jquery.qubit': 'js/vendor/jquery.qubit', 'js/vendor/jquery.qubit': 'js/vendor/jquery.qubit',
'js/utils/navigation': 'js/utils/navigation', 'js/utils/navigation': 'js/utils/navigation',
// Backbone classes loaded explicitly until they are converted to use RequireJS // Backbone classes loaded explicitly until they are converted to use RequireJS
'js/models/notification': 'js/models/notification', 'js/models/notification': 'js/models/notification',
'js/views/file_uploader': 'js/views/file_uploader', 'js/views/file_uploader': 'js/views/file_uploader',
...@@ -94,7 +91,7 @@ ...@@ -94,7 +91,7 @@
'js/bookmarks/views/bookmarks_list_button': 'js/bookmarks/views/bookmarks_list_button', 'js/bookmarks/views/bookmarks_list_button': 'js/bookmarks/views/bookmarks_list_button',
'js/bookmarks/views/bookmarks_list': 'js/bookmarks/views/bookmarks_list', 'js/bookmarks/views/bookmarks_list': 'js/bookmarks/views/bookmarks_list',
'js/bookmarks/views/bookmark_button': 'js/bookmarks/views/bookmark_button', 'js/bookmarks/views/bookmark_button': 'js/bookmarks/views/bookmark_button',
'js/views/message': 'js/views/message', 'js/views/message_banner': 'js/views/message_banner',
// edxnotes // edxnotes
'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min', 'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min',
...@@ -744,11 +741,9 @@ ...@@ -744,11 +741,9 @@
'lms/include/teams/js/spec/views/topics_spec.js', 'lms/include/teams/js/spec/views/topics_spec.js',
'lms/include/teams/js/spec/views/team_profile_header_actions_spec.js', 'lms/include/teams/js/spec/views/team_profile_header_actions_spec.js',
'lms/include/js/spec/financial-assistance/financial_assistance_form_view_spec.js', 'lms/include/js/spec/financial-assistance/financial_assistance_form_view_spec.js',
'lms/include/teams/js/spec/views/team_join_spec.js'
'lms/include/js/spec/discovery/discovery_spec.js',
'lms/include/js/spec/ccx/schedule_spec.js',
'lms/include/js/spec/bookmarks/bookmarks_list_view_spec.js', 'lms/include/js/spec/bookmarks/bookmarks_list_view_spec.js',
'lms/include/js/spec/bookmarks/bookmark_button_view_spec.js' 'lms/include/js/spec/bookmarks/bookmark_button_view_spec.js',
'lms/include/js/spec/views/message_banner_spec.js'
]); ]);
}).call(this, requirejs, define); }).call(this, requirejs, define);
...@@ -361,90 +361,20 @@ define([ ...@@ -361,90 +361,20 @@ define([
expect($('.search-button')).toBeVisible(); expect($('.search-button')).toBeVisible();
} }
function rendersSearchResults () { describe('CourseSearchForm', function () {
var searchResults = [{ beforeEach(function () {
location: ['section', 'subsection', 'unit'], loadFixtures('js/fixtures/search/course_search_form.html');
url: '/some/url/to/content', this.form = new CourseSearchForm();
content_type: 'text', this.onClear = jasmine.createSpy('onClear');
course_name: '', this.onSearch = jasmine.createSpy('onSearch');
excerpt: 'this is a short excerpt' this.form.on('clear', this.onClear);
}]; this.form.on('search', this.onSearch);
this.collection.set(searchResults); });
this.collection.latestModelsCount = 1; it('trims input string', trimsInputString);
this.collection.totalCount = 1; it('handles calls to doSearch', doesSearch);
it('triggers a search event and changes to active state', triggersSearchEvent);
this.resultsView.render(); it('clears search when clicking on cancel button', clearsSearchOnCancel);
expect(this.resultsView.$el.find('ol')[0]).toExist(); it('clears search when search box is empty', clearsSearchOnEmpty);
expect(this.resultsView.$el.find('li').length).toEqual(1);
expect(this.resultsView.$el).toContainHtml('Search Results');
expect(this.resultsView.$el).toContainHtml('this is a short excerpt');
this.collection.set(searchResults);
this.collection.totalCount = 2;
this.resultsView.renderNext();
expect(this.resultsView.$el.find('.search-count')).toContainHtml('2');
expect(this.resultsView.$el.find('li').length).toEqual(2);
}
function showsMoreResultsLink () {
this.collection.totalCount = 123;
this.collection.hasNextPage = function () { return true; };
this.resultsView.render();
expect(this.resultsView.$el.find('a.search-load-next')[0]).toExist();
this.collection.totalCount = 123;
this.collection.hasNextPage = function () { return false; };
this.resultsView.render();
expect(this.resultsView.$el.find('a.search-load-next')[0]).not.toExist();
}
function triggersNextPageEvent () {
var onNext = jasmine.createSpy('onNext');
this.resultsView.on('next', onNext);
this.collection.totalCount = 123;
this.collection.hasNextPage = function () { return true; };
this.resultsView.render();
this.resultsView.$el.find('a.search-load-next').click();
expect(onNext).toHaveBeenCalled();
}
function showsLoadMoreSpinner () {
this.collection.totalCount = 123;
this.collection.hasNextPage = function () { return true; };
this.resultsView.render();
expect(this.resultsView.$el.find('a.search-load-next .icon')).toBeHidden();
this.resultsView.loadNext();
// toBeVisible does not work with inline
expect(this.resultsView.$el.find('a.search-load-next .icon')).toHaveCss({ 'display': 'inline' });
this.resultsView.renderNext();
expect(this.resultsView.$el.find('a.search-load-next .icon')).toBeHidden();
}
function beforeEachHelper(SearchResultsView) {
appendSetFixtures(
'<section id="courseware-search-results"></section>' +
'<section id="course-content"></section>' +
'<section id="dashboard-search-results"></section>' +
'<section id="my-courses"></section>'
);
TemplateHelpers.installTemplates([
'templates/search/course_search_item',
'templates/search/dashboard_search_item',
'templates/search/course_search_results',
'templates/search/dashboard_search_results',
'templates/search/search_list',
'templates/search/search_loading',
'templates/search/search_error'
]);
var MockCollection = Backbone.Collection.extend({
hasNextPage: function () {},
latestModelsCount: 0,
pageSize: 20,
latestModels: function () {
return SearchCollection.prototype.latestModels.apply(this, arguments);
}
}); });
describe('DashSearchForm', function () { describe('DashSearchForm', function () {
...@@ -557,12 +487,12 @@ define([ ...@@ -557,12 +487,12 @@ define([
function beforeEachHelper(SearchResultsView) { function beforeEachHelper(SearchResultsView) {
appendSetFixtures( appendSetFixtures(
'<section id="courseware-search-results"></section>' + '<div class="courseware-results"></div>' +
'<section id="course-content"></section>' + '<section id="course-content"></section>' +
'<section id="dashboard-search-results"></section>' + '<section id="dashboard-search-results"></section>' +
'<section id="my-courses"></section>' '<section id="my-courses"></section>'
); );
TemplateHelpers.installTemplates([ TemplateHelpers.installTemplates([
'templates/search/course_search_item', 'templates/search/course_search_item',
'templates/search/dashboard_search_item', 'templates/search/dashboard_search_item',
...@@ -573,12 +503,6 @@ define([ ...@@ -573,12 +503,6 @@ define([
'templates/search/search_error' 'templates/search/search_error'
]); ]);
var courseId = 'a/b/c';
CourseSearchFactory(courseId);
spyOn(Backbone.history, 'navigate');
this.$contentElement = $('#course-content');
this.$searchResults = $('.courseware-results');
var MockCollection = Backbone.Collection.extend({ var MockCollection = Backbone.Collection.extend({
hasNextPage: function () {}, hasNextPage: function () {},
latestModelsCount: 0, latestModelsCount: 0,
...@@ -749,7 +673,7 @@ define([ ...@@ -749,7 +673,7 @@ define([
beforeEach(function () { beforeEach(function () {
loadFixtures('js/fixtures/search/course_search_form.html'); loadFixtures('js/fixtures/search/course_search_form.html');
appendSetFixtures( appendSetFixtures(
'<section id="courseware-search-results"></section>' + '<div class="courseware-results"></div>' +
'<section id="course-content"></section>' '<section id="course-content"></section>'
); );
loadTemplates.call(this); loadTemplates.call(this);
...@@ -758,7 +682,7 @@ define([ ...@@ -758,7 +682,7 @@ define([
CourseSearchFactory(courseId); CourseSearchFactory(courseId);
spyOn(Backbone.history, 'navigate'); spyOn(Backbone.history, 'navigate');
this.$contentElement = $('#course-content'); this.$contentElement = $('#course-content');
this.$searchResults = $('#courseware-search-results'); this.$searchResults = $('.courseware-results');
}); });
it('shows loading message on search', showsLoadingMessage); it('shows loading message on search', showsLoadingMessage);
...@@ -825,4 +749,4 @@ define([ ...@@ -825,4 +749,4 @@ define([
}); });
}); });
}); });
\ No newline at end of file
...@@ -8,7 +8,7 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers ...@@ -8,7 +8,7 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
'js/student_profile/views/learner_profile_view', 'js/student_profile/views/learner_profile_view',
'js/student_profile/views/learner_profile_fields', 'js/student_profile/views/learner_profile_fields',
'js/student_profile/views/learner_profile_factory', 'js/student_profile/views/learner_profile_factory',
'js/views/message' 'js/views/message_banner'
], ],
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews, function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews,
UserAccountModel, UserPreferencesModel, LearnerProfileView, LearnerProfileFields, LearnerProfilePage) { UserAccountModel, UserPreferencesModel, LearnerProfileView, LearnerProfileFields, LearnerProfilePage) {
......
...@@ -2,10 +2,10 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers ...@@ -2,10 +2,10 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
'js/spec/student_account/helpers', 'js/spec/student_account/helpers',
'js/student_account/models/user_account_model', 'js/student_account/models/user_account_model',
'js/student_profile/views/learner_profile_fields', 'js/student_profile/views/learner_profile_fields',
'js/views/message' 'js/views/message_banner'
], ],
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, UserAccountModel, LearnerProfileFields, function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, UserAccountModel, LearnerProfileFields,
MessageView) { MessageBannerView) {
'use strict'; 'use strict';
describe("edx.user.LearnerProfileFields", function () { describe("edx.user.LearnerProfileFields", function () {
...@@ -31,9 +31,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers ...@@ -31,9 +31,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
accountSettingsModel.url = Helpers.USER_ACCOUNTS_API_URL; accountSettingsModel.url = Helpers.USER_ACCOUNTS_API_URL;
var messageView = new MessageView({ var messageView = new MessageBannerView({
el: $('.message-banner'), el: $('.message-banner')
templateId: '#message_banner-tpl'
}); });
return new LearnerProfileFields.ProfileImageFieldView({ return new LearnerProfileFields.ProfileImageFieldView({
......
...@@ -7,11 +7,11 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers ...@@ -7,11 +7,11 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
'js/student_profile/views/learner_profile_fields', 'js/student_profile/views/learner_profile_fields',
'js/student_profile/views/learner_profile_view', 'js/student_profile/views/learner_profile_view',
'js/student_account/views/account_settings_fields', 'js/student_account/views/account_settings_fields',
'js/views/message' 'js/views/message_banner'
], ],
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews, function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews,
UserAccountModel, AccountPreferencesModel, LearnerProfileFields, LearnerProfileView, UserAccountModel, AccountPreferencesModel, LearnerProfileFields, LearnerProfileView,
AccountSettingsFieldViews, MessageView) { AccountSettingsFieldViews, MessageBannerView) {
'use strict'; 'use strict';
describe("edx.user.LearnerProfileView", function () { describe("edx.user.LearnerProfileView", function () {
...@@ -45,9 +45,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers ...@@ -45,9 +45,8 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers
accountSettingsPageUrl: '/account/settings/' accountSettingsPageUrl: '/account/settings/'
}); });
var messageView = new MessageView({ var messageView = new MessageBannerView({
el: $('.message-banner'), el: $('.message-banner')
templateId: '#message_banner-tpl'
}); });
var profileImageFieldView = new LearnerProfileFields.ProfileImageFieldView({ var profileImageFieldView = new LearnerProfileFields.ProfileImageFieldView({
......
define(['backbone', 'jquery', 'underscore', 'js/views/message', 'js/common_helpers/template_helpers' define(['backbone', 'jquery', 'underscore',
'common/js/spec_helpers/template_helpers', 'js/views/message_banner'
], ],
function (Backbone, $, _, MessageView, TemplateHelpers) { function (Backbone, $, _, TemplateHelpers, MessageBannerView) {
'use strict'; 'use strict';
describe("MessageView", function () { describe("MessageBannerView", function () {
var messageEl = '.message-banner';
beforeEach(function () { beforeEach(function () {
setFixtures('<div class="message-banner"></div>'); setFixtures('<div class="message-banner"></div>');
TemplateHelpers.installTemplate("templates/fields/message_banner"); TemplateHelpers.installTemplate("templates/fields/message_banner");
TemplateHelpers.installTemplate("templates/message_view");
}); });
var createMessageView = function (messageContainer, templateId) { it('renders message correctly', function() {
return new MessageView({ var messageSelector = '.message-banner';
el: $(messageContainer), var messageView = new MessageBannerView({
templateId: templateId el: $(messageSelector)
}); });
};
it('renders correctly with the /fields/message_banner template', function() {
var messageView = createMessageView(messageSelector, '#message_banner-tpl');
messageView.showMessage('I am message view'); messageView.showMessage('I am message view');
expect($(messageEl).text().trim()).toBe('I am message view'); // Verify error message
expect($(messageSelector).text().trim()).toBe('I am message view');
messageView.hideMessage();
expect($(messageEl).text().trim()).toBe('');
});
it('renders correctly with the /message_view template', function() {
var messageView = createMessageView(messageEl, '#message-tpl');
var icon = '<i class="fa fa-thumbs-up"></i>';
messageView.showMessage('I am message view', icon);
expect($(messageEl).text().trim()).toBe('I am message view');
expect($(messageEl).html()).toContain(icon);
messageView.hideMessage(); messageView.hideMessage();
expect($(messageEl).text().trim()).toBe(''); expect($(messageSelector).text().trim()).toBe('');
}); });
}); });
}); });
...@@ -8,10 +8,10 @@ ...@@ -8,10 +8,10 @@
'js/student_profile/views/learner_profile_fields', 'js/student_profile/views/learner_profile_fields',
'js/student_profile/views/learner_profile_view', 'js/student_profile/views/learner_profile_view',
'js/student_account/views/account_settings_fields', 'js/student_account/views/account_settings_fields',
'js/views/message', 'js/views/message_banner',
'string_utils' 'string_utils'
], function (gettext, $, _, Backbone, Logger, AccountSettingsModel, AccountPreferencesModel, FieldsView, ], function (gettext, $, _, Backbone, Logger, AccountSettingsModel, AccountPreferencesModel, FieldsView,
LearnerProfileFieldsView, LearnerProfileView, AccountSettingsFieldViews, MessageView) { LearnerProfileFieldsView, LearnerProfileView, AccountSettingsFieldViews, MessageBannerView) {
return function (options) { return function (options) {
...@@ -36,9 +36,8 @@ ...@@ -36,9 +36,8 @@
var editable = options.own_profile ? 'toggle' : 'never'; var editable = options.own_profile ? 'toggle' : 'never';
var messageView = new MessageView({ var messageView = new MessageBannerView({
el: $('.message-banner'), el: $('.message-banner')
templateId: '#message_banner-tpl'
}); });
var accountPrivacyFieldView = new LearnerProfileFieldsView.AccountPrivacyFieldView({ var accountPrivacyFieldView = new LearnerProfileFieldsView.AccountPrivacyFieldView({
......
var onVideoFail = function(e) {
if(e === 'NO_DEVICES_FOUND') {
$('#no-webcam').show();
$('#face_capture_button').hide();
$('#photo_id_capture_button').hide();
}
else {
console.log('Failed to get camera access!', e);
}
};
// Returns true if we are capable of video capture (regardless of whether the
// user has given permission).
function initVideoCapture() {
window.URL = window.URL || window.webkitURL;
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia || navigator.msGetUserMedia;
return !(navigator.getUserMedia === undefined);
}
var submitReverificationPhotos = function() {
// add photos to the form
$('<input>').attr({
type: 'hidden',
name: 'face_image',
value: $("#face_image")[0].src,
}).appendTo("#reverify_form");
$('<input>').attr({
type: 'hidden',
name: 'photo_id_image',
value: $("#photo_id_image")[0].src,
}).appendTo("#reverify_form");
$("#reverify_form").submit();
};
var submitMidcourseReverificationPhotos = function() {
$('<input>').attr({
type: 'hidden',
name: 'face_image',
value: $("#face_image")[0].src,
}).appendTo("#reverify_form");
$("#reverify_form").submit();
};
function showSubmissionError() {
if (xhr.status === 400) {
$('#order-error .copy p').html(xhr.responseText);
}
$('#order-error').show();
$("html, body").animate({ scrollTop: 0 });
}
function submitForm(data) {
for (prop in data) {
$('<input>').attr({
type: 'hidden',
name: prop,
value: data[prop]
}).appendTo('#pay_form');
}
$("#pay_form").submit();
}
function refereshPageMessage() {
$('#photo-error').show();
$("html, body").animate({ scrollTop: 0 });
}
var submitToPaymentProcessing = function() {
$(".payment-button").addClass('is-disabled').attr('aria-disabled', true);
var contribution_input = $("input[name='contribution']:checked");
var contribution = 0;
if(contribution_input.attr('id') == 'contribution-other') {
contribution = $("input[name='contribution-other-amt']").val();
}
else {
contribution = contribution_input.val();
}
var course_id = $("input[name='course_id']").val();
$.ajax({
url: "/verify_student/create_order",
type: 'POST',
data: {
"course_id" : course_id,
"contribution": contribution,
"face_image" : $("#face_image")[0].src,
"photo_id_image" : $("#photo_id_image")[0].src
},
success:function(data) {
if (data.success) {
submitForm(data);
} else {
refereshPageMessage();
}
},
error:function() {
$(".payment-button").removeClass('is-disabled').attr('aria-disabled', false);
showSubmissionError();
}
});
};
function doResetButton(resetButton, captureButton, approveButton, nextButtonNav, nextLink) {
approveButton.removeClass('approved');
nextButtonNav.addClass('is-not-ready');
nextLink.attr('href', "#");
captureButton.show();
resetButton.hide();
approveButton.hide();
}
function doApproveButton(approveButton, nextButtonNav, nextLink) {
nextButtonNav.removeClass('is-not-ready');
approveButton.addClass('approved');
nextLink.attr('href', "#next");
}
function doSnapshotButton(captureButton, resetButton, approveButton) {
captureButton.hide();
resetButton.show();
approveButton.show();
}
function submitNameChange(event) {
event.preventDefault();
$("#lean_overlay").fadeOut(200);
$("#edit-name").css({ 'display' : 'none' });
var full_name = $('input[name="name"]').val();
var xhr = $.post(
"/change_name",
{
"new_name" : full_name,
"rationale": "Want to match ID for ID Verified Certificates."
},
function() {
$('#full-name').html(full_name);
}
)
.fail(function(jqXhr) {
$('.message-copy').html(jqXhr.responseText);
});
}
function initSnapshotHandler(names, hasHtml5CameraSupport) {
var name = names.pop();
if (name == undefined) {
return;
}
var video = $('#' + name + '_video');
var canvas = $('#' + name + '_canvas');
var image = $('#' + name + "_image");
var captureButton = $("#" + name + "_capture_button");
var resetButton = $("#" + name + "_reset_button");
var approveButton = $("#" + name + "_approve_button");
var nextButtonNav = $("#" + name + "_next_button_nav");
var nextLink = $("#" + name + "_next_link");
var flashCapture = $("#" + name + "_flash");
var ctx = null;
if (hasHtml5CameraSupport) {
ctx = canvas[0].getContext('2d');
}
var localMediaStream = null;
function snapshot(event) {
if (hasHtml5CameraSupport) {
if (localMediaStream) {
ctx.drawImage(video[0], 0, 0);
image[0].src = canvas[0].toDataURL('image/png');
}
else {
return false;
}
video[0].pause();
}
else {
if (flashCapture[0].cameraAuthorized()) {
image[0].src = flashCapture[0].snap();
}
else {
return false;
}
}
doSnapshotButton(captureButton, resetButton, approveButton);
return false;
}
function reset() {
image[0].src = "";
if (hasHtml5CameraSupport) {
video[0].play();
}
else {
flashCapture[0].reset();
}
doResetButton(resetButton, captureButton, approveButton, nextButtonNav, nextLink);
return false;
}
function approve() {
doApproveButton(approveButton, nextButtonNav, nextLink);
return false;
}
// Initialize state for this picture taker
captureButton.show();
resetButton.hide();
approveButton.hide();
nextButtonNav.addClass('is-not-ready');
nextLink.attr('href', "#");
// Connect event handlers...
video.click(snapshot);
captureButton.click(snapshot);
resetButton.click(reset);
approveButton.click(approve);
// If it's flash-based, we can just immediate initialize the next one.
// If it's HTML5 based, we have to do it in the callback from getUserMedia
// so that Firefox doesn't eat the second request.
if (hasHtml5CameraSupport) {
navigator.getUserMedia({video: true}, function(stream) {
video[0].src = window.URL.createObjectURL(stream);
localMediaStream = stream;
// We do this in a recursive call on success because Firefox seems to
// simply eat the request if you stack up two on top of each other before
// the user has a chance to approve the first one.
//
// This appears to be necessary for older versions of Firefox (before 28).
// For more info, see https://github.com/edx/edx-platform/pull/3053
initSnapshotHandler(names, hasHtml5CameraSupport);
}, onVideoFail);
}
else {
initSnapshotHandler(names, hasHtml5CameraSupport);
}
}
function browserHasFlash() {
var hasFlash = false;
try {
var fo = new ActiveXObject('ShockwaveFlash.ShockwaveFlash');
if(fo) hasFlash = true;
} catch(e) {
if(navigator.mimeTypes["application/x-shockwave-flash"] != undefined) hasFlash = true;
}
return hasFlash;
}
function objectTagForFlashCamera(name) {
// detect whether or not flash is available
if(browserHasFlash()) {
// I manually update this to have ?v={2,3,4, etc} to avoid caching of flash
// objects on local dev.
return '<object type="application/x-shockwave-flash" id="' +
name + '" name="' + name + '" data=' +
'"/static/js/verify_student/CameraCapture.swf?v=3"' +
'width="500" height="375"><param name="quality" ' +
'value="high"><param name="allowscriptaccess" ' +
'value="sameDomain"></object>';
}
else {
// display a message informing the user to install flash
$('#no-flash').show();
}
}
function waitForFlashLoad(func, flash_object) {
if(!flash_object.hasOwnProperty('percentLoaded') || flash_object.percentLoaded() < 100){
setTimeout(function() {
waitForFlashLoad(func, flash_object);
},
50);
}
else {
func(flash_object);
}
}
$(document).ready(function() {
$(".carousel-nav").addClass('sr');
$(".payment-button").click(function(){
analytics.pageview("Payment Form");
submitToPaymentProcessing();
});
$("#reverify_button").click(function() {
submitReverificationPhotos();
});
$("#midcourse_reverify_button").click(function() {
submitMidcourseReverificationPhotos();
});
// prevent browsers from keeping this button checked
$("#confirm_pics_good").prop("checked", false);
$("#confirm_pics_good").change(function() {
$(".payment-button").toggleClass('disabled');
$("#reverify_button").toggleClass('disabled');
$("#midcourse_reverify_button").toggleClass('disabled');
});
// add in handlers to add/remove the correct classes to the body
// when moving between steps
$('#face_next_link').click(function(){
analytics.pageview("Capture ID Photo");
$('#photo-error').hide();
$('body').addClass('step-photos-id').removeClass('step-photos-cam');
});
$('#photo_id_next_link').click(function(){
analytics.pageview("Review Photos");
$('body').addClass('step-review').removeClass('step-photos-id');
});
// set up edit information dialog
$('#edit-name div[role="alert"]').hide();
$('#edit-name .action-save').click(submitNameChange);
var hasHtml5CameraSupport = initVideoCapture();
// If HTML5 WebRTC capture is not supported, we initialize jpegcam
if (!hasHtml5CameraSupport) {
$("#face_capture_div").html(objectTagForFlashCamera("face_flash"));
$("#photo_id_capture_div").html(objectTagForFlashCamera("photo_id_flash"));
// wait for the flash object to be loaded and then check for a camera
if(browserHasFlash()) {
waitForFlashLoad(function(flash_object) {
if(!flash_object.hasOwnProperty('hasCamera')){
onVideoFail('NO_DEVICES_FOUND');
}
}, $('#face_flash')[0]);
}
}
analytics.pageview("Capture Face Photo");
initSnapshotHandler(["photo_id", "face"], hasHtml5CameraSupport);
$('a[rel="external"]').attr({
title: gettext('This link will open in a new browser window/tab'),
target: '_blank'
});
});
...@@ -17,17 +17,15 @@ ...@@ -17,17 +17,15 @@
if (_.isUndefined(this.message) || _.isNull(this.message)) { if (_.isUndefined(this.message) || _.isNull(this.message)) {
this.$el.html(''); this.$el.html('');
} else { } else {
this.$el.html(this.template({ this.$el.html(_.template(messageBannerTemplate, _.extend(this.options, {
message: this.message, message: this.message
icon: this.icon })));
}));
} }
return this; return this;
}, },
showMessage: function (message, icon) { showMessage: function (message) {
this.message = message; this.message = message;
this.icon = icon;
this.render(); this.render();
}, },
......
...@@ -62,7 +62,6 @@ lib_paths: ...@@ -62,7 +62,6 @@ lib_paths:
- xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min.js - xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min.js
- xmodule_js/common_static/js/test/i18n.js - xmodule_js/common_static/js/test/i18n.js
- xmodule_js/common_static/js/vendor/date.js - xmodule_js/common_static/js/vendor/date.js
- xmodule_js/common_static/js/vendor/moment-with-locales.min.js
- xmodule_js/common_static/js/vendor/moment.min.js - xmodule_js/common_static/js/vendor/moment.min.js
- xmodule_js/common_static/js/vendor/moment-with-locales.min.js - xmodule_js/common_static/js/vendor/moment-with-locales.min.js
- xmodule_js/common_static/common/js/utils/edx.utils.validate.js - xmodule_js/common_static/common/js/utils/edx.utils.validate.js
...@@ -117,7 +116,6 @@ fixture_paths: ...@@ -117,7 +116,6 @@ fixture_paths:
- support/templates - support/templates
- js/fixtures/bookmarks - js/fixtures/bookmarks
- templates/bookmarks - templates/bookmarks
- templates/message_view.underscore
requirejs: requirejs:
paths: paths:
......
...@@ -33,6 +33,7 @@ ...@@ -33,6 +33,7 @@
'teams/js/teams_tab_factory', 'teams/js/teams_tab_factory',
'support/js/certificates_factory', 'support/js/certificates_factory',
'support/js/enrollment_factory', 'support/js/enrollment_factory',
'js/bookmarks/bookmarks_factory'
]), ]),
/** /**
......
;(function (require, define) {
var paths = {}, config;
// jquery, underscore, gettext, URI, tinymce, or jquery.tinymce may already
// have been loaded and we do not want to load them a second time. Check if
// it is the case and use the global var instead.
if (window.jQuery) {
define("jquery", [], function() {return window.jQuery;});
} else {
paths.jquery = "js/vendor/jquery.min";
}
if (window._) {
define("underscore", [], function() {return window._;});
} else {
paths.jquery = "js/vendor/underscore-min";
}
if (window.gettext) {
define("gettext", [], function() {return window.gettext;});
} else {
paths.gettext = "/i18n";
}
if (window.Logger) {
define("logger", [], function() {return window.Logger;});
} else {
paths.logger = "js/src/logger";
}
if (window.URI) {
define("URI", [], function() {return window.URI;});
} else {
paths.URI = "js/vendor/URI.min";
}
if (window.tinymce) {
define('tinymce', [], function() {return window.tinymce;});
} else {
paths.tinymce = "js/vendor/tinymce/js/tinymce/tinymce.full.min";
}
if (window.jquery && window.jquery.tinymce) {
define("jquery.tinymce", [], function() {return window.jquery.tinymce;});
} else {
paths.tinymce = "js/vendor/tinymce/js/tinymce/jquery.tinymce.min";
}
config = {
// NOTE: baseUrl has been previously set in lms/static/templates/main.html
waitSeconds: 60,
paths: {
"annotator_1.2.9": "js/vendor/edxnotes/annotator-full.min",
"date": "js/vendor/date",
"text": 'js/vendor/requirejs/text',
"backbone": "js/vendor/backbone-min",
"backbone-super": "js/vendor/backbone-super",
"backbone.paginator": "js/vendor/backbone.paginator.min",
"underscore.string": "js/vendor/underscore.string.min",
// Files needed by OVA
"annotator": "js/vendor/ova/annotator-full",
"annotator-harvardx": "js/vendor/ova/annotator-full-firebase-auth",
"video.dev": "js/vendor/ova/video.dev",
"vjs.youtube": 'js/vendor/ova/vjs.youtube',
"rangeslider": 'js/vendor/ova/rangeslider',
"share-annotator": 'js/vendor/ova/share-annotator',
"richText-annotator": 'js/vendor/ova/richText-annotator',
"reply-annotator": 'js/vendor/ova/reply-annotator',
"grouping-annotator": 'js/vendor/ova/grouping-annotator',
"tags-annotator": 'js/vendor/ova/tags-annotator',
"diacritic-annotator": 'js/vendor/ova/diacritic-annotator',
"flagging-annotator": 'js/vendor/ova/flagging-annotator',
"jquery-Watch": 'js/vendor/ova/jquery-Watch',
"openseadragon": 'js/vendor/ova/openseadragon',
"osda": 'js/vendor/ova/OpenSeaDragonAnnotation',
"ova": 'js/vendor/ova/ova',
"catch": 'js/vendor/ova/catch/js/catch',
"handlebars": 'js/vendor/ova/catch/js/handlebars-1.1.2',
"moment": "js/vendor/moment-with-locales.min"
// end of files needed by OVA
},
shim: {
"annotator_1.2.9": {
deps: ["jquery"],
exports: "Annotator"
},
"date": {
exports: "Date"
},
"jquery": {
exports: "$"
},
"underscore": {
exports: "_"
},
"backbone": {
deps: ["underscore", "jquery"],
exports: "Backbone"
},
"backbone.paginator": {
deps: ["backbone"],
exports: "Backbone.Paginator"
},
"backbone-super": {
deps: ["backbone"]
},
"logger": {
exports: "Logger"
},
// Needed by OVA
"video.dev": {
exports:"videojs"
},
"vjs.youtube": {
deps: ["video.dev"]
},
"rangeslider": {
deps: ["video.dev"]
},
"annotator": {
exports: "Annotator"
},
"annotator-harvardx":{
deps: ["annotator"]
},
"share-annotator": {
deps: ["annotator"]
},
"richText-annotator": {
deps: ["annotator", "tinymce"]
},
"reply-annotator": {
deps: ["annotator"]
},
"tags-annotator": {
deps: ["annotator"]
},
"diacritic-annotator": {
deps: ["annotator"]
},
"flagging-annotator": {
deps: ["annotator"]
},
"grouping-annotator": {
deps: ["annotator"]
},
"ova": {
exports: "ova",
deps: [
"annotator", "annotator-harvardx", "video.dev", "vjs.youtube", "rangeslider", "share-annotator",
"richText-annotator", "reply-annotator", "tags-annotator", "flagging-annotator",
"grouping-annotator", "diacritic-annotator", "jquery-Watch", "catch", "handlebars", "URI"
]
},
"osda": {
exports: "osda",
deps: [
"annotator", "annotator-harvardx", "video.dev", "vjs.youtube", "rangeslider", "share-annotator",
"richText-annotator", "reply-annotator", "tags-annotator", "flagging-annotator",
"grouping-annotator", "diacritic-annotator", "openseadragon", "jquery-Watch", "catch", "handlebars",
"URI"
]
}
// End of needed by OVA
}
};
for (var key in paths) {
if ({}.hasOwnProperty.call(paths, key)) {
config.paths[key] = paths[key];
}
}
require.config(config);
}).call(this, require || RequireJS.require, define || RequireJS.define);
...@@ -25,12 +25,6 @@ ${page_title_breadcrumbs(course_name())} ...@@ -25,12 +25,6 @@ ${page_title_breadcrumbs(course_name())}
<%block name="header_extras"> <%block name="header_extras">
% for template_name in ["message_banner"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="fields/${template_name}.underscore" />
</script>
% endfor
% for template_name in ["image-modal"]: % for template_name in ["image-modal"]:
<script type="text/template" id="${template_name}-tpl"> <script type="text/template" id="${template_name}-tpl">
<%static:include path="common/templates/${template_name}.underscore" /> <%static:include path="common/templates/${template_name}.underscore" />
...@@ -53,18 +47,6 @@ ${page_title_breadcrumbs(course_name())} ...@@ -53,18 +47,6 @@ ${page_title_breadcrumbs(course_name())}
% endfor % endfor
% endif % endif
% for template_name in ["bookmarks-list"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="bookmarks/${template_name}.underscore" />
</script>
% endfor
% for template_name in ["message_view"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="${template_name}.underscore" />
</script>
% endfor
</%block> </%block>
<%block name="headextra"> <%block name="headextra">
...@@ -92,11 +74,15 @@ ${page_title_breadcrumbs(course_name())} ...@@ -92,11 +74,15 @@ ${page_title_breadcrumbs(course_name())}
<%static:js group='discussion'/> <%static:js group='discussion'/>
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'): % if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
<%static:require_module module_name="js/search/course/course_search_factory" class_name="CourseSearchFactory"> <%static:require_module module_name="js/search/course/course_search_factory" class_name="CourseSearchFactory">
var courseId = $('#courseware-search-results').data('courseId'); var courseId = $('.courseware-results').data('courseId');
CourseSearchFactory(courseId); CourseSearchFactory(courseId);
</%static:require_module> </%static:require_module>
% endif % endif
<%static:require_module module_name="js/bookmarks/bookmarks_factory" class_name="BookmarksFactory">
BookmarksFactory();
</%static:require_module>
<%include file="../discussion/_js_body_dependencies.html" /> <%include file="../discussion/_js_body_dependencies.html" />
% if staff_access: % if staff_access:
<%include file="xqa_interface.html"/> <%include file="xqa_interface.html"/>
...@@ -131,7 +117,7 @@ ${fragment.foot_html()} ...@@ -131,7 +117,7 @@ ${fragment.foot_html()}
</%block> </%block>
<div class="coursewide-message-banner" aria-live="polite"></div> <div class="message-banner" aria-live="polite"></div>
% if default_tab: % if default_tab:
<%include file="/courseware/course_navigation.html" /> <%include file="/courseware/course_navigation.html" />
...@@ -144,17 +130,6 @@ ${fragment.foot_html()} ...@@ -144,17 +130,6 @@ ${fragment.foot_html()}
% if disable_accordion is UNDEFINED or not disable_accordion: % if disable_accordion is UNDEFINED or not disable_accordion:
<div class="course-index"> <div class="course-index">
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
<div id="courseware-search-bar" class="search-bar courseware-search-bar" role="search" aria-label="Course">
<form>
<label for="course-search-input" class="sr">${_('Course Search')}</label>
<div class="search-field-wrapper">
<input id="course-search-input" type="text" class="search-field"/>
<button type="submit" class="search-button">
${_('search')} <i class="icon fa fa-search" aria-hidden="true"></i>
<header id="open_close_accordion">
<a href="#">${_("close")}</a>
</header>
<div class="wrapper-course-modes"> <div class="wrapper-course-modes">
...@@ -181,12 +156,10 @@ ${fragment.foot_html()} ...@@ -181,12 +156,10 @@ ${fragment.foot_html()}
</div> </div>
% endif % endif
<div class="accordion">
<nav class="course-navigation" aria-label="${_('Course')}">
</div> </div>
<div id="accordion" style="display: none"> <div class="accordion">
<nav aria-label="${_('Course Navigation')}"> <nav class="course-navigation" aria-label="${_('Course')}">
% if accordion.strip(): % if accordion.strip():
${accordion} ${accordion}
% else: % else:
...@@ -231,11 +204,11 @@ ${fragment.foot_html()} ...@@ -231,11 +204,11 @@ ${fragment.foot_html()}
${fragment.body_html()} ${fragment.body_html()}
</section> </section>
<section class="courseware-results-wrapper"> <section class="courseware-results-wrapper">
<div id="loading-message" aria-live="polite" aria-relevant="all"></div> <div id="loading-message" aria-live="polite" aria-relevant="all"></div>
<div id="error-message" aria-live="polite"></div> <div id="error-message" aria-live="polite"></div>
<div class="courseware-results search-results" data-course-id="${course.id}" data-lang-code="${language_preference}"></div> <div class="courseware-results search-results" data-course-id="${course.id}" data-lang-code="${language_preference}"></div>
</section> </section>
</div> </div>
</div> </div>
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime from __future__ import unicode_literals
from south.db import db
from south.v2 import SchemaMigration from django.db import migrations, models
from django.db import models import model_utils.fields
import xmodule_django.models
import jsonfield.fields
class Migration(SchemaMigration): import django.utils.timezone
from django.conf import settings
def forwards(self, orm):
# Adding model 'Bookmark'
db.create_table('bookmarks_bookmark', ( class Migration(migrations.Migration):
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)), dependencies = [
('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), ]
('course_key', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
('usage_key', self.gf('xmodule_django.models.LocationKeyField')(max_length=255, db_index=True)), operations = [
('display_name', self.gf('django.db.models.fields.CharField')(default='', max_length=255)), migrations.CreateModel(
('path', self.gf('jsonfield.fields.JSONField')()), name='Bookmark',
)) fields=[
db.send_create_signal('bookmarks', ['Bookmark']) ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
def backwards(self, orm): ('course_key', xmodule_django.models.CourseKeyField(max_length=255, db_index=True)),
# Deleting model 'Bookmark' ('usage_key', xmodule_django.models.LocationKeyField(max_length=255, db_index=True)),
db.delete_table('bookmarks_bookmark') ('_path', jsonfield.fields.JSONField(help_text=b'Path in course tree to the block', db_column=b'path')),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
],
models = { ),
'auth.group': { migrations.CreateModel(
'Meta': {'object_name': 'Group'}, name='XBlockCache',
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), fields=[
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)),
}, ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
'auth.permission': { ('course_key', xmodule_django.models.CourseKeyField(max_length=255, db_index=True)),
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, ('usage_key', xmodule_django.models.LocationKeyField(unique=True, max_length=255, db_index=True)),
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), ('display_name', models.CharField(default=b'', max_length=255)),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), ('_paths', jsonfield.fields.JSONField(default=[], help_text=b'All paths in course tree to the corresponding block.', db_column=b'paths')),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), ],
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) options={
}, 'abstract': False,
'auth.user': { },
'Meta': {'object_name': 'User'}, ),
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), migrations.AddField(
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), model_name='bookmark',
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), name='xblock_cache',
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), field=models.ForeignKey(to='bookmarks.XBlockCache'),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), ),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), migrations.AlterUniqueTogether(
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), name='bookmark',
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), unique_together=set([('user', 'usage_key')]),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), ),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), ]
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'bookmarks.bookmark': {
'Meta': {'object_name': 'Bookmark'},
'course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'display_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'path': ('jsonfield.fields.JSONField', [], {}),
'usage_key': ('xmodule_django.models.LocationKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['bookmarks']
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'XBlockCache'
db.create_table('bookmarks_xblockcache', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)),
('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)),
('course_key', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
('usage_key', self.gf('xmodule_django.models.LocationKeyField')(unique=True, max_length=255, db_index=True)),
('display_name', self.gf('django.db.models.fields.CharField')(default='', max_length=255)),
('_paths', self.gf('jsonfield.fields.JSONField')(default=[], db_column='paths')),
))
db.send_create_signal('bookmarks', ['XBlockCache'])
# Deleting field 'Bookmark.display_name'
db.delete_column('bookmarks_bookmark', 'display_name')
# Deleting field 'Bookmark.path'
db.delete_column('bookmarks_bookmark', 'path')
# Adding field 'Bookmark._path'
db.add_column('bookmarks_bookmark', '_path',
self.gf('jsonfield.fields.JSONField')(default='', db_column='path'),
keep_default=False)
# Adding field 'Bookmark.xblock_cache'
db.add_column('bookmarks_bookmark', 'xblock_cache',
self.gf('django.db.models.fields.related.ForeignKey')(default=0, to=orm['bookmarks.XBlockCache']),
keep_default=False)
# Adding unique constraint on 'Bookmark', fields ['user', 'usage_key']
db.create_unique('bookmarks_bookmark', ['user_id', 'usage_key'])
def backwards(self, orm):
# Removing unique constraint on 'Bookmark', fields ['user', 'usage_key']
db.delete_unique('bookmarks_bookmark', ['user_id', 'usage_key'])
# Deleting model 'XBlockCache'
db.delete_table('bookmarks_xblockcache')
# Adding field 'Bookmark.display_name'
db.add_column('bookmarks_bookmark', 'display_name',
self.gf('django.db.models.fields.CharField')(default='', max_length=255),
keep_default=False)
# Adding field 'Bookmark.path'
db.add_column('bookmarks_bookmark', 'path',
self.gf('jsonfield.fields.JSONField')(default=''),
keep_default=False)
# Deleting field 'Bookmark._path'
db.delete_column('bookmarks_bookmark', 'path')
# Deleting field 'Bookmark.xblock_cache'
db.delete_column('bookmarks_bookmark', 'xblock_cache_id')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'bookmarks.bookmark': {
'Meta': {'unique_together': "(('user', 'usage_key'),)", 'object_name': 'Bookmark'},
'_path': ('jsonfield.fields.JSONField', [], {'db_column': "'path'"}),
'course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'usage_key': ('xmodule_django.models.LocationKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'xblock_cache': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['bookmarks.XBlockCache']"})
},
'bookmarks.xblockcache': {
'Meta': {'object_name': 'XBlockCache'},
'_paths': ('jsonfield.fields.JSONField', [], {'default': '[]', 'db_column': "'paths'"}),
'course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'display_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'usage_key': ('xmodule_django.models.LocationKeyField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['bookmarks']
\ No newline at end of file
...@@ -56,6 +56,9 @@ class Bookmark(TimeStampedModel): ...@@ -56,6 +56,9 @@ class Bookmark(TimeStampedModel):
""" """
unique_together = ('user', 'usage_key') unique_together = ('user', 'usage_key')
def __unicode__(self):
return self.resource_id
@classmethod @classmethod
def create(cls, data): def create(cls, data):
""" """
...@@ -197,6 +200,9 @@ class XBlockCache(TimeStampedModel): ...@@ -197,6 +200,9 @@ class XBlockCache(TimeStampedModel):
db_column='paths', default=[], help_text='All paths in course tree to the corresponding block.' db_column='paths', default=[], help_text='All paths in course tree to the corresponding block.'
) )
def __unicode__(self):
return unicode(self.usage_key)
@property @property
def paths(self): def paths(self):
""" """
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
Serializers for Bookmarks. Serializers for Bookmarks.
""" """
from rest_framework import serializers from rest_framework import serializers
from openedx.core.lib.api.serializers import CourseKeyField, UsageKeyField
from . import DEFAULT_FIELDS from . import DEFAULT_FIELDS
from .models import Bookmark from .models import Bookmark
...@@ -11,12 +12,12 @@ class BookmarkSerializer(serializers.ModelSerializer): ...@@ -11,12 +12,12 @@ class BookmarkSerializer(serializers.ModelSerializer):
""" """
Serializer for the Bookmark model. Serializer for the Bookmark model.
""" """
id = serializers.Field(source='resource_id') # pylint: disable=invalid-name id = serializers.SerializerMethodField() # pylint: disable=invalid-name
course_id = serializers.Field(source='course_key') course_id = CourseKeyField(source='course_key')
usage_id = serializers.Field(source='usage_key') usage_id = UsageKeyField(source='usage_key')
block_type = serializers.Field(source='usage_key.block_type') block_type = serializers.ReadOnlyField(source='usage_key.block_type')
display_name = serializers.Field(source='display_name') display_name = serializers.ReadOnlyField()
path = serializers.SerializerMethodField('path_data') path = serializers.SerializerMethodField()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass # Don't pass the 'fields' arg up to the superclass
...@@ -46,14 +47,17 @@ class BookmarkSerializer(serializers.ModelSerializer): ...@@ -46,14 +47,17 @@ class BookmarkSerializer(serializers.ModelSerializer):
'created', 'created',
) )
def resource_id(self, bookmark): def get_id(self, bookmark):
""" """
Return the REST resource id: {username,usage_id}. Return the REST resource id: {username,usage_id}.
""" """
return "{0},{1}".format(bookmark.user.username, bookmark.usage_key) return "{0},{1}".format(bookmark.user.username, bookmark.usage_key)
def path_data(self, bookmark): def get_path(self, bookmark):
""" """
Serialize and return the path data of the bookmark. Serialize and return the path data of the bookmark.
""" """
return [path_item._asdict() for path_item in bookmark.path] path_items = [path_item._asdict() for path_item in bookmark.path]
for path_item in path_items:
path_item['usage_key'] = unicode(path_item['usage_key'])
return path_items
...@@ -10,7 +10,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError ...@@ -10,7 +10,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from request_cache.middleware import RequestCache from request_cache.middleware import RequestCache
from . import DEFAULT_FIELDS, OPTIONAL_FIELDS, api from . import DEFAULT_FIELDS, api
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -42,8 +42,7 @@ class BookmarksService(object): ...@@ -42,8 +42,7 @@ class BookmarksService(object):
fetch (Bool): if the bookmarks should be fetched and cached if they already aren't. fetch (Bool): if the bookmarks should be fetched and cached if they already aren't.
""" """
store = modulestore() store = modulestore()
if hasattr(store, 'fill_in_run'): course_key = store.fill_in_run(course_key)
course_key = store.fill_in_run(course_key)
if course_key.run is None: if course_key.run is None:
return [] return []
cache_key = CACHE_KEY_TEMPLATE.format(self._user.id, course_key) cache_key = CACHE_KEY_TEMPLATE.format(self._user.id, course_key)
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
Tasks for bookmarks. Tasks for bookmarks.
""" """
import logging import logging
from django.db import transaction from django.db import transaction
from celery.task import task # pylint: disable=import-error,no-name-in-module from celery.task import task # pylint: disable=import-error,no-name-in-module
...@@ -120,7 +119,7 @@ def _update_xblocks_cache(course_key): ...@@ -120,7 +119,7 @@ def _update_xblocks_cache(course_key):
block_cache.paths = paths block_cache.paths = paths
block_cache.save() block_cache.save()
with transaction.commit_on_success(): with transaction.atomic():
block_caches = XBlockCache.objects.filter(course_key=course_key) block_caches = XBlockCache.objects.filter(course_key=course_key)
for block_cache in block_caches: for block_cache in block_caches:
block_data = blocks_data.pop(unicode(block_cache.usage_key), None) block_data = blocks_data.pop(unicode(block_cache.usage_key), None)
...@@ -128,7 +127,7 @@ def _update_xblocks_cache(course_key): ...@@ -128,7 +127,7 @@ def _update_xblocks_cache(course_key):
update_block_cache_if_needed(block_cache, block_data) update_block_cache_if_needed(block_cache, block_data)
for block_data in blocks_data.values(): for block_data in blocks_data.values():
with transaction.commit_on_success(): with transaction.atomic():
paths = _paths_from_data(block_data['paths']) paths = _paths_from_data(block_data['paths'])
log.info(u'Creating XBlockCache with usage_key: %s', unicode(block_data['usage_key'])) log.info(u'Creating XBlockCache with usage_key: %s', unicode(block_data['usage_key']))
block_cache, created = XBlockCache.objects.get_or_create(usage_key=block_data['usage_key'], defaults={ block_cache, created = XBlockCache.objects.get_or_create(usage_key=block_data['usage_key'], defaults={
......
...@@ -16,7 +16,9 @@ LOCATION = partial(COURSE_KEY.make_usage_key, u'problem') ...@@ -16,7 +16,9 @@ LOCATION = partial(COURSE_KEY.make_usage_key, u'problem')
class BookmarkFactory(DjangoModelFactory): class BookmarkFactory(DjangoModelFactory):
""" Simple factory class for generating Bookmark """ """ Simple factory class for generating Bookmark """
FACTORY_FOR = Bookmark
class Meta(object):
model = Bookmark
user = factory.SubFactory(UserFactory) user = factory.SubFactory(UserFactory)
course_key = COURSE_KEY course_key = COURSE_KEY
...@@ -31,7 +33,9 @@ class BookmarkFactory(DjangoModelFactory): ...@@ -31,7 +33,9 @@ class BookmarkFactory(DjangoModelFactory):
class XBlockCacheFactory(DjangoModelFactory): class XBlockCacheFactory(DjangoModelFactory):
""" Simple factory class for generating XblockCache. """ """ Simple factory class for generating XblockCache. """
FACTORY_FOR = XBlockCache
class Meta(object):
model = XBlockCache
course_key = COURSE_KEY course_key = COURSE_KEY
usage_key = factory.Sequence(u'4x://edx/100/block/{0}'.format) usage_key = factory.Sequence(u'4x://edx/100/block/{0}'.format)
......
...@@ -120,7 +120,7 @@ class BookmarksAPITests(BookmarkApiEventTestMixin, BookmarksTestsBase): ...@@ -120,7 +120,7 @@ class BookmarksAPITests(BookmarkApiEventTestMixin, BookmarksTestsBase):
""" """
self.assertEqual(len(api.get_bookmarks(user=self.user, course_key=self.course.id)), 2) self.assertEqual(len(api.get_bookmarks(user=self.user, course_key=self.course.id)), 2)
with self.assertNumQueries(4): with self.assertNumQueries(8):
bookmark_data = api.create_bookmark(user=self.user, usage_key=self.vertical_2.location) bookmark_data = api.create_bookmark(user=self.user, usage_key=self.vertical_2.location)
self.assert_bookmark_event_emitted( self.assert_bookmark_event_emitted(
...@@ -141,7 +141,7 @@ class BookmarksAPITests(BookmarkApiEventTestMixin, BookmarksTestsBase): ...@@ -141,7 +141,7 @@ class BookmarksAPITests(BookmarkApiEventTestMixin, BookmarksTestsBase):
""" """
self.assertEqual(len(api.get_bookmarks(user=self.user, course_key=self.course.id)), 2) self.assertEqual(len(api.get_bookmarks(user=self.user, course_key=self.course.id)), 2)
with self.assertNumQueries(4): with self.assertNumQueries(8):
bookmark_data = api.create_bookmark(user=self.user, usage_key=self.vertical_2.location) bookmark_data = api.create_bookmark(user=self.user, usage_key=self.vertical_2.location)
self.assert_bookmark_event_emitted( self.assert_bookmark_event_emitted(
......
...@@ -68,7 +68,7 @@ class BookmarksServiceTests(BookmarksTestsBase): ...@@ -68,7 +68,7 @@ class BookmarksServiceTests(BookmarksTestsBase):
self.bookmark_service.set_bookmarked(usage_key=UsageKey.from_string("i4x://ed/ed/ed/interactive")) self.bookmark_service.set_bookmarked(usage_key=UsageKey.from_string("i4x://ed/ed/ed/interactive"))
) )
with self.assertNumQueries(4): with self.assertNumQueries(8):
self.assertTrue(self.bookmark_service.set_bookmarked(usage_key=self.vertical_2.location)) self.assertTrue(self.bookmark_service.set_bookmarked(usage_key=self.vertical_2.location))
def test_unset_bookmarked(self): def test_unset_bookmarked(self):
......
...@@ -140,8 +140,8 @@ class XBlockCacheTaskTests(BookmarksTestsBase): ...@@ -140,8 +140,8 @@ class XBlockCacheTaskTests(BookmarksTestsBase):
) )
@ddt.data( @ddt.data(
('course', 19), ('course', 47),
('other_course', 13) ('other_course', 34)
) )
@ddt.unpack @ddt.unpack
def test_update_xblocks_cache(self, course_attr, expected_sql_queries): def test_update_xblocks_cache(self, course_attr, expected_sql_queries):
...@@ -162,5 +162,5 @@ class XBlockCacheTaskTests(BookmarksTestsBase): ...@@ -162,5 +162,5 @@ class XBlockCacheTaskTests(BookmarksTestsBase):
path_item.usage_key, expected_cache_data[usage_key][path_index][path_item_index + 1] path_item.usage_key, expected_cache_data[usage_key][path_index][path_item_index + 1]
) )
with self.assertNumQueries(1): with self.assertNumQueries(3):
_update_xblocks_cache(course.id) _update_xblocks_cache(course.id)
...@@ -95,7 +95,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase): ...@@ -95,7 +95,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase):
if check_all_fields: if check_all_fields:
query_parameters += '&fields=path,display_name' query_parameters += '&fields=path,display_name'
with self.assertNumQueries(7): # 2 queries for bookmark table. with self.assertNumQueries(9): # 2 queries for bookmark table.
response = self.send_get( response = self.send_get(
client=self.client, client=self.client,
url=reverse('bookmarks'), url=reverse('bookmarks'),
...@@ -138,7 +138,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase): ...@@ -138,7 +138,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase):
page_size = 5 page_size = 5
query_parameters = 'course_id={}&page_size={}'.format(urllib.quote(unicode(course.id)), page_size) query_parameters = 'course_id={}&page_size={}'.format(urllib.quote(unicode(course.id)), page_size)
with self.assertNumQueries(7): # 2 queries for bookmark table. with self.assertNumQueries(9): # 2 queries for bookmark table.
response = self.send_get( response = self.send_get(
client=self.client, client=self.client,
url=reverse('bookmarks'), url=reverse('bookmarks'),
...@@ -171,7 +171,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase): ...@@ -171,7 +171,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase):
Test that requesting bookmarks with invalid data returns 0 records. Test that requesting bookmarks with invalid data returns 0 records.
""" """
# Invalid course id. # Invalid course id.
with self.assertNumQueries(5): # No queries for bookmark table. with self.assertNumQueries(7): # No queries for bookmark table.
response = self.send_get( response = self.send_get(
client=self.client, client=self.client,
url=reverse('bookmarks'), url=reverse('bookmarks'),
...@@ -189,7 +189,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase): ...@@ -189,7 +189,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase):
""" """
# Without course id we would return all the bookmarks for that user. # Without course id we would return all the bookmarks for that user.
with self.assertNumQueries(7): # 2 queries for bookmark table. with self.assertNumQueries(9): # 2 queries for bookmark table.
response = self.send_get( response = self.send_get(
client=self.client, client=self.client,
url=reverse('bookmarks') url=reverse('bookmarks')
...@@ -214,7 +214,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase): ...@@ -214,7 +214,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase):
Test that an anonymous client (not logged in) cannot call GET or POST. Test that an anonymous client (not logged in) cannot call GET or POST.
""" """
query_parameters = 'course_id={}'.format(self.course_id) query_parameters = 'course_id={}'.format(self.course_id)
with self.assertNumQueries(1): # No queries for bookmark table. with self.assertNumQueries(4): # No queries for bookmark table.
self.send_get( self.send_get(
client=self.anonymous_client, client=self.anonymous_client,
url=reverse('bookmarks'), url=reverse('bookmarks'),
...@@ -222,7 +222,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase): ...@@ -222,7 +222,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase):
expected_status=401 expected_status=401
) )
with self.assertNumQueries(1): # No queries for bookmark table. with self.assertNumQueries(4): # No queries for bookmark table.
self.send_post( self.send_post(
client=self.anonymous_client, client=self.anonymous_client,
url=reverse('bookmarks'), url=reverse('bookmarks'),
...@@ -234,7 +234,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase): ...@@ -234,7 +234,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase):
""" """
Test that posting a bookmark successfully returns newly created data with 201 code. Test that posting a bookmark successfully returns newly created data with 201 code.
""" """
with self.assertNumQueries(9): with self.assertNumQueries(15):
response = self.send_post( response = self.send_post(
client=self.client, client=self.client,
url=reverse('bookmarks'), url=reverse('bookmarks'),
...@@ -255,10 +255,10 @@ class BookmarksListViewTests(BookmarksViewsTestsBase): ...@@ -255,10 +255,10 @@ class BookmarksListViewTests(BookmarksViewsTestsBase):
Scenarios: Scenarios:
1) Invalid usage id. 1) Invalid usage id.
2) Without usage id. 2) Without usage id.
3) With empty request.DATA 3) With empty request.data
""" """
# Send usage_id with invalid format. # Send usage_id with invalid format.
with self.assertNumQueries(5): # No queries for bookmark table. with self.assertNumQueries(7): # No queries for bookmark table.
response = self.send_post( response = self.send_post(
client=self.client, client=self.client,
url=reverse('bookmarks'), url=reverse('bookmarks'),
...@@ -268,7 +268,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase): ...@@ -268,7 +268,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase):
self.assertEqual(response.data['user_message'], u'Invalid usage_id: invalid.') self.assertEqual(response.data['user_message'], u'Invalid usage_id: invalid.')
# Send data without usage_id. # Send data without usage_id.
with self.assertNumQueries(4): # No queries for bookmark table. with self.assertNumQueries(7): # No queries for bookmark table.
response = self.send_post( response = self.send_post(
client=self.client, client=self.client,
url=reverse('bookmarks'), url=reverse('bookmarks'),
...@@ -279,7 +279,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase): ...@@ -279,7 +279,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase):
self.assertEqual(response.data['developer_message'], u'Parameter usage_id not provided.') self.assertEqual(response.data['developer_message'], u'Parameter usage_id not provided.')
# Send empty data dictionary. # Send empty data dictionary.
with self.assertNumQueries(4): # No queries for bookmark table. with self.assertNumQueries(7): # No queries for bookmark table.
response = self.send_post( response = self.send_post(
client=self.client, client=self.client,
url=reverse('bookmarks'), url=reverse('bookmarks'),
...@@ -293,7 +293,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase): ...@@ -293,7 +293,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase):
""" """
Test that posting a bookmark for a block that does not exist returns a 400. Test that posting a bookmark for a block that does not exist returns a 400.
""" """
with self.assertNumQueries(5): # No queries for bookmark table. with self.assertNumQueries(7): # No queries for bookmark table.
response = self.send_post( response = self.send_post(
client=self.client, client=self.client,
url=reverse('bookmarks'), url=reverse('bookmarks'),
...@@ -322,7 +322,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase): ...@@ -322,7 +322,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase):
@ddt.data( @ddt.data(
{'page_size': -1, 'expected_bookmarks_count': 2, 'expected_page_size': 10, 'expected_page_number': 1}, {'page_size': -1, 'expected_bookmarks_count': 2, 'expected_page_size': 10, 'expected_page_number': 1},
{'page_size': 0, 'expected_bookmarks_count': 2, 'expected_page_size': 10, 'expected_page_number': 1}, {'page_size': 0, 'expected_bookmarks_count': 2, 'expected_page_size': 10, 'expected_page_number': 1},
{'page_size': 999, 'expected_bookmarks_count': 2, 'expected_page_size': 500, 'expected_page_number': 1} {'page_size': 999, 'expected_bookmarks_count': 2, 'expected_page_size': 100, 'expected_page_number': 1}
) )
def test_listed_event_for_different_page_size_values(self, mock_tracker, page_size, expected_bookmarks_count, def test_listed_event_for_different_page_size_values(self, mock_tracker, page_size, expected_bookmarks_count,
expected_page_size, expected_page_number): expected_page_size, expected_page_number):
...@@ -371,7 +371,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase): ...@@ -371,7 +371,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase):
""" """
Test that requesting bookmark returns data with 200 code. Test that requesting bookmark returns data with 200 code.
""" """
with self.assertNumQueries(6): # 1 query for bookmark table. with self.assertNumQueries(8): # 1 query for bookmark table.
response = self.send_get( response = self.send_get(
client=self.client, client=self.client,
url=reverse( url=reverse(
...@@ -388,7 +388,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase): ...@@ -388,7 +388,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase):
""" """
Test that requesting bookmark that belongs to other user returns 404 status code. Test that requesting bookmark that belongs to other user returns 404 status code.
""" """
with self.assertNumQueries(5): # No queries for bookmark table. with self.assertNumQueries(8): # No queries for bookmark table.
self.send_get( self.send_get(
client=self.client, client=self.client,
url=reverse( url=reverse(
...@@ -402,7 +402,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase): ...@@ -402,7 +402,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase):
""" """
Test that requesting bookmark that does not exist returns 404 status code. Test that requesting bookmark that does not exist returns 404 status code.
""" """
with self.assertNumQueries(6): # 1 query for bookmark table. with self.assertNumQueries(8): # 1 query for bookmark table.
response = self.send_get( response = self.send_get(
client=self.client, client=self.client,
url=reverse( url=reverse(
...@@ -424,7 +424,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase): ...@@ -424,7 +424,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase):
""" """
Test that requesting bookmark with invalid usage id returns 400. Test that requesting bookmark with invalid usage id returns 400.
""" """
with self.assertNumQueries(5): # No queries for bookmark table. with self.assertNumQueries(7): # No queries for bookmark table.
response = self.send_get( response = self.send_get(
client=self.client, client=self.client,
url=reverse( url=reverse(
...@@ -440,14 +440,14 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase): ...@@ -440,14 +440,14 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase):
Test that an anonymous client (not logged in) cannot call GET or DELETE. Test that an anonymous client (not logged in) cannot call GET or DELETE.
""" """
url = reverse('bookmarks_detail', kwargs={'username': self.user.username, 'usage_id': 'i4x'}) url = reverse('bookmarks_detail', kwargs={'username': self.user.username, 'usage_id': 'i4x'})
with self.assertNumQueries(4): # No queries for bookmark table. with self.assertNumQueries(7): # No queries for bookmark table.
self.send_get( self.send_get(
client=self.anonymous_client, client=self.anonymous_client,
url=url, url=url,
expected_status=401 expected_status=401
) )
with self.assertNumQueries(1): with self.assertNumQueries(4):
self.send_delete( self.send_delete(
client=self.anonymous_client, client=self.anonymous_client,
url=url, url=url,
...@@ -463,7 +463,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase): ...@@ -463,7 +463,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase):
bookmarks_data = response.data['results'] bookmarks_data = response.data['results']
self.assertEqual(len(bookmarks_data), 2) self.assertEqual(len(bookmarks_data), 2)
with self.assertNumQueries(7): # 2 queries for bookmark table. with self.assertNumQueries(10): # 2 queries for bookmark table.
self.send_delete( self.send_delete(
client=self.client, client=self.client,
url=reverse( url=reverse(
...@@ -480,7 +480,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase): ...@@ -480,7 +480,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase):
""" """
Test that delete bookmark that belongs to other user returns 404. Test that delete bookmark that belongs to other user returns 404.
""" """
with self.assertNumQueries(5): # No queries for bookmark table. with self.assertNumQueries(8): # No queries for bookmark table.
self.send_delete( self.send_delete(
client=self.client, client=self.client,
url=reverse( url=reverse(
...@@ -494,7 +494,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase): ...@@ -494,7 +494,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase):
""" """
Test that delete bookmark that does not exist returns 404. Test that delete bookmark that does not exist returns 404.
""" """
with self.assertNumQueries(6): # 1 query for bookmark table. with self.assertNumQueries(8): # 1 query for bookmark table.
response = self.send_delete( response = self.send_delete(
client=self.client, client=self.client,
url=reverse( url=reverse(
...@@ -516,7 +516,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase): ...@@ -516,7 +516,7 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase):
""" """
Test that delete bookmark with invalid usage id returns 400. Test that delete bookmark with invalid usage id returns 400.
""" """
with self.assertNumQueries(5): # No queries for bookmark table. with self.assertNumQueries(7): # No queries for bookmark table.
response = self.send_delete( response = self.send_delete(
client=self.client, client=self.client,
url=reverse( url=reverse(
...@@ -533,8 +533,8 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase): ...@@ -533,8 +533,8 @@ class BookmarksDetailViewTests(BookmarksViewsTestsBase):
""" """
url = reverse('bookmarks_detail', kwargs={'username': self.user.username, 'usage_id': 'i4x'}) url = reverse('bookmarks_detail', kwargs={'username': self.user.username, 'usage_id': 'i4x'})
self.client.login(username=self.user.username, password=self.TEST_PASSWORD) self.client.login(username=self.user.username, password=self.TEST_PASSWORD)
with self.assertNumQueries(5): # No queries for bookmark table. with self.assertNumQueries(8): # No queries for bookmark table.
self.assertEqual(405, self.client.put(url).status_code) self.assertEqual(405, self.client.put(url).status_code)
with self.assertNumQueries(4): with self.assertNumQueries(8):
self.assertEqual(405, self.client.post(url).status_code) self.assertEqual(405, self.client.post(url).status_code)
...@@ -12,20 +12,21 @@ from django.utils.translation import ugettext as _, ugettext_noop ...@@ -12,20 +12,21 @@ from django.utils.translation import ugettext as _, ugettext_noop
from rest_framework import status from rest_framework import status
from rest_framework import permissions from rest_framework import permissions
from rest_framework.authentication import OAuth2Authentication, SessionAuthentication from rest_framework.authentication import SessionAuthentication
from rest_framework.generics import ListCreateAPIView from rest_framework.generics import ListCreateAPIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework_oauth.authentication import OAuth2Authentication
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.keys import CourseKey, UsageKey
from openedx.core.lib.api.permissions import IsUserInUrl from openedx.core.lib.api.permissions import IsUserInUrl
from openedx.core.lib.api.serializers import PaginationSerializer
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from lms.djangoapps.lms_xblock.runtime import unquote_slashes from lms.djangoapps.lms_xblock.runtime import unquote_slashes
from openedx.core.lib.api.paginators import DefaultPagination
from . import DEFAULT_FIELDS, OPTIONAL_FIELDS, api from . import DEFAULT_FIELDS, OPTIONAL_FIELDS, api
from .serializers import BookmarkSerializer from .serializers import BookmarkSerializer
...@@ -33,6 +34,28 @@ from .serializers import BookmarkSerializer ...@@ -33,6 +34,28 @@ from .serializers import BookmarkSerializer
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class BookmarksPagination(DefaultPagination):
"""
Paginator for bookmarks API.
"""
page_size = 10
max_page_size = 100
def get_paginated_response(self, data):
"""
Annotate the response with pagination information.
"""
response = super(BookmarksPagination, self).get_paginated_response(data)
# Add `current_page` value, it's needed for pagination footer.
response.data["current_page"] = self.page.number
# Add `start` value, it's needed for the pagination header.
response.data["start"] = (self.page.number - 1) * self.get_page_size(self.request)
return response
class BookmarksViewMixin(object): class BookmarksViewMixin(object):
""" """
Shared code for bookmarks views. Shared code for bookmarks views.
...@@ -129,12 +152,8 @@ class BookmarksListView(ListCreateAPIView, BookmarksViewMixin): ...@@ -129,12 +152,8 @@ class BookmarksListView(ListCreateAPIView, BookmarksViewMixin):
""" """
authentication_classes = (OAuth2Authentication, SessionAuthentication) authentication_classes = (OAuth2Authentication, SessionAuthentication)
pagination_class = BookmarksPagination
permission_classes = (permissions.IsAuthenticated,) permission_classes = (permissions.IsAuthenticated,)
paginate_by = 10
max_paginate_by = 500
paginate_by_param = 'page_size'
pagination_serializer_class = PaginationSerializer
serializer_class = BookmarkSerializer serializer_class = BookmarkSerializer
def get_serializer_context(self): def get_serializer_context(self):
...@@ -143,7 +162,7 @@ class BookmarksListView(ListCreateAPIView, BookmarksViewMixin): ...@@ -143,7 +162,7 @@ class BookmarksListView(ListCreateAPIView, BookmarksViewMixin):
""" """
context = super(BookmarksListView, self).get_serializer_context() context = super(BookmarksListView, self).get_serializer_context()
if self.request.method == 'GET': if self.request.method == 'GET':
context['fields'] = self.fields_to_return(self.request.QUERY_PARAMS) context['fields'] = self.fields_to_return(self.request.query_params)
return context return context
def get_queryset(self): def get_queryset(self):
...@@ -154,7 +173,7 @@ class BookmarksListView(ListCreateAPIView, BookmarksViewMixin): ...@@ -154,7 +173,7 @@ class BookmarksListView(ListCreateAPIView, BookmarksViewMixin):
If the course_id is specified in the request parameters, If the course_id is specified in the request parameters,
the queryset will only include bookmarks from that course. the queryset will only include bookmarks from that course.
""" """
course_id = self.request.QUERY_PARAMS.get('course_id', None) course_id = self.request.query_params.get('course_id', None)
if course_id: if course_id:
try: try:
...@@ -167,14 +186,14 @@ class BookmarksListView(ListCreateAPIView, BookmarksViewMixin): ...@@ -167,14 +186,14 @@ class BookmarksListView(ListCreateAPIView, BookmarksViewMixin):
return api.get_bookmarks( return api.get_bookmarks(
user=self.request.user, course_key=course_key, user=self.request.user, course_key=course_key,
fields=self.fields_to_return(self.request.QUERY_PARAMS), serialized=False fields=self.fields_to_return(self.request.query_params), serialized=False
) )
def paginate_queryset(self, queryset, page_size=None): def paginate_queryset(self, queryset):
""" Override GenericAPIView.paginate_queryset for the purpose of eventing """ """ Override GenericAPIView.paginate_queryset for the purpose of eventing """
page = super(BookmarksListView, self).paginate_queryset(queryset, page_size) page = super(BookmarksListView, self).paginate_queryset(queryset)
course_id = self.request.QUERY_PARAMS.get('course_id') course_id = self.request.query_params.get('course_id')
if course_id: if course_id:
try: try:
CourseKey.from_string(course_id) CourseKey.from_string(course_id)
...@@ -183,9 +202,9 @@ class BookmarksListView(ListCreateAPIView, BookmarksViewMixin): ...@@ -183,9 +202,9 @@ class BookmarksListView(ListCreateAPIView, BookmarksViewMixin):
event_data = { event_data = {
'list_type': 'all_courses', 'list_type': 'all_courses',
'bookmarks_count': page.paginator.count, 'bookmarks_count': self.paginator.page.paginator.count,
'page_size': self.get_paginate_by(), 'page_size': self.paginator.page.paginator.per_page,
'page_number': page.number, 'page_number': self.paginator.page.number,
} }
if course_id is not None: if course_id is not None:
event_data['list_type'] = 'per_course' event_data['list_type'] = 'per_course'
...@@ -200,10 +219,10 @@ class BookmarksListView(ListCreateAPIView, BookmarksViewMixin): ...@@ -200,10 +219,10 @@ class BookmarksListView(ListCreateAPIView, BookmarksViewMixin):
POST /api/bookmarks/v1/bookmarks/ POST /api/bookmarks/v1/bookmarks/
Request data: {"usage_id": "<usage-id>"} Request data: {"usage_id": "<usage-id>"}
""" """
if not request.DATA: if not request.data:
return self.error_response(ugettext_noop(u'No data provided.')) return self.error_response(ugettext_noop(u'No data provided.'))
usage_id = request.DATA.get('usage_id', None) usage_id = request.data.get('usage_id', None)
if not usage_id: if not usage_id:
return self.error_response(ugettext_noop(u'Parameter usage_id not provided.')) return self.error_response(ugettext_noop(u'Parameter usage_id not provided.'))
...@@ -297,7 +316,7 @@ class BookmarksDetailView(APIView, BookmarksViewMixin): ...@@ -297,7 +316,7 @@ class BookmarksDetailView(APIView, BookmarksViewMixin):
bookmark_data = api.get_bookmark( bookmark_data = api.get_bookmark(
user=request.user, user=request.user,
usage_key=usage_key_or_response, usage_key=usage_key_or_response,
fields=self.fields_to_return(request.QUERY_PARAMS) fields=self.fields_to_return(request.query_params)
) )
except ObjectDoesNotExist: except ObjectDoesNotExist:
error_message = ugettext_noop( error_message = ugettext_noop(
......
...@@ -6,33 +6,18 @@ import logging ...@@ -6,33 +6,18 @@ import logging
from django.conf import settings from django.conf import settings
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
import pytz import pytz
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider, CreditEligibility, CreditRequest from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider, CreditEligibility, CreditRequest
from openedx.core.djangoapps.credit.signature import get_shared_secret_key, signature from openedx.core.djangoapps.credit.signature import get_shared_secret_key, signature
from openedx.core.lib.api.serializers import CourseKeyField
from util.date_utils import from_timestamp from util.date_utils import from_timestamp
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class CourseKeyField(serializers.Field):
""" Serializer field for a model CourseKey field. """
def to_representation(self, data):
"""Convert a course key to unicode. """
return unicode(data)
def to_internal_value(self, data):
"""Convert unicode to a course key. """
try:
return CourseKey.from_string(data)
except InvalidKeyError as ex:
raise serializers.ValidationError("Invalid course key: {msg}".format(msg=ex.msg))
class CreditCourseSerializer(serializers.ModelSerializer): class CreditCourseSerializer(serializers.ModelSerializer):
""" CreditCourse Serializer """ """ CreditCourse Serializer """
......
...@@ -8,8 +8,6 @@ from ..profile_images.views import ProfileImageView ...@@ -8,8 +8,6 @@ from ..profile_images.views import ProfileImageView
from .accounts.views import AccountView from .accounts.views import AccountView
from .preferences.views import PreferencesView, PreferencesDetailView from .preferences.views import PreferencesView, PreferencesDetailView
from django.conf.urls import patterns, url
USERNAME_PATTERN = r'(?P<username>[\w.+-]+)' USERNAME_PATTERN = r'(?P<username>[\w.+-]+)'
urlpatterns = patterns( urlpatterns = patterns(
......
...@@ -3,6 +3,8 @@ Serializers to be used in APIs. ...@@ -3,6 +3,8 @@ Serializers to be used in APIs.
""" """
from rest_framework import serializers from rest_framework import serializers
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
class CollapsedReferenceSerializer(serializers.HyperlinkedModelSerializer): class CollapsedReferenceSerializer(serializers.HyperlinkedModelSerializer):
...@@ -37,3 +39,33 @@ class CollapsedReferenceSerializer(serializers.HyperlinkedModelSerializer): ...@@ -37,3 +39,33 @@ class CollapsedReferenceSerializer(serializers.HyperlinkedModelSerializer):
class Meta(object): class Meta(object):
fields = ("url",) fields = ("url",)
class CourseKeyField(serializers.Field):
""" Serializer field for a model CourseKey field. """
def to_representation(self, data):
"""Convert a course key to unicode. """
return unicode(data)
def to_internal_value(self, data):
"""Convert unicode to a course key. """
try:
return CourseKey.from_string(data)
except InvalidKeyError as ex:
raise serializers.ValidationError("Invalid course key: {msg}".format(msg=ex.msg))
class UsageKeyField(serializers.Field):
""" Serializer field for a model UsageKey field. """
def to_representation(self, data):
"""Convert a usage key to unicode. """
return unicode(data)
def to_internal_value(self, data):
"""Convert unicode to a usage key. """
try:
return UsageKey.from_string(data)
except InvalidKeyError as ex:
raise serializers.ValidationError("Invalid usage key: {msg}".format(msg=ex.msg))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment