intranet-pictures 0.0.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/lib/intranet/pictures/json_db_provider.rb +196 -0
  3. data/lib/intranet/pictures/responder.rb +247 -0
  4. data/lib/intranet/pictures/version.rb +1 -1
  5. data/lib/intranet/resources/haml/pictures_browse.haml +14 -0
  6. data/lib/intranet/resources/haml/pictures_home.haml +36 -0
  7. data/lib/intranet/resources/haml/pictures_photoswipe.haml +23 -0
  8. data/lib/intranet/resources/locales/en.yml +28 -0
  9. data/lib/intranet/resources/locales/fr.yml +28 -0
  10. data/lib/intranet/resources/www/group_thumbnail.jpg +0 -0
  11. data/lib/intranet/resources/www/jpictures.js +42 -0
  12. data/lib/intranet/resources/www/photoswipe/LICENSE +21 -0
  13. data/lib/intranet/resources/www/photoswipe/default-skin/default-skin.css +484 -0
  14. data/lib/intranet/resources/www/photoswipe/default-skin/default-skin.png +0 -0
  15. data/lib/intranet/resources/www/photoswipe/default-skin/default-skin.svg +1 -0
  16. data/lib/intranet/resources/www/photoswipe/default-skin/preloader.gif +0 -0
  17. data/lib/intranet/resources/www/photoswipe/photoswipe-ui-default.js +861 -0
  18. data/lib/intranet/resources/www/photoswipe/photoswipe-ui-default.min.js +4 -0
  19. data/lib/intranet/resources/www/photoswipe/photoswipe.css +179 -0
  20. data/lib/intranet/resources/www/photoswipe/photoswipe.js +3734 -0
  21. data/lib/intranet/resources/www/photoswipe/photoswipe.min.js +4 -0
  22. data/lib/intranet/resources/www/style.css +81 -0
  23. data/spec/intranet/pictures/alpha.png +0 -0
  24. data/spec/intranet/pictures/json_db_provider_spec.rb +273 -0
  25. data/spec/intranet/pictures/responder_spec.rb +499 -0
  26. data/spec/intranet/pictures/sample-db.json +71 -0
  27. data/spec/intranet/pictures/white.jpg +0 -0
  28. data/spec/spec_helper.rb +6 -2
  29. metadata +52 -13
