").append(x.parseHTML(e)).find(i):e)}).complete(r&&function(e,t){s.each(r,o||[e.responseText,t,e])}),this},x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){x.fn[t]=function(e){return this.on(t,e)}}),x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:yn,type:"GET",isLocal:Cn.test(mn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Dn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":x.parseJSON,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?_n(_n(e,x.ajaxSettings),t):_n(x.ajaxSettings,e)},ajaxPrefilter:Hn(An),ajaxTransport:Hn(jn),ajax:function(e,n){"object"==typeof e&&(n=e,e=t),n=n||{};var r,i,o,a,s,l,u,c,p=x.ajaxSetup({},n),f=p.context||p,d=p.context&&(f.nodeType||f.jquery)?x(f):x.event,h=x.Deferred(),g=x.Callbacks("once memory"),m=p.statusCode||{},y={},v={},b=0,w="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c){c={};while(t=Tn.exec(a))c[t[1].toLowerCase()]=t[2]}t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=v[n]=v[n]||e,y[e]=t),this},overrideMimeType:function(e){return b||(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)m[t]=[m[t],e[t]];else C.always(e[C.status]);return this},abort:function(e){var t=e||w;return u&&u.abort(t),k(0,t),this}};if(h.promise(C).complete=g.add,C.success=C.done,C.error=C.fail,p.url=((e||p.url||yn)+"").replace(xn,"").replace(kn,mn[1]+"//"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=x.trim(p.dataType||"*").toLowerCase().match(T)||[""],null==p.crossDomain&&(r=En.exec(p.url.toLowerCase()),p.crossDomain=!(!r||r[1]===mn[1]&&r[2]===mn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(mn[3]||("http:"===mn[1]?"80":"443")))),p.data&&p.processData&&"string"!=typeof p.data&&(p.data=x.param(p.data,p.traditional)),qn(An,p,n,C),2===b)return C;l=p.global,l&&0===x.active++&&x.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Nn.test(p.type),o=p.url,p.hasContent||(p.data&&(o=p.url+=(bn.test(o)?"&":"?")+p.data,delete p.data),p.cache===!1&&(p.url=wn.test(o)?o.replace(wn,"$1_="+vn++):o+(bn.test(o)?"&":"?")+"_="+vn++)),p.ifModified&&(x.lastModified[o]&&C.setRequestHeader("If-Modified-Since",x.lastModified[o]),x.etag[o]&&C.setRequestHeader("If-None-Match",x.etag[o])),(p.data&&p.hasContent&&p.contentType!==!1||n.contentType)&&C.setRequestHeader("Content-Type",p.contentType),C.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Dn+"; q=0.01":""):p.accepts["*"]);for(i in p.headers)C.setRequestHeader(i,p.headers[i]);if(p.beforeSend&&(p.beforeSend.call(f,C,p)===!1||2===b))return C.abort();w="abort";for(i in{success:1,error:1,complete:1})C[i](p[i]);if(u=qn(jn,p,n,C)){C.readyState=1,l&&d.trigger("ajaxSend",[C,p]),p.async&&p.timeout>0&&(s=setTimeout(function(){C.abort("timeout")},p.timeout));try{b=1,u.send(y,k)}catch(N){if(!(2>b))throw N;k(-1,N)}}else k(-1,"No Transport");function k(e,n,r,i){var c,y,v,w,T,N=n;2!==b&&(b=2,s&&clearTimeout(s),u=t,a=i||"",C.readyState=e>0?4:0,c=e>=200&&300>e||304===e,r&&(w=Mn(p,C,r)),w=On(p,w,C,c),c?(p.ifModified&&(T=C.getResponseHeader("Last-Modified"),T&&(x.lastModified[o]=T),T=C.getResponseHeader("etag"),T&&(x.etag[o]=T)),204===e||"HEAD"===p.type?N="nocontent":304===e?N="notmodified":(N=w.state,y=w.data,v=w.error,c=!v)):(v=N,(e||!N)&&(N="error",0>e&&(e=0))),C.status=e,C.statusText=(n||N)+"",c?h.resolveWith(f,[y,N,C]):h.rejectWith(f,[C,N,v]),C.statusCode(m),m=t,l&&d.trigger(c?"ajaxSuccess":"ajaxError",[C,p,c?y:v]),g.fireWith(f,[C,N]),l&&(d.trigger("ajaxComplete",[C,p]),--x.active||x.event.trigger("ajaxStop")))}return C},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,n){return x.get(e,t,n,"script")}}),x.each(["get","post"],function(e,n){x[n]=function(e,r,i,o){return x.isFunction(r)&&(o=o||i,i=r,r=t),x.ajax({url:e,type:n,dataType:o,data:r,success:i})}});function Mn(e,n,r){var i,o,a,s,l=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),o===t&&(o=e.mimeType||n.getResponseHeader("Content-Type"));if(o)for(s in l)if(l[s]&&l[s].test(o)){u.unshift(s);break}if(u[0]in r)a=u[0];else{for(s in r){if(!u[0]||e.converters[s+" "+u[0]]){a=s;break}i||(i=s)}a=a||i}return a?(a!==u[0]&&u.unshift(a),r[a]):t}function On(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(p){return{state:"parsererror",error:a?p:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),x.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=a.head||x("head")[0]||a.documentElement;return{send:function(t,i){n=a.createElement("script"),n.async=!0,e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,t){(t||!n.readyState||/loaded|complete/.test(n.readyState))&&(n.onload=n.onreadystatechange=null,n.parentNode&&n.parentNode.removeChild(n),n=null,t||i(200,"success"))},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(t,!0)}}}});var Fn=[],Bn=/(=)\?(?=&|$)|\?\?/;x.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Fn.pop()||x.expando+"_"+vn++;return this[e]=!0,e}}),x.ajaxPrefilter("json jsonp",function(n,r,i){var o,a,s,l=n.jsonp!==!1&&(Bn.test(n.url)?"url":"string"==typeof n.data&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Bn.test(n.data)&&"data");return l||"jsonp"===n.dataTypes[0]?(o=n.jsonpCallback=x.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,l?n[l]=n[l].replace(Bn,"$1"+o):n.jsonp!==!1&&(n.url+=(bn.test(n.url)?"&":"?")+n.jsonp+"="+o),n.converters["script json"]=function(){return s||x.error(o+" was not called"),s[0]},n.dataTypes[0]="json",a=e[o],e[o]=function(){s=arguments},i.always(function(){e[o]=a,n[o]&&(n.jsonpCallback=r.jsonpCallback,Fn.push(o)),s&&x.isFunction(a)&&a(s[0]),s=a=t}),"script"):t});var Pn,Rn,Wn=0,$n=e.ActiveXObject&&function(){var e;for(e in Pn)Pn[e](t,!0)};function In(){try{return new e.XMLHttpRequest}catch(t){}}function zn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}x.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&In()||zn()}:In,Rn=x.ajaxSettings.xhr(),x.support.cors=!!Rn&&"withCredentials"in Rn,Rn=x.support.ajax=!!Rn,Rn&&x.ajaxTransport(function(n){if(!n.crossDomain||x.support.cors){var r;return{send:function(i,o){var a,s,l=n.xhr();if(n.username?l.open(n.type,n.url,n.async,n.username,n.password):l.open(n.type,n.url,n.async),n.xhrFields)for(s in n.xhrFields)l[s]=n.xhrFields[s];n.mimeType&&l.overrideMimeType&&l.overrideMimeType(n.mimeType),n.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");try{for(s in i)l.setRequestHeader(s,i[s])}catch(u){}l.send(n.hasContent&&n.data||null),r=function(e,i){var s,u,c,p;try{if(r&&(i||4===l.readyState))if(r=t,a&&(l.onreadystatechange=x.noop,$n&&delete Pn[a]),i)4!==l.readyState&&l.abort();else{p={},s=l.status,u=l.getAllResponseHeaders(),"string"==typeof l.responseText&&(p.text=l.responseText);try{c=l.statusText}catch(f){c=""}s||!n.isLocal||n.crossDomain?1223===s&&(s=204):s=p.text?200:404}}catch(d){i||o(-1,d)}p&&o(s,c,p,u)},n.async?4===l.readyState?setTimeout(r):(a=++Wn,$n&&(Pn||(Pn={},x(e).unload($n)),Pn[a]=r),l.onreadystatechange=r):r()},abort:function(){r&&r(t,!0)}}}});var Xn,Un,Vn=/^(?:toggle|show|hide)$/,Yn=RegExp("^(?:([+-])=|)("+w+")([a-z%]*)$","i"),Jn=/queueHooks$/,Gn=[nr],Qn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=Yn.exec(t),o=i&&i[3]||(x.cssNumber[e]?"":"px"),a=(x.cssNumber[e]||"px"!==o&&+r)&&Yn.exec(x.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,x.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};function Kn(){return setTimeout(function(){Xn=t}),Xn=x.now()}function Zn(e,t,n){var r,i=(Qn[t]||[]).concat(Qn["*"]),o=0,a=i.length;for(;a>o;o++)if(r=i[o].call(n,t,e))return r}function er(e,t,n){var r,i,o=0,a=Gn.length,s=x.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;var t=Xn||Kn(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;for(;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:x.extend({},t),opts:x.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:Xn||Kn(),duration:n.duration,tweens:[],createTween:function(t,n){var r=x.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(tr(c,u.opts.specialEasing);a>o;o++)if(r=Gn[o].call(u,e,c,u.opts))return r;return x.map(c,Zn,u),x.isFunction(u.opts.start)&&u.opts.start.call(e,u),x.fx.timer(x.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function tr(e,t){var n,r,i,o,a;for(n in e)if(r=x.camelCase(n),i=t[r],o=e[n],x.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=x.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}x.Animation=x.extend(er,{tweener:function(e,t){x.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;i>r;r++)n=e[r],Qn[n]=Qn[n]||[],Qn[n].unshift(t)},prefilter:function(e,t){t?Gn.unshift(e):Gn.push(e)}});function nr(e,t,n){var r,i,o,a,s,l,u=this,c={},p=e.style,f=e.nodeType&&nn(e),d=x._data(e,"fxshow");n.queue||(s=x._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,u.always(function(){u.always(function(){s.unqueued--,x.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],"inline"===x.css(e,"display")&&"none"===x.css(e,"float")&&(x.support.inlineBlockNeedsLayout&&"inline"!==ln(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",x.support.shrinkWrapBlocks||u.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],Vn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(f?"hide":"show"))continue;c[r]=d&&d[r]||x.style(e,r)}if(!x.isEmptyObject(c)){d?"hidden"in d&&(f=d.hidden):d=x._data(e,"fxshow",{}),o&&(d.hidden=!f),f?x(e).show():u.done(function(){x(e).hide()}),u.done(function(){var t;x._removeData(e,"fxshow");for(t in c)x.style(e,t,c[t])});for(r in c)a=Zn(f?d[r]:0,r,u),r in d||(d[r]=a.start,f&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function rr(e,t,n,r,i){return new rr.prototype.init(e,t,n,r,i)}x.Tween=rr,rr.prototype={constructor:rr,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(x.cssNumber[n]?"":"px")},cur:function(){var e=rr.propHooks[this.prop];return e&&e.get?e.get(this):rr.propHooks._default.get(this)},run:function(e){var t,n=rr.propHooks[this.prop];return this.pos=t=this.options.duration?x.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):rr.propHooks._default.set(this),this}},rr.prototype.init.prototype=rr.prototype,rr.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=x.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){x.fx.step[e.prop]?x.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[x.cssProps[e.prop]]||x.cssHooks[e.prop])?x.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},rr.propHooks.scrollTop=rr.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},x.each(["toggle","show","hide"],function(e,t){var n=x.fn[t];x.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ir(t,!0),e,r,i)}}),x.fn.extend({fadeTo:function(e,t,n,r){return this.filter(nn).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=x.isEmptyObject(e),o=x.speed(t,n,r),a=function(){var t=er(this,x.extend({},e),o);(i||x._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return"string"!=typeof e&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=null!=e&&e+"queueHooks",o=x.timers,a=x._data(this);if(n)a[n]&&a[n].stop&&i(a[n]);else for(n in a)a[n]&&a[n].stop&&Jn.test(n)&&i(a[n]);for(n=o.length;n--;)o[n].elem!==this||null!=e&&o[n].queue!==e||(o[n].anim.stop(r),t=!1,o.splice(n,1));(t||!r)&&x.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=x._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=x.timers,a=r?r.length:0;for(n.finish=!0,x.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}});function ir(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=Zt[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}x.each({slideDown:ir("show"),slideUp:ir("hide"),slideToggle:ir("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){x.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),x.speed=function(e,t,n){var r=e&&"object"==typeof e?x.extend({},e):{complete:n||!n&&t||x.isFunction(e)&&e,duration:e,easing:n&&t||t&&!x.isFunction(t)&&t};return r.duration=x.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in x.fx.speeds?x.fx.speeds[r.duration]:x.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){x.isFunction(r.old)&&r.old.call(this),r.queue&&x.dequeue(this,r.queue)},r},x.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},x.timers=[],x.fx=rr.prototype.init,x.fx.tick=function(){var e,n=x.timers,r=0;for(Xn=x.now();n.length>r;r++)e=n[r],e()||n[r]!==e||n.splice(r--,1);n.length||x.fx.stop(),Xn=t},x.fx.timer=function(e){e()&&x.timers.push(e)&&x.fx.start()},x.fx.interval=13,x.fx.start=function(){Un||(Un=setInterval(x.fx.tick,x.fx.interval))},x.fx.stop=function(){clearInterval(Un),Un=null},x.fx.speeds={slow:600,fast:200,_default:400},x.fx.step={},x.expr&&x.expr.filters&&(x.expr.filters.animated=function(e){return x.grep(x.timers,function(t){return e===t.elem}).length}),x.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){x.offset.setOffset(this,e,t)});var n,r,o={top:0,left:0},a=this[0],s=a&&a.ownerDocument;if(s)return n=s.documentElement,x.contains(n,a)?(typeof a.getBoundingClientRect!==i&&(o=a.getBoundingClientRect()),r=or(s),{top:o.top+(r.pageYOffset||n.scrollTop)-(n.clientTop||0),left:o.left+(r.pageXOffset||n.scrollLeft)-(n.clientLeft||0)}):o},x.offset={setOffset:function(e,t,n){var r=x.css(e,"position");"static"===r&&(e.style.position="relative");var i=x(e),o=i.offset(),a=x.css(e,"top"),s=x.css(e,"left"),l=("absolute"===r||"fixed"===r)&&x.inArray("auto",[a,s])>-1,u={},c={},p,f;l?(c=i.position(),p=c.top,f=c.left):(p=parseFloat(a)||0,f=parseFloat(s)||0),x.isFunction(t)&&(t=t.call(e,n,o)),null!=t.top&&(u.top=t.top-o.top+p),null!=t.left&&(u.left=t.left-o.left+f),"using"in t?t.using.call(e,u):i.css(u)}},x.fn.extend({position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===x.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),x.nodeName(e[0],"html")||(n=e.offset()),n.top+=x.css(e[0],"borderTopWidth",!0),n.left+=x.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-x.css(r,"marginTop",!0),left:t.left-n.left-x.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||s;while(e&&!x.nodeName(e,"html")&&"static"===x.css(e,"position"))e=e.offsetParent;return e||s})}}),x.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);x.fn[e]=function(i){return x.access(this,function(e,i,o){var a=or(e);return o===t?a?n in a?a[n]:a.document.documentElement[i]:e[i]:(a?a.scrollTo(r?x(a).scrollLeft():o,r?o:x(a).scrollTop()):e[i]=o,t)},e,i,arguments.length,null)}});function or(e){return x.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}x.each({Height:"height",Width:"width"},function(e,n){x.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){x.fn[i]=function(i,o){var a=arguments.length&&(r||"boolean"!=typeof i),s=r||(i===!0||o===!0?"margin":"border");return x.access(this,function(n,r,i){var o;return x.isWindow(n)?n.document.documentElement["client"+e]:9===n.nodeType?(o=n.documentElement,Math.max(n.body["scroll"+e],o["scroll"+e],n.body["offset"+e],o["offset"+e],o["client"+e])):i===t?x.css(n,r,s):x.style(n,r,i,s)},n,a?i:t,a,null)}})}),x.fn.size=function(){return this.length},x.fn.andSelf=x.fn.addBack,"object"==typeof module&&module&&"object"==typeof module.exports?module.exports=x:(e.jQuery=e.$=x,"function"==typeof define&&define.amd&&define("jquery",[],function(){return x}))})(window);
diff --git a/vendor-local/lib/python/django_browserid/static/browserid/persona-buttons.css b/vendor-local/lib/python/django_browserid/static/browserid/persona-buttons.css
new file mode 100755
index 000000000..cd7e387b7
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/static/browserid/persona-buttons.css
@@ -0,0 +1,228 @@
+/* Link body */
+.persona-button{
+ color: #fff;
+ display: inline-block;
+ font-size: 14px;
+ font-family: Helvetica, Arial, sans-serif;
+ font-weight: bold;
+ line-height: 1.1;
+ overflow: hidden;
+ position: relative;
+ text-decoration: none;
+ text-shadow: 0 1px rgba(0,0,0,0.5), 0 0 2px rgba(0,0,0,0.2);
+
+ background: #297dc3;
+ background: -moz-linear-gradient(top, #43a6e2, #287cc2);
+ background: -ms-linear-gradient(top, #43a6e2, #287cc2);
+ background: -o-linear-gradient(top, #43a6e2, #287cc2);
+ background: -webkit-linear-gradient(top, #43a6e2, #287cc2);
+ background: linear-gradient(top, #43a6e2, #287cc2);
+
+ -moz-border-radius: 3px;
+ -ms-border-radius: 3px;
+ -o-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ border-radius: 3px;
+
+ -moz-box-shadow: 0 1px 0 rgba(0,0,0,0.2);
+ -ms-box-shadow: 0 1px 0 rgba(0,0,0,0.2);
+ -o-box-shadow: 0 1px 0 rgba(0,0,0,0.2);
+ -webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.2);
+ box-shadow: 0 1px 0 rgba(0,0,0,0.2);
+}
+
+.persona-button:hover{
+ background: #21669f;
+ background: -moz-linear-gradient(top, #3788b9, #21669f);
+ background: -ms-linear-gradient(top, #3788b9, #21669f);
+ background: -o-linear-gradient(top, #3788b9, #21669f);
+ background: -webkit-linear-gradient(top, #3788b9, #21669f);
+ background: linear-gradient(top, #3788b9, #21669f);
+}
+
+.persona-button:active, .persona-button:focus{
+ top: 1px;
+ -moz-box-shadow: none;
+ -ms-box-shadow: none;
+ -o-box-shadow: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
+.persona-button span{
+ display: inline-block;
+ padding: 5px 10px 5px 40px;
+}
+
+/* Icon */
+.persona-button span:after{
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAPCAYAAAA/I0V3AAAA4klEQVR42o2RWaqEMBRE3YaCiDjPwQGcd9CrysLv4wTyoLFD90dxqbp1EgdPRB7Kskznea6Zn/aPoKoqUUrJOI5m4l2QBfSyLHKep1zXZSae3An1fS/7vst931bGkzuhaZrsLVbGkzuheZ7lOI6HyJ2QUkqv6yrbtv0LT+6E7G0UrfBfP3lZlpoXH4ZBmHgn5Pv+KwxDfqp0XQdgJp6c/RsUBIGOokiSJDE/s21bACbe5Ozp0TdAHMdSFIXUdS1N01C2wpObPT36HifwCJzI0iX29Oh7XP0E3CB9L01TzM+i/wePv4ZE5RtAngAAAABJRU5ErkJggg==) 10px center no-repeat;
+ content: '';
+ display: block;
+ width: 31px;
+
+ position: absolute;
+ bottom: 0;
+ left: -3px;
+ top: 0;
+ z-index: 10;
+}
+
+/* Icon background */
+.persona-button span:before{
+ content: '';
+ display: block;
+ height: 100%;
+ width: 20px;
+
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ top: 0;
+ z-index: 1;
+
+ background: #42a9dd;
+ background: -moz-linear-gradient(top, #50b8e8, #3095ce);
+ background: -ms-linear-gradient(top, #50b8e8, #3095ce);
+ background: -o-linear-gradient(top, #50b8e8, #3095ce);
+ background: -webkit-linear-gradient(top, #50b8e8, #3095ce);
+ background: linear-gradient(top, #50b8e8, #3095ce);
+
+ -moz-border-radius: 3px 0 0 3px;
+ -ms-border-radius: 3px 0 0 3px;
+ -o-border-radius: 3px 0 0 3px;
+ -webkit-border-radius: 3px 0 0 3px;
+ border-radius: 3px 0 0 3px;
+}
+
+/* Triangle */
+.persona-button:before{
+ background: #42a9dd;
+ content: '';
+ display: block;
+ height: 26px;
+ width: 26px;
+
+ position: absolute;
+ left: 2px;
+ top: 50%;
+ margin-top: -13px;
+ z-index: 0;
+
+ background: -moz-linear-gradient(-45deg, #50b8e8, #3095ce);
+ background: -ms-linear-gradient(-45deg, #50b8e8, #3095ce);
+ background: -o-linear-gradient(-45deg, #50b8e8, #3095ce);
+ background: -webkit-linear-gradient(-45deg, #50b8e8, #3095ce);
+ background: linear-gradient(-45deg, #3095ce, #50b8e8); /* flipped for updated spec */
+
+ -moz-box-shadow: 1px -1px 1px rgba(0,0,0,0.1);
+ -ms-box-shadow: 1px -1px 1px rgba(0,0,0,0.1);
+ -o-box-shadow: 1px -1px 1px rgba(0,0,0,0.1);
+ -webkit-box-shadow: 1px -1px 1px rgba(0,0,0,0.1);
+ box-shadow: 1px -1px 1px rgba(0,0,0,0.1);
+
+ -moz-transform: rotate(45deg);
+ -ms-transform: rotate(45deg);
+ -o-transform: rotate(45deg);
+ -webkit-transform: rotate(45deg);
+ transform: rotate(45deg);
+}
+
+/* Inset shadow (required here because the icon background clips it when on the `a` element) */
+.persona-button:after{
+ content: '';
+ display: block;
+ height: 100%;
+ width: 100%;
+
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ z-index: 10;
+
+ -moz-border-radius: 3px;
+ -ms-border-radius: 3px;
+ -o-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ border-radius: 3px;
+
+ -moz-box-shadow: inset 0 -1px 0 rgba(0,0,0,0.3);
+ -ms-box-shadow: inset 0 -1px 0 rgba(0,0,0,0.3);
+ -o-box-shadow: inset 0 -1px 0 rgba(0,0,0,0.3);
+ -webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,0.3);
+ box-shadow: inset 0 -1px 0 rgba(0,0,0,0.3);
+}
+
+/* ========================================================
+ * Dark button
+ * ===================================================== */
+.persona-button.dark{
+ background: #3c3c3c;
+ background: -moz-linear-gradient(top, #606060, #3c3c3c);
+ background: -ms-linear-gradient(top, #606060, #3c3c3c);
+ background: -o-linear-gradient(top, #606060, #3c3c3c);
+ background: -webkit-linear-gradient(top, #606060, #3c3c3c);
+ background: linear-gradient(top, #606060, #3c3c3c);
+}
+.persona-button.dark:hover{
+ background: #2d2d2d;
+ background: -moz-linear-gradient(top, #484848, #2d2d2d);
+ background: -ms-linear-gradient(top, #484848, #2d2d2d);
+ background: -o-linear-gradient(top, #484848, #2d2d2d);
+ background: -webkit-linear-gradient(top, #484848, #2d2d2d);
+ background: linear-gradient(top, #484848, #2d2d2d);
+}
+.persona-button.dark span:before{ /* Icon BG */
+ background: #d34f2d;
+ background: -moz-linear-gradient(top, #ebac45, #d34f2d);
+ background: -ms-linear-gradient(top, #ebac45, #d34f2d);
+ background: -o-linear-gradient(top, #ebac45, #d34f2d);
+ background: -webkit-linear-gradient(top, #ebac45, #d34f2d);
+ background: linear-gradient(top, #ebac45, #d34f2d);
+}
+.persona-button.dark:before{ /* Triangle */
+ background: #d34f2d;
+ background: -moz-linear-gradient(-45deg, #ebac45, #d34f2d);
+ background: -ms-linear-gradient(-45deg, #ebac45, #d34f2d);
+ background: -o-linear-gradient(-45deg, #ebac45, #d34f2d);
+ background: -webkit-linear-gradient(-45deg, #ebac45, #d34f2d);
+ background: linear-gradient(-45deg, #d34f2d, #ebac45); /* flipped for updated spec */
+}
+
+/* ========================================================
+ * Orange button
+ * ===================================================== */
+.persona-button.orange{
+ background: #ee731a;
+ background: -moz-linear-gradient(top, #ee731a, #d03116);
+ background: -ms-linear-gradient(top, #ee731a, #d03116);
+ background: -o-linear-gradient(top, #ee731a, #d03116);
+ background: -webkit-linear-gradient(top, #ee731a, #d03116);
+ background: linear-gradient(top, #ee731a, #d03116);
+}
+.persona-button.orange:hover{
+ background: #cb6216;
+ background: -moz-linear-gradient(top, #cb6216, #b12a13);
+ background: -ms-linear-gradient(top, #cb6216, #b12a13);
+ background: -o-linear-gradient(top, #cb6216, #b12a13);
+ background: -webkit-linear-gradient(top, #cb6216, #b12a13);
+ background: linear-gradient(top, #cb6216, #b12a13);
+}
+.persona-button.orange span:before{ /* Icon BG */
+ background: #e84a21;
+ background: -moz-linear-gradient(top, #f7ad27, #e84a21);
+ background: -ms-linear-gradient(top, #f7ad27, #e84a21);
+ background: -o-linear-gradient(top, #f7ad27, #e84a21);
+ background: -webkit-linear-gradient(top, #f7ad27, #e84a21);
+ background: linear-gradient(top, #f7ad27, #e84a21);
+}
+.persona-button.orange:before{ /* Triangle */
+ background: #e84a21;
+ background: -moz-linear-gradient(-45deg, #f7ad27, #e84a21);
+ background: -ms-linear-gradient(-45deg, #f7ad27, #e84a21);
+ background: -o-linear-gradient(-45deg, #f7ad27, #e84a21);
+ background: -webkit-linear-gradient(-45deg, #f7ad27, #e84a21);
+ background: linear-gradient(-45deg, #e84a21, #f7ad27); /* flipped for updated spec */
+}
diff --git a/vendor-local/lib/python/django_browserid/templates/browserid/admin_login.html b/vendor-local/lib/python/django_browserid/templates/browserid/admin_login.html
new file mode 100644
index 000000000..3822b9318
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/templates/browserid/admin_login.html
@@ -0,0 +1,89 @@
+{% extends 'admin/login.html' %}
+{% load admin_static browserid %}
+
+{% comment %}
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+{% endcomment %}
+
+{% block extrastyle %}
+ {{ block.super }}
+ {% browserid_css %}
+
+{% endblock %}
+
+{% block content %}
+ {% browserid_info %}
+ {% if include_password_form %}
+ {{ block.super }}
+
+ or
+
+ {% endif %}
+
+
+ {% browserid_login text='Sign in with email' color='dark' next=app_path link_class='admin-browserid-login' %}
+
+
There was a problem signing you in. Please try again.
+
+
+
+
+
+{% endblock %}
diff --git a/vendor-local/lib/python/django_browserid/templates/browserid/button.html b/vendor-local/lib/python/django_browserid/templates/browserid/button.html
new file mode 100644
index 000000000..2b25839c0
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/templates/browserid/button.html
@@ -0,0 +1,9 @@
+{% comment %}
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+{% endcomment %}
+
+
+ {{ text }}
+
diff --git a/vendor-local/lib/python/django_browserid/templates/browserid/info.html b/vendor-local/lib/python/django_browserid/templates/browserid/info.html
new file mode 100644
index 000000000..ed71f4a85
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/templates/browserid/info.html
@@ -0,0 +1,5 @@
+{# Store useful data for the JavaScript to use. #}
+
+
diff --git a/vendor-local/lib/python/django_browserid/templatetags/__init__.py b/vendor-local/lib/python/django_browserid/templatetags/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/vendor-local/lib/python/django_browserid/templatetags/browserid.py b/vendor-local/lib/python/django_browserid/templatetags/browserid.py
new file mode 100644
index 000000000..e3c6852c4
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/templatetags/browserid.py
@@ -0,0 +1,32 @@
+from django import template
+
+from fancy_tag import fancy_tag
+
+from django_browserid import helpers
+
+
+register = template.Library()
+
+
+@fancy_tag(register, takes_context=True)
+def browserid_info(context, **kwargs):
+ return helpers.browserid_info(**kwargs)
+
+
+@fancy_tag(register, takes_context=True)
+def browserid_login(context, **kwargs):
+ return helpers.browserid_login(**kwargs)
+
+
+@fancy_tag(register, takes_context=True)
+def browserid_logout(context, **kwargs):
+ return helpers.browserid_logout(**kwargs)
+
+
+@fancy_tag(register, takes_context=True)
+def browserid_js(context, **kwargs):
+ return helpers.browserid_js(**kwargs)
+
+@fancy_tag(register, takes_context=True)
+def browserid_css(context, **kwargs):
+ return helpers.browserid_css(**kwargs)
diff --git a/vendor-local/lib/python/django_browserid/tests/__init__.py b/vendor-local/lib/python/django_browserid/tests/__init__.py
new file mode 100644
index 000000000..a61ee71e1
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/tests/__init__.py
@@ -0,0 +1,71 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import json
+
+from django.test import TestCase as DjangoTestCase
+from django.utils.encoding import smart_text
+from django.utils.functional import wraps
+
+from mock import patch
+from nose.tools import eq_
+
+from django_browserid.auth import BrowserIDBackend
+from django_browserid.base import MockVerifier
+
+
+def fake_create_user(email):
+ pass
+
+
+class mock_browserid(object):
+ """
+ Mock verification in :class:`django_browserid.auth.BrowserIDBackend`.
+
+ Can be used as a context manager or as a decorator:
+
+ with mock_browserid('a@b.com'):
+ django_browserid.verify('random-token') # = {'status': 'okay',
+ # 'email': 'a@b.com',
+ # ...}
+
+ @mock_browserid(None)
+ def browserid_test():
+ django_browserid.verify('random-token') # = False
+ """
+ def __init__(self, email, **kwargs):
+ """
+ :param email:
+ Email to return in the verification result. If None, the verification will fail.
+
+ :param kwargs:
+ Keyword arguments are passed on to :class:`django_browserid.base.MockVerifier`, which
+ updates the verification result with them.
+ """
+ self.patcher = patch.object(BrowserIDBackend, 'get_verifier')
+ self.return_value = MockVerifier(email, **kwargs)
+
+ def __enter__(self):
+ mock = self.patcher.start()
+ mock.return_value = self.return_value
+ return mock
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.patcher.stop()
+
+ def __call__(self, func):
+ @wraps(func)
+ def inner(*args, **kwargs):
+ with self:
+ return func(*args, **kwargs)
+ return inner
+
+
+class TestCase(DjangoTestCase):
+ def assert_json_equals(self, json_str, value):
+ return eq_(json.loads(smart_text(json_str)), value)
+
+ def shortDescription(self):
+ # Stop nose using the test docstring and instead the test method
+ # name.
+ pass
diff --git a/vendor-local/lib/python/django_browserid/tests/models.py b/vendor-local/lib/python/django_browserid/tests/models.py
new file mode 100644
index 000000000..ed362530c
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/tests/models.py
@@ -0,0 +1,21 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from django.db import models
+
+try:
+ from django.contrib.auth.models import AbstractBaseUser
+except ImportError:
+ AbstractBaseUser = object
+
+
+class CustomUser(AbstractBaseUser):
+ USERNAME_FIELD = 'email'
+
+ email = models.EmailField(unique=True, db_index=True)
+
+ def get_full_name(self):
+ return self.email
+
+ def get_short_name(self):
+ return self.email
diff --git a/vendor-local/lib/python/django_browserid/tests/settings.py b/vendor-local/lib/python/django_browserid/tests/settings.py
new file mode 100644
index 000000000..252cce038
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/tests/settings.py
@@ -0,0 +1,37 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+TEST_RUNNER = 'django_nose.runner.NoseTestSuiteRunner'
+
+SECRET_KEY = 'asdf'
+
+DATABASES = {
+ 'default': {
+ 'NAME': 'test.db',
+ 'ENGINE': 'django.db.backends.sqlite3',
+ }
+}
+
+INSTALLED_APPS = (
+ 'django_nose',
+ 'django_browserid',
+ 'django_browserid.tests',
+
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.staticfiles',
+)
+
+ROOT_URLCONF = 'django_browserid.tests.urls'
+
+AUTHENTICATION_BACKENDS = (
+ 'django.contrib.auth.backends.ModelBackend',
+ 'django_browserid.auth.BrowserIDBackend',
+)
+
+BROWSERID_CREATE_USER = True
+BROWSERID_USERNAME_ALGO = None
+
+STATIC_URL = 'static/'
+
+BROWSERID_AUDIENCES = ['http://testserver']
diff --git a/vendor-local/lib/python/django_browserid/tests/test_admin.py b/vendor-local/lib/python/django_browserid/tests/test_admin.py
new file mode 100644
index 000000000..71f10ed3f
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/tests/test_admin.py
@@ -0,0 +1,53 @@
+from django.contrib import admin
+from django.db import models
+
+from mock import Mock
+
+from django_browserid.admin import BrowserIDAdminSite
+from django_browserid.tests import TestCase
+
+
+class BrowserIDAdminSiteTests(TestCase):
+ def test_copy_registry(self):
+ """
+ copy_registry should register the ModelAdmins from the given
+ site on the BrowserIDAdminSite.
+ """
+ django_site = admin.AdminSite()
+ browserid_site = BrowserIDAdminSite()
+
+ class TestModel(models.Model):
+ pass
+ class TestModelAdmin(admin.ModelAdmin):
+ pass
+
+ browserid_site.register = Mock()
+ django_site.register(TestModel, TestModelAdmin)
+
+ browserid_site.copy_registry(django_site)
+ browserid_site.register.assert_any_call(TestModel, TestModelAdmin)
+
+ def test_copy_registry_multiple(self):
+ django_site = admin.AdminSite()
+ browserid_site = BrowserIDAdminSite()
+
+ class TestModel1(models.Model):
+ pass
+ class TestModel2(models.Model):
+ pass
+ class TestModel3(models.Model):
+ pass
+ class TestModelAdmin(admin.ModelAdmin):
+ pass
+ class TestModel2Admin(admin.ModelAdmin):
+ pass
+
+ browserid_site.register = Mock()
+ django_site.register(TestModel1, TestModelAdmin)
+ django_site.register(TestModel2, TestModel2Admin)
+ django_site.register(TestModel3, TestModelAdmin)
+
+ browserid_site.copy_registry(django_site)
+ browserid_site.register.assert_any_call(TestModel1, TestModelAdmin)
+ browserid_site.register.assert_any_call(TestModel2, TestModel2Admin)
+ browserid_site.register.assert_any_call(TestModel3, TestModelAdmin)
diff --git a/vendor-local/lib/python/django_browserid/tests/test_auth.py b/vendor-local/lib/python/django_browserid/tests/test_auth.py
new file mode 100644
index 000000000..f249fe393
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/tests/test_auth.py
@@ -0,0 +1,281 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.db import IntegrityError
+
+from mock import ANY, Mock, patch
+
+from django_browserid.auth import AutoLoginBackend, BrowserIDBackend, default_username_algo
+from django_browserid.base import MockVerifier
+from django_browserid.tests import mock_browserid, TestCase
+
+try:
+ from django.contrib.auth import get_user_model
+ from django_browserid.tests.models import CustomUser
+except ImportError:
+ get_user_model = False
+
+
+def new_user(email, username=None):
+ """Creates a user with the specified email for testing."""
+ if username is None:
+ username = default_username_algo(email)
+ return User.objects.create_user(username, email)
+
+
+class BrowserIDBackendTests(TestCase):
+ def setUp(self):
+ self.backend = BrowserIDBackend()
+ self.verifier = Mock()
+ self.backend.get_verifier = lambda: self.verifier
+
+ def test_verify_failure(self):
+ """If verification fails, return None."""
+ self.verifier.verify.return_value = False
+ self.assertEqual(self.backend.verify('asdf', 'qwer'), None)
+ self.verifier.verify.assert_called_with('asdf', 'qwer')
+
+ def test_verify_success(self):
+ """
+ If verification succeeds, return the email address from the
+ verification result.
+ """
+ self.verifier.verify.return_value = Mock(email='bob@example.com')
+ self.assertEqual(self.backend.verify('asdf', 'qwer'), 'bob@example.com')
+ self.verifier.verify.assert_called_with('asdf', 'qwer')
+
+ def test_verify_no_audience_request(self):
+ """
+ If no audience is provided but a request is, retrieve the
+ audience from the request using get_audience.
+ """
+ request = Mock()
+ with patch('django_browserid.auth.get_audience') as get_audience:
+ self.backend.verify('asdf', request=request)
+ get_audience.assert_called_with(request)
+ self.verifier.verify.assert_called_with('asdf', get_audience.return_value)
+
+ def test_verify_no_audience_no_assertion_no_service(self):
+ """
+ If the assertion isn't provided, or the audience and request
+ aren't provided, return None.
+ """
+ self.assertEqual(self.backend.verify(audience='asdf'), None)
+ self.assertEqual(self.backend.verify(assertion='asdf'), None)
+ with patch('django_browserid.auth.get_audience') as get_audience:
+ get_audience.return_value = None
+ self.assertEqual(self.backend.verify('asdf', request=Mock()), None)
+
+ def test_verify_kwargs(self):
+ """Any extra kwargs should be passed to the verifier."""
+ self.backend.verify('asdf', 'asdf', request='blah', foo='bar', baz=1)
+ self.verifier.verify.assert_called_with('asdf', 'asdf', foo='bar', baz=1)
+
+ def auth(self, verified_email=None, **kwargs):
+ """
+ Attempt to authenticate a user with BrowserIDBackend.
+
+ If verified_email is None, verification will fail, otherwise it will
+ pass and return the specified email.
+ """
+ self.backend.verify = Mock(return_value=verified_email)
+ return self.backend.authenticate(assertion='asdf', audience='asdf', **kwargs)
+
+ def test_duplicate_emails(self):
+ """
+ If there are two users with the same email address, return None.
+ """
+ new_user('a@example.com', 'test1')
+ new_user('a@example.com', 'test2')
+ self.assertTrue(self.auth('a@example.com') is None)
+
+ def test_auth_success(self):
+ """
+ If a single user is found with the verified email, return an
+ instance of their user object.
+ """
+ user = new_user('a@example.com')
+ self.assertEqual(self.auth('a@example.com'), user)
+
+ @patch.object(settings, 'BROWSERID_CREATE_USER', False)
+ def test_no_create_user(self):
+ """
+ If user creation is disabled and no user is found, return None.
+ """
+ self.assertTrue(self.auth('a@example.com') is None)
+
+ @patch.object(settings, 'BROWSERID_CREATE_USER', True)
+ def test_create_user(self):
+ """
+ If user creation is enabled and no user is found, return a new
+ User.
+ """
+ user = self.auth('a@example.com')
+ self.assertTrue(user is not None)
+ self.assertTrue(isinstance(user, User))
+ self.assertEqual(user.email, 'a@example.com')
+
+ @patch.object(settings, 'BROWSERID_CREATE_USER',
+ 'django_browserid.tests.test_auth.new_user')
+ @patch('django_browserid.tests.test_auth.new_user')
+ def test_custom_create_user(self, create_user):
+ """
+ If user creation is enabled with a custom create function and no
+ user is found, return the new user created with the custom
+ function.
+ """
+ create_user.return_value = 'test'
+ self.assertEqual(self.auth('a@example.com'), 'test')
+ create_user.assert_called_with('a@example.com')
+
+ @patch.object(settings, 'BROWSERID_USERNAME_ALGO')
+ @patch.object(settings, 'BROWSERID_CREATE_USER', True)
+ def test_custom_username_algorithm(self, username_algo):
+ """If a custom username algorithm is specified, use it!"""
+ username_algo.return_value = 'test'
+ user = self.auth('a@b.com')
+ self.assertEqual(user.username, 'test')
+
+ @patch('django_browserid.auth.user_created')
+ @patch.object(settings, 'BROWSERID_CREATE_USER', True)
+ def test_user_created_signal(self, user_created):
+ """
+ Test that the user_created signal is called when a new user is
+ created.
+ """
+ user = self.auth('a@b.com')
+ user_created.send.assert_called_with(ANY, user=user)
+
+ def test_get_user(self):
+ """
+ Check if user returned by BrowserIDBackend.get_user is correct.
+ """
+ user = new_user('a@example.com')
+ backend = BrowserIDBackend()
+ self.assertEqual(backend.get_user(user.pk), user)
+
+ def test_overriding_valid_email(self):
+ class PickyBackend(BrowserIDBackend):
+ def is_valid_email(self, email):
+ return email != 'a@example.com'
+
+ new_user('a@example.com', 'test1')
+ new_user('b@example.com', 'test2')
+
+ with mock_browserid('a@example.com'):
+ backend = PickyBackend()
+ result = backend.authenticate(assertion='asdf', audience='asdf')
+ self.assertTrue(not result)
+
+ with mock_browserid('b@example.com'):
+ backend = PickyBackend()
+ result = backend.authenticate(assertion='asdf', audience='asdf')
+ self.assertTrue(result)
+
+ @patch('django_browserid.auth.logger')
+ def test_create_user_integrity_error(self, logger):
+ """
+ If an IntegrityError is raised during user creation, attempt to
+ re-fetch the user in case the user was created since we checked
+ for the existing account.
+ """
+ backend = BrowserIDBackend()
+ backend.User = Mock()
+ error = IntegrityError()
+ backend.User.objects.create_user.side_effect = error
+ backend.User.objects.get.return_value = 'asdf'
+
+ self.assertEqual(backend.create_user('a@example.com'), 'asdf')
+
+ # If get raises a DoesNotExist exception, re-raise the original exception.
+ backend.User.DoesNotExist = Exception
+ backend.User.objects.get.side_effect = backend.User.DoesNotExist
+ with self.assertRaises(IntegrityError) as e:
+ backend.create_user('a@example.com')
+ self.assertEqual(e.exception, error)
+
+ def test_authenticate_verify_exception(self):
+ """
+ If the verifier raises an exception, log it as a warning and
+ return None.
+ """
+ backend = BrowserIDBackend()
+ verifier = Mock()
+ exception = Exception()
+
+ backend.get_verifier = lambda: verifier
+ verifier.verify.side_effect = exception
+
+ with patch('django_browserid.auth.logger') as logger:
+ self.assertEqual(backend.authenticate('asdf', 'asdf'), None)
+ logger.warn.assert_called_with(exception)
+
+
+
+if get_user_model:
+ # Only run custom user model tests if we're using a version of Django that
+ # supports it.
+ @patch.object(settings, 'AUTH_USER_MODEL', 'tests.CustomUser')
+ class CustomUserModelTests(TestCase):
+ def _auth(self, backend=None, verified_email=None):
+ if backend is None:
+ backend = BrowserIDBackend()
+
+ with mock_browserid(verified_email):
+ return backend.authenticate(assertion='asdf', audience='asdf')
+
+ def test_existing_user(self):
+ """If a custom user exists with the given email, return them."""
+ user = CustomUser.objects.create(email='a@test.com')
+ authed_user = self._auth(verified_email='a@test.com')
+ self.assertEqual(user, authed_user)
+
+ @patch.object(settings, 'BROWSERID_CREATE_USER', True)
+ def test_create_new_user(self):
+ """
+ If a custom user does not exist with the given email, create a new
+ user and return them.
+ """
+ class CustomUserBrowserIDBackend(BrowserIDBackend):
+ def create_user(self, email):
+ return CustomUser.objects.create(email=email)
+ user = self._auth(backend=CustomUserBrowserIDBackend(),
+ verified_email='b@test.com')
+ self.assertTrue(isinstance(user, CustomUser))
+ self.assertEqual(user.email, 'b@test.com')
+
+
+class AutoLoginBackendTests(TestCase):
+ def setUp(self):
+ self.backend = AutoLoginBackend()
+
+ def test_verify_with_email(self):
+ """
+ If BROWSERID_AUTOLOGIN_EMAIL is set, use it to auth the user.
+ """
+ with self.settings(BROWSERID_AUTOLOGIN_EMAIL='bob@example.com',
+ BROWSERID_AUTOLOGIN_ENABLED=True):
+ self.assertEqual(self.backend.verify(), 'bob@example.com')
+
+ def test_verify_without_email(self):
+ """
+ If BROWSERID_AUTOLOGIN_EMAIL is not set, do not auth the user.
+ """
+ with self.settings(BROWSERID_AUTOLOGIN_EMAIL='', BROWSERID_AUTOLOGIN_ENABLED=True):
+ del settings.BROWSERID_AUTOLOGIN_EMAIL
+ self.assertEqual(self.backend.verify(), None)
+
+ def test_verify_disabled(self):
+ """
+ If BROWSERID_AUTOLOGIN_ENABLED is False, do not auth the user
+ in any case.
+ """
+ with self.settings(BROWSERID_AUTOLOGIN_EMAIL='', BROWSERID_AUTOLOGIN_ENABLED=False):
+ del settings.BROWSERID_AUTOLOGIN_EMAIL
+ self.assertEqual(self.backend.verify(), None)
+
+ with self.settings(BROWSERID_AUTOLOGIN_EMAIL='bob@example.com',
+ BROWSERID_AUTOLOGIN_ENABLED=False):
+ self.assertEqual(self.backend.verify(), None)
diff --git a/vendor-local/lib/python/django_browserid/tests/test_base.py b/vendor-local/lib/python/django_browserid/tests/test_base.py
new file mode 100644
index 000000000..6bb84bccf
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/tests/test_base.py
@@ -0,0 +1,435 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from datetime import datetime
+
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.test.client import RequestFactory
+from django.test.utils import override_settings
+from django.utils import six
+
+import requests
+from mock import Mock, patch
+from nose.plugins.skip import SkipTest
+from nose.tools import eq_, ok_
+
+from django_browserid import base
+from django_browserid.compat import pybrowserid_found
+from django_browserid.tests import TestCase
+
+
+class SanityCheckTests(TestCase):
+ def setUp(self):
+ self.factory = RequestFactory()
+
+ @override_settings(DEBUG=True)
+ def test_debug_true(self):
+ """
+ If DEBUG is True and BROWSERID_DISABLE_SANITY_CHECKS is not set,
+ run the checks.
+ """
+ request = self.factory.get('/')
+ ok_(base.sanity_checks(request))
+
+ @override_settings(DEBUG=False)
+ def test_debug_false(self):
+ """
+ If DEBUG is True and BROWSERID_DISABLE_SANITY_CHECKS is not set,
+ run the checks.
+ """
+ request = self.factory.get('/')
+ ok_(not base.sanity_checks(request))
+
+ @override_settings(BROWSERID_DISABLE_SANITY_CHECKS=True)
+ def test_disable_sanity_checks(self):
+ """
+ If BROWSERID_DISABLE_SANITY_CHECKS is True, do not run any
+ checks.
+ """
+ request = self.factory.get('/')
+ ok_(not base.sanity_checks(request))
+
+ @override_settings(BROWSERID_DISABLE_SANITY_CHECKS=False, SESSION_COOKIE_SECURE=True)
+ def test_sanity_session_cookie(self):
+ """
+ If SESSION_COOKIE_SECURE == True and the current request isn't
+ https, log a debug message warning about it.
+ """
+ request = self.factory.get('/')
+ request.is_secure = Mock(return_value=False)
+ with patch('django_browserid.base.logger.warning') as warning:
+ base.sanity_checks(request)
+ ok_(warning.called)
+
+ @override_settings(BROWSERID_DISABLE_SANITY_CHECKS=False,
+ MIDDLEWARE_CLASSES=['csp.middleware.CSPMiddleware'])
+ @patch('django_browserid.base.logger.warning')
+ def test_sanity_csp(self, warning):
+ """
+ If the django-csp middleware is present and Persona isn't
+ allowed by CSP, log a debug message warning about it.
+ """
+ request = self.factory.get('/')
+
+ # Test if allowed properly.
+ with self.settings(CSP_DEFAULT_SRC=[],
+ CSP_SCRIPT_SRC=['https://login.persona.org'],
+ CSP_FRAME_SRC=['https://login.persona.org']):
+ base.sanity_checks(request)
+ ok_(not warning.called)
+ warning.reset_mock()
+
+ # Test fallback to default-src.
+ with self.settings(CSP_DEFAULT_SRC=['https://login.persona.org'],
+ CSP_SCRIPT_SRC=[],
+ CSP_FRAME_SRC=[]):
+ base.sanity_checks(request)
+ ok_(not warning.called)
+ warning.reset_mock()
+
+ # Test incorrect csp.
+ with self.settings(CSP_DEFAULT_SRC=[],
+ CSP_SCRIPT_SRC=[],
+ CSP_FRAME_SRC=[]):
+ base.sanity_checks(request)
+ ok_(warning.called)
+ warning.reset_mock()
+
+ # Test partial incorrectness.
+ with self.settings(CSP_DEFAULT_SRC=[],
+ CSP_SCRIPT_SRC=['https://login.persona.org'],
+ CSP_FRAME_SRC=[]):
+ base.sanity_checks(request)
+ ok_(warning.called)
+
+ @override_settings(BROWSERID_DISABLE_SANITY_CHECKS=False,
+ MIDDLEWARE_CLASSES=['csp.middleware.CSPMiddleware'])
+ @patch('django_browserid.base.logger.warning')
+ def test_unset_csp(self, warning):
+ """Check for errors when CSP settings aren't specified."""
+ request = self.factory.get('/')
+ correct = ['https://login.persona.org']
+ setting_kwargs = {
+ 'CSP_DEFAULT_SRC': correct,
+ 'CSP_SCRIPT_SRC': correct,
+ 'CSP_FRAME_SRC': correct
+ }
+
+ # There's no easy way to use a variable for deleting the
+ # attribute on the settings object, so we can't easily turn this
+ # into a function, sadly.
+ with self.settings(**setting_kwargs):
+ del settings.CSP_DEFAULT_SRC
+ base.sanity_checks(request)
+ ok_(not warning.called)
+ warning.reset_mock()
+
+ with self.settings(**setting_kwargs):
+ del settings.CSP_FRAME_SRC
+ base.sanity_checks(request)
+ ok_(not warning.called)
+ warning.reset_mock()
+
+ with self.settings(**setting_kwargs):
+ del settings.CSP_SCRIPT_SRC
+ base.sanity_checks(request)
+ ok_(not warning.called)
+ warning.reset_mock()
+
+
+class GetAudienceTests(TestCase):
+ def setUp(self):
+ self.factory = RequestFactory()
+
+ def test_setting_missing(self):
+ """
+ If BROWSERID_AUDIENCES isn't defined, raise
+ ImproperlyConfigured.
+ """
+ request = self.factory.get('/')
+
+ with patch('django_browserid.base.settings') as settings:
+ del settings.BROWSERID_AUDIENCES
+ settings.DEBUG = False
+
+ with self.assertRaises(ImproperlyConfigured):
+ base.get_audience(request)
+
+ def test_same_origin_found(self):
+ """
+ If an audience is found in BROWSERID_AUDIENCES with the same
+ origin as the request URI, return it.
+ """
+ request = self.factory.get('http://testserver')
+
+ audiences = ['https://example.com', 'http://testserver']
+ with self.settings(BROWSERID_AUDIENCES=audiences, DEBUG=False):
+ eq_(base.get_audience(request), 'http://testserver')
+
+ def test_no_audience(self):
+ """
+ If no matching audiences is found in BROWSERID_AUDIENCES, raise
+ ImproperlyConfigured.
+ """
+ request = self.factory.get('http://testserver')
+
+ with self.settings(BROWSERID_AUDIENCES=['https://example.com']):
+ with self.assertRaises(ImproperlyConfigured):
+ base.get_audience(request)
+
+ def test_missing_setting_but_in_debug(self):
+ """
+ If no BROWSERID_AUDIENCES is set but in DEBUG just use the
+ current protocal and host.
+ """
+ request = self.factory.get('/')
+
+ # Simulate that no BROWSERID_AUDIENCES has been set
+ with patch('django_browserid.base.settings') as settings:
+ del settings.BROWSERID_AUDIENCES
+ settings.DEBUG = True
+ eq_(base.get_audience(request), 'http://testserver')
+
+ def test_no_audience_but_in_debug(self):
+ """
+ If no BROWSERID_AUDIENCES is set but in DEBUG just use the
+ current protocal and host.
+ """
+ request = self.factory.get('/')
+
+ # Simulate that no BROWSERID_AUDIENCES has been set
+ with self.settings(BROWSERID_AUDIENCES=[], DEBUG=True):
+ eq_(base.get_audience(request), 'http://testserver')
+
+
+class VerificationResultTests(TestCase):
+ def test_getattr_attribute_exists(self):
+ """
+ If a value exists in the response dict, it should be an
+ attribute on the result.
+ """
+ result = base.VerificationResult({'myattr': 'foo'})
+ eq_(result.myattr, 'foo')
+
+ def test_getattr_attribute_doesnt_exist(self):
+ """
+ If a value doesn't exist in the response dict, accessing it as
+ an attribute should raise an AttributeError.
+ """
+ result = base.VerificationResult({'myattr': 'foo'})
+ with self.assertRaises(AttributeError):
+ result.bar
+
+ def test_expires_no_attribute(self):
+ """
+ If no expires attribute was in the response, raise an
+ AttributeError.
+ """
+ result = base.VerificationResult({'myattr': 'foo'})
+ with self.assertRaises(AttributeError):
+ result.expires
+
+ def test_expires_invalid_timestamp(self):
+ """
+ If the expires attribute cannot be parsed as a timestamp, return
+ the raw string instead.
+ """
+ result = base.VerificationResult({'expires': 'foasdfhas'})
+ eq_(result.expires, 'foasdfhas')
+
+ def test_expires_valid_timestamp(self):
+ """
+ If expires contains a valid millisecond timestamp, return a
+ corresponding datetime.
+ """
+ result = base.VerificationResult({'expires': '1379307128000'})
+ eq_(datetime(2013, 9, 16, 4, 52, 8), result.expires)
+
+ def test_nonzero_failure(self):
+ """
+ If the response status is not 'okay', the result should be
+ falsy.
+ """
+ ok_(not base.VerificationResult({'status': 'failure'}))
+
+ def test_nonzero_okay(self):
+ """
+ If the response status is 'okay', the result should be truthy.
+ """
+ ok_(base.VerificationResult({'status': 'okay'}))
+
+ def test_str_success(self):
+ """
+ If the result is successful, include 'Success' and the email in
+ the string.
+ """
+ result = base.VerificationResult({'status': 'okay', 'email': 'a@example.com'})
+ eq_(six.text_type(result), '
')
+
+ # If the email is missing, don't include it.
+ result = base.VerificationResult({'status': 'okay'})
+ eq_(six.text_type(result), '')
+
+ def test_str_failure(self):
+ """
+ If the result is a failure, include 'Failure' in the string.
+ """
+ result = base.VerificationResult({'status': 'failure'})
+ eq_(six.text_type(result), '')
+
+ def test_str_unicode(self):
+ """Ensure that __str__ can handle unicode values."""
+ result = base.VerificationResult({'status': 'okay', 'email': six.u('\x80@example.com')})
+ eq_(six.text_type(result), six.u(''))
+
+
+class RemoteVerifierTests(TestCase):
+ def _response(self, **kwargs):
+ return Mock(spec=requests.Response, **kwargs)
+
+ def test_verify_requests_parameters(self):
+ """
+ If a subclass overrides requests_parameters, the parameters
+ should be passed to requests.post.
+ """
+ class MyVerifier(base.RemoteVerifier):
+ requests_parameters = {'foo': 'bar'}
+ verifier = MyVerifier()
+
+ with patch('django_browserid.base.requests.post') as post:
+ post.return_value = self._response(content='{"status":"failure"}')
+ verifier.verify('asdf', 'http://testserver')
+
+ # foo parameter passed with 'bar' value.
+ eq_(post.call_args[1]['foo'], 'bar')
+
+ def test_verify_kwargs(self):
+ """
+ Any keyword arguments passed to verify should be passed on as
+ POST arguments.
+ """
+ verifier = base.RemoteVerifier()
+
+ with patch('django_browserid.base.requests.post') as post:
+ post.return_value = self._response(content='{"status":"failure"}')
+ verifier.verify('asdf', 'http://testserver', foo='bar', baz=5)
+
+ # foo parameter passed with 'bar' value.
+ eq_(post.call_args[1]['data']['foo'], 'bar')
+ eq_(post.call_args[1]['data']['baz'], 5)
+
+ def test_verify_request_exception(self):
+ """
+ If a RequestException is raised during the POST, raise a
+ BrowserIDException with the RequestException as the cause.
+ """
+ verifier = base.RemoteVerifier()
+ request_exception = requests.exceptions.RequestException()
+
+ with patch('django_browserid.base.requests.post') as post:
+ post.side_effect = request_exception
+ with self.assertRaises(base.BrowserIDException) as cm:
+ verifier.verify('asdf', 'http://testserver')
+
+ eq_(cm.exception.exc, request_exception)
+
+ def test_verify_invalid_json(self):
+ """
+ If the response contains invalid JSON, return a failure result.
+ """
+ verifier = base.RemoteVerifier()
+
+ with patch('django_browserid.base.requests.post') as post:
+ response = self._response(content='{asg9=3{{{}}{')
+ response.json.side_effect = ValueError("Couldn't parse json")
+ post.return_value = response
+ result = verifier.verify('asdf', 'http://testserver')
+ ok_(not result)
+ ok_(result.reason.startswith('Could not parse verifier response'))
+
+
+ def test_verify_success(self):
+ """
+ If the response contains valid JSON, return a result object for
+ that response.
+ """
+ verifier = base.RemoteVerifier()
+
+ with patch('django_browserid.base.requests.post') as post:
+ response = self._response(
+ content='{"status": "okay", "email": "foo@example.com"}')
+ response.json.return_value = {"status": "okay", "email": "foo@example.com"}
+ post.return_value = response
+ result = verifier.verify('asdf', 'http://testserver')
+ ok_(result)
+ eq_(result.email, 'foo@example.com')
+
+
+class MockVerifierTests(TestCase):
+ def test_verify_no_email(self):
+ """
+ If the given email is None, verify should return a failure
+ result.
+ """
+ verifier = base.MockVerifier(None)
+ result = verifier.verify('asdf', 'http://testserver')
+ ok_(not result)
+ eq_(result.reason, 'No email given to MockVerifier.')
+
+ def test_verify_email(self):
+ """
+ If an email is given to the constructor, return a successful
+ result.
+ """
+ verifier = base.MockVerifier('a@example.com')
+ result = verifier.verify('asdf', 'http://testserver')
+ ok_(result)
+ eq_(result.audience, 'http://testserver')
+ eq_(result.email, 'a@example.com')
+
+ def test_verify_result_attributes(self):
+ """Extra kwargs to the constructor are added to the result."""
+ verifier = base.MockVerifier('a@example.com', foo='bar', baz=5)
+ result = verifier.verify('asdf', 'http://testserver')
+ eq_(result.foo, 'bar')
+ eq_(result.baz, 5)
+
+
+class LocalVerifierTests(TestCase):
+ def setUp(self):
+ # Skip tests if PyBrowserID is not installed.
+ if not pybrowserid_found:
+ raise SkipTest
+
+ self.verifier = base.LocalVerifier()
+
+ def test_verify_error(self):
+ """
+ If verify raises a PyBrowserIDError, return a failure
+ result.
+ """
+ from browserid.errors import Error as PyBrowserIDError
+
+ pybid_verifier = Mock()
+ error = PyBrowserIDError()
+ self.verifier.pybid_verifier = pybid_verifier
+
+ pybid_verifier.verify.side_effect = error
+
+ result = self.verifier.verify('asdf', 'qwer')
+ pybid_verifier.verify.assert_called_with('asdf', 'qwer')
+ self.assertFalse(result)
+ self.assertEqual(result.reason, error)
+
+ def test_verify_success(self):
+ pybid_verifier = Mock()
+ self.verifier.pybid_verifier = pybid_verifier
+
+ response = {'status': 'okay'}
+ pybid_verifier.verify.return_value = response
+
+ result = self.verifier.verify('asdf', 'qwer')
+ pybid_verifier.verify.assert_called_with('asdf', 'qwer')
+ self.assertTrue(result)
+ self.assertEqual(result._response, response)
diff --git a/vendor-local/lib/python/django_browserid/tests/test_helpers.py b/vendor-local/lib/python/django_browserid/tests/test_helpers.py
new file mode 100644
index 000000000..bc0150ee7
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/tests/test_helpers.py
@@ -0,0 +1,205 @@
+from django.utils.functional import lazy
+
+from mock import patch
+from nose.tools import eq_
+
+from django_browserid import helpers
+from django_browserid.tests import TestCase
+
+
+def _lazy_request_args():
+ return {'siteName': 'asdf'}
+lazy_request_args = lazy(_lazy_request_args, dict)
+
+
+class BrowserIDInfoTests(TestCase):
+ def setUp(self):
+ patcher = patch('django_browserid.helpers.render_to_string')
+ self.addCleanup(patcher.stop)
+ self.render_to_string = patcher.start()
+
+ def test_defaults(self):
+ with self.settings(BROWSERID_REQUEST_ARGS={'foo': 'bar', 'baz': 1}):
+ output = helpers.browserid_info()
+
+ eq_(output, self.render_to_string.return_value)
+ expected_info = {
+ 'loginUrl': '/browserid/login/',
+ 'logoutUrl': '/browserid/logout/',
+ 'csrfUrl': '/browserid/csrf/',
+ 'requestArgs': {'foo': 'bar', 'baz': 1},
+ }
+ self.render_to_string.assertCalledWith('browserid/info.html', {'info': expected_info})
+
+ def test_lazy_request_args(self):
+ with self.settings(BROWSERID_REQUEST_ARGS=lazy_request_args()):
+ output = helpers.browserid_info()
+
+ eq_(output, self.render_to_string.return_value)
+ expected_info = {
+ 'loginUrl': '/browserid/login/',
+ 'logoutUrl': '/browserid/logout/',
+ 'csrfUrl': '/browserid/csrf/',
+ 'requestArgs': {'siteName': 'asdf'},
+ }
+ self.render_to_string.assertCalledWith('browserid/info.html', {'info': expected_info})
+
+
+class BrowserIDJSTests(TestCase):
+ def test_basic(self):
+ output = helpers.browserid_js()
+ self.assertHTMLEqual(output, """
+
+
+
+ """)
+
+ def test_no_shim(self):
+ output = helpers.browserid_js(include_shim=False)
+ self.assertHTMLEqual(output, """
+
+
+ """)
+
+ def test_custom_shim(self):
+ with self.settings(BROWSERID_SHIM='http://example.com/test.js'):
+ output = helpers.browserid_js()
+ self.assertHTMLEqual(output, """
+
+
+
+ """)
+
+ def test_autologin_email(self):
+ """
+ If BROWSERID_AUTOLOGIN_ENABLED is True, do not include the shim
+ and include the autologin mock script.
+ """
+ with self.settings(BROWSERID_AUTOLOGIN_ENABLED=True):
+ output = helpers.browserid_js()
+ self.assertHTMLEqual(output, """
+
+
+
+ """)
+
+
+class BrowserIDCSSTests(TestCase):
+ def test_basic(self):
+ output = helpers.browserid_css()
+ self.assertHTMLEqual(output, """
+
+ """)
+
+
+class BrowserIDButtonTests(TestCase):
+ def test_basic(self):
+ button = helpers.browserid_button(text='asdf', next='1234', link_class='fake-button',
+ href="/test", attrs={'target': '_blank'})
+ self.assertHTMLEqual(button, """
+
+ asdf
+
+ """)
+
+ def test_json_attrs(self):
+ button = helpers.browserid_button(text='qwer', next='5678', link_class='fake-button',
+ attrs='{"target": "_blank"}')
+ self.assertHTMLEqual(button, """
+
+ qwer
+
+ """)
+
+
+class BrowserIDLoginTests(TestCase):
+ def test_login_class(self):
+ with self.settings(LOGIN_REDIRECT_URL='/'):
+ button = helpers.browserid_login(link_class='go button')
+ self.assertHTMLEqual(button, """
+
+ Sign in
+
+ """)
+
+ def test_default_class(self):
+ """
+ If no class is provided, it should default to
+ 'browserid-login persona-button'.
+ """
+ with self.settings(LOGIN_REDIRECT_URL='/'):
+ button = helpers.browserid_login()
+ self.assertHTMLEqual(button, """
+
+ Sign in
+
+ """)
+
+ def test_color_class(self):
+ with self.settings(LOGIN_REDIRECT_URL='/'):
+ button = helpers.browserid_login(color='dark')
+ self.assertHTMLEqual(button, """
+
+ Sign in
+
+ """)
+
+ def test_color_custom_class(self):
+ """
+ If using a color and a custom link class, persona-button should
+ be added to the link class.
+ """
+ with self.settings(LOGIN_REDIRECT_URL='/'):
+ button = helpers.browserid_login(link_class='go button', color='dark')
+ self.assertHTMLEqual(button, """
+
+ Sign in
+
+ """)
+
+ def test_next(self):
+ button = helpers.browserid_login(next='/foo/bar')
+ self.assertHTMLEqual(button, """
+
+ Sign in
+
+ """)
+
+ def test_next_default(self):
+ """next should default to LOGIN_REDIRECT_URL"""
+ with self.settings(LOGIN_REDIRECT_URL='/foo/bar'):
+ button = helpers.browserid_login()
+ self.assertHTMLEqual(button, """
+
+ Sign in
+
+ """)
+
+
+class BrowserIDLogoutTests(TestCase):
+ def test_logout_class(self):
+ with self.settings(LOGOUT_REDIRECT_URL='/'):
+ button = helpers.browserid_logout(link_class='go button')
+ self.assertHTMLEqual(button, """
+
+ Sign out
+
+ """)
+
+ def test_next(self):
+ button = helpers.browserid_logout(next='/foo/bar')
+ self.assertHTMLEqual(button, """
+
+ Sign out
+
+ """)
+
+ def test_next_default(self):
+ """next should default to LOGOUT_REDIRECT_URL"""
+ with self.settings(LOGOUT_REDIRECT_URL='/foo/bar'):
+ button = helpers.browserid_logout()
+ self.assertHTMLEqual(button, """
+
+ Sign out
+
+ """)
diff --git a/vendor-local/lib/python/django_browserid/tests/test_http.py b/vendor-local/lib/python/django_browserid/tests/test_http.py
new file mode 100644
index 000000000..e577e7233
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/tests/test_http.py
@@ -0,0 +1,20 @@
+from nose.tools import eq_
+
+from django_browserid.http import JSONResponse
+from django_browserid.tests import TestCase
+
+
+class JSONResponseTests(TestCase):
+ def test_basic(self):
+ response = JSONResponse({'blah': 'foo', 'bar': 7})
+ self.assert_json_equals(response.content, {'blah': 'foo', 'bar': 7})
+ eq_(response.status_code, 200)
+
+ response = JSONResponse(['baz', {'biff': False}])
+ self.assert_json_equals(response.content, ['baz', {'biff': False}])
+ eq_(response.status_code, 200)
+
+ def test_status(self):
+ response = JSONResponse({'blah': 'foo', 'bar': 7}, status=404)
+ self.assert_json_equals(response.content, {'blah': 'foo', 'bar': 7})
+ eq_(response.status_code, 404)
diff --git a/vendor-local/lib/python/django_browserid/tests/test_urls.py b/vendor-local/lib/python/django_browserid/tests/test_urls.py
new file mode 100644
index 000000000..a5aa52f88
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/tests/test_urls.py
@@ -0,0 +1,32 @@
+from django.core.urlresolvers import resolve
+from django.test.client import RequestFactory
+from django.utils.six.moves import reload_module
+
+from mock import Mock
+
+from django_browserid import urls
+from django_browserid.views import Verify
+from django_browserid.tests import TestCase
+
+
+class MyVerifyClass(Verify):
+ as_view = Mock()
+
+
+class UrlTests(TestCase):
+ def setUp(self):
+ self.factory = RequestFactory()
+
+ def test_override_verify_class(self):
+ """
+ Reload so that the settings.BROWSERID_VERIFY_CLASS takes effect.
+ """
+ path = 'django_browserid.tests.test_urls.MyVerifyClass'
+ with self.settings(BROWSERID_VERIFY_CLASS=path):
+ reload_module(urls)
+
+ view = resolve('/browserid/login/', urls).func
+ self.assertEqual(view, MyVerifyClass.as_view.return_value)
+
+ # Reset urls back to normal.
+ reload_module(urls)
diff --git a/vendor-local/lib/python/django_browserid/tests/test_util.py b/vendor-local/lib/python/django_browserid/tests/test_util.py
new file mode 100644
index 000000000..2ab592ec7
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/tests/test_util.py
@@ -0,0 +1,87 @@
+import json
+
+from django.core.exceptions import ImproperlyConfigured
+from django.test.utils import override_settings
+from django.utils import six
+from django.utils.functional import lazy
+
+from mock import Mock, patch
+from nose.tools import eq_
+
+from django_browserid.tests import TestCase
+from django_browserid.util import import_from_setting, LazyEncoder
+
+
+def _lazy_string():
+ return 'blah'
+lazy_string = lazy(_lazy_string, six.text_type)()
+
+
+class TestLazyEncoder(TestCase):
+ def test_lazy(self):
+ thing = ['foo', lazy_string]
+ thing_json = json.dumps(thing, cls=LazyEncoder)
+ eq_('["foo", "blah"]', thing_json)
+
+
+class ImportFromSettingTests(TestCase):
+ def test_no_setting(self):
+ """If the setting doesn't exist, raise ImproperlyConfigured."""
+ with self.assertRaises(ImproperlyConfigured):
+ import_from_setting('DOES_NOT_EXIST')
+
+ @override_settings(TEST_SETTING={})
+ def test_invalid_import(self):
+ """
+ If the setting isn't a proper string, raise
+ ImproperlyConfigured.
+ """
+ with self.assertRaises(ImproperlyConfigured):
+ import_from_setting('TEST_SETTING')
+
+ @patch('django_browserid.util.import_module')
+ @override_settings(TEST_SETTING='foo.bar.baz')
+ def test_failed_import(self, import_module):
+ """
+ If there is an error importing the module, raise
+ ImproperlyConfigured.
+ """
+ import_module.side_effect = ImportError
+ with self.assertRaises(ImproperlyConfigured):
+ import_from_setting('TEST_SETTING')
+ import_module.assert_called_with('foo.bar')
+
+ @patch('django_browserid.util.import_module')
+ @override_settings(TEST_SETTING='foo.bar.baz')
+ def test_error_importing(self, import_module):
+ """
+ If there is an error importing the module, raise
+ ImproperlyConfigured.
+ """
+ import_module.side_effect = ImportError
+ with self.assertRaises(ImproperlyConfigured):
+ import_from_setting('TEST_SETTING')
+ import_module.assert_called_with('foo.bar')
+
+ @patch('django_browserid.util.import_module')
+ @override_settings(TEST_SETTING='foo.bar.baz')
+ def test_missing_attribute(self, import_module):
+ """
+ If the module is imported, but the function isn't found, raise
+ ImproperlyConfigured.
+ """
+ import_module.return_value = Mock(spec=[])
+ with self.assertRaises(ImproperlyConfigured):
+ import_from_setting('TEST_SETTING')
+
+ @patch('django_browserid.util.import_module')
+ @override_settings(TEST_SETTING='foo.bar.baz')
+ def test_existing_attribute(self, import_module):
+ """
+ If the module is imported and has the requested function,
+ return it.
+ """
+ module = Mock(spec=['baz'])
+ import_module.return_value = module
+ self.assertEqual(import_from_setting('TEST_SETTING'), module.baz)
+
diff --git a/vendor-local/lib/python/django_browserid/tests/test_views.py b/vendor-local/lib/python/django_browserid/tests/test_views.py
new file mode 100644
index 000000000..8e225fb85
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/tests/test_views.py
@@ -0,0 +1,246 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from django.contrib import auth
+from django.test.client import RequestFactory
+from django.utils import six
+from django.utils.functional import lazy
+
+from mock import Mock, patch, PropertyMock
+from nose.tools import eq_, ok_
+
+from django_browserid import BrowserIDException, views
+from django_browserid.tests import mock_browserid, TestCase
+
+
+class JSONViewTests(TestCase):
+ def test_http_method_not_allowed(self):
+ class TestView(views.JSONView):
+ def get(self, request, *args, **kwargs):
+ return 'asdf'
+ response = TestView().http_method_not_allowed()
+ eq_(response.status_code, 405)
+ ok_(set(['GET']).issubset(set(response['Allow'].split(', '))))
+ self.assert_json_equals(response.content, {'error': 'Method not allowed.'})
+
+ def test_http_method_not_allowed_allowed_methods(self):
+ class GetPostView(views.JSONView):
+ def get(self, request, *args, **kwargs):
+ return 'asdf'
+
+ def post(self, request, *args, **kwargs):
+ return 'qwer'
+ response = GetPostView().http_method_not_allowed()
+ ok_(set(['GET', 'POST']).issubset(set(response['Allow'].split(', '))))
+
+ class GetPostPutDeleteHeadView(views.JSONView):
+ def get(self, request, *args, **kwargs):
+ return 'asdf'
+
+ def post(self, request, *args, **kwargs):
+ return 'qwer'
+
+ def put(self, request, *args, **kwargs):
+ return 'qwer'
+
+ def delete(self, request, *args, **kwargs):
+ return 'qwer'
+
+ def head(self, request, *args, **kwargs):
+ return 'qwer'
+ response = GetPostPutDeleteHeadView().http_method_not_allowed()
+ expected_methods = set(['GET', 'POST', 'PUT', 'DELETE', 'HEAD'])
+ actual_methods = set(response['Allow'].split(', '))
+ ok_(expected_methods.issubset(actual_methods))
+
+
+class GetNextTests(TestCase):
+ def setUp(self):
+ self.factory = RequestFactory()
+
+ def test_no_param(self):
+ """If next isn't in the POST params, return None."""
+ request = self.factory.post('/')
+ eq_(views._get_next(request), None)
+
+ def test_is_safe(self):
+ """Return the value of next if it is considered safe."""
+ request = self.factory.post('/', {'next': '/asdf'})
+ request.get_host = lambda: 'myhost'
+
+ with patch.object(views, 'is_safe_url', return_value=True) as is_safe_url:
+ eq_(views._get_next(request), '/asdf')
+ is_safe_url.assert_called_with('/asdf', host='myhost')
+
+ def test_isnt_safe(self):
+ """If next isn't safe, return None."""
+ request = self.factory.post('/', {'next': '/asdf'})
+ request.get_host = lambda: 'myhost'
+
+ with patch.object(views, 'is_safe_url', return_value=False) as is_safe_url:
+ eq_(views._get_next(request), None)
+ is_safe_url.assert_called_with('/asdf', host='myhost')
+
+
+class VerifyTests(TestCase):
+ def setUp(self):
+ self.factory = RequestFactory()
+
+ def verify(self, request_type, **kwargs):
+ """
+ Call the verify view function. Kwargs are passed as GET or POST
+ arguments.
+ """
+ if request_type == 'get':
+ request = self.factory.get('/browserid/verify', kwargs)
+ else:
+ request = self.factory.post('/browserid/verify', kwargs)
+
+ verify_view = views.Verify.as_view()
+ with patch.object(auth, 'login'):
+ response = verify_view(request)
+
+ return response
+
+ def test_no_assertion(self):
+ """If no assertion is given, return a failure result."""
+ with self.settings(LOGIN_REDIRECT_URL_FAILURE='/fail'):
+ response = self.verify('post', blah='asdf')
+ eq_(response.status_code, 403)
+ self.assert_json_equals(response.content, {'redirect': '/fail'})
+
+ @mock_browserid(None)
+ def test_auth_fail(self):
+ """If authentication fails, redirect to the failure URL."""
+ with self.settings(LOGIN_REDIRECT_URL_FAILURE='/fail'):
+ response = self.verify('post', assertion='asdf')
+ eq_(response.status_code, 403)
+ self.assert_json_equals(response.content, {'redirect': '/fail'})
+
+ @mock_browserid('test@example.com')
+ def test_auth_success_redirect_success(self):
+ """If authentication succeeds, redirect to the success URL."""
+ user = auth.models.User.objects.create_user('asdf', 'test@example.com')
+
+ request = self.factory.post('/browserid/verify', {'assertion': 'asdf'})
+ with self.settings(LOGIN_REDIRECT_URL='/success'):
+ with patch('django_browserid.views.auth.login') as login:
+ verify = views.Verify.as_view()
+ response = verify(request)
+
+ login.assert_called_with(request, user)
+ eq_(response.status_code, 200)
+ self.assert_json_equals(response.content,
+ {'email': 'test@example.com', 'redirect': '/success'})
+
+ def test_sanity_checks(self):
+ """Run sanity checks on all incoming requests."""
+ with patch('django_browserid.views.sanity_checks') as sanity_checks:
+ self.verify('post')
+ ok_(sanity_checks.called)
+
+ @patch('django_browserid.views.auth.login')
+ def test_login_success_no_next(self, *args):
+ """
+ If _get_next returns None, use success_url for the redirect
+ parameter.
+ """
+ view = views.Verify()
+ view.request = self.factory.post('/')
+ view.user = Mock(email='a@b.com')
+
+ with patch('django_browserid.views._get_next', return_value=None) as _get_next:
+ with patch.object(views.Verify, 'success_url', '/?asdf'):
+ response = view.login_success()
+
+ self.assert_json_equals(response.content, {'email': 'a@b.com', 'redirect': '/?asdf'})
+ _get_next.assert_called_with(view.request)
+
+ @patch('django_browserid.views.auth.login')
+ def test_login_success_next(self, *args):
+ """
+ If _get_next returns a URL, use it for the redirect parameter.
+ """
+ view = views.Verify()
+ view.request = self.factory.post('/')
+ view.user = Mock(email='a@b.com')
+
+ with patch('django_browserid.views._get_next', return_value='/?qwer') as _get_next:
+ with patch.object(views.Verify, 'success_url', '/?asdf'):
+ response = view.login_success()
+
+ self.assert_json_equals(response.content, {'email': 'a@b.com', 'redirect': '/?qwer'})
+ _get_next.assert_called_with(view.request)
+
+
+class LogoutTests(TestCase):
+ def setUp(self):
+ self.factory = RequestFactory()
+
+ _get_next_patch = patch('django_browserid.views._get_next')
+ self._get_next = _get_next_patch.start()
+ self.addCleanup(_get_next_patch.stop)
+
+ def test_redirect(self):
+ """Include LOGOUT_REDIRECT_URL in the response."""
+ request = self.factory.post('/')
+ logout = views.Logout.as_view()
+ self._get_next.return_value = None
+
+ with patch.object(views.Logout, 'redirect_url', '/test/foo'):
+ with patch('django_browserid.views.auth.logout') as auth_logout:
+ response = logout(request)
+
+ auth_logout.assert_called_with(request)
+ eq_(response.status_code, 200)
+ self.assert_json_equals(response.content, {'redirect': '/test/foo'})
+
+ def test_redirect_next(self):
+ """
+ If _get_next returns a URL, use it for the redirect parameter.
+ """
+ request = self.factory.post('/')
+ logout = views.Logout.as_view()
+ self._get_next.return_value = '/test/bar'
+
+ with patch.object(views.Logout, 'redirect_url', '/test/foo'):
+ with patch('django_browserid.views.auth.logout'):
+ response = logout(request)
+
+ self.assert_json_equals(response.content, {'redirect': '/test/bar'})
+
+
+class CsrfTokenTests(TestCase):
+ def setUp(self):
+ self.factory = RequestFactory()
+ self.view = views.CsrfToken()
+
+ def test_lazy_token_called(self):
+ """
+ If the csrf_token variable in the RequestContext is a lazy
+ callable, make sure it is called during the view.
+ """
+ global _lazy_csrf_token_called
+ _lazy_csrf_token_called = False
+
+ # I'd love to use a Mock here instead, but lazy doesn't behave
+ # well with Mocks for some reason.
+ def _lazy_csrf_token():
+ global _lazy_csrf_token_called
+ _lazy_csrf_token_called = True
+ return 'asdf'
+ csrf_token = lazy(_lazy_csrf_token, six.text_type)()
+
+ request = self.factory.get('/browserid/csrf/')
+ with patch('django_browserid.views.RequestContext') as RequestContext:
+ RequestContext.return_value = {'csrf_token': csrf_token}
+ response = self.view.get(request)
+
+ eq_(response.status_code, 200)
+ eq_(response.content, b'asdf')
+ ok_(_lazy_csrf_token_called)
+
+ def test_never_cache(self):
+ request = self.factory.get('/browserid/csrf/')
+ response = self.view.get(request)
+ eq_(response['Cache-Control'], 'max-age=0')
diff --git a/vendor-local/lib/python/django_browserid/tests/urls.py b/vendor-local/lib/python/django_browserid/tests/urls.py
new file mode 100644
index 000000000..5a7f9a351
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/tests/urls.py
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from django.conf.urls import include, patterns, url
+from django.http import HttpResponse
+
+
+urlpatterns = patterns('',
+ (r'', include('django_browserid.urls')),
+ url(r'^epic-fail/', lambda r: HttpResponse('this is a stub'),
+ name='epic_fail')
+)
diff --git a/vendor-local/lib/python/django_browserid/urls.py b/vendor-local/lib/python/django_browserid/urls.py
new file mode 100644
index 000000000..25cbfa39c
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/urls.py
@@ -0,0 +1,29 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import logging
+
+from django.conf.urls import patterns, url
+from django.core.exceptions import ImproperlyConfigured
+
+from django_browserid import views
+from django_browserid.util import import_from_setting
+
+
+logger = logging.getLogger(__name__)
+
+
+try:
+ Verify = import_from_setting('BROWSERID_VERIFY_CLASS')
+ logger.debug('django_browserid using custom Verify view ' +
+ '.'.join([Verify.__module__, Verify.__name__]))
+except ImproperlyConfigured as e:
+ logger.debug('django_browserid using default Verify view.')
+ Verify = views.Verify
+
+
+urlpatterns = patterns('',
+ url(r'^browserid/login/$', Verify.as_view(), name='browserid.login'),
+ url(r'^browserid/logout/$', views.Logout.as_view(), name='browserid.logout'),
+ url(r'^browserid/csrf/$', views.CsrfToken.as_view(), name='browserid.csrf'),
+)
diff --git a/vendor-local/lib/python/django_browserid/util.py b/vendor-local/lib/python/django_browserid/util.py
new file mode 100644
index 000000000..0ea5a9cd0
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/util.py
@@ -0,0 +1,54 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import json
+
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.functional import Promise
+from django.utils.importlib import import_module
+
+try:
+ from django.utils.encoding import force_unicode as force_text
+except ImportError:
+ from django.utils.encoding import force_text # Python 3
+
+
+class LazyEncoder(json.JSONEncoder):
+ """
+ JSONEncoder that turns Promises into unicode strings to support functions
+ like ugettext_lazy and reverse_lazy.
+ """
+ def default(self, obj):
+ if isinstance(obj, Promise):
+ return force_text(obj)
+ return super(LazyEncoder, self).default(obj)
+
+
+def import_from_setting(setting):
+ """
+ Attempt to load a module attribute from a module as specified by a setting.
+
+ :raises:
+ ImproperlyConfigured if anything goes wrong.
+ """
+ try:
+ path = getattr(settings, setting)
+ except AttributeError as e:
+ raise ImproperlyConfigured('Setting {0} not found.'.format(setting))
+
+ try:
+ i = path.rfind('.')
+ module, attr = path[:i], path[i + 1:]
+ except AttributeError as e:
+ raise ImproperlyConfigured('Setting {0} should be an import path.'.format(setting))
+
+ try:
+ mod = import_module(module)
+ except ImportError as e:
+ raise ImproperlyConfigured('Error importing `{0}`: {1}'.format(path, e))
+
+ try:
+ return getattr(mod, attr)
+ except AttributeError as e:
+ raise ImproperlyConfigured('Module {0} does not define `{1}`.'.format(module, attr))
diff --git a/vendor-local/lib/python/django_browserid/views.py b/vendor-local/lib/python/django_browserid/views.py
new file mode 100644
index 000000000..d62733811
--- /dev/null
+++ b/vendor-local/lib/python/django_browserid/views.py
@@ -0,0 +1,141 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import logging
+
+from django.conf import settings
+from django.contrib import auth
+from django.http import HttpResponse
+from django.template import RequestContext
+from django.utils import six
+from django.utils.http import is_safe_url
+from django.views.decorators.cache import never_cache
+from django.views.generic import View
+
+from django_browserid.base import sanity_checks
+from django_browserid.http import JSONResponse
+
+
+logger = logging.getLogger(__name__)
+
+
+class JSONView(View):
+ def http_method_not_allowed(self, *args, **kwargs):
+ response = JSONResponse({'error': 'Method not allowed.'}, status=405)
+ allowed_methods = [m.upper() for m in self.http_method_names if hasattr(self, m)]
+ response['Allow'] = ', '.join(allowed_methods)
+ return response
+
+
+def _get_next(request):
+ """
+ Get the next parameter from the request's POST arguments and
+ validate it.
+
+ :returns:
+ The next parameter or None if it was not found or invalid.
+ """
+ next = request.POST.get('next')
+ if is_safe_url(next, host=request.get_host()):
+ return next
+ else:
+ return None
+
+
+class Verify(JSONView):
+ """
+ Send an assertion to the remote verification service, and log the
+ user in upon success.
+ """
+ @property
+ def failure_url(self):
+ """
+ URL to redirect users to when login fails. This uses the value
+ of ``settings.LOGIN_REDIRECT_URL_FAILURE``, and defaults to
+ ``'/'`` if the setting doesn't exist.
+ """
+ return getattr(settings, 'LOGIN_REDIRECT_URL_FAILURE', '/')
+
+ @property
+ def success_url(self):
+ """
+ URL to redirect users to when login succeeds. This uses the
+ value of ``settings.LOGIN_REDIRECT_URL``, and defaults to
+ ``'/'`` if the setting doesn't exist.
+ """
+ return getattr(settings, 'LOGIN_REDIRECT_URL', '/')
+
+ def login_success(self):
+ """Log the user into the site."""
+ auth.login(self.request, self.user)
+
+ return JSONResponse({
+ 'email': self.user.email,
+ 'redirect': _get_next(self.request) or self.success_url
+ })
+
+ def login_failure(self):
+ """
+ Redirect the user to a login-failed page. By default a 403 is
+ returned.
+ """
+ return JSONResponse({'redirect': self.failure_url}, status=403)
+
+ def post(self, *args, **kwargs):
+ """
+ Send the given assertion to the remote verification service and,
+ depending on the result, trigger login success or failure.
+ """
+ assertion = self.request.POST.get('assertion')
+ if not assertion:
+ return self.login_failure()
+
+ self.user = auth.authenticate(request=self.request, assertion=assertion)
+ if self.user and self.user.is_active:
+ return self.login_success()
+
+ return self.login_failure()
+
+ def dispatch(self, request, *args, **kwargs):
+ """
+ Run some sanity checks on the request prior to dispatching it.
+ """
+ sanity_checks(request)
+ return super(Verify, self).dispatch(request, *args, **kwargs)
+
+
+class CsrfToken(JSONView):
+ """Fetch a CSRF token for the frontend JavaScript."""
+ @never_cache
+ def get(self, request):
+ # Different CSRF libraries (namely session_csrf) store the CSRF
+ # token in different places. The only way to retrieve the token
+ # that works with both the built-in CSRF and session_csrf is to
+ # pull it from the template context processors via
+ # RequestContext.
+ context = RequestContext(request)
+
+ # csrf_token might be a lazy value that triggers side-effects,
+ # so we need to force it to a string.
+ csrf_token = six.text_type(context.get('csrf_token', ''))
+
+ return HttpResponse(csrf_token)
+
+
+class Logout(JSONView):
+ @property
+ def redirect_url(self):
+ """
+ URL to redirect users to post-login. Uses
+ ``settings.LOGOUT_REDIRECT_URL`` and defaults to ``/`` if the
+ setting isn't found.
+ """
+ return getattr(settings, 'LOGOUT_REDIRECT_URL', '/')
+
+ def post(self, request):
+ """Log the user out."""
+ auth.logout(request)
+
+ return JSONResponse({
+ 'redirect': _get_next(self.request) or self.redirect_url
+ })
diff --git a/vendor-local/lib/python/docs/Makefile b/vendor-local/lib/python/docs/Makefile
new file mode 100644
index 000000000..45da08b57
--- /dev/null
+++ b/vendor-local/lib/python/docs/Makefile
@@ -0,0 +1,130 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
+
+help:
+ @echo "Please use \`make ' where is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-browserid.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-browserid.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/django-browserid"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-browserid"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ make -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/vendor-local/lib/python/docs/__init__.py b/vendor-local/lib/python/docs/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/vendor-local/lib/python/docs/api/javascript.rst b/vendor-local/lib/python/docs/api/javascript.rst
new file mode 100644
index 000000000..80f535ea1
--- /dev/null
+++ b/vendor-local/lib/python/docs/api/javascript.rst
@@ -0,0 +1,97 @@
+JavaScript API
+==============
+Normally, you simply include ``browserid/api.js`` and
+``browserid/browserid.js`` on a page, and buttons generated by the
+:ref:`template helpers` will just work. If, however, you want
+more control, you can use the JavaScript API, defined in ``api.js`` directly.
+
+For example, if you wanted to trigger login and show a message when there is
+an error:
+
+.. code-block:: js
+
+ $('.loginButton').click(function() {
+ django_browserid.login().then(function(verifyResult) {
+ window.location = verifyResult.redirect;
+ }, function(jqXHR) {
+ window.alert('There was an error logging in, please try again.');
+ });
+ });
+
+.. note:: See also ``browserid/browserid.js`` for an example of using the API.
+
+This part of the documentation describes the JavaScript API defined in
+``api.js``.
+
+.. js:data:: django_browserid
+
+ Global object containing the JavaScript API for interacting with
+ django-browserid.
+
+ Most functions return `jQuery Deferreds`_ for registering asynchronous
+ callbacks.
+
+ .. _`jQuery Deferreds`: https://api.jquery.com/jQuery.Deferred/
+
+
+ .. js:function:: login([requestArgs])
+
+ Retrieve an assertion and use it to log the user into your site.
+
+ :param object requestArgs: Options to pass to `navigator.id.request`_.
+ :returns: Deferred that resolves once the user has been logged in.
+
+ .. _`navigator.id.request`: https://developer.mozilla.org/en-US/docs/DOM/navigator.id.request
+
+
+ .. js:function:: logout()
+
+ Log the user out of your site.
+
+ :returns: Deferred that resolves once the user has been logged out.
+
+
+ .. js:function:: getAssertion([requestArgs])
+
+ Retrieve an assertion via BrowserID.
+
+ :returns: Deferred that resolves with the assertion once it is retrieved.
+
+
+ .. js:function:: verifyAssertion(assertion)
+
+ Verify that the given assertion is valid, and log the user in.
+
+ :param string assertion: Assertion to verify.
+ :returns: Deferred that resolves with the login view response once login
+ is complete.
+
+
+ .. js:function:: getInfo()
+
+ Fetch information from the
+ :func:`browserid_info ` tag,
+ such as the parameters for the Persona popup.
+
+ :returns: Object containing the data from the info tag.
+
+
+ .. js:function:: getCsrfToken()
+
+ Fetch a CSRF token from the
+ :attr:`CsrfToken view ` via an AJAX
+ request.
+
+ :returns: Deferred that resolves with the CSRF token.
+
+
+ .. js:function:: registerWatchHandlers([onReady])
+
+ Register callbacks with navigator.id.watch that make the API work. This
+ must be called before calling any other API methods.
+
+ :param function onReady: Callback that will be executed after the user
+ agent is ready to process login requests. This is passed as the
+ ``onready`` argument to `navigator.id.watch`_
+
+ .. _`navigator.id.watch`: https://developer.mozilla.org/docs/Web/API/navigator.id.watch
diff --git a/vendor-local/lib/python/docs/api/python.rst b/vendor-local/lib/python/docs/api/python.rst
new file mode 100644
index 000000000..09dddc8bb
--- /dev/null
+++ b/vendor-local/lib/python/docs/api/python.rst
@@ -0,0 +1,114 @@
+Python API
+==========
+This part of the documentation describes the interfaces for using
+django-browserid.
+
+
+.. py:module:: django_browserid
+
+.. _template-helpers:
+
+Template Helpers
+----------------
+.. py:module:: django_browserid.helpers
+
+Template helpers are the functions used in your templates that output HTML for
+login and logout buttons, as well as the CSS and JS tags for making the buttons
+function and display correctly.
+
+.. autofunction:: browserid_info
+
+.. autofunction:: browserid_login
+
+.. autofunction:: browserid_logout
+
+.. autofunction:: browserid_js
+
+.. autofunction:: browserid_css
+
+
+Admin Site
+----------
+.. py:module:: django_browserid.admin
+
+Admin site integration allows you to support login via django-browserid on the
+Django built-in admin interface.
+
+.. autoclass:: BrowserIDAdminSite
+ :members: include_password_form, copy_registry
+
+.. autodata:: site
+ :annotation:
+
+
+Authentication Backends
+-----------------------
+.. py:module:: django_browserid.auth
+
+There are a few different authentication backends to choose from depending on
+how you want to authenticate users.
+
+.. autoclass:: BrowserIDBackend
+ :members:
+
+.. autoclass:: LocalBrowserIDBackend
+ :show-inheritance:
+
+
+Views
+-----
+.. py:module:: django_browserid.views
+
+django-browserid works primarily through AJAX requests to the views below in
+order to log users in and out and to send information required for the login
+process, such as a CSRF token.
+
+.. autoclass:: Verify
+ :members:
+ :show-inheritance:
+
+.. autoclass:: Logout
+ :members:
+ :show-inheritance:
+
+.. autoclass:: CsrfToken
+ :members:
+ :show-inheritance:
+
+
+Signals
+-------
+.. py:module:: django_browserid.signals
+
+.. autodata:: user_created
+ :annotation:
+
+
+Exceptions
+----------
+.. autoexception:: django_browserid.base.BrowserIDException
+ :members:
+
+
+Verification
+------------
+.. py:currentmodule:: django_browserid
+
+The verification classes allow you to verify if a user-provided assertion is
+valid according to the Identity Provider specified by the user's email address.
+Generally you don't have to use these directly, but they are available for
+sites with complex authentication needs.
+
+.. autoclass:: django_browserid.RemoteVerifier
+ :members: verify
+
+.. autoclass:: django_browserid.LocalVerifier
+ :members: verify
+
+.. autoclass:: django_browserid.MockVerifier
+ :members: __init__, verify
+
+.. autoclass:: django_browserid.VerificationResult
+ :members: expires
+
+.. autofunction:: django_browserid.get_audience
diff --git a/vendor-local/lib/python/docs/conf.py b/vendor-local/lib/python/docs/conf.py
new file mode 100644
index 000000000..fc1f04b84
--- /dev/null
+++ b/vendor-local/lib/python/docs/conf.py
@@ -0,0 +1,237 @@
+# -*- coding: utf-8 -*-
+#
+# django-browserid documentation build configuration file, created by
+# sphinx-quickstart on Wed Mar 14 00:04:52 2012.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# Add project root where setup.py lives:
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+# Set up Django so we can import django_browserid.
+os.environ['DJANGO_SETTINGS_MODULE'] = 'docs.settings'
+
+# Now we can import django_browserid
+from django_browserid import __version__
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'django-browserid'
+copyright = '2014, Mozilla Foundation'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = __version__
+# The full version, including alpha/beta/rc tags.
+release = version # keeping things simple
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# Set the sort order of automatically documented members.
+autodoc_member_order = 'bysource'
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# Use ReadTheDocs theme locally, but not when built on RTD itself. RTD
+# will be confused if we specify the custom theme there.
+on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
+if not on_rtd:
+ import sphinx_rtd_theme
+ html_theme = 'sphinx_rtd_theme'
+ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+else:
+ html_theme = 'default'
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# " v documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'django-browseriddoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'django-browserid.tex', 'django-browserid Documentation',
+ 'Mozilla Foundation', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'django-browserid', 'django-browserid Documentation',
+ ['Mozilla Foundation'], 1)
+]
diff --git a/vendor-local/lib/python/docs/contributor/authors.rst b/vendor-local/lib/python/docs/contributor/authors.rst
new file mode 100644
index 000000000..2e96919c6
--- /dev/null
+++ b/vendor-local/lib/python/docs/contributor/authors.rst
@@ -0,0 +1,5 @@
+Authors
+=======
+
+
+.. include:: ../../AUTHORS.rst
diff --git a/vendor-local/lib/python/docs/contributor/changelog.rst b/vendor-local/lib/python/docs/contributor/changelog.rst
new file mode 100644
index 000000000..e7cc38f86
--- /dev/null
+++ b/vendor-local/lib/python/docs/contributor/changelog.rst
@@ -0,0 +1,5 @@
+Changelog
+=========
+
+
+.. include:: ../../CHANGELOG.rst
diff --git a/vendor-local/lib/python/docs/contributor/guidelines.rst b/vendor-local/lib/python/docs/contributor/guidelines.rst
new file mode 100644
index 000000000..ac7b6bcf3
--- /dev/null
+++ b/vendor-local/lib/python/docs/contributor/guidelines.rst
@@ -0,0 +1 @@
+.. include:: ../../CONTRIBUTING.rst
diff --git a/vendor-local/lib/python/docs/contributor/setup.rst b/vendor-local/lib/python/docs/contributor/setup.rst
new file mode 100644
index 000000000..14b84491a
--- /dev/null
+++ b/vendor-local/lib/python/docs/contributor/setup.rst
@@ -0,0 +1,83 @@
+Contributor Setup
+=================
+So you want to contribute to django-browserid? Great! We really appreciate any
+help you can give!
+
+The documentation below should help you set up a development environment and
+run the tests to ensure that your changes work properly.
+
+
+Get the code
+------------
+You can check out the code from the `github repository`_:
+
+.. code-block:: sh
+
+ git clone git://github.com/mozilla/django-browserid.git
+ cd django-browserid
+
+It is a good idea to create a `virtualenv`_ (the example here uses
+`virtualenvwrapper`_) for isolating your development environment. To create a
+virtualenv and install all development packages:
+
+.. code-block:: sh
+
+ mkvirtualenv django-browserid
+ pip install -r requirements.txt
+
+
+Running tests
+-------------
+To check if your changes break any existing functionality, you can run the
+test suite:
+
+.. code-block:: sh
+
+ ./setup.py test
+
+Before submitting a pull request, you should run the test suite in all the
+Django/Python combinations that we support. We support running the tests in all
+these combinations via tox_:
+
+.. code-block:: sh
+
+ pip install tox
+ tox
+
+
+Documenation
+------------
+If you make changes to the documentation, you can build it locally with this
+command:
+
+.. code-block:: sh
+
+ make -C docs/ html
+
+The generated files can be found in ``docs/_build/html``.
+
+
+JavaScript Tests
+----------------
+To run the JavaScript tests, you must have `node.js`_ installed. Then, use the
+npm command to install the test dependencies:
+
+.. code-block:: sh
+
+ npm install
+
+After that, you can run the JavaScript tests with the following command from
+the repo root:
+
+.. code-block:: sh
+
+ npm test
+
+
+.. _`github repository`: https://github.com/mozilla/django-browserid
+.. _virtualenv: http://www.virtualenv.org/
+.. _virtualenvwrapper: http://virtualenvwrapper.readthedocs.org/
+.. _`node.js`: https://nodejs.org/
+.. _karma: https://karma-runner.github.io/
+.. _`karma-mocha`: https://github.com/karma-runner/karma-mocha
+.. _tox: http://tox.readthedocs.org/en/latest/
diff --git a/vendor-local/lib/python/docs/index.rst b/vendor-local/lib/python/docs/index.rst
new file mode 100644
index 000000000..dd9583092
--- /dev/null
+++ b/vendor-local/lib/python/docs/index.rst
@@ -0,0 +1,65 @@
+django-browserid
+================
+Release v\ |version|. (:doc:`Quickstart `)
+
+django-browserid is a Python library that integrates BrowserID_ authentication
+into Django_.
+
+BrowserID is an open, decentralized protocol for authenticating users based on
+email addresses. django-browserid provides the necessary hooks to get Django
+to authenticate users via BrowserID. By default, django-browserid relies on
+Persona_ for the client-side JavaScript shim and for assertion verification.
+
+django-browserid is tested on Python 2.6 to 3.3 and Django 1.4 to 1.6. See
+`tox.ini`_ for more details.
+
+django-browserid depends on:
+
+- Requests_ >= 1.0.0
+- fancy_tag_ == 0.2.0
+- jQuery_ >= 1.8 (if you are using ``api.js`` and ``browserid.js``).
+
+django-browserid is a work in progress. Contributions are welcome. Feel free
+to fork_ and contribute!
+
+.. _Django: http://www.djangoproject.com/
+.. _BrowserID: https://github.com/mozilla/id-specs/blob/prod/browserid/index.md
+.. _Persona: https://persona.org
+.. _tox.ini: https://github.com/mozilla/django-browserid/blob/master/tox.ini
+.. _Requests: http://docs.python-requests.org/
+.. _fancy_tag: https://github.com/trapeze/fancy_tag
+.. _jQuery: http://jquery.com/
+.. _fork: https://github.com/mozilla/django-browserid
+
+
+User Guide
+----------
+.. toctree::
+ :maxdepth: 2
+
+ user/intro
+ user/quickstart
+ user/customization
+ user/extras
+ user/settings
+ user/deploying
+ user/upgrading
+ user/troubleshooting
+
+API Documentation
+-----------------
+.. toctree::
+ :maxdepth: 2
+
+ api/python
+ api/javascript
+
+Contributor Guide
+-----------------
+.. toctree::
+ :maxdepth: 1
+
+ contributor/setup
+ contributor/guidelines
+ contributor/changelog
+ contributor/authors
diff --git a/vendor-local/lib/python/docs/make.bat b/vendor-local/lib/python/docs/make.bat
new file mode 100644
index 000000000..af86c06b0
--- /dev/null
+++ b/vendor-local/lib/python/docs/make.bat
@@ -0,0 +1,170 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^` where ^ is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-browserid.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-browserid.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+:end
diff --git a/vendor-local/lib/python/docs/settings.py b/vendor-local/lib/python/docs/settings.py
new file mode 100644
index 000000000..691e3a6a3
--- /dev/null
+++ b/vendor-local/lib/python/docs/settings.py
@@ -0,0 +1,2 @@
+# Django settings file for building the documentation.
+SECRET_KEY = 'asdf'
diff --git a/vendor-local/lib/python/docs/user/customization.rst b/vendor-local/lib/python/docs/user/customization.rst
new file mode 100644
index 000000000..8544b14f7
--- /dev/null
+++ b/vendor-local/lib/python/docs/user/customization.rst
@@ -0,0 +1,259 @@
+Customization
+=============
+Now that you've got django-browserid installed and configured, it's time to see
+how to customize it to your needs.
+
+
+Local Assertion Verification
+----------------------------
+When a user authenticates via django-browserid, they do so by sending your site
+an assertion, which, when verified, gives you an email address for the user.
+Normally, this verification is handled by sending the assertion to a
+`verification service hosted by Mozilla`_.
+
+However, you can also verify assertions locally and avoid relying on the
+verification service. To do so, you must install PyBrowserID_. django-browserid
+checks for PyBrowserID, and if it is found, it enables the use of the
+:class:`LocalVerifier ` class.
+
+Once you've installed PyBrowserID, add the
+:class:`LocalBrowserIDBackend `
+class to your ``AUTHENTICATION_BACKENDS`` setting:
+
+.. code-block:: python
+
+ AUTHENTICATION_BACKENDS = (
+ 'django_browserid.auth.LocalBrowserIDBackend',
+ )
+
+.. note:: Because the BrowserID certificate format has not been finalized,
+ PyBrowserID may fail to verify a valid assertion if the format
+ changes. Be aware of the risks before enabling local verification.
+
+.. _`verification service hosted by Mozilla`: https://developer.mozilla.org/en-US/Persona/Remote_Verification_API
+.. _PyBrowserID: https://pypi.python.org/pypi/PyBrowserID/
+
+
+Customizing the Verify View
+---------------------------
+Many common customizations involve overriding methods on the
+:class:`Verify ` class. But how do you use a
+custom ``Verify`` subclass?
+
+You can substitute a custom verification view by setting
+:attr:`BROWSERID_VERIFY_CLASS ` to
+the import path for your view:
+
+.. code-block:: python
+
+ BROWSERID_VERIFY_CLASS = 'project.application.views.MyCustomVerifyClass'
+
+
+Customizing the Authentication Backend
+--------------------------------------
+Another common way to customize django-browserid is to subclass
+:class:`BrowserIDBackend `. To use a
+custom ``BrowserIDBackend`` class, simply use the python path to your custom
+class in the ``AUTHENTICATION_BACKENDS`` setting instead of the path to
+``BrowserIDBackend``.
+
+
+Post-login Response
+-------------------
+After logging the user in, the default view redirects the user to
+:attr:`LOGIN_REDIRECT_URL ` or
+:attr:`LOGIN_REDIRECT_URL_FAILURE `,
+depending on if login succeeded or failed. You can modify those settings to
+change where they are redirected to.
+
+.. note:: You can use ``django.core.urlresolvers.reverse_lazy`` to generate a
+ URL for these settings from a URL pattern name or function name.
+
+You can also override the
+:attr:`success_url `, you'll want to override
+:attr:`login_success ` to
+False, which will cause authentication to fail if a user signs in with an
+unrecognized email address.
+
+If you want to customize how new users are created (perhaps you want to
+generate a display name for them), you can override the
+:attr:`create_user ` method
+on ``BrowserIDBackend``:
+
+.. code-block:: python
+
+ from django_browserid.auth import BrowserIDBackend
+
+ class CustomBackend(BrowserIDBackend):
+ def create_user(self, email):
+ username = my_custom_username_algo()
+ return self.User.objects.create_user(username, email)
+
+.. note:: ``self.User`` points to the User model defined in
+ ``AUTH_USER_MODEL`` for custom User model support. See `Custom User Models`_
+ for more details.
+
+
+Limiting Authentication
+-----------------------
+There are two ways to limit who can authenticate with your site: prohibiting
+certain email addresses, or filtering the queryset that emails are compared to.
+
+filter_users_by_email
+~~~~~~~~~~~~~~~~~~~~~
+:attr:`filter_users_by_email `. If you are using a custom User model, and the model has
+an ``email`` attribute that can store email addresses, django-browserid should
+work out-of-the-box for you.
+
+If this isn't the case, then you will probably have to override the
+:attr:`is_valid_email `
+methods to work with your custom User class.
+
+.. _custom_user_model: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#specifying-a-custom-user-model
+
+
+.. _customjs:
+
+Using the JavaScript API
+------------------------
+django-browserid comes with two JavaScript files to include in your webpage:
+
+1. ``api.js``: An API for triggering logins via BrowserID and verifying
+ assertions via the server.
+
+2. ``browserid.js``: A basic example of hooking up links with the JavaScript
+ API.
+
+``browserid.js`` only covers basic use cases. If your site has more complex
+behavior behind trigger login, you should replace ``browserid.js`` in your
+templates with your own JavaScript file that uses the django-browserid
+JavaScript API.
+
+.. seealso::
+
+ :js:data:`JavaScript API `
+ API Documentation for ``api.js``.
+
+
+Django Admin Support
+--------------------
+If you want to use BrowserID for login on the built-in Django admin interface,
+you must use the
+:data:`django-browserid admin site ` instead of
+the default Django admin site:
+
+.. code-block:: python
+
+ from django.contrib import admin
+
+ from django_browserid.admin import site as browserid_admin
+
+ from myapp.foo.models import Bar
+
+
+ class BarAdmin(admin.ModelAdmin):
+ pass
+ browserid_admin.register(Bar, BarAdmin)
+
+You must also use the django-browserid admin site in your ``urls.py`` file:
+
+.. code-block:: python
+
+ from django.conf.urls import patterns, include, url
+
+ # Autodiscover admin.py files in your project.
+ from django.contrib import admin
+ admin.autodiscover()
+
+ # copy_registry copies ModelAdmins registered with the default site, like
+ # the built-in Django User model.
+ from django_browserid.admin import site as browserid_admin
+ browserid_admin.copy_registry(admin.site)
+
+ urlpatterns = patterns('',
+ # ...
+ url(r'^admin/', include(browserid_admin.urls)),
+ )
+
+.. seealso::
+
+ :class:`django_browserid.admin.BrowserIDAdminSite`
+ API documentation for BrowserIDAdminSite, including how to customize the
+ login page (such as including a normal login alongside BrowserID login).
+
+
+Alternative Template Languages
+------------------------------
+By default, django-browserid supports use in Django templates as well as use in
+Jinja2_ templates via the jingo_ library. Template helpers are registered as
+helper functions with jingo, so you can use them directly in Jinja2 templates:
+
+.. code-block:: jinja
+
+
+ {% if user.is_authenticated() %}
+ {{ browserid_logout(text='Logout') }}
+ {% else %}
+ {{ browserid_login(text='Login', color='dark') }}
+ {% endif %}
+
+ {{ browserid_js() }}
+
+For other libraries or template languages, you will have to register the
+django-browserid helpers manually. The relevant helper functions can be found
+in the :py:mod:`django_browserid.helpers` module.
+
+.. _Jinja2: http://jinja.pocoo.org/
+.. _jingo: https://github.com/jbalogh/jingo
diff --git a/vendor-local/lib/python/docs/user/deploying.rst b/vendor-local/lib/python/docs/user/deploying.rst
new file mode 100644
index 000000000..f769d5872
--- /dev/null
+++ b/vendor-local/lib/python/docs/user/deploying.rst
@@ -0,0 +1,15 @@
+Deploying in Production
+=======================
+Deploying django-browserid in a production environment requires a few extra
+changes from the setup described in the :doc:`Quickstart `:
+
+- The :attr:`BROWSERID_AUDIENCES `
+ setting is required when ``DEBUG`` is set to False. Ensure that all the
+ domains that users will access your site from are listed in this setting.
+
+- `Optional`: It is a good idea to minify the static JS and CSS files you're
+ using. `django-compressor`_ and `jingo-minify`_ are examples of libraries
+ you can use for minification.
+
+.. _django-compressor: http://django-compressor.readthedocs.org/en/latest/
+.. _jingo-minify: https://github.com/jsocol/jingo-minify
diff --git a/vendor-local/lib/python/docs/user/extras.rst b/vendor-local/lib/python/docs/user/extras.rst
new file mode 100644
index 000000000..785edc5eb
--- /dev/null
+++ b/vendor-local/lib/python/docs/user/extras.rst
@@ -0,0 +1,60 @@
+Extras
+======
+django-browserid comes with a few extra pieces to make development easier.
+They're documented below.
+
+
+.. _offline-development:
+
+Offline Development
+-------------------
+Because django-browsered :ref:`relies on the Persona service
+`, offline development is not supported by default.
+To work around this, django-browserid includes an auto-login system that lets
+you specify an email to log the user in with when they click a login button.
+
+.. warning:: Auto-login is a huge security hole as it bypasses authentication.
+ Only use it for local development on your own computer; **never**
+ use it on a publicly-visible machine or your live, production
+ website.
+
+
+Enable auto-login
+~~~~~~~~~~~~~~~~~
+
+To enable auto-login:
+
+1. Add the ``AutoLoginBackend`` class to the ``AUTHENTICATION_BACKENDS`` setting.
+2. Set :attr:`BROWSERID_AUTOLOGIN_EMAIL `
+ to the email you want to be logged in as.
+3. Set :attr:`BROWSERID_AUTOLOGIN_ENABLED `
+ to ``True``.
+4. If you are not using
+ :py:func:`browserid_js template helper `,
+ you have to manually add ``browserid/autologin.js`` to your site.
+
+For example:
+
+.. code-block:: python
+
+ AUTHENTICATION_BACKENDS = (
+ 'django_browserid.auth.AutoLoginBackend',
+ 'django_browserid.auth.BrowserIDBackend', # After auto-login.
+ )
+
+ BROWSERID_AUTOLOGIN_EMAIL = 'bob@example.com'
+ BROWSERID_AUTOLOGIN_ENABLED = True
+
+Once these are set, any login button that uses the :doc:`JavaScript API
+` will not attempt to show the Persona popup, and will
+immediately log you in with the email you set above.
+
+
+Disable auto-login
+~~~~~~~~~~~~~~~~~~
+
+To disable auto-login:
+
+1. Set :attr:`BROWSERID_AUTOLOGIN_ENABLED `
+ to ``False``.
+2. If you added ``browserid/autologin.js`` to your site, you must remove it.
diff --git a/vendor-local/lib/python/docs/user/intro.rst b/vendor-local/lib/python/docs/user/intro.rst
new file mode 100644
index 000000000..23bf1c5ef
--- /dev/null
+++ b/vendor-local/lib/python/docs/user/intro.rst
@@ -0,0 +1,58 @@
+Introduction
+============
+
+How does it work?
+-----------------
+At a high level, this is what happens when a user wants to log into a site that
+uses django-browserid:
+
+1. A user clicks a login button on your web page.
+2. The JavaScript shim (hosted by Persona_) displays a pop-up asking for the
+ email address the user wants to log in with.
+3. If necessary, the pop-up prompts the user for additional info to
+ authenticate them. For example, if the user enters an `@mozilla.com` email,
+ the Mozilla LDAP Identity Provider will prompt them for their LDAP password.
+4. The JavaScript receives an "assertion" from the Identity Provider and
+ submits it to the site's backend via AJAX.
+5. The backend sends the assertion to the `Remote verification service`_, which
+ verifies the assertion and returns the result, including the email address
+ of the user if verification was successful.
+6. The backend finds a user account matching that email (creating it if one
+ isn't found) and logs the user in as that account.
+7. The backend returns a URL that the JavaScript redirects the user to.
+
+Note that this is just an example flow. Several of these steps can be
+customized for your site; for example, you may not want user accounts to be
+created automatically. This behavior can be changed to suit whatever needs you
+have.
+
+A `detailed explanation of the BrowserID protocol`_ is available on MDN.
+
+.. _`detailed explanation of the BrowserID protocol`: https://developer.mozilla.org/Persona/Protocol_Overview
+.. _Persona: https://www.persona.org
+.. _`Remote Verification Service`: https://developer.mozilla.org/Persona/Remote_Verification_API
+
+
+.. _persona-dependence:
+
+Persona
+-------
+By default, django-browserid relies on Persona, which is a set of
+BrowserID-related services hosted by Mozilla. It's possible, but annoying, to
+use django-browserid without these dependencies.
+
+Currently, django-browserid relies on Persona for:
+
+- The `Cross-browser API Library`_, which implements the ``navigator.id`` API
+ for browsers that don't natively support BrowserID.
+- The `Fallback Identity Provider`_ for emails from servers that don't support
+ BrowserID.
+- The `Remote verification service`_, which handles assertion verification for
+ sites that don't want to verify assertions themselves.
+
+In the future, django-browserid will remove the need to depend on these
+Mozilla-centric services. Local verification and a self-hosted cross-browser
+API will greatly reduce the reliance on Mozilla's servers for authentication.
+
+.. _`Cross-browser API Library`: https://developer.mozilla.org/Persona/Bootstrapping_Persona#Cross-browser_API_Library
+.. _`Fallback Identity Provider`: https://developer.mozilla.org/Persona/Bootstrapping_Persona#Fallback_Identity_Provider
diff --git a/vendor-local/lib/python/docs/user/quickstart.rst b/vendor-local/lib/python/docs/user/quickstart.rst
new file mode 100644
index 000000000..940bc1225
--- /dev/null
+++ b/vendor-local/lib/python/docs/user/quickstart.rst
@@ -0,0 +1,116 @@
+Quickstart
+==========
+Follow these instructions to get set up with a basic install of
+django-browserid:
+
+Installation
+------------
+You can use pip to install django-browserid and requirements:
+
+.. code-block:: sh
+
+ $ pip install django-browserid
+
+
+Configuration
+-------------
+After installation, you'll need to configure your site to use django-browserid.
+Start by making the following changes to your ``settings.py`` file:
+
+.. code-block:: python
+
+ # Add 'django_browserid' to INSTALLED_APPS.
+ INSTALLED_APPS = (
+ # ...
+ 'django.contrib.auth',
+ 'django_browserid', # Load after auth
+ # ...
+ )
+
+ # Add the django_browserid authentication backend.
+ AUTHENTICATION_BACKENDS = (
+ # ...
+ 'django.contrib.auth.backends.ModelBackend',
+ 'django_browserid.auth.BrowserIDBackend',
+ # ...
+ )
+
+Next, edit your ``urls.py`` file and add the following:
+
+.. code-block:: python
+
+ urlpatterns = patterns('',
+ # ...
+ (r'', include('django_browserid.urls')),
+ # ...
+ )
+
+.. note:: The django-browserid urlconf *must not* have a regex with the
+ include. Use a blank string, as shown above.
+
+Finally, you'll need to add the login button and info tag to your Django
+templates, along with the CSS and JS files necessary to make it work:
+
+.. code-block:: html+django
+
+ {% load browserid %}
+
+
+
+
+
+ {% browserid_info %}
+ {% if user.is_authenticated %}
+ Current user: {{ user.email }}
+ {% browserid_logout text='Logout' %}
+ {% else %}
+ {% browserid_login text='Login' color='dark' %}
+ {% endif %}
+
+
+
+
+
+
+
+
+.. note:: ``api.js`` and ``browserid.js`` require `jQuery`_ 1.8 or higher.
+
+.. note:: The ``browserid_info`` tag is required on any page that users can log
+ in from. It's recommended to put it just below the ```` tag.
+
+And that's it! You can now log into your site using Persona!
+
+Once you're ready, you should check out :doc:`how to customize django-browserid
+` to your liking.
+
+.. _jQuery: http://jquery.com/
+
+
+Note for Jinja2 / Jingo Users
+-----------------------------
+If you're using Jinja2_ via jingo_, here's a version of the example above
+written in Jinja2:
+
+.. code-block:: jinja
+
+
+
+ {{ browserid_css() }}
+
+
+ {{ browserid_info() }}
+ {% if user.is_authenticated() %}
+ Current user: {{ user.email }}
+ {{ browserid_logout(text='Logout') }}
+ {% else %}
+ {{ browserid_login(text='Login', color='dark') }}
+ {% endif %}
+
+
+ {{ browserid_js() }}
+
+
+
+.. _Jinja2: http://jinja.pocoo.org/
+.. _jingo: https://github.com/jbalogh/jingo
diff --git a/vendor-local/lib/python/docs/user/settings.rst b/vendor-local/lib/python/docs/user/settings.rst
new file mode 100644
index 000000000..099dd412b
--- /dev/null
+++ b/vendor-local/lib/python/docs/user/settings.rst
@@ -0,0 +1,124 @@
+Settings
+========
+.. currentmodule:: django.conf.settings
+
+This document describes the Django settings that can be used to customize the
+behavior of django-browserid.
+
+
+Core Settings
+-------------
+.. attribute:: BROWSERID_AUDIENCES
+
+ :default: No default
+
+ List of audiences that your site accepts. An audience is the protocol,
+ domain name, and (optionally) port that users access your site from. This
+ list is used to determine the audience a user is part of (how they are
+ accessing your site), which is used during verification to ensure that the
+ assertion given to you by the user was intended for your site.
+
+ Without this, other sites that the user has authenticated with via Persona
+ could use their assertions to impersonate the user on your site.
+
+ Note that this does not have to be a publicly accessible URL, so local URLs
+ like ``http://localhost:8000`` or ``http://127.0.0.1`` are acceptable as
+ long as they match what you are using to access your site.
+
+
+Redirect URLs
+-------------
+.. note:: If you want to use named URLs instead of directly including URLs into
+ your settings file, you can use `reverse_lazy`_ to do so.
+
+.. attribute:: LOGIN_REDIRECT_URL
+
+ :default: ``'/accounts/profile'``
+
+ Path to redirect to on successful login. If you don't specify this, the
+ default Django value will be used.
+
+.. attribute:: LOGIN_REDIRECT_URL_FAILURE
+
+ :default: ``'/'``
+
+ Path to redirect to on an unsuccessful login attempt.
+
+.. attribute:: LOGOUT_REDIRECT_URL
+
+ :default: ``'/'``
+
+ Path to redirect to on logout.
+
+.. _reverse_lazy: https://docs.djangoproject.com/en/dev/ref/urlresolvers/#reverse-lazy
+
+
+Customizing the Login Popup
+---------------------------
+.. attribute:: BROWSERID_REQUEST_ARGS
+
+ :default: ``{}``
+
+ Controls the arguments passed to ``navigator.id.request``, which are used to
+ customize the login popup box. To see a list of valid keys and what they do,
+ check out the `navigator.id.request documentation`_.
+
+ .. _navigator.id.request documentation: https://developer.mozilla.org/en-US/docs/DOM/navigator.id.request
+
+
+Customizing the Verify View
+---------------------------
+.. attribute:: BROWSERID_VERIFY_CLASS
+
+ :default: ``django_browserid.views.Verify``
+
+ Allows you to substitute a custom class-based view for verifying assertions.
+ For example, the string 'myapp.users.views.Verify' would import `Verify`
+ from `myapp.users.views` and use it in place of the default view.
+
+ When using a custom view, it is generally a good idea to subclass the
+ default Verify and override the methods you want to change.
+
+.. attribute:: BROWSERID_CREATE_USER
+
+ :default: ``True``
+
+ If ``True`` or ``False``, enables or disables automatic user creation during
+ authentication. If set to a string, it is treated as an import path
+ pointing to a custom user creation function.
+
+.. attribute:: BROWSERID_DISABLE_SANITY_CHECKS
+
+ :default: False
+
+ Controls whether the ``Verify`` view performs some helpful checks for common
+ mistakes. Useful if you're getting warnings for things you know aren't
+ errors.
+
+
+Using a Different Identity Provider
+-----------------------------------
+.. attribute:: BROWSERID_SHIM
+
+ :default: 'https://login.persona.org/include.js'
+
+ The URL to use for the BrowserID JavaScript shim.
+
+
+Extras
+------
+.. attribute:: BROWSERID_AUTOLOGIN_ENABLED
+
+ :default: ``False``
+
+ If ``True``, enables auto-login. You must also set the auto-login email and
+ authentication backend for auto-login to function. See the documentation on
+ :ref:`offline development ` for more info.
+
+.. attribute:: BROWSERID_AUTOLOGIN_EMAIL
+
+ :default: Not set
+
+ The email to log users in as when auto-login is enabled. See the
+ documentation on :ref:`offline development ` for more
+ info.
diff --git a/vendor-local/lib/python/docs/user/troubleshooting.rst b/vendor-local/lib/python/docs/user/troubleshooting.rst
new file mode 100644
index 000000000..a962e0269
--- /dev/null
+++ b/vendor-local/lib/python/docs/user/troubleshooting.rst
@@ -0,0 +1,161 @@
+Troubleshooting
+===============
+If you are having trouble getting django-browserid to work properly, try
+reading through the sections below for help on dealing with common issues.
+
+
+Logging Errors
+--------------
+Before you do anything else, check to see if django-browserid is logging issues
+by setting up a logger for ``django_browserid`` in your logging config. Here's
+a sample config that will log messages from django-browserid to the console:
+
+.. code-block:: python
+
+ LOGGING = {
+ 'version': 1,
+ 'handlers': {
+ 'console':{
+ 'level': 'DEBUG',
+ 'class': 'logging.StreamHandler'
+ },
+ },
+ 'loggers': {
+ 'django_browserid': {
+ 'handlers': ['console'],
+ 'level': 'DEBUG',
+ }
+ },
+ }
+
+
+If you recently updated...
+--------------------------
+If you are hitting problems after updating django-browserid, check to make sure
+your installed copy matches the tagged version on Github. In particular,
+leftover ``*.pyc`` files may cause unintended side effects. This is common when
+installing without using a package manager like ``pip``.
+
+
+Nothing happens when clicking the login button
+----------------------------------------------
+If nothing happens when you click the login button on your website, check that
+you've included ``api.js`` and ``browserid.js`` on your webpage:
+
+.. code-block:: html+django
+
+
+
+
+
+CSP WARN: Directive "..." violated by https://browserid.org/include.js
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+You may see this warning in your browser's error console when your site uses
+`Content Security Policy`_ without making an exception for the persona.org
+external JavaScript include.
+
+To fix this, include https://login.persona.org in your script-src and frame-src
+directive. If you're using the `django-csp`_ library, the following settings
+will work::
+
+ CSP_SCRIPT_SRC = ("'self'", 'https://login.persona.org')
+ CSP_FRAME_SRC = ("'self'", 'https://login.persona.org')
+
+.. _Content Security Policy: https://developer.mozilla.org/en/Security/CSP
+.. _django-csp: https://github.com/mozilla/django-csp
+
+
+Login fails silently after the Persona popup closes
+---------------------------------------------------
+There are a few reasons why login may fail without an error message after the
+Persona popup closes:
+
+SESSION_COOKIE_SECURE is False
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+`SESSION_COOKIE_SECURE` controls if the `secure` flag is set on the session
+cookie. If set to True for site running in an environment that doesn't use
+HTTPS, the session cookie won't be sent by your browser because you're using an
+HTTP connection.
+
+The solution is to set `SESSION_COOKIE_SECURE` to False on your local instance
+in your settings file:
+
+.. code-block:: python
+
+ SESSION_COOKIE_SECURE = False
+
+No cache configured
+~~~~~~~~~~~~~~~~~~~
+Several projects (especially projects based on playdoh_, which uses
+`django-session-csrf`_) store session info in the cache rather than the
+database, and if your local instance has no cache configured, the session
+information will not be stored and login will fail silently.
+
+To solve this issue, you should configure your local instance to use an
+in-memory cache with the following in your local settings file::
+
+ CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ 'LOCATION': 'unique-snowflake'
+ }
+ }
+
+.. _playdoh: https://github.com/mozilla/playdoh
+.. _django-session-csrf: https://github.com/mozilla/django-session-csrf
+
+
+Login fails with an error message on a valid account
+----------------------------------------------------
+If you see a login error page after attempting to login, but you know that
+your Persona account is valid and should be able to login, check for these
+issues:
+
+Your website uses HTTPS but django-browserid thinks it's using HTTP
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+If you are using django-browserid behind a load balancer that uses HTTP
+internally for your SSL connections, you may experience failed logins. The
+``request.is_secure()`` method determines if a request is using HTTPS by
+checking for the header specified by the `SECURE_PROXY_SSL_HEADER`_ setting. If
+this is unset or the header is missing, Django assumes the request uses HTTP.
+
+Because the audiences stored in
+:attr:`BROWSERID_AUDIENCES ` include
+the protocol used to access the site, you may get an error when
+django-browserid checks the audiences against the URL from the request due to
+the request thinking it's not using SSL when it is.
+
+Make sure that ``SECURE_PROXY_SSL_HEADER`` is set to an appropriate value for
+your load balancer. An example configuration using nginx_ might look like this:
+
+.. code-block:: python
+
+ # settings.py
+ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
+
+.. code-block:: nginx
+
+ # nginx config
+ location / {
+ proxy_pass http://127.0.0.1:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Protocol https; # Tell django we're using https
+ }
+
+.. _SECURE_PROXY_SSL_HEADER: https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header
+.. _nginx: http://wiki.nginx.org/
+
+
+Still having issues? Ask for help!
+----------------------------------
+If your issue isn't listed above and you're having trouble tracking it down,
+you can try asking for help from:
+
+- The #webdev channel on `irc.mozilla.org`_,
+- The `dev-webdev@lists.mozilla.org`_ mailing list,
+- or by emailing :doc:`the maintainers ` directly.
+
+.. _irc.mozilla.org: http://irc.mozilla.org
+.. _dev-webdev@lists.mozilla.org: https://lists.mozilla.org/listinfo/dev-webdev
diff --git a/vendor-local/lib/python/docs/user/upgrading.rst b/vendor-local/lib/python/docs/user/upgrading.rst
new file mode 100644
index 000000000..fb3ef5b4f
--- /dev/null
+++ b/vendor-local/lib/python/docs/user/upgrading.rst
@@ -0,0 +1,87 @@
+Upgrading
+=========
+If you're looking to upgrade from an older version of django-browserid, you're
+in the right place. This document describes the major changes required to get
+your site up to the latest and greatest!
+
+
+0.10.1 to 0.11
+--------------
+No changes are necessary to switch from 0.10.1 to 0.11.
+
+
+0.9 to 0.10.1
+-------------
+- The minimum supported version of requests is now 1.0.0, and six has been
+ removed from the requirements.
+
+- Replace the ``SITE_URL`` setting with ``BROWSERID_AUDIENCES``, which is
+ essentially the same setting, but must be a list of strings (wrapping your
+ old ``SITE_URL`` value with square brackets to make it a list is fine):
+
+ .. code-block:: python
+
+ BROWSERID_AUDIENCES = ['https://www.example.com']
+
+ - On local development installs, you can remove ``SITE_URL`` entirely, as
+ ``BROWSERID_AUDIENCES`` isn't required when ``DEBUG`` is True.
+
+- In your root urlconf, remove any regex in front of the include for
+ django-browserid urls. Because the new JavaScript relies on views being
+ available at certain URLs, you must not change the path that the
+ django-browserid views are served:
+
+ .. code-block:: python
+
+ urlpatterns = patterns('',
+ # ...
+ (r'', include('django_browserid.urls')),
+ # ...
+ )
+
+- Remove ``django_browserid.context_processors.browserid`` from your
+ ``TEMPLATE_CONTEXT_PROCESSORS`` setting, as the context processor no longer
+ exists.
+
+- ``browserid.js`` has been split into ``api.js``, which contains just the
+ JavaScript API, and ``browserid.js``, which contains the sample code for
+ hooking up login buttons. If you aren't using the ``browserid_js`` helper to
+ include the JavaScript on the page, you probably need to update your project
+ to either include both or just ``api.js``.
+
+- The included JavaScript requires jQuery 1.8 or higher instead of jQuery 1.7.
+
+
+0.8 to 0.9
+----------
+- Six v1.3 or higher is now required.
+
+
+0.7.1 to 0.8
+------------
+- fancy_tag 0.2.0 has been added to the required libraries.
+
+- Rename the ``browserid_form`` context processor to ``browserid`` in the
+ ``TEMPLATE_CONTEXT_PROCESSORS`` setting:
+
+ .. code-block:: python
+
+ TEMPLATE_CONTEXT_PROCESSORS = (
+ # ...
+ 'django_browserid.context_processors.browserid',
+ # ...
+ )
+
+- Replace custom login button code with the new template helpers,
+ ``browserid_info``, ``browserid_login``, and ``browserid_logout``.
+
+ - ``browserid_info`` should be added just below ```` on any page that
+ includes a login button.
+
+ - ``browserid_login`` and ``browserid_logout`` output login and logout links
+ respectively.
+
+- It's now recommended to include the JavaScript for the login buttons using
+ the ``browserid_js`` helper, which outputs the appropriate ``