@@ -0,0 +1,4 @@
1
+ /*! PhotoSwipe - v4.1.3 - 2019-01-08
2
+ * http://photoswipe.com
3
+ * Copyright (c) 2019 Dmitry Semenov; */
4
+ !function(a,b){"function"==typeof define&&define.amd?define(b):"object"==typeof exports?module.exports=b():a.PhotoSwipe=b()}(this,function(){"use strict";var a=function(a,b,c,d){var e={features:null,bind:function(a,b,c,d){var e=(d?"remove":"add")+"EventListener";b=b.split(" ");for(var f=0;f<b.length;f++)b[f]&&a[e](b[f],c,!1)},isArray:function(a){return a instanceof Array},createEl:function(a,b){var c=document.createElement(b||"div");return a&&(c.className=a),c},getScrollY:function(){var a=window.pageYOffset;return void 0!==a?a:document.documentElement.scrollTop},unbind:function(a,b,c){e.bind(a,b,c,!0)},removeClass:function(a,b){var c=new RegExp("(\\s|^)"+b+"(\\s|$)");a.className=a.className.replace(c," ").replace(/^\s\s*/,"").replace(/\s\s*$/,"")},addClass:function(a,b){e.hasClass(a,b)||(a.className+=(a.className?" ":"")+b)},hasClass:function(a,b){return a.className&&new RegExp("(^|\\s)"+b+"(\\s|$)").test(a.className)},getChildByClass:function(a,b){for(var c=a.firstChild;c;){if(e.hasClass(c,b))return c;c=c.nextSibling}},arraySearch:function(a,b,c){for(var d=a.length;d--;)if(a[d][c]===b)return d;return-1},extend:function(a,b,c){for(var d in b)if(b.hasOwnProperty(d)){if(c&&a.hasOwnProperty(d))continue;a[d]=b[d]}},easing:{sine:{out:function(a){return Math.sin(a*(Math.PI/2))},inOut:function(a){return-(Math.cos(Math.PI*a)-1)/2}},cubic:{out:function(a){return--a*a*a+1}}},detectFeatures:function(){if(e.features)return e.features;var a=e.createEl(),b=a.style,c="",d={};if(d.oldIE=document.all&&!document.addEventListener,d.touch="ontouchstart"in window,window.requestAnimationFrame&&(d.raf=window.requestAnimationFrame,d.caf=window.cancelAnimationFrame),d.pointerEvent=!!window.PointerEvent||navigator.msPointerEnabled,!d.pointerEvent){var f=navigator.userAgent;if(/iP(hone|od)/.test(navigator.platform)){var g=navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/);g&&g.length>0&&(g=parseInt(g[1],10),g>=1&&g<8&&(d.isOldIOSPhone=!0))}var h=f.match(/Android\s([0-9\.]*)/),i=h?h[1]:0;i=parseFloat(i),i>=1&&(i<4.4&&(d.isOldAndroid=!0),d.androidVersion=i),d.isMobileOpera=/opera mini|opera mobi/i.test(f)}for(var j,k,l=["transform","perspective","animationName"],m=["","webkit","Moz","ms","O"],n=0;n<4;n++){c=m[n];for(var o=0;o<3;o++)j=l[o],k=c+(c?j.charAt(0).toUpperCase()+j.slice(1):j),!d[j]&&k in b&&(d[j]=k);c&&!d.raf&&(c=c.toLowerCase(),d.raf=window[c+"RequestAnimationFrame"],d.raf&&(d.caf=window[c+"CancelAnimationFrame"]||window[c+"CancelRequestAnimationFrame"]))}if(!d.raf){var p=0;d.raf=function(a){var b=(new Date).getTime(),c=Math.max(0,16-(b-p)),d=window.setTimeout(function(){a(b+c)},c);return p=b+c,d},d.caf=function(a){clearTimeout(a)}}return d.svg=!!document.createElementNS&&!!document.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect,e.features=d,d}};e.detectFeatures(),e.features.oldIE&&(e.bind=function(a,b,c,d){b=b.split(" ");for(var e,f=(d?"detach":"attach")+"Event",g=function(){c.handleEvent.call(c)},h=0;h<b.length;h++)if(e=b[h])if("object"==typeof c&&c.handleEvent){if(d){if(!c["oldIE"+e])return!1}else c["oldIE"+e]=g;a[f]("on"+e,c["oldIE"+e])}else a[f]("on"+e,c)});var f=this,g=25,h=3,i={allowPanToNext:!0,spacing:.12,bgOpacity:1,mouseUsed:!1,loop:!0,pinchToClose:!0,closeOnScroll:!0,closeOnVerticalDrag:!0,verticalDragRange:.75,hideAnimationDuration:333,showAnimationDuration:333,showHideOpacity:!1,focus:!0,escKey:!0,arrowKeys:!0,mainScrollEndFriction:.35,panEndFriction:.35,isClickableElement:function(a){return"A"===a.tagName},getDoubleTapZoom:function(a,b){return a?1:b.initialZoomLevel<.7?1:1.33},maxSpreadZoom:1.33,modal:!0,scaleMode:"fit"};e.extend(i,d);var j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,$,_,aa,ba,ca,da,ea,fa,ga,ha,ia,ja,ka,la,ma=function(){return{x:0,y:0}},na=ma(),oa=ma(),pa=ma(),qa={},ra=0,sa={},ta=ma(),ua=0,va=!0,wa=[],xa={},ya=!1,za=function(a,b){e.extend(f,b.publicMethods),wa.push(a)},Aa=function(a){var b=ac();return a>b-1?a-b:a<0?b+a:a},Ba={},Ca=function(a,b){return Ba[a]||(Ba[a]=[]),Ba[a].push(b)},Da=function(a){var b=Ba[a];if(b){var c=Array.prototype.slice.call(arguments);c.shift();for(var d=0;d<b.length;d++)b[d].apply(f,c)}},Ea=function(){return(new Date).getTime()},Fa=function(a){ja=a,f.bg.style.opacity=a*i.bgOpacity},Ga=function(a,b,c,d,e){(!ya||e&&e!==f.currItem)&&(d/=e?e.fitRatio:f.currItem.fitRatio),a[E]=u+b+"px, "+c+"px"+v+" scale("+d+")"},Ha=function(a){ea&&(a&&(s>f.currItem.fitRatio?ya||(mc(f.currItem,!1,!0),ya=!0):ya&&(mc(f.currItem),ya=!1)),Ga(ea,pa.x,pa.y,s))},Ia=function(a){a.container&&Ga(a.container.style,a.initialPosition.x,a.initialPosition.y,a.initialZoomLevel,a)},Ja=function(a,b){b[E]=u+a+"px, 0px"+v},Ka=function(a,b){if(!i.loop&&b){var c=m+(ta.x*ra-a)/ta.x,d=Math.round(a-tb.x);(c<0&&d>0||c>=ac()-1&&d<0)&&(a=tb.x+d*i.mainScrollEndFriction)}tb.x=a,Ja(a,n)},La=function(a,b){var c=ub[a]-sa[a];return oa[a]+na[a]+c-c*(b/t)},Ma=function(a,b){a.x=b.x,a.y=b.y,b.id&&(a.id=b.id)},Na=function(a){a.x=Math.round(a.x),a.y=Math.round(a.y)},Oa=null,Pa=function(){Oa&&(e.unbind(document,"mousemove",Pa),e.addClass(a,"pswp--has_mouse"),i.mouseUsed=!0,Da("mouseUsed")),Oa=setTimeout(function(){Oa=null},100)},Qa=function(){e.bind(document,"keydown",f),N.transform&&e.bind(f.scrollWrap,"click",f),i.mouseUsed||e.bind(document,"mousemove",Pa),e.bind(window,"resize scroll orientationchange",f),Da("bindEvents")},Ra=function(){e.unbind(window,"resize scroll orientationchange",f),e.unbind(window,"scroll",r.scroll),e.unbind(document,"keydown",f),e.unbind(document,"mousemove",Pa),N.transform&&e.unbind(f.scrollWrap,"click",f),V&&e.unbind(window,p,f),clearTimeout(O),Da("unbindEvents")},Sa=function(a,b){var c=ic(f.currItem,qa,a);return b&&(da=c),c},Ta=function(a){return a||(a=f.currItem),a.initialZoomLevel},Ua=function(a){return a||(a=f.currItem),a.w>0?i.maxSpreadZoom:1},Va=function(a,b,c,d){return d===f.currItem.initialZoomLevel?(c[a]=f.currItem.initialPosition[a],!0):(c[a]=La(a,d),c[a]>b.min[a]?(c[a]=b.min[a],!0):c[a]<b.max[a]&&(c[a]=b.max[a],!0))},Wa=function(){if(E){var b=N.perspective&&!G;return u="translate"+(b?"3d(":"("),void(v=N.perspective?", 0px)":")")}E="left",e.addClass(a,"pswp--ie"),Ja=function(a,b){b.left=a+"px"},Ia=function(a){var b=a.fitRatio>1?1:a.fitRatio,c=a.container.style,d=b*a.w,e=b*a.h;c.width=d+"px",c.height=e+"px",c.left=a.initialPosition.x+"px",c.top=a.initialPosition.y+"px"},Ha=function(){if(ea){var a=ea,b=f.currItem,c=b.fitRatio>1?1:b.fitRatio,d=c*b.w,e=c*b.h;a.width=d+"px",a.height=e+"px",a.left=pa.x+"px",a.top=pa.y+"px"}}},Xa=function(a){var b="";i.escKey&&27===a.keyCode?b="close":i.arrowKeys&&(37===a.keyCode?b="prev":39===a.keyCode&&(b="next")),b&&(a.ctrlKey||a.altKey||a.shiftKey||a.metaKey||(a.preventDefault?a.preventDefault():a.returnValue=!1,f[b]()))},Ya=function(a){a&&(Y||X||fa||T)&&(a.preventDefault(),a.stopPropagation())},Za=function(){f.setScrollOffset(0,e.getScrollY())},$a={},_a=0,ab=function(a){$a[a]&&($a[a].raf&&I($a[a].raf),_a--,delete $a[a])},bb=function(a){$a[a]&&ab(a),$a[a]||(_a++,$a[a]={})},cb=function(){for(var a in $a)$a.hasOwnProperty(a)&&ab(a)},db=function(a,b,c,d,e,f,g){var h,i=Ea();bb(a);var j=function(){if($a[a]){if(h=Ea()-i,h>=d)return ab(a),f(c),void(g&&g());f((c-b)*e(h/d)+b),$a[a].raf=H(j)}};j()},eb={shout:Da,listen:Ca,viewportSize:qa,options:i,isMainScrollAnimating:function(){return fa},getZoomLevel:function(){return s},getCurrentIndex:function(){return m},isDragging:function(){return V},isZooming:function(){return aa},setScrollOffset:function(a,b){sa.x=a,M=sa.y=b,Da("updateScrollOffset",sa)},applyZoomPan:function(a,b,c,d){pa.x=b,pa.y=c,s=a,Ha(d)},init:function(){if(!j&&!k){var c;f.framework=e,f.template=a,f.bg=e.getChildByClass(a,"pswp__bg"),J=a.className,j=!0,N=e.detectFeatures(),H=N.raf,I=N.caf,E=N.transform,L=N.oldIE,f.scrollWrap=e.getChildByClass(a,"pswp__scroll-wrap"),f.container=e.getChildByClass(f.scrollWrap,"pswp__container"),n=f.container.style,f.itemHolders=y=[{el:f.container.children[0],wrap:0,index:-1},{el:f.container.children[1],wrap:0,index:-1},{el:f.container.children[2],wrap:0,index:-1}],y[0].el.style.display=y[2].el.style.display="none",Wa(),r={resize:f.updateSize,orientationchange:function(){clearTimeout(O),O=setTimeout(function(){qa.x!==f.scrollWrap.clientWidth&&f.updateSize()},500)},scroll:Za,keydown:Xa,click:Ya};var d=N.isOldIOSPhone||N.isOldAndroid||N.isMobileOpera;for(N.animationName&&N.transform&&!d||(i.showAnimationDuration=i.hideAnimationDuration=0),c=0;c<wa.length;c++)f["init"+wa[c]]();if(b){var g=f.ui=new b(f,e);g.init()}Da("firstUpdate"),m=m||i.index||0,(isNaN(m)||m<0||m>=ac())&&(m=0),f.currItem=_b(m),(N.isOldIOSPhone||N.isOldAndroid)&&(va=!1),a.setAttribute("aria-hidden","false"),i.modal&&(va?a.style.position="fixed":(a.style.position="absolute",a.style.top=e.getScrollY()+"px")),void 0===M&&(Da("initialLayout"),M=K=e.getScrollY());var l="pswp--open ";for(i.mainClass&&(l+=i.mainClass+" "),i.showHideOpacity&&(l+="pswp--animate_opacity "),l+=G?"pswp--touch":"pswp--notouch",l+=N.animationName?" pswp--css_animation":"",l+=N.svg?" pswp--svg":"",e.addClass(a,l),f.updateSize(),o=-1,ua=null,c=0;c<h;c++)Ja((c+o)*ta.x,y[c].el.style);L||e.bind(f.scrollWrap,q,f),Ca("initialZoomInEnd",function(){f.setContent(y[0],m-1),f.setContent(y[2],m+1),y[0].el.style.display=y[2].el.style.display="block",i.focus&&a.focus(),Qa()}),f.setContent(y[1],m),f.updateCurrItem(),Da("afterInit"),va||(w=setInterval(function(){_a||V||aa||s!==f.currItem.initialZoomLevel||f.updateSize()},1e3)),e.addClass(a,"pswp--visible")}},close:function(){j&&(j=!1,k=!0,Da("close"),Ra(),cc(f.currItem,null,!0,f.destroy))},destroy:function(){Da("destroy"),Xb&&clearTimeout(Xb),a.setAttribute("aria-hidden","true"),a.className=J,w&&clearInterval(w),e.unbind(f.scrollWrap,q,f),e.unbind(window,"scroll",f),zb(),cb(),Ba=null},panTo:function(a,b,c){c||(a>da.min.x?a=da.min.x:a<da.max.x&&(a=da.max.x),b>da.min.y?b=da.min.y:b<da.max.y&&(b=da.max.y)),pa.x=a,pa.y=b,Ha()},handleEvent:function(a){a=a||window.event,r[a.type]&&r[a.type](a)},goTo:function(a){a=Aa(a);var b=a-m;ua=b,m=a,f.currItem=_b(m),ra-=b,Ka(ta.x*ra),cb(),fa=!1,f.updateCurrItem()},next:function(){f.goTo(m+1)},prev:function(){f.goTo(m-1)},updateCurrZoomItem:function(a){if(a&&Da("beforeChange",0),y[1].el.children.length){var b=y[1].el.children[0];ea=e.hasClass(b,"pswp__zoom-wrap")?b.style:null}else ea=null;da=f.currItem.bounds,t=s=f.currItem.initialZoomLevel,pa.x=da.center.x,pa.y=da.center.y,a&&Da("afterChange")},invalidateCurrItems:function(){x=!0;for(var a=0;a<h;a++)y[a].item&&(y[a].item.needsUpdate=!0)},updateCurrItem:function(a){if(0!==ua){var b,c=Math.abs(ua);if(!(a&&c<2)){f.currItem=_b(m),ya=!1,Da("beforeChange",ua),c>=h&&(o+=ua+(ua>0?-h:h),c=h);for(var d=0;d<c;d++)ua>0?(b=y.shift(),y[h-1]=b,o++,Ja((o+2)*ta.x,b.el.style),f.setContent(b,m-c+d+1+1)):(b=y.pop(),y.unshift(b),o--,Ja(o*ta.x,b.el.style),f.setContent(b,m+c-d-1-1));if(ea&&1===Math.abs(ua)){var e=_b(z);e.initialZoomLevel!==s&&(ic(e,qa),mc(e),Ia(e))}ua=0,f.updateCurrZoomItem(),z=m,Da("afterChange")}}},updateSize:function(b){if(!va&&i.modal){var c=e.getScrollY();if(M!==c&&(a.style.top=c+"px",M=c),!b&&xa.x===window.innerWidth&&xa.y===window.innerHeight)return;xa.x=window.innerWidth,xa.y=window.innerHeight,a.style.height=xa.y+"px"}if(qa.x=f.scrollWrap.clientWidth,qa.y=f.scrollWrap.clientHeight,Za(),ta.x=qa.x+Math.round(qa.x*i.spacing),ta.y=qa.y,Ka(ta.x*ra),Da("beforeResize"),void 0!==o){for(var d,g,j,k=0;k<h;k++)d=y[k],Ja((k+o)*ta.x,d.el.style),j=m+k-1,i.loop&&ac()>2&&(j=Aa(j)),g=_b(j),g&&(x||g.needsUpdate||!g.bounds)?(f.cleanSlide(g),f.setContent(d,j),1===k&&(f.currItem=g,f.updateCurrZoomItem(!0)),g.needsUpdate=!1):d.index===-1&&j>=0&&f.setContent(d,j),g&&g.container&&(ic(g,qa),mc(g),Ia(g));x=!1}t=s=f.currItem.initialZoomLevel,da=f.currItem.bounds,da&&(pa.x=da.center.x,pa.y=da.center.y,Ha(!0)),Da("resize")},zoomTo:function(a,b,c,d,f){b&&(t=s,ub.x=Math.abs(b.x)-pa.x,ub.y=Math.abs(b.y)-pa.y,Ma(oa,pa));var g=Sa(a,!1),h={};Va("x",g,h,a),Va("y",g,h,a);var i=s,j={x:pa.x,y:pa.y};Na(h);var k=function(b){1===b?(s=a,pa.x=h.x,pa.y=h.y):(s=(a-i)*b+i,pa.x=(h.x-j.x)*b+j.x,pa.y=(h.y-j.y)*b+j.y),f&&f(b),Ha(1===b)};c?db("customZoomTo",0,1,c,d||e.easing.sine.inOut,k):k(1)}},fb=30,gb=10,hb={},ib={},jb={},kb={},lb={},mb=[],nb={},ob=[],pb={},qb=0,rb=ma(),sb=0,tb=ma(),ub=ma(),vb=ma(),wb=function(a,b){return a.x===b.x&&a.y===b.y},xb=function(a,b){return Math.abs(a.x-b.x)<g&&Math.abs(a.y-b.y)<g},yb=function(a,b){return pb.x=Math.abs(a.x-b.x),pb.y=Math.abs(a.y-b.y),Math.sqrt(pb.x*pb.x+pb.y*pb.y)},zb=function(){Z&&(I(Z),Z=null)},Ab=function(){V&&(Z=H(Ab),Qb())},Bb=function(){return!("fit"===i.scaleMode&&s===f.currItem.initialZoomLevel)},Cb=function(a,b){return!(!a||a===document)&&(!(a.getAttribute("class")&&a.getAttribute("class").indexOf("pswp__scroll-wrap")>-1)&&(b(a)?a:Cb(a.parentNode,b)))},Db={},Eb=function(a,b){return Db.prevent=!Cb(a.target,i.isClickableElement),Da("preventDragEvent",a,b,Db),Db.prevent},Fb=function(a,b){return b.x=a.pageX,b.y=a.pageY,b.id=a.identifier,b},Gb=function(a,b,c){c.x=.5*(a.x+b.x),c.y=.5*(a.y+b.y)},Hb=function(a,b,c){if(a-Q>50){var d=ob.length>2?ob.shift():{};d.x=b,d.y=c,ob.push(d),Q=a}},Ib=function(){var a=pa.y-f.currItem.initialPosition.y;return 1-Math.abs(a/(qa.y/2))},Jb={},Kb={},Lb=[],Mb=function(a){for(;Lb.length>0;)Lb.pop();return F?(la=0,mb.forEach(function(a){0===la?Lb[0]=a:1===la&&(Lb[1]=a),la++})):a.type.indexOf("touch")>-1?a.touches&&a.touches.length>0&&(Lb[0]=Fb(a.touches[0],Jb),a.touches.length>1&&(Lb[1]=Fb(a.touches[1],Kb))):(Jb.x=a.pageX,Jb.y=a.pageY,Jb.id="",Lb[0]=Jb),Lb},Nb=function(a,b){var c,d,e,g,h=0,j=pa[a]+b[a],k=b[a]>0,l=tb.x+b.x,m=tb.x-nb.x;return c=j>da.min[a]||j<da.max[a]?i.panEndFriction:1,j=pa[a]+b[a]*c,!i.allowPanToNext&&s!==f.currItem.initialZoomLevel||(ea?"h"!==ga||"x"!==a||X||(k?(j>da.min[a]&&(c=i.panEndFriction,h=da.min[a]-j,d=da.min[a]-oa[a]),(d<=0||m<0)&&ac()>1?(g=l,m<0&&l>nb.x&&(g=nb.x)):da.min.x!==da.max.x&&(e=j)):(j<da.max[a]&&(c=i.panEndFriction,h=j-da.max[a],d=oa[a]-da.max[a]),(d<=0||m>0)&&ac()>1?(g=l,m>0&&l<nb.x&&(g=nb.x)):da.min.x!==da.max.x&&(e=j))):g=l,"x"!==a)?void(fa||$||s>f.currItem.fitRatio&&(pa[a]+=b[a]*c)):(void 0!==g&&(Ka(g,!0),$=g!==nb.x),da.min.x!==da.max.x&&(void 0!==e?pa.x=e:$||(pa.x+=b.x*c)),void 0!==g)},Ob=function(a){if(!("mousedown"===a.type&&a.button>0)){if($b)return void a.preventDefault();if(!U||"mousedown"!==a.type){if(Eb(a,!0)&&a.preventDefault(),Da("pointerDown"),F){var b=e.arraySearch(mb,a.pointerId,"id");b<0&&(b=mb.length),mb[b]={x:a.pageX,y:a.pageY,id:a.pointerId}}var c=Mb(a),d=c.length;_=null,cb(),V&&1!==d||(V=ha=!0,e.bind(window,p,f),S=ka=ia=T=$=Y=W=X=!1,ga=null,Da("firstTouchStart",c),Ma(oa,pa),na.x=na.y=0,Ma(kb,c[0]),Ma(lb,kb),nb.x=ta.x*ra,ob=[{x:kb.x,y:kb.y}],Q=P=Ea(),Sa(s,!0),zb(),Ab()),!aa&&d>1&&!fa&&!$&&(t=s,X=!1,aa=W=!0,na.y=na.x=0,Ma(oa,pa),Ma(hb,c[0]),Ma(ib,c[1]),Gb(hb,ib,vb),ub.x=Math.abs(vb.x)-pa.x,ub.y=Math.abs(vb.y)-pa.y,ba=ca=yb(hb,ib))}}},Pb=function(a){if(a.preventDefault(),F){var b=e.arraySearch(mb,a.pointerId,"id");if(b>-1){var c=mb[b];c.x=a.pageX,c.y=a.pageY}}if(V){var d=Mb(a);if(ga||Y||aa)_=d;else if(tb.x!==ta.x*ra)ga="h";else{var f=Math.abs(d[0].x-kb.x)-Math.abs(d[0].y-kb.y);Math.abs(f)>=gb&&(ga=f>0?"h":"v",_=d)}}},Qb=function(){if(_){var a=_.length;if(0!==a)if(Ma(hb,_[0]),jb.x=hb.x-kb.x,jb.y=hb.y-kb.y,aa&&a>1){if(kb.x=hb.x,kb.y=hb.y,!jb.x&&!jb.y&&wb(_[1],ib))return;Ma(ib,_[1]),X||(X=!0,Da("zoomGestureStarted"));var b=yb(hb,ib),c=Vb(b);c>f.currItem.initialZoomLevel+f.currItem.initialZoomLevel/15&&(ka=!0);var d=1,e=Ta(),g=Ua();if(c<e)if(i.pinchToClose&&!ka&&t<=f.currItem.initialZoomLevel){var h=e-c,j=1-h/(e/1.2);Fa(j),Da("onPinchClose",j),ia=!0}else d=(e-c)/e,d>1&&(d=1),c=e-d*(e/3);else c>g&&(d=(c-g)/(6*e),d>1&&(d=1),c=g+d*e);d<0&&(d=0),ba=b,Gb(hb,ib,rb),na.x+=rb.x-vb.x,na.y+=rb.y-vb.y,Ma(vb,rb),pa.x=La("x",c),pa.y=La("y",c),S=c>s,s=c,Ha()}else{if(!ga)return;if(ha&&(ha=!1,Math.abs(jb.x)>=gb&&(jb.x-=_[0].x-lb.x),Math.abs(jb.y)>=gb&&(jb.y-=_[0].y-lb.y)),kb.x=hb.x,kb.y=hb.y,0===jb.x&&0===jb.y)return;if("v"===ga&&i.closeOnVerticalDrag&&!Bb()){na.y+=jb.y,pa.y+=jb.y;var k=Ib();return T=!0,Da("onVerticalDrag",k),Fa(k),void Ha()}Hb(Ea(),hb.x,hb.y),Y=!0,da=f.currItem.bounds;var l=Nb("x",jb);l||(Nb("y",jb),Na(pa),Ha())}}},Rb=function(a){if(N.isOldAndroid){if(U&&"mouseup"===a.type)return;a.type.indexOf("touch")>-1&&(clearTimeout(U),U=setTimeout(function(){U=0},600))}Da("pointerUp"),Eb(a,!1)&&a.preventDefault();var b;if(F){var c=e.arraySearch(mb,a.pointerId,"id");if(c>-1)if(b=mb.splice(c,1)[0],navigator.msPointerEnabled){var d={4:"mouse",2:"touch",3:"pen"};b.type=d[a.pointerType],b.type||(b.type=a.pointerType||"mouse")}else b.type=a.pointerType||"mouse"}var g,h=Mb(a),j=h.length;if("mouseup"===a.type&&(j=0),2===j)return _=null,!0;1===j&&Ma(lb,h[0]),0!==j||ga||fa||(b||("mouseup"===a.type?b={x:a.pageX,y:a.pageY,type:"mouse"}:a.changedTouches&&a.changedTouches[0]&&(b={x:a.changedTouches[0].pageX,y:a.changedTouches[0].pageY,type:"touch"})),Da("touchRelease",a,b));var k=-1;if(0===j&&(V=!1,e.unbind(window,p,f),zb(),aa?k=0:sb!==-1&&(k=Ea()-sb)),sb=1===j?Ea():-1,g=k!==-1&&k<150?"zoom":"swipe",aa&&j<2&&(aa=!1,1===j&&(g="zoomPointerUp"),Da("zoomGestureEnded")),_=null,Y||X||fa||T)if(cb(),R||(R=Sb()),R.calculateSwipeSpeed("x"),T){var l=Ib();if(l<i.verticalDragRange)f.close();else{var m=pa.y,n=ja;db("verticalDrag",0,1,300,e.easing.cubic.out,function(a){pa.y=(f.currItem.initialPosition.y-m)*a+m,Fa((1-n)*a+n),Ha()}),Da("onVerticalDrag",1)}}else{if(($||fa)&&0===j){var o=Ub(g,R);if(o)return;g="zoomPointerUp"}if(!fa)return"swipe"!==g?void Wb():void(!$&&s>f.currItem.fitRatio&&Tb(R))}},Sb=function(){var a,b,c={lastFlickOffset:{},lastFlickDist:{},lastFlickSpeed:{},slowDownRatio:{},slowDownRatioReverse:{},speedDecelerationRatio:{},speedDecelerationRatioAbs:{},distanceOffset:{},backAnimDestination:{},backAnimStarted:{},calculateSwipeSpeed:function(d){ob.length>1?(a=Ea()-Q+50,b=ob[ob.length-2][d]):(a=Ea()-P,b=lb[d]),c.lastFlickOffset[d]=kb[d]-b,c.lastFlickDist[d]=Math.abs(c.lastFlickOffset[d]),c.lastFlickDist[d]>20?c.lastFlickSpeed[d]=c.lastFlickOffset[d]/a:c.lastFlickSpeed[d]=0,Math.abs(c.lastFlickSpeed[d])<.1&&(c.lastFlickSpeed[d]=0),c.slowDownRatio[d]=.95,c.slowDownRatioReverse[d]=1-c.slowDownRatio[d],c.speedDecelerationRatio[d]=1},calculateOverBoundsAnimOffset:function(a,b){c.backAnimStarted[a]||(pa[a]>da.min[a]?c.backAnimDestination[a]=da.min[a]:pa[a]<da.max[a]&&(c.backAnimDestination[a]=da.max[a]),void 0!==c.backAnimDestination[a]&&(c.slowDownRatio[a]=.7,c.slowDownRatioReverse[a]=1-c.slowDownRatio[a],c.speedDecelerationRatioAbs[a]<.05&&(c.lastFlickSpeed[a]=0,c.backAnimStarted[a]=!0,db("bounceZoomPan"+a,pa[a],c.backAnimDestination[a],b||300,e.easing.sine.out,function(b){pa[a]=b,Ha()}))))},calculateAnimOffset:function(a){c.backAnimStarted[a]||(c.speedDecelerationRatio[a]=c.speedDecelerationRatio[a]*(c.slowDownRatio[a]+c.slowDownRatioReverse[a]-c.slowDownRatioReverse[a]*c.timeDiff/10),c.speedDecelerationRatioAbs[a]=Math.abs(c.lastFlickSpeed[a]*c.speedDecelerationRatio[a]),c.distanceOffset[a]=c.lastFlickSpeed[a]*c.speedDecelerationRatio[a]*c.timeDiff,pa[a]+=c.distanceOffset[a])},panAnimLoop:function(){if($a.zoomPan&&($a.zoomPan.raf=H(c.panAnimLoop),c.now=Ea(),c.timeDiff=c.now-c.lastNow,c.lastNow=c.now,c.calculateAnimOffset("x"),c.calculateAnimOffset("y"),Ha(),c.calculateOverBoundsAnimOffset("x"),c.calculateOverBoundsAnimOffset("y"),c.speedDecelerationRatioAbs.x<.05&&c.speedDecelerationRatioAbs.y<.05))return pa.x=Math.round(pa.x),pa.y=Math.round(pa.y),Ha(),void ab("zoomPan")}};return c},Tb=function(a){return a.calculateSwipeSpeed("y"),da=f.currItem.bounds,a.backAnimDestination={},a.backAnimStarted={},Math.abs(a.lastFlickSpeed.x)<=.05&&Math.abs(a.lastFlickSpeed.y)<=.05?(a.speedDecelerationRatioAbs.x=a.speedDecelerationRatioAbs.y=0,a.calculateOverBoundsAnimOffset("x"),a.calculateOverBoundsAnimOffset("y"),!0):(bb("zoomPan"),a.lastNow=Ea(),void a.panAnimLoop())},Ub=function(a,b){var c;fa||(qb=m);var d;if("swipe"===a){var g=kb.x-lb.x,h=b.lastFlickDist.x<10;g>fb&&(h||b.lastFlickOffset.x>20)?d=-1:g<-fb&&(h||b.lastFlickOffset.x<-20)&&(d=1)}var j;d&&(m+=d,m<0?(m=i.loop?ac()-1:0,j=!0):m>=ac()&&(m=i.loop?0:ac()-1,j=!0),j&&!i.loop||(ua+=d,ra-=d,c=!0));var k,l=ta.x*ra,n=Math.abs(l-tb.x);return c||l>tb.x==b.lastFlickSpeed.x>0?(k=Math.abs(b.lastFlickSpeed.x)>0?n/Math.abs(b.lastFlickSpeed.x):333,k=Math.min(k,400),k=Math.max(k,250)):k=333,qb===m&&(c=!1),fa=!0,Da("mainScrollAnimStart"),db("mainScroll",tb.x,l,k,e.easing.cubic.out,Ka,function(){cb(),fa=!1,qb=-1,(c||qb!==m)&&f.updateCurrItem(),Da("mainScrollAnimComplete")}),c&&f.updateCurrItem(!0),c},Vb=function(a){return 1/ca*a*t},Wb=function(){var a=s,b=Ta(),c=Ua();s<b?a=b:s>c&&(a=c);var d,g=1,h=ja;return ia&&!S&&!ka&&s<b?(f.close(),!0):(ia&&(d=function(a){Fa((g-h)*a+h)}),f.zoomTo(a,0,200,e.easing.cubic.out,d),!0)};za("Gestures",{publicMethods:{initGestures:function(){var a=function(a,b,c,d,e){A=a+b,B=a+c,C=a+d,D=e?a+e:""};F=N.pointerEvent,F&&N.touch&&(N.touch=!1),F?navigator.msPointerEnabled?a("MSPointer","Down","Move","Up","Cancel"):a("pointer","down","move","up","cancel"):N.touch?(a("touch","start","move","end","cancel"),G=!0):a("mouse","down","move","up"),p=B+" "+C+" "+D,q=A,F&&!G&&(G=navigator.maxTouchPoints>1||navigator.msMaxTouchPoints>1),f.likelyTouchDevice=G,r[A]=Ob,r[B]=Pb,r[C]=Rb,D&&(r[D]=r[C]),N.touch&&(q+=" mousedown",p+=" mousemove mouseup",r.mousedown=r[A],r.mousemove=r[B],r.mouseup=r[C]),G||(i.allowPanToNext=!1)}}});var Xb,Yb,Zb,$b,_b,ac,bc,cc=function(b,c,d,g){Xb&&clearTimeout(Xb),$b=!0,Zb=!0;var h;b.initialLayout?(h=b.initialLayout,b.initialLayout=null):h=i.getThumbBoundsFn&&i.getThumbBoundsFn(m);var j=d?i.hideAnimationDuration:i.showAnimationDuration,k=function(){ab("initialZoom"),d?(f.template.removeAttribute("style"),f.bg.removeAttribute("style")):(Fa(1),c&&(c.style.display="block"),e.addClass(a,"pswp--animated-in"),Da("initialZoom"+(d?"OutEnd":"InEnd"))),g&&g(),$b=!1};if(!j||!h||void 0===h.x)return Da("initialZoom"+(d?"Out":"In")),s=b.initialZoomLevel,Ma(pa,b.initialPosition),Ha(),a.style.opacity=d?0:1,Fa(1),void(j?setTimeout(function(){k()},j):k());var n=function(){var c=l,g=!f.currItem.src||f.currItem.loadError||i.showHideOpacity;b.miniImg&&(b.miniImg.style.webkitBackfaceVisibility="hidden"),d||(s=h.w/b.w,pa.x=h.x,pa.y=h.y-K,f[g?"template":"bg"].style.opacity=.001,Ha()),bb("initialZoom"),d&&!c&&e.removeClass(a,"pswp--animated-in"),g&&(d?e[(c?"remove":"add")+"Class"](a,"pswp--animate_opacity"):setTimeout(function(){e.addClass(a,"pswp--animate_opacity")},30)),Xb=setTimeout(function(){if(Da("initialZoom"+(d?"Out":"In")),d){var f=h.w/b.w,i={x:pa.x,y:pa.y},l=s,m=ja,n=function(b){1===b?(s=f,pa.x=h.x,pa.y=h.y-M):(s=(f-l)*b+l,pa.x=(h.x-i.x)*b+i.x,pa.y=(h.y-M-i.y)*b+i.y),Ha(),g?a.style.opacity=1-b:Fa(m-b*m)};c?db("initialZoom",0,1,j,e.easing.cubic.out,n,k):(n(1),Xb=setTimeout(k,j+20))}else s=b.initialZoomLevel,Ma(pa,b.initialPosition),Ha(),Fa(1),g?a.style.opacity=1:Fa(1),Xb=setTimeout(k,j+20)},d?25:90)};n()},dc={},ec=[],fc={index:0,errorMsg:'<div class="pswp__error-msg"><a href="%url%" target="_blank">The image</a> could not be loaded.</div>',forceProgressiveLoading:!1,preload:[1,1],getNumItemsFn:function(){return Yb.length}},gc=function(){return{center:{x:0,y:0},max:{x:0,y:0},min:{x:0,y:0}}},hc=function(a,b,c){var d=a.bounds;d.center.x=Math.round((dc.x-b)/2),d.center.y=Math.round((dc.y-c)/2)+a.vGap.top,d.max.x=b>dc.x?Math.round(dc.x-b):d.center.x,d.max.y=c>dc.y?Math.round(dc.y-c)+a.vGap.top:d.center.y,d.min.x=b>dc.x?0:d.center.x,d.min.y=c>dc.y?a.vGap.top:d.center.y},ic=function(a,b,c){if(a.src&&!a.loadError){var d=!c;if(d&&(a.vGap||(a.vGap={top:0,bottom:0}),Da("parseVerticalMargin",a)),dc.x=b.x,dc.y=b.y-a.vGap.top-a.vGap.bottom,d){var e=dc.x/a.w,f=dc.y/a.h;a.fitRatio=e<f?e:f;var g=i.scaleMode;"orig"===g?c=1:"fit"===g&&(c=a.fitRatio),c>1&&(c=1),a.initialZoomLevel=c,a.bounds||(a.bounds=gc())}if(!c)return;return hc(a,a.w*c,a.h*c),d&&c===a.initialZoomLevel&&(a.initialPosition=a.bounds.center),a.bounds}return a.w=a.h=0,a.initialZoomLevel=a.fitRatio=1,a.bounds=gc(),a.initialPosition=a.bounds.center,a.bounds},jc=function(a,b,c,d,e,g){b.loadError||d&&(b.imageAppended=!0,mc(b,d,b===f.currItem&&ya),c.appendChild(d),g&&setTimeout(function(){b&&b.loaded&&b.placeholder&&(b.placeholder.style.display="none",b.placeholder=null)},500))},kc=function(a){a.loading=!0,a.loaded=!1;var b=a.img=e.createEl("pswp__img","img"),c=function(){a.loading=!1,a.loaded=!0,a.loadComplete?a.loadComplete(a):a.img=null,b.onload=b.onerror=null,b=null};return b.onload=c,b.onerror=function(){a.loadError=!0,c()},b.src=a.src,b},lc=function(a,b){if(a.src&&a.loadError&&a.container)return b&&(a.container.innerHTML=""),a.container.innerHTML=i.errorMsg.replace("%url%",a.src),!0},mc=function(a,b,c){if(a.src){b||(b=a.container.lastChild);var d=c?a.w:Math.round(a.w*a.fitRatio),e=c?a.h:Math.round(a.h*a.fitRatio);a.placeholder&&!a.loaded&&(a.placeholder.style.width=d+"px",a.placeholder.style.height=e+"px"),b.style.width=d+"px",b.style.height=e+"px"}},nc=function(){if(ec.length){for(var a,b=0;b<ec.length;b++)a=ec[b],a.holder.index===a.index&&jc(a.index,a.item,a.baseDiv,a.img,!1,a.clearPlaceholder);ec=[]}};za("Controller",{publicMethods:{lazyLoadItem:function(a){a=Aa(a);var b=_b(a);b&&(!b.loaded&&!b.loading||x)&&(Da("gettingData",a,b),b.src&&kc(b))},initController:function(){e.extend(i,fc,!0),f.items=Yb=c,_b=f.getItemAt,ac=i.getNumItemsFn,bc=i.loop,ac()<3&&(i.loop=!1),Ca("beforeChange",function(a){var b,c=i.preload,d=null===a||a>=0,e=Math.min(c[0],ac()),g=Math.min(c[1],ac());for(b=1;b<=(d?g:e);b++)f.lazyLoadItem(m+b);for(b=1;b<=(d?e:g);b++)f.lazyLoadItem(m-b)}),Ca("initialLayout",function(){f.currItem.initialLayout=i.getThumbBoundsFn&&i.getThumbBoundsFn(m)}),Ca("mainScrollAnimComplete",nc),Ca("initialZoomInEnd",nc),Ca("destroy",function(){for(var a,b=0;b<Yb.length;b++)a=Yb[b],a.container&&(a.container=null),a.placeholder&&(a.placeholder=null),a.img&&(a.img=null),a.preloader&&(a.preloader=null),a.loadError&&(a.loaded=a.loadError=!1);ec=null})},getItemAt:function(a){return a>=0&&(void 0!==Yb[a]&&Yb[a])},allowProgressiveImg:function(){return i.forceProgressiveLoading||!G||i.mouseUsed||screen.width>1200},setContent:function(a,b){i.loop&&(b=Aa(b));var c=f.getItemAt(a.index);c&&(c.container=null);var d,g=f.getItemAt(b);if(!g)return void(a.el.innerHTML="");Da("gettingData",b,g),a.index=b,a.item=g;var h=g.container=e.createEl("pswp__zoom-wrap");if(!g.src&&g.html&&(g.html.tagName?h.appendChild(g.html):h.innerHTML=g.html),lc(g),ic(g,qa),!g.src||g.loadError||g.loaded)g.src&&!g.loadError&&(d=e.createEl("pswp__img","img"),d.style.opacity=1,d.src=g.src,mc(g,d),jc(b,g,h,d,!0));else{if(g.loadComplete=function(c){if(j){if(a&&a.index===b){if(lc(c,!0))return c.loadComplete=c.img=null,ic(c,qa),Ia(c),void(a.index===m&&f.updateCurrZoomItem());c.imageAppended?!$b&&c.placeholder&&(c.placeholder.style.display="none",c.placeholder=null):N.transform&&(fa||$b)?ec.push({item:c,baseDiv:h,img:c.img,index:b,holder:a,clearPlaceholder:!0}):jc(b,c,h,c.img,fa||$b,!0)}c.loadComplete=null,c.img=null,Da("imageLoadComplete",b,c)}},e.features.transform){var k="pswp__img pswp__img--placeholder";k+=g.msrc?"":" pswp__img--placeholder--blank";var l=e.createEl(k,g.msrc?"img":"");g.msrc&&(l.src=g.msrc),mc(g,l),h.appendChild(l),g.placeholder=l}g.loading||kc(g),f.allowProgressiveImg()&&(!Zb&&N.transform?ec.push({item:g,baseDiv:h,img:g.img,index:b,holder:a}):jc(b,g,h,g.img,!0,!0))}Zb||b!==m?Ia(g):(ea=h.style,cc(g,d||g.img)),a.el.innerHTML="",a.el.appendChild(h)},cleanSlide:function(a){a.img&&(a.img.onload=a.img.onerror=null),a.loaded=a.loading=a.img=a.imageAppended=!1}}});var oc,pc={},qc=function(a,b,c){var d=document.createEvent("CustomEvent"),e={origEvent:a,target:a.target,releasePoint:b,pointerType:c||"touch"};d.initCustomEvent("pswpTap",!0,!0,e),a.target.dispatchEvent(d)};za("Tap",{publicMethods:{initTap:function(){Ca("firstTouchStart",f.onTapStart),Ca("touchRelease",f.onTapRelease),Ca("destroy",function(){pc={},oc=null})},onTapStart:function(a){a.length>1&&(clearTimeout(oc),oc=null)},onTapRelease:function(a,b){if(b&&!Y&&!W&&!_a){var c=b;if(oc&&(clearTimeout(oc),oc=null,xb(c,pc)))return void Da("doubleTap",c);if("mouse"===b.type)return void qc(a,b,"mouse");var d=a.target.tagName.toUpperCase();if("BUTTON"===d||e.hasClass(a.target,"pswp__single-tap"))return void qc(a,b);Ma(pc,c),oc=setTimeout(function(){qc(a,b),oc=null},300)}}}});var rc;za("DesktopZoom",{publicMethods:{initDesktopZoom:function(){L||(G?Ca("mouseUsed",function(){f.setupDesktopZoom()}):f.setupDesktopZoom(!0))},setupDesktopZoom:function(b){rc={};var c="wheel mousewheel DOMMouseScroll";Ca("bindEvents",function(){e.bind(a,c,f.handleMouseWheel)}),Ca("unbindEvents",function(){rc&&e.unbind(a,c,f.handleMouseWheel)}),f.mouseZoomedIn=!1;var d,g=function(){f.mouseZoomedIn&&(e.removeClass(a,"pswp--zoomed-in"),f.mouseZoomedIn=!1),s<1?e.addClass(a,"pswp--zoom-allowed"):e.removeClass(a,"pswp--zoom-allowed"),h()},h=function(){d&&(e.removeClass(a,"pswp--dragging"),d=!1)};Ca("resize",g),Ca("afterChange",g),Ca("pointerDown",function(){f.mouseZoomedIn&&(d=!0,e.addClass(a,"pswp--dragging"))}),Ca("pointerUp",h),b||g()},handleMouseWheel:function(a){if(s<=f.currItem.fitRatio)return i.modal&&(!i.closeOnScroll||_a||V?a.preventDefault():E&&Math.abs(a.deltaY)>2&&(l=!0,f.close())),!0;if(a.stopPropagation(),rc.x=0,"deltaX"in a)1===a.deltaMode?(rc.x=18*a.deltaX,rc.y=18*a.deltaY):(rc.x=a.deltaX,rc.y=a.deltaY);else if("wheelDelta"in a)a.wheelDeltaX&&(rc.x=-.16*a.wheelDeltaX),a.wheelDeltaY?rc.y=-.16*a.wheelDeltaY:rc.y=-.16*a.wheelDelta;else{if(!("detail"in a))return;rc.y=a.detail}Sa(s,!0);var b=pa.x-rc.x,c=pa.y-rc.y;(i.modal||b<=da.min.x&&b>=da.max.x&&c<=da.min.y&&c>=da.max.y)&&a.preventDefault(),f.panTo(b,c)},toggleDesktopZoom:function(b){b=b||{x:qa.x/2+sa.x,y:qa.y/2+sa.y};var c=i.getDoubleTapZoom(!0,f.currItem),d=s===c;f.mouseZoomedIn=!d,f.zoomTo(d?f.currItem.initialZoomLevel:c,b,333),e[(d?"remove":"add")+"Class"](a,"pswp--zoomed-in")}}});var sc,tc,uc,vc,wc,xc,yc,zc,Ac,Bc,Cc,Dc,Ec={history:!0,galleryUID:1},Fc=function(){return Cc.hash.substring(1)},Gc=function(){sc&&clearTimeout(sc),uc&&clearTimeout(uc)},Hc=function(){var a=Fc(),b={};if(a.length<5)return b;var c,d=a.split("&");for(c=0;c<d.length;c++)if(d[c]){var e=d[c].split("=");e.length<2||(b[e[0]]=e[1])}if(i.galleryPIDs){var f=b.pid;for(b.pid=0,c=0;c<Yb.length;c++)if(Yb[c].pid===f){b.pid=c;break}}else b.pid=parseInt(b.pid,10)-1;return b.pid<0&&(b.pid=0),b},Ic=function(){if(uc&&clearTimeout(uc),_a||V)return void(uc=setTimeout(Ic,500));vc?clearTimeout(tc):vc=!0;var a=m+1,b=_b(m);b.hasOwnProperty("pid")&&(a=b.pid);var c=yc+"&gid="+i.galleryUID+"&pid="+a;zc||Cc.hash.indexOf(c)===-1&&(Bc=!0);var d=Cc.href.split("#")[0]+"#"+c;Dc?"#"+c!==window.location.hash&&history[zc?"replaceState":"pushState"]("",document.title,d):zc?Cc.replace(d):Cc.hash=c,zc=!0,tc=setTimeout(function(){vc=!1},60)};za("History",{publicMethods:{initHistory:function(){if(e.extend(i,Ec,!0),i.history){Cc=window.location,Bc=!1,Ac=!1,zc=!1,yc=Fc(),Dc="pushState"in history,yc.indexOf("gid=")>-1&&(yc=yc.split("&gid=")[0],yc=yc.split("?gid=")[0]),Ca("afterChange",f.updateURL),Ca("unbindEvents",function(){e.unbind(window,"hashchange",f.onHashChange)});var a=function(){xc=!0,Ac||(Bc?history.back():yc?Cc.hash=yc:Dc?history.pushState("",document.title,Cc.pathname+Cc.search):Cc.hash=""),Gc()};Ca("unbindEvents",function(){l&&a()}),Ca("destroy",function(){xc||a()}),Ca("firstUpdate",function(){m=Hc().pid});var b=yc.indexOf("pid=");b>-1&&(yc=yc.substring(0,b),"&"===yc.slice(-1)&&(yc=yc.slice(0,-1))),setTimeout(function(){j&&e.bind(window,"hashchange",f.onHashChange)},40)}},onHashChange:function(){return Fc()===yc?(Ac=!0,void f.close()):void(vc||(wc=!0,f.goTo(Hc().pid),wc=!1))},updateURL:function(){Gc(),wc||(zc?sc=setTimeout(Ic,800):Ic())}}}),e.extend(f,eb)};return a});
@@ -0,0 +1,81 @@
1
+ /**
2
+ * pictures/style.css
3
+ * Design for the Pictures module of the IntraNet.
4
+ */
5
+
6
+ /******************************* Recent groups ********************************/
7
+
8
+ p.see_more {
9
+ text-align: right;
10
+ }
11
+ p.see_more a {
12
+ color: #748ea3;
13
+ }
14
+
15
+ /******************************* Picture groups *******************************/
16
+
17
+ ul.groups {
18
+ display: grid;
19
+ grid-template-columns: repeat(4, 1fr);
20
+ grid-auto-flow: dense;
21
+ gap: 30px;
22
+ padding: 0px;
23
+ margin: 1em 15px 0px; /* top sides bottom */
24
+ }
25
+ ul.groups.wide {
26
+ grid-template-columns: repeat(2, 1fr);
27
+ }
28
+ ul.groups li {
29
+ display: block;
30
+ width: 100%;
31
+ max-width: 270px;
32
+ margin: 0px;
33
+ background: #ffffff;
34
+ text-align: center;
35
+ overflow: hidden;
36
+ transition: all 0.2s;
37
+ border: 1px solid #eeeff2;
38
+ border-radius: 3px;
39
+ justify-self: center;
40
+ }
41
+ ul.groups.wide li {
42
+ max-width: 570px;
43
+ }
44
+
45
+ /* Small screens only */
46
+ @media only screen and (max-width: 1023px), only screen and (max-device-width: 1023px) {
47
+ ul.groups { grid-template-columns: repeat(2, 1fr); }
48
+ ul.groups.wide { grid-template-columns: repeat(1, 1fr); }
49
+ }
50
+
51
+ ul.groups li a {
52
+ display: block;
53
+ height: 100%;
54
+ }
55
+ ul.groups li figure {
56
+ margin: 0px;
57
+ }
58
+ ul.groups li figure div {
59
+ height: 0px;
60
+ padding-bottom: 74.075%; /* 200:270 ratio */
61
+ background: no-repeat center;
62
+ }
63
+ ul.groups.wide li figure div {
64
+ padding-bottom: 43.860%; /* 250:570 ratio */
65
+ }
66
+ ul.groups li figcaption {
67
+ display: block;
68
+ white-space: nowrap;
69
+ text-overflow: ellipsis;
70
+ overflow: hidden;
71
+ padding: 1em;
72
+ }
73
+ ul.groups li em {
74
+ display: inline-block;
75
+ margin-top: 0.5em;
76
+ font-size: 90%;
77
+ }
78
+
79
+ ul.groups li:hover, ul.groups li:focus {
80
+ box-shadow: 0px 1px 15px rgba(0,0,0,0.08);
81
+ }
Binary file
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'intranet/pictures/json_db_provider'
4
+
5
+ RSpec.describe Intranet::Pictures::JsonDbProvider do
6
+ describe '.initialize' do
7
+ it 'should propagate the filesystem exceptions' do
8
+ file = File.join(__dir__, 'non-existant.json')
9
+ expect { described_class.new(file) }.to raise_error(Errno::ENOENT)
10
+ end
11
+ it 'should propagate the JSON parser exceptions (file format)' do
12
+ file = __FILE__
13
+ expect { described_class.new(file) }.to raise_error(JSON::ParserError)
14
+ end
15
+ end
16
+
17
+ describe '#title' do
18
+ before do
19
+ @provider = described_class.new(File.join(__dir__, 'sample-db.json'))
20
+ end
21
+
22
+ it 'should return the gallery title' do
23
+ expect(@provider.title).to eql('My Gallery')
24
+ end
25
+ end
26
+
27
+ describe '#group_types' do
28
+ before do
29
+ @provider = described_class.new(File.join(__dir__, 'sample-db.json'))
30
+ end
31
+
32
+ it 'should return the available group types' do
33
+ expect(@provider.group_types).to eql(%w[group1 group2])
34
+ end
35
+ end
36
+
37
+ describe '#list_groups' do
38
+ before do
39
+ @provider = described_class.new(File.join(__dir__, 'sample-db.json'))
40
+ end
41
+
42
+ context 'given a valid +type+' do
43
+ it 'should return the list of groups of the given +type+ without the +uri+ key' do
44
+ expect(@provider.list_groups('group1')).to eql(
45
+ [
46
+ { 'id' => 'group1_title1', 'title' => 'Group 1, Title 1', 'brief' => 'brief_text_1' },
47
+ { 'id' => 'group1_title2', 'title' => 'Group 1, Title 2' }
48
+ ]
49
+ )
50
+ expect(@provider.list_groups('group2')).to eql(
51
+ [
52
+ { 'id' => 'group2_title1', 'title' => 'Group 2, Title 1', 'value' => 'abcd' },
53
+ { 'id' => 'group2_title2', 'title' => 'Group 2, Title 2', 'value' => 'bcde', 'brief' => 'brief_text_2' },
54
+ { 'id' => 'group2_title3', 'title' => 'Group 2, Title 3', 'value' => 'aabb' }
55
+ ]
56
+ )
57
+ end
58
+
59
+ it 'should return only the groups matching the given +selector+' do
60
+ selector = { 'group1' => 'group1_title1' }
61
+ expect(@provider.list_groups('group2', selector)).to eql(
62
+ [
63
+ { 'id' => 'group2_title2', 'title' => 'Group 2, Title 2', 'value' => 'bcde', 'brief' => 'brief_text_2' },
64
+ { 'id' => 'group2_title3', 'title' => 'Group 2, Title 3', 'value' => 'aabb' }
65
+ ]
66
+ )
67
+
68
+ selector = { 'uri' => 'white.jpg' }
69
+ expect(@provider.list_groups('group1', selector)).to eql(
70
+ [
71
+ { 'id' => 'group1_title1', 'title' => 'Group 1, Title 1', 'brief' => 'brief_text_1' }
72
+ ]
73
+ )
74
+
75
+ selector = { 'group1' => 'group1_title2', 'value' => true }
76
+ expect(@provider.list_groups('group2', selector)).to eql(
77
+ [
78
+ { 'id' => 'group2_title1', 'title' => 'Group 2, Title 1', 'value' => 'abcd' },
79
+ { 'id' => 'group2_title3', 'title' => 'Group 2, Title 3', 'value' => 'aabb' }
80
+ ]
81
+ )
82
+
83
+ selector = { 'group2' => 'group2_title2', 'value' => false }
84
+ expect(@provider.list_groups('group2', selector)).to eql(
85
+ [
86
+ { 'id' => 'group2_title2', 'title' => 'Group 2, Title 2', 'value' => 'bcde', 'brief' => 'brief_text_2' }
87
+ ]
88
+ )
89
+
90
+ selector = { 'group3' => 'foo_bar_boz' } # referencing undefined fields
91
+ expect(@provider.list_groups('group2', selector)).to eql([])
92
+ end
93
+ end
94
+
95
+ context 'given an invalid +type+' do
96
+ it 'should raise KeyError' do
97
+ expect { @provider.list_groups('invalid') }.to raise_error(KeyError)
98
+ end
99
+ end
100
+
101
+ context 'given a valid +sort_by+ key' do
102
+ it 'should sort the groups by the given key' do
103
+ expect(@provider.list_groups('group1', {}, 'title', false)).to eql(
104
+ [
105
+ { 'id' => 'group1_title2', 'title' => 'Group 1, Title 2' },
106
+ { 'id' => 'group1_title1', 'title' => 'Group 1, Title 1', 'brief' => 'brief_text_1' }
107
+ ]
108
+ )
109
+
110
+ expect(@provider.list_groups('group2', {}, 'value')).to eql(
111
+ [
112
+ { 'id' => 'group2_title3', 'title' => 'Group 2, Title 3', 'value' => 'aabb' },
113
+ { 'id' => 'group2_title1', 'title' => 'Group 2, Title 1', 'value' => 'abcd' },
114
+ { 'id' => 'group2_title2', 'title' => 'Group 2, Title 2', 'value' => 'bcde', 'brief' => 'brief_text_2' }
115
+ ]
116
+ )
117
+ end
118
+ end
119
+
120
+ context 'given an invalid +sort_by+ key' do
121
+ it 'should raise KeyError' do
122
+ expect { @provider.list_groups('group1', {}, 'invalid') }.to raise_error(KeyError)
123
+ end
124
+ end
125
+ end
126
+
127
+ describe '#list_pictures' do
128
+ before do
129
+ @provider = described_class.new(File.join(__dir__, 'sample-db.json'))
130
+ end
131
+
132
+ it 'should return the list of pictures without the uri key' do
133
+ expect(@provider.list_pictures).to eql(
134
+ [
135
+ { 'datetime' => '2019:07:22 09:41:31', 'group1' => 'group1_title1', 'group2' => 'group2_title2' },
136
+ { 'datetime' => '2020:06:19 07:51:05', 'value' => false, 'group1' => 'group1_title2', 'group2' => 'group2_title2' },
137
+ { 'datetime' => '2020:06:20 18:14:09', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title1' },
138
+ { 'datetime' => '2020:06:20 06:09:54', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title3' },
139
+ { 'datetime' => '2019:07:22 09:45:17', 'group1' => 'group1_title1', 'group2' => 'group2_title3' }
140
+ ]
141
+ )
142
+ end
143
+
144
+ it 'should return only the pictures matching the given selector' do
145
+ selector = { 'group1' => 'group1_title2' }
146
+ expect(@provider.list_pictures(selector)).to eql(
147
+ [
148
+ { 'datetime' => '2020:06:19 07:51:05', 'value' => false, 'group1' => 'group1_title2', 'group2' => 'group2_title2' },
149
+ { 'datetime' => '2020:06:20 18:14:09', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title1' },
150
+ { 'datetime' => '2020:06:20 06:09:54', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title3' }
151
+ ]
152
+ )
153
+ end
154
+
155
+ context 'given a valid +sort_by+ key' do
156
+ it 'should sort the pictures by the given key' do
157
+ selector = { 'group1' => 'group1_title2' }
158
+ expect(@provider.list_pictures(selector, 'group2')).to eql(
159
+ [
160
+ { 'datetime' => '2020:06:20 18:14:09', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title1' },
161
+ { 'datetime' => '2020:06:19 07:51:05', 'value' => false, 'group1' => 'group1_title2', 'group2' => 'group2_title2' },
162
+ { 'datetime' => '2020:06:20 06:09:54', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title3' }
163
+ ]
164
+ )
165
+
166
+ expect(@provider.list_pictures(selector, 'datetime', false)).to eql(
167
+ [
168
+ { 'datetime' => '2020:06:20 18:14:09', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title1' },
169
+ { 'datetime' => '2020:06:20 06:09:54', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title3' },
170
+ { 'datetime' => '2020:06:19 07:51:05', 'value' => false, 'group1' => 'group1_title2', 'group2' => 'group2_title2' }
171
+ ]
172
+ )
173
+ end
174
+ end
175
+
176
+ context 'given an invalid +sort_by+ key' do
177
+ it 'should raise KeyError' do
178
+ expect { @provider.list_pictures({}, 'invalid') }.to raise_error(KeyError)
179
+ end
180
+ end
181
+ end
182
+
183
+ describe '#picture' do
184
+ before do
185
+ @provider = described_class.new(File.join(__dir__, 'sample-db.json'))
186
+ end
187
+
188
+ context 'when +selector+ matches exactly one picture, with +uri+ pointing to a valid image file' do
189
+ it 'should return the file mime type and content' do
190
+ selector = { 'uri' => 'white.jpg' }
191
+ expect(@provider.picture(selector)).to eql(
192
+ ['image/jpeg', File.read(File.join(__dir__, 'white.jpg'))]
193
+ )
194
+ selector = { 'uri' => './alpha.png' }
195
+ expect(@provider.picture(selector)).to eql(
196
+ ['image/png', File.read(File.join(__dir__, 'alpha.png'))]
197
+ )
198
+ end
199
+ end
200
+
201
+ context 'when +selector+ matches exactly one picture, with a +uri+ pointing to a non-existing file' do
202
+ it 'should raise KeyError' do
203
+ selector = { 'uri' => 'pic3.jpg' }
204
+ expect { @provider.picture(selector) }.to raise_error(KeyError)
205
+ end
206
+ end
207
+
208
+ context 'when +selector+ matches exactly one picture, with a +uri+ pointing to a non-image file' do
209
+ it 'should raise KeyError' do
210
+ selector = { 'uri' => 'sample-db.json' }
211
+ expect { @provider.picture(selector) }.to raise_error(KeyError)
212
+ end
213
+ end
214
+
215
+ context 'when +selector+ matches exactly one picture, with +uri+ not defined' do
216
+ it 'should raise KeyError' do
217
+ selector = { 'datetime' => '2019:07:22 09:45:17' }
218
+ expect { @provider.picture(selector) }.to raise_error(KeyError)
219
+ end
220
+ end
221
+
222
+ context 'when +selector+ matches zero or more than one picture' do
223
+ it 'should raise KeyError' do
224
+ selector = { 'group1' => 'group1_title2' }
225
+ expect { @provider.picture(selector) }.to raise_error(KeyError)
226
+ selector = { 'uri' => 'black.jpg' }
227
+ expect { @provider.picture(selector) }.to raise_error(KeyError)
228
+ end
229
+ end
230
+ end
231
+
232
+ describe '#group_thumbnail' do
233
+ before do
234
+ @provider = described_class.new(File.join(__dir__, 'sample-db.json'))
235
+ end
236
+
237
+ context 'when +selector+ matches exactly one group, with +uri+ pointing to a valid image file' do
238
+ it 'should return the thumbnail file mime type and content' do
239
+ selector = { 'group1' => 'group1_title1' }
240
+ expect(@provider.group_thumbnail('group1', selector)).to eql(
241
+ ['image/jpeg', File.read(File.join(__dir__, 'white.jpg'))]
242
+ )
243
+ selector = { 'uri' => 'pic3.jpg' }
244
+ expect(@provider.group_thumbnail('group2', selector)).to eql(
245
+ ['image/png', File.read(File.join(__dir__, 'alpha.png'))]
246
+ )
247
+ end
248
+ end
249
+
250
+ context 'when +selector+ matches exactly one group, with a +uri+ pointing to a non-existing file' do
251
+ it 'should raise KeyError' do
252
+ selector = { 'group1' => 'group1_title2' }
253
+ expect { @provider.group_thumbnail('group1', selector) }.to raise_error(KeyError)
254
+ end
255
+ end
256
+
257
+ context 'when +selector+ matches exactly one group, with +uri+ not defined' do
258
+ it 'should return nil' do
259
+ selector = { 'group2' => 'group2_title2' }
260
+ expect(@provider.group_thumbnail('group2', selector)).to be_nil
261
+ end
262
+ end
263
+
264
+ context 'when +selector+ matches zero or more than one group' do
265
+ it 'should raise KeyError' do
266
+ selector = { 'group1' => 'invalid' }
267
+ expect { @provider.group_thumbnail('group1', selector) }.to raise_error(KeyError)
268
+ selector = { 'value' => true }
269
+ expect { @provider.group_thumbnail('group2', selector) }.to raise_error(KeyError)
270
+ end
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,499 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'intranet/core'
4
+ require 'intranet/logger'
5
+ require 'intranet/abstract_responder'
6
+ require 'intranet/pictures/responder'
7
+
8
+ RSpec.describe Intranet::Pictures::Responder do
9
+ it 'should inherit from Intranet::AbstractResponder' do
10
+ expect(described_class.superclass).to eql(Intranet::AbstractResponder)
11
+ end
12
+
13
+ it 'should define its name, version and homepage' do
14
+ expect { described_class.module_name }.not_to raise_error
15
+ expect { described_class.module_version }.not_to raise_error
16
+ expect { described_class.module_homepage }.not_to raise_error
17
+ end
18
+
19
+ before do
20
+ logger = Intranet::Logger.new(Intranet::Logger::FATAL)
21
+ @core = Intranet::Core.new(logger)
22
+
23
+ @provider = Intranet::Pictures::JsonDbProvider.new(File.join(__dir__, 'sample-db.json'))
24
+ @responder = described_class.new(@provider)
25
+ @core.register_module(
26
+ @responder, ['pictures'], File.absolute_path('../../../lib/intranet/resources', __dir__)
27
+ )
28
+ end
29
+
30
+ describe '#in_menu?' do
31
+ it 'should return the value provided at initialization' do
32
+ expect(described_class.new(nil, [], [], false).in_menu?).to be false
33
+ expect(described_class.new(nil, [], [], true).in_menu?).to be true
34
+ end
35
+ end
36
+
37
+ describe '#resources_dir' do
38
+ it 'should return the absolute path of the resources directory' do
39
+ expect(described_class.new(nil, [], [], false).resources_dir).to eql(
40
+ File.absolute_path('../../../lib/intranet/resources', __dir__)
41
+ )
42
+ end
43
+ end
44
+
45
+ describe '#title' do
46
+ it 'should return the title of the webpage provided by the module' do
47
+ expect(@responder.title).to eql('My Gallery')
48
+ end
49
+ end
50
+
51
+ describe '#css_dependencies' do
52
+ it 'should return the list of CSS dependencies' do
53
+ expect(@responder.css_dependencies).to include(
54
+ 'design/style.css',
55
+ 'design/photoswipe/photoswipe.css',
56
+ 'design/photoswipe/default-skin/default-skin.css'
57
+ )
58
+ end
59
+ end
60
+
61
+ describe '#js_dependencies' do
62
+ it 'should return the list of JavaScript dependencies' do
63
+ expect(@responder.js_dependencies).to include(
64
+ 'design/jpictures.js',
65
+ 'design/photoswipe/photoswipe.min.js',
66
+ 'design/photoswipe/photoswipe-ui-default.min.js'
67
+ )
68
+ end
69
+ end
70
+
71
+ describe '#generate_page' do
72
+ def photoswipe_markup # rubocop:disable Metrics/MethodLength
73
+ "<div aria-hidden class='pswp' role='dialog' tabindex='-1'>\n" \
74
+ "<div class='pswp__bg'></div>\n" \
75
+ "<div class='pswp__scroll-wrap'>\n" \
76
+ "<div class='pswp__container'>\n" \
77
+ "<div class='pswp__item'></div>\n" \
78
+ "<div class='pswp__item'></div>\n" \
79
+ "<div class='pswp__item'></div>\n" \
80
+ "</div>\n" \
81
+ "<div class='pswp__ui pswp__ui--hidden'>\n" \
82
+ "<div class='pswp__top-bar'>\n" \
83
+ "<div class='pswp__counter'></div>\n" \
84
+ "<button class='pswp__button pswp__button--close' title='#{I18n.t('pictures.viewer.close')} (Esc)'></button>\n" \
85
+ "<button class='pswp__button pswp__button--fs' title='#{I18n.t('pictures.viewer.fullscreen')}'></button>\n" \
86
+ "<button class='pswp__button pswp__button--zoom' title='#{I18n.t('pictures.viewer.zoom')}'></button>\n" \
87
+ "<div class='pswp__preloader'>\n<div class='pswp__preloader__icn'>\n<div class='pswp__preloader__cut'>\n<div class='pswp__preloader__donut'></div>\n</div>\n</div>\n</div>\n</div>\n" \
88
+ "<div class='pswp__share-modal pswp__share-modal--hidden pswp__single-tap'>\n<div class='pswp__share-tooltip'></div>\n</div>\n" \
89
+ "<button class='pswp__button pswp__button--arrow--left' title='#{I18n.t('pictures.viewer.previous')}'></button>\n" \
90
+ "<button class='pswp__button pswp__button--arrow--right' title='#{I18n.t('pictures.viewer.next')}'></button>\n" \
91
+ "<div class='pswp__caption'>\n<div class='pswp__caption__center'></div>\n</div>\n" \
92
+ "</div>\n</div>\n</div>\n\n"
93
+ end
94
+
95
+ context 'when asked for \'/index.html\'' do
96
+ it 'should return a partial HTML content showing recent groups according to configuration' do
97
+ # Nominal case with limit
98
+ recents = [{ group_type: 'group2', sort_by: 'value', asc: false, limit: 2 }]
99
+ @responder.instance_variable_set(:@recents, recents)
100
+ code, mime, content = @responder.generate_page('/index.html', {})
101
+ expect(code).to eql(206)
102
+ expect(mime).to eql('text/html')
103
+ expect(content).to eql(
104
+ Hash[content: "<section>\n<h2>My Gallery</h2>\n" \
105
+ "<ul class='breadcrumb'>\n" \
106
+ "<li>\n<a href='/index.html'>#{I18n.t('nav.home')}</a>\n</li>\n" \
107
+ "<li>#{I18n.t('pictures.menu')}</li>\n" \
108
+ "<li>My Gallery</li>\n" \
109
+ "</ul>\n\n" \
110
+ "<h3>#{I18n.t('pictures.recents.group2')}</h3>\n" \
111
+ "<ul class='groups'>\n" \
112
+ "<li title='Group 2, Title 2'>\n<a onclick='openImagesGallery(&quot;group2=group2_title2&amp;sort_by=datetime&quot;);'>\n<figure>\n<div style='background-image: url(&quot;api/group/group2?group2=group2_title2&quot;)'></div>\n<figcaption>\nGroup 2, Title 2\n<br>\n<em>brief_text_2</em>\n</figcaption>\n</figure>\n</a>\n</li>\n" \
113
+ "<li title='Group 2, Title 1'>\n<a onclick='openImagesGallery(&quot;group2=group2_title1&amp;sort_by=datetime&quot;);'>\n<figure>\n<div style='background-image: url(&quot;api/group/group2?group2=group2_title1&quot;)'></div>\n<figcaption>\nGroup 2, Title 1\n</figcaption>\n</figure>\n</a>\n</li>\n" \
114
+ "</ul>\n" \
115
+ "<p class='see_more'>\n<a href='browse_group2.html?sort_by=value&amp;sort_order=desc'>#{I18n.t('pictures.see_more')}</a>\n</p>\n" \
116
+ "</section>\n" + photoswipe_markup,
117
+ title: 'My Gallery']
118
+ )
119
+
120
+ # Nominal case with limit
121
+ recents = [{ group_type: 'group2', sort_by: 'value', asc: true, limit: 2 }]
122
+ @responder.instance_variable_set(:@recents, recents)
123
+ code, mime, content = @responder.generate_page('/index.html', {})
124
+ expect(code).to eql(206)
125
+ expect(mime).to eql('text/html')
126
+ expect(content).to eql(
127
+ Hash[content: "<section>\n<h2>My Gallery</h2>\n" \
128
+ "<ul class='breadcrumb'>\n" \
129
+ "<li>\n<a href='/index.html'>#{I18n.t('nav.home')}</a>\n</li>\n" \
130
+ "<li>#{I18n.t('pictures.menu')}</li>\n" \
131
+ "<li>My Gallery</li>\n" \
132
+ "</ul>\n\n" \
133
+ "<h3>#{I18n.t('pictures.recents.group2')}</h3>\n" \
134
+ "<ul class='groups'>\n" \
135
+ "<li title='Group 2, Title 3'>\n<a onclick='openImagesGallery(&quot;group2=group2_title3&amp;sort_by=datetime&quot;);'>\n<figure>\n<div style='background-image: url(&quot;api/group/group2?group2=group2_title3&quot;)'></div>\n<figcaption>\nGroup 2, Title 3\n</figcaption>\n</figure>\n</a>\n</li>\n" \
136
+ "<li title='Group 2, Title 1'>\n<a onclick='openImagesGallery(&quot;group2=group2_title1&amp;sort_by=datetime&quot;);'>\n<figure>\n<div style='background-image: url(&quot;api/group/group2?group2=group2_title1&quot;)'></div>\n<figcaption>\nGroup 2, Title 1\n</figcaption>\n</figure>\n</a>\n</li>\n" \
137
+ "</ul>\n" \
138
+ "<p class='see_more'>\n<a href='browse_group2.html?sort_by=value'>#{I18n.t('pictures.see_more')}</a>\n</p>\n" \
139
+ "</section>\n" + photoswipe_markup,
140
+ title: 'My Gallery']
141
+ )
142
+
143
+ # Nominal case without limit
144
+ recents = [{ group_type: 'group2' }]
145
+ @responder.instance_variable_set(:@recents, recents)
146
+ code, mime, content = @responder.generate_page('/index.html', {})
147
+ expect(code).to eql(206)
148
+ expect(mime).to eql('text/html')
149
+ expect(content).to eql(
150
+ Hash[content: "<section>\n<h2>My Gallery</h2>\n" \
151
+ "<ul class='breadcrumb'>\n" \
152
+ "<li>\n<a href='/index.html'>#{I18n.t('nav.home')}</a>\n</li>\n" \
153
+ "<li>#{I18n.t('pictures.menu')}</li>\n" \
154
+ "<li>My Gallery</li>\n" \
155
+ "</ul>\n\n" \
156
+ "<h3>#{I18n.t('pictures.recents.group2')}</h3>\n" \
157
+ "<ul class='groups'>\n" \
158
+ "<li title='Group 2, Title 3'>\n<a onclick='openImagesGallery(&quot;group2=group2_title3&amp;sort_by=datetime&quot;);'>\n<figure>\n<div style='background-image: url(&quot;api/group/group2?group2=group2_title3&quot;)'></div>\n<figcaption>\nGroup 2, Title 3\n</figcaption>\n</figure>\n</a>\n</li>\n" \
159
+ "<li title='Group 2, Title 2'>\n<a onclick='openImagesGallery(&quot;group2=group2_title2&amp;sort_by=datetime&quot;);'>\n<figure>\n<div style='background-image: url(&quot;api/group/group2?group2=group2_title2&quot;)'></div>\n<figcaption>\nGroup 2, Title 2\n<br>\n<em>brief_text_2</em>\n</figcaption>\n</figure>\n</a>\n</li>\n" \
160
+ "<li title='Group 2, Title 1'>\n<a onclick='openImagesGallery(&quot;group2=group2_title1&amp;sort_by=datetime&quot;);'>\n<figure>\n<div style='background-image: url(&quot;api/group/group2?group2=group2_title1&quot;)'></div>\n<figcaption>\nGroup 2, Title 1\n</figcaption>\n</figure>\n</a>\n</li>\n" \
161
+ "</ul>\n" \
162
+ "</section>\n" + photoswipe_markup,
163
+ title: 'My Gallery']
164
+ )
165
+
166
+ # Incorrect recents specification
167
+ recents = [{ group_type: 'invalid', sort_by: 'id' }]
168
+ @responder.instance_variable_set(:@recents, recents)
169
+ code, mime, content = @responder.generate_page('/index.html', {})
170
+ expect(code).to eql(404)
171
+ expect(mime).to be_empty
172
+ expect(content).to be_empty
173
+ end
174
+
175
+ it 'should return a partial HTML content showing all groups according to configuration' do
176
+ # Nominal case without recents
177
+ home_groups = [{ group_type: 'group2', asc: true, browse: 'group1', browse_sort_by: 'uri', browse_asc: true }]
178
+ @responder.instance_variable_set(:@home_groups, home_groups)
179
+ code, mime, content = @responder.generate_page('/index.html', {})
180
+ expect(code).to eql(206)
181
+ expect(mime).to eql('text/html')
182
+ expect(content).to eql(
183
+ Hash[content: "<section>\n<h2>My Gallery</h2>\n" \
184
+ "<ul class='breadcrumb'>\n" \
185
+ "<li>\n<a href='/index.html'>#{I18n.t('nav.home')}</a>\n</li>\n" \
186
+ "<li>#{I18n.t('pictures.menu')}</li>\n" \
187
+ "<li>My Gallery</li>\n" \
188
+ "</ul>\n\n" \
189
+ "<h3>#{I18n.t('pictures.browse_by.group2')}</h3>\n" \
190
+ "<ul class='groups wide'>\n" \
191
+ "<li title='Group 2, Title 1'>\n<a href='browse_group1.html?sort_by=uri&amp;group2=group2_title1'>\n<figure>\n<div style='background-image: url(&quot;api/group/group2?group2=group2_title1&quot;)'></div>\n<figcaption>\nGroup 2, Title 1\n</figcaption>\n</figure>\n</a>\n</li>\n" \
192
+ "<li title='Group 2, Title 2'>\n<a href='browse_group1.html?sort_by=uri&amp;group2=group2_title2'>\n<figure>\n<div style='background-image: url(&quot;api/group/group2?group2=group2_title2&quot;)'></div>\n<figcaption>\nGroup 2, Title 2\n<br>\n<em>brief_text_2</em>\n</figcaption>\n</figure>\n</a>\n</li>\n" \
193
+ "<li title='Group 2, Title 3'>\n<a href='browse_group1.html?sort_by=uri&amp;group2=group2_title3'>\n<figure>\n<div style='background-image: url(&quot;api/group/group2?group2=group2_title3&quot;)'></div>\n<figcaption>\nGroup 2, Title 3\n</figcaption>\n</figure>\n</a>\n</li>\n" \
194
+ "</ul>\n" \
195
+ "</section>\n",
196
+ title: 'My Gallery']
197
+ )
198
+
199
+ # Nominal case with recents
200
+ recents = [{ group_type: 'group2' }]
201
+ @responder.instance_variable_set(:@recents, recents)
202
+ home_groups = [{ group_type: 'group1', asc: false, browse: 'group2', browse_sort_by: 'value', browse_asc: false }]
203
+ @responder.instance_variable_set(:@home_groups, home_groups)
204
+ code, mime, content = @responder.generate_page('/index.html', {})
205
+ expect(code).to eql(206)
206
+ expect(mime).to eql('text/html')
207
+ expect(content).to eql(
208
+ Hash[content: "<section>\n<h2>My Gallery</h2>\n" \
209
+ "<ul class='breadcrumb'>\n" \
210
+ "<li>\n<a href='/index.html'>#{I18n.t('nav.home')}</a>\n</li>\n" \
211
+ "<li>#{I18n.t('pictures.menu')}</li>\n" \
212
+ "<li>My Gallery</li>\n" \
213
+ "</ul>\n\n" \
214
+ "<h3>#{I18n.t('pictures.recents.group2')}</h3>\n" \
215
+ "<ul class='groups'>\n" \
216
+ "<li title='Group 2, Title 3'>\n<a onclick='openImagesGallery(&quot;group2=group2_title3&amp;sort_by=datetime&quot;);'>\n<figure>\n<div style='background-image: url(&quot;api/group/group2?group2=group2_title3&quot;)'></div>\n<figcaption>\nGroup 2, Title 3\n</figcaption>\n</figure>\n</a>\n</li>\n" \
217
+ "<li title='Group 2, Title 2'>\n<a onclick='openImagesGallery(&quot;group2=group2_title2&amp;sort_by=datetime&quot;);'>\n<figure>\n<div style='background-image: url(&quot;api/group/group2?group2=group2_title2&quot;)'></div>\n<figcaption>\nGroup 2, Title 2\n<br>\n<em>brief_text_2</em>\n</figcaption>\n</figure>\n</a>\n</li>\n" \
218
+ "<li title='Group 2, Title 1'>\n<a onclick='openImagesGallery(&quot;group2=group2_title1&amp;sort_by=datetime&quot;);'>\n<figure>\n<div style='background-image: url(&quot;api/group/group2?group2=group2_title1&quot;)'></div>\n<figcaption>\nGroup 2, Title 1\n</figcaption>\n</figure>\n</a>\n</li>\n" \
219
+ "</ul>\n" \
220
+ "<h3>#{I18n.t('pictures.browse_by.group1')}</h3>\n" \
221
+ "<ul class='groups wide'>\n" \
222
+ "<li title='Group 1, Title 2'>\n<a href='browse_group2.html?sort_by=value&amp;sort_order=desc&amp;group1=group1_title2'>\n<figure>\n<div style='background-image: url(&quot;api/group/group1?group1=group1_title2&quot;)'></div>\n<figcaption>\nGroup 1, Title 2\n</figcaption>\n</figure>\n</a>\n</li>\n" \
223
+ "<li title='Group 1, Title 1'>\n<a href='browse_group2.html?sort_by=value&amp;sort_order=desc&amp;group1=group1_title1'>\n<figure>\n<div style='background-image: url(&quot;api/group/group1?group1=group1_title1&quot;)'></div>\n<figcaption>\nGroup 1, Title 1\n<br>\n<em>brief_text_1</em>\n</figcaption>\n</figure>\n</a>\n</li>\n" \
224
+ "</ul>\n" \
225
+ "</section>\n" + photoswipe_markup,
226
+ title: 'My Gallery']
227
+ )
228
+ end
229
+ end
230
+
231
+ context 'when asked for \'/browse_*.html\'' do
232
+ it 'should return a partial HTML content with selected groups' do
233
+ # Existing group, no selector nor sort order
234
+ query = {}
235
+ code, mime, content = @responder.generate_page('/browse_group1.html', query)
236
+ expect(code).to eql(206)
237
+ expect(mime).to eql('text/html')
238
+ expect(content).to eql(
239
+ Hash[content: "<section>\n<h2>My Gallery</h2>\n" \
240
+ "<ul class='breadcrumb'>\n" \
241
+ "<li>\n<a href='/index.html'>#{I18n.t('nav.home')}</a>\n</li>\n" \
242
+ "<li>#{I18n.t('pictures.menu')}</li>\n" \
243
+ "<li>\n<a href='index.html'>My Gallery</a>\n</li>\n" \
244
+ "<li>#{I18n.t('pictures.nav.group1')}</li>\n" \
245
+ "</ul>\n\n" \
246
+ "<ul class='groups'>\n" \
247
+ "<li title='Group 1, Title 1'>\n<a onclick='openImagesGallery(&quot;group1=group1_title1&amp;sort_by=datetime&quot;);'>\n<figure>\n<div style='background-image: url(&quot;api/group/group1?group1=group1_title1&quot;)'></div>\n<figcaption>\nGroup 1, Title 1\n<br>\n<em>brief_text_1</em>\n</figcaption>\n</figure>\n</a>\n</li>\n" \
248
+ "<li title='Group 1, Title 2'>\n<a onclick='openImagesGallery(&quot;group1=group1_title2&amp;sort_by=datetime&quot;);'>\n<figure>\n<div style='background-image: url(&quot;api/group/group1?group1=group1_title2&quot;)'></div>\n<figcaption>\nGroup 1, Title 2\n</figcaption>\n</figure>\n</a>\n</li>\n" \
249
+ "</ul>\n" \
250
+ "</section>\n" + photoswipe_markup,
251
+ title: 'My Gallery']
252
+ )
253
+
254
+ # Existing group, valid selector, valid sort order
255
+ query = { 'group1' => 'group1_title1', 'sort_by' => 'id', 'sort_order' => 'desc' }
256
+ code, mime, content = @responder.generate_page('/browse_group2.html', query)
257
+ expect(code).to eql(206)
258
+ expect(mime).to eql('text/html')
259
+ expect(content).to eql(
260
+ Hash[content: "<section>\n<h2>My Gallery</h2>\n" \
261
+ "<ul class='breadcrumb'>\n" \
262
+ "<li>\n<a href='/index.html'>#{I18n.t('nav.home')}</a>\n</li>\n" \
263
+ "<li>#{I18n.t('pictures.menu')}</li>\n" \
264
+ "<li>\n<a href='index.html'>My Gallery</a>\n</li>\n" \
265
+ "<li>#{I18n.t('pictures.nav.group2')} (Group 1, Title 1)</li>\n" \
266
+ "</ul>\n\n" \
267
+ "<ul class='groups'>\n" \
268
+ "<li title='Group 2, Title 3'>\n<a onclick='openImagesGallery(&quot;group1=group1_title1&amp;group2=group2_title3&amp;sort_by=datetime&quot;);'>\n<figure>\n<div style='background-image: url(&quot;api/group/group2?group2=group2_title3&quot;)'></div>\n<figcaption>\nGroup 2, Title 3\n</figcaption>\n</figure>\n</a>\n</li>\n" \
269
+ "<li title='Group 2, Title 2'>\n<a onclick='openImagesGallery(&quot;group1=group1_title1&amp;group2=group2_title2&amp;sort_by=datetime&quot;);'>\n<figure>\n<div style='background-image: url(&quot;api/group/group2?group2=group2_title2&quot;)'></div>\n<figcaption>\nGroup 2, Title 2\n<br>\n<em>brief_text_2</em>\n</figcaption>\n</figure>\n</a>\n</li>\n" \
270
+ "</ul>\n" \
271
+ "</section>\n" + photoswipe_markup,
272
+ title: 'My Gallery']
273
+ )
274
+
275
+ # Invalid selector
276
+ query = { 'foo' => 'bar' }
277
+ code, mime, content = @responder.generate_page('/browse_group2.html', query)
278
+ expect(code).to eql(206)
279
+ expect(mime).to eql('text/html')
280
+ expect(content).to eql(
281
+ Hash[content: "<section>\n<h2>My Gallery</h2>\n" \
282
+ "<ul class='breadcrumb'>\n" \
283
+ "<li>\n<a href='/index.html'>#{I18n.t('nav.home')}</a>\n</li>\n" \
284
+ "<li>#{I18n.t('pictures.menu')}</li>\n" \
285
+ "<li>\n<a href='index.html'>My Gallery</a>\n</li>\n" \
286
+ "<li>#{I18n.t('pictures.nav.group2')} (bar)</li>\n" \
287
+ "</ul>\n\n" \
288
+ "<ul class='groups'>\n</ul>\n" \
289
+ "</section>\n" + photoswipe_markup,
290
+ title: 'My Gallery']
291
+ )
292
+
293
+ # Invalid group type
294
+ code, mime, content = @responder.generate_page('/browse_foo.html', {})
295
+ expect(code).to eql(404)
296
+ expect(mime).to be_empty
297
+ expect(content).to be_empty
298
+
299
+ # Invalid sort order
300
+ query = { 'sort_order' => 'foo' }
301
+ code, mime, content = @responder.generate_page('/browse_group2.html', query)
302
+ expect(code).to eql(404)
303
+ expect(mime).to be_empty
304
+ expect(content).to be_empty
305
+ end
306
+ end
307
+
308
+ context 'when asked for \'/api/groups\'' do
309
+ it 'should return a JSON representation of the selected groups' do
310
+ # Existing group, no selector nor sort order
311
+ query = {}
312
+ code, mime, content = @responder.generate_page('/api/groups/group1', query)
313
+ expect(code).to eql(200)
314
+ expect(mime).to eql('application/json')
315
+ expect(content).to eql(
316
+ [
317
+ { 'id' => 'group1_title1', 'title' => 'Group 1, Title 1', 'brief' => 'brief_text_1' },
318
+ { 'id' => 'group1_title2', 'title' => 'Group 1, Title 2' }
319
+ ].to_json
320
+ )
321
+
322
+ # Existing group, valid selector, no sort order
323
+ query = { 'group1' => 'group1_title1' }
324
+ code, mime, content = @responder.generate_page('/api/groups/group2', query)
325
+ expect(code).to eql(200)
326
+ expect(mime).to eql('application/json')
327
+ expect(content).to eql(
328
+ [
329
+ { 'id' => 'group2_title2', 'title' => 'Group 2, Title 2', 'value' => 'bcde', 'brief' => 'brief_text_2' },
330
+ { 'id' => 'group2_title3', 'title' => 'Group 2, Title 3', 'value' => 'aabb' }
331
+ ].to_json
332
+ )
333
+
334
+ # Existing group, valid selector, valid sort order
335
+ query = { 'group1' => 'group1_title1', 'sort_by' => 'value', 'sort_order' => 'asc' }
336
+ code, mime, content = @responder.generate_page('/api/groups/group2', query)
337
+ expect(code).to eql(200)
338
+ expect(mime).to eql('application/json')
339
+ expect(content).to eql(
340
+ [
341
+ { 'id' => 'group2_title3', 'title' => 'Group 2, Title 3', 'value' => 'aabb' },
342
+ { 'id' => 'group2_title2', 'title' => 'Group 2, Title 2', 'value' => 'bcde', 'brief' => 'brief_text_2' }
343
+ ].to_json
344
+ )
345
+ query = { 'group1' => 'group1_title1', 'sort_by' => 'id', 'sort_order' => 'desc' }
346
+ code, mime, content = @responder.generate_page('/api/groups/group2', query)
347
+ expect(code).to eql(200)
348
+ expect(mime).to eql('application/json')
349
+ expect(content).to eql(
350
+ [
351
+ { 'id' => 'group2_title3', 'title' => 'Group 2, Title 3', 'value' => 'aabb' },
352
+ { 'id' => 'group2_title2', 'title' => 'Group 2, Title 2', 'value' => 'bcde', 'brief' => 'brief_text_2' }
353
+ ].to_json
354
+ )
355
+
356
+ # Invalid selector
357
+ query = { 'foo' => 'bar' }
358
+ code, mime, content = @responder.generate_page('/api/groups/group2', query)
359
+ expect(code).to eql(200)
360
+ expect(mime).to eql('application/json')
361
+ expect(content).to eql([].to_json)
362
+
363
+ # Invalid group type
364
+ code, mime, content = @responder.generate_page('/api/groups/foo', {})
365
+ expect(code).to eql(404)
366
+ expect(mime).to be_empty
367
+ expect(content).to be_empty
368
+
369
+ # Invalid sort order
370
+ query = { 'sort_order' => 'foo' }
371
+ code, mime, content = @responder.generate_page('/api/groups/group2', query)
372
+ expect(code).to eql(404)
373
+ expect(mime).to be_empty
374
+ expect(content).to be_empty
375
+ end
376
+ end
377
+
378
+ context 'when asked for \'/api/group\'' do
379
+ it 'should return the selected group thumnail' do
380
+ # Existing group with thumbnail
381
+ query = { 'group2' => 'group2_title1' }
382
+ code, mime, content = @responder.generate_page('/api/group/group2', query)
383
+ expect(code).to eql(200)
384
+ expect(mime).to eql('image/png')
385
+ expect(content).to eql(File.read(File.join(__dir__, 'alpha.png')))
386
+
387
+ # Existing group with no specified thumbnail
388
+ query = { 'group2' => 'group2_title2' }
389
+ code, mime, content = @responder.generate_page('/api/group/group2', query)
390
+ expect(code).to eql(200)
391
+ expect(mime).to eql('image/jpeg')
392
+ expect(content).to eql(
393
+ File.read(File.join(__dir__, '../../../lib/intranet/resources/www/group_thumbnail.jpg'))
394
+ )
395
+
396
+ # Existing group with non-existant thumbnail
397
+ query = { 'group1' => 'group1_title2' }
398
+ code, mime, content = @responder.generate_page('/api/group/group1', query)
399
+ expect(code).to eql(404)
400
+ expect(mime).to be_empty
401
+ expect(content).to be_empty
402
+ end
403
+ end
404
+
405
+ context 'when asked for \'/api/pictures\'' do
406
+ it 'should return a JSON representation of the selected pictures' do
407
+ # All pictures (no selector nor sort order)
408
+ code, mime, content = @responder.generate_page('/api/pictures', {})
409
+ expect(code).to eql(200)
410
+ expect(mime).to eql('application/json')
411
+ expect(content).to eql(
412
+ [
413
+ { 'datetime' => '2019:07:22 09:41:31', 'group1' => 'group1_title1', 'group2' => 'group2_title2' },
414
+ { 'datetime' => '2020:06:19 07:51:05', 'value' => false, 'group1' => 'group1_title2', 'group2' => 'group2_title2' },
415
+ { 'datetime' => '2020:06:20 18:14:09', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title1' },
416
+ { 'datetime' => '2020:06:20 06:09:54', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title3' },
417
+ { 'datetime' => '2019:07:22 09:45:17', 'group1' => 'group1_title1', 'group2' => 'group2_title3' }
418
+ ].to_json
419
+ )
420
+
421
+ # Valid selector, no sort order
422
+ query = { 'group1' => 'group1_title2', 'value' => true }
423
+ code, mime, content = @responder.generate_page('/api/pictures', query)
424
+ expect(code).to eql(200)
425
+ expect(mime).to eql('application/json')
426
+ expect(content).to eql(
427
+ [
428
+ { 'datetime' => '2020:06:20 18:14:09', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title1' },
429
+ { 'datetime' => '2020:06:20 06:09:54', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title3' }
430
+ ].to_json
431
+ )
432
+
433
+ # Valid selector, valid sort order
434
+ query = { 'group1' => 'group1_title2', 'value' => true, 'sort_by' => 'datetime' }
435
+ code, mime, content = @responder.generate_page('/api/pictures', query)
436
+ expect(code).to eql(200)
437
+ expect(mime).to eql('application/json')
438
+ expect(content).to eql(
439
+ [
440
+ { 'datetime' => '2020:06:20 06:09:54', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title3' },
441
+ { 'datetime' => '2020:06:20 18:14:09', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title1' }
442
+ ].to_json
443
+ )
444
+ query = { 'group1' => 'group1_title2', 'value' => true, 'sort_by' => 'group2',
445
+ 'sort_order' => 'desc' }
446
+ code, mime, content = @responder.generate_page('/api/pictures', query)
447
+ expect(code).to eql(200)
448
+ expect(mime).to eql('application/json')
449
+ expect(content).to eql(
450
+ [
451
+ { 'datetime' => '2020:06:20 06:09:54', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title3' },
452
+ { 'datetime' => '2020:06:20 18:14:09', 'value' => true, 'group1' => 'group1_title2', 'group2' => 'group2_title1' }
453
+ ].to_json
454
+ )
455
+
456
+ # Invalid selector
457
+ query = { 'a' => 'b' }
458
+ code, mime, content = @responder.generate_page('/api/pictures', query)
459
+ expect(code).to eql(200)
460
+ expect(mime).to eql('application/json')
461
+ expect(content).to eql([].to_json)
462
+
463
+ # Invalid sort order
464
+ query = { 'sort_order' => 'foo' }
465
+ code, mime, content = @responder.generate_page('/api/pictures', query)
466
+ expect(code).to eql(404)
467
+ expect(mime).to be_empty
468
+ expect(content).to be_empty
469
+ end
470
+ end
471
+
472
+ context 'when asked for \'/api/picture\'' do
473
+ it 'should return the selected picture' do
474
+ # Existing picture
475
+ query = { 'uri' => 'white.jpg' }
476
+ code, mime, content = @responder.generate_page('/api/picture', query)
477
+ expect(code).to eql(200)
478
+ expect(mime).to eql('image/jpeg')
479
+ expect(content).to eql(File.read(File.join(__dir__, 'white.jpg')))
480
+
481
+ # Invalid selector
482
+ query = { 'value' => true }
483
+ code, mime, content = @responder.generate_page('/api/picture', query)
484
+ expect(code).to eql(404)
485
+ expect(mime).to be_empty
486
+ expect(content).to be_empty
487
+ end
488
+ end
489
+
490
+ context 'otherwise' do
491
+ it 'should return an HTTP 404 error' do
492
+ expect(@responder.generate_page('index.html', {})).to eql([404, '', ''])
493
+ expect(@responder.generate_page('/api/groups', {})).to eql([404, '', ''])
494
+ expect(@responder.generate_page('/api/group', {})).to eql([404, '', ''])
495
+ expect(@responder.generate_page('/api/pictures/foo', {})).to eql([404, '', ''])
496
+ end
497
+ end
498
+ end
499
+ end