[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]

Re: [Ovirt-devel] [Patch] 1st pass at paging



Scott Seago wrote:
jay wrote:
For this first pass, I added paging to just one place (the host list in pool/show) so everyone could agree/disagree on the path. Once this is set, I can quickly add it in the various places it will be needed. Background: * I am using a rails plugin for the actual pagination called will_paginate. This basically extends active record by adding a wrapper to the find method. The license for this plugin has been approved by Brian Faustyn, who told me it was MIT and should be fine with our gpl code. * For the javascript, I am using jquery and a jquery plugin. These are also MIT licenses.

I have not included these libraries in the patch as they are 3rd party code ad I wasn't sure what our process was on that.. Just let me know if anyone wants them included in the patch.

I think it looks fine overall -- we do need the libraries in the patch, since I think we'd agreed that for the plugin we'd just bundle it with the WUI. So wherever in the rails checkout these need to be we should include them -- in additon to the appropriate license file. We'll also need to update the RPM spec to reflect the fact that we've got MIT and GPL code in there.

Scott
I'm back from the future, and I brought a patch for the new libraries with me! I kept it separate from the original, so it wouldn't be too huge, but let me know if it would be better to combine them. Licenses are included with the appropriate code. Let me know how the spec should be changed if you want me to do that, and I will send along a patch for that as well. Lastly, yes, we agreed these would be bundled in the wui app (plugins go in vendor/plugins) and the patch should reflect the correct info there.
diff --git a/wui/src/public/javascripts/jquery.pack.js b/wui/src/public/javascripts/jquery.pack.js
new file mode 100644
index 0000000..74cdfee
--- /dev/null
+++ b/wui/src/public/javascripts/jquery.pack.js
@@ -0,0 +1,11 @@
+/*
+ * jQuery 1.2.3 - New Wave Javascript
+ *
+ * Copyright (c) 2008 John Resig (jquery.com)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2008-02-06 00:21:25 -0500 (Wed, 06 Feb 2008) $
+ * $Rev: 4663 $
+ */
+eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('(J(){7(1e.3N)L w=1e.3N;L E=1e.3N=J(a,b){K 1B E.2l.4T(a,b)};7(1e.$)L D=1e.$;1e.$=E;L u=/^[^<]*(<(.|\\s)+>)[^>]*$|^#(\\w+)$/;L G=/^.[^:#\\[\\.]*$/;E.1n=E.2l={4T:J(d,b){d=d||T;7(d.15){6[0]=d;6.M=1;K 6}N 7(1o d=="25"){L c=u.2O(d);7(c&&(c[1]||!b)){7(c[1])d=E.4a([c[1]],b);N{L a=T.5J(c[3]);7(a)7(a.2w!=c[3])K E().2s(d);N{6[0]=a;6.M=1;K 6}N d=[]}}N K 1B E(b).2s(d)}N 7(E.1q(d))K 1B E(T)[E.1n.21?"21":"3U"](d);K 6.6E(d.1k==1M&&d||(d.5h||d.M&&d!=1e&&!d.15&&d[0]!=10&&d[0].15)&&E.2I(d)||[d])},5h:"1.2.3",87:J(){K 6.M},M:0,22:J(a){K a==10?E.2I(6):6[a]},2F:J(b){L a=E(b);a.54=6;K a},6E:J(a){6.M=0;1M.2l.1g.1i(6,a);K 6},R:J(a,b){K E.R(6,a,b)},4X:J(b){L a=-1;6.R(J(i){7(6==b)a=i});K a},1J:J(c,a,b){L d=c;7(c.1k==4e)7(a==10)K 6.M&&E[b||"1J"](6[0],c)||10;N{d={};d[c]=a}K 6.R(J(i){Q(c 1p d)E.1J(b?6.W:6,c,E.1l(6,d[c],b,i,c))})},1j:J(b,a){7((b==\'27\'||b==\'1R\')&&2M(a)<0)a=10;K 6.1J(b,a,"2o")},1u:J(b){7(1o b!="3V"&&b!=V)K 6.4x().3t((6[0]&&6[0].2i||T).5r(b));L a="";E.R(b||6,J(){E.R(6.3p,J(){7(6.15!=8)a+=6.15!=1?6.6K:E.1n.1u([6])})});K a},5m:J(b){7(6[0])E(b,6[0].2i).5k().3o(6[0]).2c(J(){L a=6;2b(a.1C)a=a.1C;K a}).3t(6);K 6},8w:J(a){K 6.R(J(){E(6).6z().5m(a)})},8p:J(a){K 6.R(J(){E(6).5m(a)})},3t:J(){K 6.3O(18,P,S,J(a){7(6.15==1)6.38(a)})},6q:J(){K 6.3O(18,P,P,J(a){7(6.15==1)6.3o(a,6.1C)})},6o:J(){K 6.3O(18,S,S,J(a){6.1a.3o(a,6)})},5a:J(){K 6.3O(18,S,P,J(a){6.1a.3o(a,6.2B)})},3h:J(){K 6.54||E([])},2s:J(b){L c=E.2c(6,J(a){K E.2s(b,a)});K 6.2F(/[^+>] [^+>]/.17(b)||b.1f("..")>-1?E.57(c):c)},5k:J(e){L f=6.2c(J(){7(E.14.1d&&!E.3E(6)){L a=6.69(P),4Y=T.3s("1x");4Y.38(a);K E.4a([4Y.3d])[0]}N K 6.69(P)});L d=f.2s("*").4R().R(J(){7(6[F]!=10)6[F]=V});7(e===P)6.2s("*").4R().R(J(i){7(6.15==3)K;L c=E.O(6,"2R");Q(L a 1p c)Q(L b 1p c[a])E.16.1b(d[i],a,c[a][b],c[a][b].O)});K f},1E:J(b){K 6.2F(E.1q(b)&&E.3y(6,J(a,i){K b.1P(a,i)})||E.3e(b,6))},56:J(b){7(b.1k==4e)7(G.17(b))K 6.2F(E.3e(b,6,P));N b=E.3e(b,6);L a=b.M&&b[b.M-1]!==10&&!b.15;K 6.1E(J(){K a?E.33(6,b)<0:6!=b})},1b:J(a){K!a?6:6.2F(E.37(6.22(),a.1k==4e?E(a).22():a.M!=10&&(!a.12||E.12(a,"3u"))?a:[a]))},3H:J(a){K a?E.3e(a,6).M>0:S},7j:J(a){K 6.3H("."+a)},5O:J(b){7(b==10){7(6.M){L c=6[0];7(E.12(c,"2k")){L e=c.3T,5I=[],11=c.11,2X=c.U=="2k-2X";7(e<0)K V;Q(L i=2X?e:0,2f=2X?e+1:11.M;i<2f;i++){L d=11[i];7(d.2p){b=E.14.1d&&!d.9J.1A.9y?d.1u:d.1A;7(2X)K b;5I.1g(b)}}K 5I}N K(6[0].1A||"").1r(/\\r/g,"")}K 10}K 6.R(J(){7(6.15!=1)K;7(b.1k==1M&&/5u|5t/.17(6.U))6.3k=(E.33(6.1A,b)>=0||E.33(6.31,b)>=0);N 7(E.12(6,"2k")){L a=b.1k==1M?b:[b];E("98",6).R(J(){6.2p=(E.33(6.1A,a)>=0||E.33(6.1u,a)>=0)});7(!a.M)6.3T=-1}N 6.1A=b})},3q:J(a){K a==10?(6.M?6[0].3d:V):6.4x().3t(a)},6S:J(a){K 6.5a(a).1V()},6Z:J(i){K 6.2K(i,i+1)},2K:J(){K 6.2F(1M.2l.2K.1i(6,18))},2c:J(b){K 6.2F(E.2c(6,J(a,i){K b.1P(a,i,a)}))},4R:J(){K 6.1b(6.54)},O:J(d,b){L a=d.23(".");a[1]=a[1]?"."+a[1]:"";7(b==V){L c=6.5n("8P"+a[1]+"!",[a[0]]);7(c==10&&6.M)c=E.O(6[0],d);K c==V&&a[1]?6.O(a[0]):c}N K 6.1N("8K"+a[1]+"!",[a[0],b]).R(J(){E.O(6,d,b)})},35:J(a){K 6.R(J(){E.35(6,a)})},3O:J(g,f,h,d){L e=6.M>1,3n;K 6.R(J(){7(!3n){3n=E.4a(g,6.2i);7(h)3n.8D()}L b=6;7(f&&E.12(6,"1O")&&E.12(3n[0],"4v"))b=6.3S("1U")[0]||6.38(6.2i.3s("1U"));L c=E([]);E.R(3n,J(){L a=e?E(6).5k(P)[0]:6;7(E.12(a,"1m")){c=c.1b(a)}N{7(a.15==1)c=c.1b(E("1m",a).1V());d.1P(b,a)}});c.R(6A)})}};E.2l.4T.2l=E.2l;J 6A(i,a){7(a.3Q)E.3P({1c:a.3Q,3l:S,1H:"1m"});N E.5g(a.1u||a.6x||a.3d||"");7(a.1a)a.1a.34(a)}E.1s=E.1n.1s=J(){L b=18[0]||{},i=1,M=18.M,5c=S,11;7(b.1k==8d){5c=b;b=18[1]||{};i=2}7(1o b!="3V"&&1o b!="J")b={};7(M==1){b=6;i=0}Q(;i<M;i++)7((11=18[i])!=V)Q(L a 1p 11){7(b===11[a])6w;7(5c&&11[a]&&1o 11[a]=="3V"&&b[a]&&!11[a].15)b[a]=E.1s(b[a],11[a]);N 7(11[a]!=10)b[a]=11[a]}K b};L F="3N"+(1B 3v()).3L(),6t=0,5b={};L H=/z-?4X|86-?84|1w|6k|7Z-?1R/i;E.1s({7Y:J(a){1e.$=D;7(a)1e.3N=w;K E},1q:J(a){K!!a&&1o a!="25"&&!a.12&&a.1k!=1M&&/J/i.17(a+"")},3E:J(a){K a.1F&&!a.1h||a.28&&a.2i&&!a.2i.1h},5g:J(a){a=E.3g(a);7(a){L b=T.3S("6f")[0]||T.1F,1m=T.3s("1m");1m.U="1u/4m";7(E.14.1d)1m.1u=a;N 1m.38(T.5r(a));b.38(1m);b.34(1m)}},12:J(b,a){K b.12&&b.12.2E()==a.2E()},1T:{},O:J(c,d,b){c=c==1e?5b:c;L a=c[F];7(!a)a=c[F]=++6t;7(d&&!E.1T[a])E.1T[a]={};7(b!=10)E.1T[a][d]=b;K d?E.1T[a][d]:a},35:J(c,b){c=c==1e?5b:c;L a=c[F];7(b){7(E.1T[a]){2V E.1T[a][b];b="";Q(b 1p E.1T[a])1Q;7(!b)E.35(c)}}N{1S{2V c[F]}1X(e){7(c.52)c.52(F)}2V E.1T[a]}},R:J(c,a,b){7(b){7(c.M==10){Q(L d 1p c)7(a.1i(c[d],b)===S)1Q}N Q(L i=0,M=c.M;i<M;i++)7(a.1i(c[i],b)===S)1Q}N{7(c.M==10){Q(L d 1p c)7(a.1P(c[d],d,c[d])===S)1Q}N Q(L i=0,M=c.M,1A=c[0];i<M&&a.1P(1A,i,1A)!==S;1A=c[++i]){}}K c},1l:J(b,a,c,i,d){7(E.1q(a))a=a.1P(b,i);K a&&a.1k==51&&c=="2o"&&!H.17(d)?a+"2S":a},1t:{1b:J(c,b){E.R((b||"").23(/\\s+/),J(i,a){7(c.15==1&&!E.1t.3Y(c.1t,a))c.1t+=(c.1t?" ":"")+a})},1V:J(c,b){7(c.15==1)c.1t=b!=10?E.3y(c.1t.23(/\\s+/),J(a){K!E.1t.3Y(b,a)}).6a(" "):""},3Y:J(b,a){K E.33(a,(b.1t||b).3X().23(/\\s+/))>-1}},68:J(b,c,a){L e={};Q(L d 1p c){e[d]=b.W[d];b.W[d]=c[d]}a.1P(b);Q(L d 1p c)b.W[d]=e[d]},1j:J(d,e,c){7(e=="27"||e=="1R"){L b,46={43:"4W",4U:"1Z",19:"3D"},3c=e=="27"?["7O","7M"]:["7J","7I"];J 5E(){b=e=="27"?d.7H:d.7F;L a=0,2N=0;E.R(3c,J(){a+=2M(E.2o(d,"7E"+6,P))||0;2N+=2M(E.2o(d,"2N"+6+"5X",P))||0});b-=24.7C(a+2N)}7(E(d).3H(":4d"))5E();N E.68(d,46,5E);K 24.2f(0,b)}K E.2o(d,e,c)},2o:J(e,k,j){L d;J 3x(b){7(!E.14.2d)K S;L a=T.4c.4K(b,V);K!a||a.4M("3x")==""}7(k=="1w"&&E.14.1d){d=E.1J(e.W,"1w");K d==""?"1":d}7(E.14.2z&&k=="19"){L c=e.W.50;e.W.50="0 7r 7o";e.W.50=c}7(k.1D(/4g/i))k=y;7(!j&&e.W&&e.W[k])d=e.W[k];N 7(T.4c&&T.4c.4K){7(k.1D(/4g/i))k="4g";k=k.1r(/([A-Z])/g,"-$1").2h();L h=T.4c.4K(e,V);7(h&&!3x(e))d=h.4M(k);N{L f=[],2C=[];Q(L a=e;a&&3x(a);a=a.1a)2C.4J(a);Q(L i=0;i<2C.M;i++)7(3x(2C[i])){f[i]=2C[i].W.19;2C[i].W.19="3D"}d=k=="19"&&f[2C.M-1]!=V?"2H":(h&&h.4M(k))||"";Q(L i=0;i<f.M;i++)7(f[i]!=V)2C[i].W.19=f[i]}7(k=="1w"&&d=="")d="1"}N 7(e.4n){L g=k.1r(/\\-(\\w)/g,J(a,b){K b.2E()});d=e.4n[k]||e.4n[g];7(!/^\\d+(2S)?$/i.17(d)&&/^\\d/.17(d)){L l=e.W.26,3K=e.3K.26;e.3K.26=e.4n.26;e.W.26=d||0;d=e.W.7f+"2S";e.W.26=l;e.3K.26=3K}}K d},4a:J(l,h){L k=[];h=h||T;7(1o h.3s==\'10\')h=h.2i||h[0]&&h[0].2i||T;E.R(l,J(i,d){7(!d)K;7(d.1k==51)d=d.3X();7(1o d=="25"){d=d.1r(/(<(\\w+)[^>]*?)\\/>/g,J(b,a,c){K c.1D(/^(aa|a6|7e|a5|4D|7a|a0|3m|9W|9U|9S)$/i)?b:a+"></"+c+">"});L f=E.3g(d).2h(),1x=h.3s("1x");L e=!f.1f("<9P")&&[1,"<2k 74=\'74\'>","</2k>"]||!f.1f("<9M")&&[1,"<73>","</73>"]||f.1D(/^<(9G|1U|9E|9B|9x)/)&&[1,"<1O>","</1O>"]||!f.1f("<4v")&&[2,"<1O><1U>","</1U></1O>"]||(!f.1f("<9w")||!f.1f("<9v"))&&[3,"<1O><1U><4v>","</4v></1U></1O>"]||!f.1f("<7e")&&[2,"<1O><1U></1U><6V>","</6V></1O>"]||E.14.1d&&[1,"1x<1x>","</1x>"]||[0,"",""];1x.3d=e[1]+d+e[2];2b(e[0]--)1x=1x.5o;7(E.14.1d){L g=!f.1f("<1O")&&f.1f("<1U")<0?1x.1C&&1x.1C.3p:e[1]=="<1O>"&&f.1f("<1U")<0?1x.3p:[];Q(L j=g.M-1;j>=0;--j)7(E.12(g[j],"1U")&&!g[j].3p.M)g[j].1a.34(g[j]);7(/^\\s/.17(d))1x.3o(h.5r(d.1D(/^\\s*/)[0]),1x.1C)}d=E.2I(1x.3p)}7(d.M===0&&(!E.12(d,"3u")&&!E.12(d,"2k")))K;7(d[0]==10||E.12(d,"3u")||d.11)k.1g(d);N k=E.37(k,d)});K k},1J:J(d,e,c){7(!d||d.15==3||d.15==8)K 10;L f=E.3E(d)?{}:E.46;7(e=="2p"&&E.14.2d)d.1a.3T;7(f[e]){7(c!=10)d[f[e]]=c;K d[f[e]]}N 7(E.14.1d&&e=="W")K E.1J(d.W,"9u",c);N 7(c==10&&E.14.1d&&E.12(d,"3u")&&(e=="9r"||e=="9o"))K d.9m(e).6K;N 7(d.28){7(c!=10){7(e=="U"&&E.12(d,"4D")&&d.1a)6Q"U 9i 9h\'t 9g 9e";d.9b(e,""+c)}7(E.14.1d&&/6O|3Q/.17(e)&&!E.3E(d))K d.4z(e,2);K d.4z(e)}N{7(e=="1w"&&E.14.1d){7(c!=10){d.6k=1;d.1E=(d.1E||"").1r(/6M\\([^)]*\\)/,"")+(2M(c).3X()=="96"?"":"6M(1w="+c*6L+")")}K d.1E&&d.1E.1f("1w=")>=0?(2M(d.1E.1D(/1w=([^)]*)/)[1])/6L).3X():""}e=e.1r(/-([a-z])/95,J(a,b){K b.2E()});7(c!=10)d[e]=c;K d[e]}},3g:J(a){K(a||"").1r(/^\\s+|\\s+$/g,"")},2I:J(b){L a=[];7(1o b!="93")Q(L i=0,M=b.M;i<M;i++)a.1g(b[i]);N a=b.2K(0);K a},33:J(b,a){Q(L i=0,M=a.M;i<M;i++)7(a[i]==b)K i;K-1},37:J(a,b){7(E.14.1d){Q(L i=0;b[i];i++)7(b[i].15!=8)a.1g(b[i])}N Q(L i=0;b[i];i++)a.1g(b[i]);K a},57:J(a){L c=[],2r={};1S{Q(L i=0,M=a.M;i<M;i++){L b=E.O(a[i]);7(!2r[b]){2r[b]=P;c.1g(a[i])}}}1X(e){c=a}K c},3y:J(c,a,d){L b=[];Q(L i=0,M=c.M;i<M;i++)7(!d&&a(c[i],i)||d&&!a(c[i],i))b.1g(c[i]);K b},2c:J(d,a){L c=[];Q(L i=0,M=d.M;i<M;i++){L b=a(d[i],i);7(b!==V&&b!=10){7(b.1k!=1M)b=[b];c=c.71(b)}}K c}});L v=8Y.8W.2h();E.14={5K:(v.1D(/.+(?:8T|8S|8R|8O)[\\/: ]([\\d.]+)/)||[])[1],2d:/77/.17(v),2z:/2z/.17(v),1d:/1d/.17(v)&&!/2z/.17(v),48:/48/.17(v)&&!/(8L|77)/.17(v)};L y=E.14.1d?"6H":"75";E.1s({8I:!E.14.1d||T.6F=="79",46:{"Q":"8F","8E":"1t","4g":y,75:y,6H:y,3d:"3d",1t:"1t",1A:"1A",2Y:"2Y",3k:"3k",8C:"8B",2p:"2p",8A:"8z",3T:"3T",6C:"6C",28:"28",12:"12"}});E.R({6B:J(a){K a.1a},8y:J(a){K E.4u(a,"1a")},8x:J(a){K E.2Z(a,2,"2B")},8v:J(a){K E.2Z(a,2,"4t")},8u:J(a){K E.4u(a,"2B")},8t:J(a){K E.4u(a,"4t")},8s:J(a){K E.5i(a.1a.1C,a)},8r:J(a){K E.5i(a.1C)},6z:J(a){K E.12(a,"8q")?a.8o||a.8n.T:E.2I(a.3p)}},J(c,d){E.1n[c]=J(b){L a=E.2c(6,d);7(b&&1o b=="25")a=E.3e(b,a);K 6.2F(E.57(a))}});E.R({6y:"3t",8m:"6q",3o:"6o",8l:"5a",8k:"6S"},J(c,b){E.1n[c]=J(){L a=18;K 6.R(J(){Q(L i=0,M=a.M;i<M;i++)E(a[i])[b](6)})}});E.R({8j:J(a){E.1J(6,a,"");7(6.15==1)6.52(a)},8i:J(a){E.1t.1b(6,a)},8h:J(a){E.1t.1V(6,a)},8g:J(a){E.1t[E.1t.3Y(6,a)?"1V":"1b"](6,a)},1V:J(a){7(!a||E.1E(a,[6]).r.M){E("*",6).1b(6).R(J(){E.16.1V(6);E.35(6)});7(6.1a)6.1a.34(6)}},4x:J(){E(">*",6).1V();2b(6.1C)6.34(6.1C)}},J(a,b){E.1n[a]=J(){K 6.R(b,18)}});E.R(["8f","5X"],J(i,c){L b=c.2h();E.1n[b]=J(a){K 6[0]==1e?E.14.2z&&T.1h["5e"+c]||E.14.2d&&1e["8e"+c]||T.6F=="79"&&T.1F["5e"+c]||T.1h["5e"+c]:6[0]==T?24.2f(24.2f(T.1h["5d"+c],T.1F["5d"+c]),24.2f(T.1h["5L"+c],T.1F["5L"+c])):a==10?(6.M?E.1j(6[0],b):V):6.1j(b,a.1k==4e?a:a+"2S")}});L C=E.14.2d&&4s(E.14.5K)<8c?"(?:[\\\\w*4r-]|\\\\\\\\.)":"(?:[\\\\w\\8b-\\8a*4r-]|\\\\\\\\.)",6v=1B 4q("^>\\\\s*("+C+"+)"),6u=1B 4q("^("+C+"+)(#)("+C+"+)"),6s=1B 4q("^([#.]?)("+C+"*)");E.1s({6r:{"":J(a,i,m){K m[2]=="*"||E.12(a,m[2])},"#":J(a,i,m){K a.4z("2w")==m[2]},":":{89:J(a,i,m){K i<m[3]-0},88:J(a,i,m){K i>m[3]-0},2Z:J(a,i,m){K m[3]-0==i},6Z:J(a,i,m){K m[3]-0==i},3j:J(a,i){K i==0},3J:J(a,i,m,r){K i==r.M-1},6n:J(a,i){K i%2==0},6l:J(a,i){K i%2},"3j-4p":J(a){K a.1a.3S("*")[0]==a},"3J-4p":J(a){K E.2Z(a.1a.5o,1,"4t")==a},"83-4p":J(a){K!E.2Z(a.1a.5o,2,"4t")},6B:J(a){K a.1C},4x:J(a){K!a.1C},82:J(a,i,m){K(a.6x||a.81||E(a).1u()||"").1f(m[3])>=0},4d:J(a){K"1Z"!=a.U&&E.1j(a,"19")!="2H"&&E.1j(a,"4U")!="1Z"},1Z:J(a){K"1Z"==a.U||E.1j(a,"19")=="2H"||E.1j(a,"4U")=="1Z"},80:J(a){K!a.2Y},2Y:J(a){K a.2Y},3k:J(a){K a.3k},2p:J(a){K a.2p||E.1J(a,"2p")},1u:J(a){K"1u"==a.U},5u:J(a){K"5u"==a.U},5t:J(a){K"5t"==a.U},59:J(a){K"59"==a.U},3I:J(a){K"3I"==a.U},58:J(a){K"58"==a.U},6j:J(a){K"6j"==a.U},6i:J(a){K"6i"==a.U},2G:J(a){K"2G"==a.U||E.12(a,"2G")},4D:J(a){K/4D|2k|6h|2G/i.17(a.12)},3Y:J(a,i,m){K E.2s(m[3],a).M},7X:J(a){K/h\\d/i.17(a.12)},7W:J(a){K E.3y(E.3G,J(b){K a==b.Y}).M}}},6g:[/^(\\[) * ?([\\w-]+) *([!*$^~=]*) *(\'?"?)(.*?)\\4 *\\]/,/^(:)([\\w-]+)\\("?\'?(.*?(\\(.*?\\))?[^(]*?)"?\'?\\)/,1B 4q("^([:.#]*)("+C+"+)")],3e:J(a,c,b){L d,2m=[];2b(a&&a!=d){d=a;L f=E.1E(a,c,b);a=f.t.1r(/^\\s*,\\s*/,"");2m=b?c=f.r:E.37(2m,f.r)}K 2m},2s:J(t,p){7(1o t!="25")K[t];7(p&&p.15!=1&&p.15!=9)K[];p=p||T;L d=[p],2r=[],3J,12;2b(t&&3J!=t){L r=[];3J=t;t=E.3g(t);L o=S;L g=6v;L m=g.2O(t);7(m){12=m[1].2E();Q(L i=0;d[i];i++)Q(L c=d[i].1C;c;c=c.2B)7(c.15==1&&(12=="*"||c.12.2E()==12))r.1g(c);d=r;t=t.1r(g,"");7(t.1f(" ")==0)6w;o=P}N{g=/^([>+~])\\s*(\\w*)/i;7((m=g.2O(t))!=V){r=[];L l={};12=m[2].2E();m=m[1];Q(L j=0,3f=d.M;j<3f;j++){L n=m=="~"||m=="+"?d[j].2B:d[j].1C;Q(;n;n=n.2B)7(n.15==1){L h=E.O(n);7(m=="~"&&l[h])1Q;7(!12||n.12.2E()==12){7(m=="~")l[h]=P;r.1g(n)}7(m=="+")1Q}}d=r;t=E.3g(t.1r(g,""));o=P}}7(t&&!o){7(!t.1f(",")){7(p==d[0])d.4l();2r=E.37(2r,d);r=d=[p];t=" "+t.6e(1,t.M)}N{L k=6u;L m=k.2O(t);7(m){m=[0,m[2],m[3],m[1]]}N{k=6s;m=k.2O(t)}m[2]=m[2].1r(/\\\\/g,"");L f=d[d.M-1];7(m[1]=="#"&&f&&f.5J&&!E.3E(f)){L q=f.5J(m[2]);7((E.14.1d||E.14.2z)&&q&&1o q.2w=="25"&&q.2w!=m[2])q=E(\'[ 2w="\'+m[2]+\'"]\',f)[0];d=r=q&&(!m[3]||E.12(q,m[3]))?[q]:[]}N{Q(L i=0;d[i];i++){L a=m[1]=="#"&&m[3]?m[3]:m[1]!=""||m[0]==""?"*":m[2];7(a=="*"&&d[i].12.2h()=="3V")a="3m";r=E.37(r,d[i].3S(a))}7(m[1]==".")r=E.55(r,m[2]);7(m[1]=="#"){L e=[];Q(L i=0;r[i];i++)7(r[i].4z("2w")==m[2]){e=[r[i]];1Q}r=e}d=r}t=t.1r(k,"")}}7(t){L b=E.1E(t,r);d=r=b.r;t=E.3g(b.t)}}7(t)d=[];7(d&&p==d[0])d.4l();2r=E.37(2r,d);K 2r},55:J(r,m,a){m=" "+m+" ";L c=[];Q(L i=0;r[i];i++){L b=(" "+r[i].1t+" ").1f(m)>=0;7(!a&&b||a&&!b)c.1g(r[i])}K c},1E:J(t,r,h){L d;2b(t&&t!=d){d=t;L p=E.6g,m;Q(L i=0;p[i];i++){m=p[i].2O(t);7(m){t=t.7V(m[0].M);m[2]=m[2].1r(/\\\\/g,"");1Q}}7(!m)1Q;7(m[1]==":"&&m[2]=="56")r=G.17(m[3])?E.1E(m[3],r,P).r:E(r).56(m[3]);N 7(m[1]==".")r=E.55(r,m[2],h);N 7(m[1]=="["){L g=[],U=m[3];Q(L i=0,3f=r.M;i<3f;i++){L a=r[i],z=a[E.46[m[2]]||m[2]];7(z==V||/6O|3Q|2p/.17(m[2]))z=E.1J(a,m[2])||\'\';7((U==""&&!!z||U=="="&&z==m[5]||U=="!="&&z!=m[5]||U=="^="&&z&&!z.1f(m[5])||U=="$="&&z.6e(z.M-m[5].M)==m[5]||(U=="*="||U=="~=")&&z.1f(m[5])>=0)^h)g.1g(a)}r=g}N 7(m[1]==":"&&m[2]=="2Z-4p"){L e={},g=[],17=/(-?)(\\d*)n((?:\\+|-)?\\d*)/.2O(m[3]=="6n"&&"2n"||m[3]=="6l"&&"2n+1"||!/\\D/.17(m[3])&&"7U+"+m[3]||m[3]),3j=(17[1]+(17[2]||1))-0,d=17[3]-0;Q(L i=0,3f=r.M;i<3f;i++){L j=r[i],1a=j.1a,2w=E.O(1a);7(!e[2w]){L c=1;Q(L n=1a.1C;n;n=n.2B)7(n.15==1)n.4k=c++;e[2w]=P}L b=S;7(3j==0){7(j.4k==d)b=P}N 7((j.4k-d)%3j==0&&(j.4k-d)/3j>=0)b=P;7(b^h)g.1g(j)}r=g}N{L f=E.6r[m[1]];7(1o f=="3V")f=f[m[2]];7(1o f=="25")f=6c("S||J(a,i){K "+f+";}");r=E.3y(r,J(a,i){K f(a,i,m,r)},h)}}K{r:r,t:t}},4u:J(b,c){L d=[];L a=b[c];2b(a&&a!=T){7(a.15==1)d.1g(a);a=a[c]}K d},2Z:J(a,e,c,b){e=e||1;L d=0;Q(;a;a=a[c])7(a.15==1&&++d==e)1Q;K a},5i:J(n,a){L r=[];Q(;n;n=n.2B){7(n.15==1&&(!a||n!=a))r.1g(n)}K r}});E.16={1b:J(f,i,g,e){7(f.15==3||f.15==8)K;7(E.14.1d&&f.53!=10)f=1e;7(!g.2D)g.2D=6.2D++;7(e!=10){L h=g;g=J(){K h.1i(6,18)};g.O=e;g.2D=h.2D}L j=E.O(f,"2R")||E.O(f,"2R",{}),1v=E.O(f,"1v")||E.O(f,"1v",J(){L a;7(1o E=="10"||E.16.5f)K a;a=E.16.1v.1i(18.3R.Y,18);K a});1v.Y=f;E.R(i.23(/\\s+/),J(c,b){L a=b.23(".");b=a[0];g.U=a[1];L d=j[b];7(!d){d=j[b]={};7(!E.16.2y[b]||E.16.2y[b].4j.1P(f)===S){7(f.3F)f.3F(b,1v,S);N 7(f.6b)f.6b("4i"+b,1v)}}d[g.2D]=g;E.16.2a[b]=P});f=V},2D:1,2a:{},1V:J(e,h,f){7(e.15==3||e.15==8)K;L i=E.O(e,"2R"),29,4X;7(i){7(h==10||(1o h=="25"&&h.7T(0)=="."))Q(L g 1p i)6.1V(e,g+(h||""));N{7(h.U){f=h.2q;h=h.U}E.R(h.23(/\\s+/),J(b,a){L c=a.23(".");a=c[0];7(i[a]){7(f)2V i[a][f.2D];N Q(f 1p i[a])7(!c[1]||i[a][f].U==c[1])2V i[a][f];Q(29 1p i[a])1Q;7(!29){7(!E.16.2y[a]||E.16.2y[a].4h.1P(e)===S){7(e.67)e.67(a,E.O(e,"1v"),S);N 7(e.66)e.66("4i"+a,E.O(e,"1v"))}29=V;2V i[a]}}})}Q(29 1p i)1Q;7(!29){L d=E.O(e,"1v");7(d)d.Y=V;E.35(e,"2R");E.35(e,"1v")}}},1N:J(g,c,d,f,h){c=E.2I(c||[]);7(g.1f("!")>=0){g=g.2K(0,-1);L a=P}7(!d){7(6.2a[g])E("*").1b([1e,T]).1N(g,c)}N{7(d.15==3||d.15==8)K 10;L b,29,1n=E.1q(d[g]||V),16=!c[0]||!c[0].36;7(16)c.4J(6.4Z({U:g,2L:d}));c[0].U=g;7(a)c[0].65=P;7(E.1q(E.O(d,"1v")))b=E.O(d,"1v").1i(d,c);7(!1n&&d["4i"+g]&&d["4i"+g].1i(d,c)===S)b=S;7(16)c.4l();7(h&&E.1q(h)){29=h.1i(d,b==V?c:c.71(b));7(29!==10)b=29}7(1n&&f!==S&&b!==S&&!(E.12(d,\'a\')&&g=="4V")){6.5f=P;1S{d[g]()}1X(e){}}6.5f=S}K b},1v:J(c){L a;c=E.16.4Z(c||1e.16||{});L b=c.U.23(".");c.U=b[0];L f=E.O(6,"2R")&&E.O(6,"2R")[c.U],42=1M.2l.2K.1P(18,1);42.4J(c);Q(L j 1p f){L d=f[j];42[0].2q=d;42[0].O=d.O;7(!b[1]&&!c.65||d.U==b[1]){L e=d.1i(6,42);7(a!==S)a=e;7(e===S){c.36();c.44()}}}7(E.14.1d)c.2L=c.36=c.44=c.2q=c.O=V;K a},4Z:J(c){L a=c;c=E.1s({},a);c.36=J(){7(a.36)a.36();a.7S=S};c.44=J(){7(a.44)a.44();a.7R=P};7(!c.2L)c.2L=c.7Q||T;7(c.2L.15==3)c.2L=a.2L.1a;7(!c.4S&&c.5w)c.4S=c.5w==c.2L?c.7P:c.5w;7(c.64==V&&c.63!=V){L b=T.1F,1h=T.1h;c.64=c.63+(b&&b.2v||1h&&1h.2v||0)-(b.62||0);c.7N=c.7L+(b&&b.2x||1h&&1h.2x||0)-(b.60||0)}7(!c.3c&&((c.4f||c.4f===0)?c.4f:c.5Z))c.3c=c.4f||c.5Z;7(!c.7b&&c.5Y)c.7b=c.5Y;7(!c.3c&&c.2G)c.3c=(c.2G&1?1:(c.2G&2?3:(c.2G&4?2:0)));K c},2y:{21:{4j:J(){5M();K},4h:J(){K}},3C:{4j:J(){7(E.14.1d)K S;E(6).2j("4P",E.16.2y.3C.2q);K P},4h:J(){7(E.14.1d)K S;E(6).3w("4P",E.16.2y.3C.2q);K P},2q:J(a){7(I(a,6))K P;18[0].U="3C";K E.16.1v.1i(6,18)}},3B:{4j:J(){7(E.14.1d)K S;E(6).2j("4O",E.16.2y.3B.2q);K P},4h:J(){7(E.14.1d)K S;E(6).3w("4O",E.16.2y.3B.2q);K P},2q:J(a){7(I(a,6))K P;18[0].U="3B";K E.16.1v.1i(6,18)}}}};E.1n.1s({2j:J(c,a,b){K c=="4H"?6.2X(c,a,b):6.R(J(){E.16.1b(6,c,b||a,b&&a)})},2X:J(d,b,c){K 6.R(J(){E.16.1b(6,d,J(a){E(6).3w(a);K(c||b).1i(6,18)},c&&b)})},3w:J(a,b){K 6.R(J(){E.16.1V(6,a,b)})},1N:J(c,a,b){K 6.R(J(){E.16.1N(c,a,6,P,b)})},5n:J(c,a,b){7(6[0])K E.16.1N(c,a,6[0],S,b);K 10},2g:J(){L b=18;K 6.4V(J(a){6.4N=0==6.4N?1:0;a.36();K b[6.4N].1i(6,18)||S})},7D:J(a,b){K 6.2j(\'3C\',a).2j(\'3B\',b)},21:J(a){5M();7(E.2Q)a.1P(T,E);N E.3A.1g(J(){K a.1P(6,E)});K 6}});E.1s({2Q:S,3A:[],21:J(){7(!E.2Q){E.2Q=P;7(E.3A){E.R(E.3A,J(){6.1i(T)});E.3A=V}E(T).5n("21")}}});L x=S;J 5M(){7(x)K;x=P;7(T.3F&&!E.14.2z)T.3F("5W",E.21,S);7(E.14.1d&&1e==3b)(J(){7(E.2Q)K;1S{T.1F.7B("26")}1X(3a){3z(18.3R,0);K}E.21()})();7(E.14.2z)T.3F("5W",J(){7(E.2Q)K;Q(L i=0;i<T.4L.M;i++)7(T.4L[i].2Y){3z(18.3R,0);K}E.21()},S);7(E.14.2d){L a;(J(){7(E.2Q)K;7(T.39!="5V"&&T.39!="1y"){3z(18.3R,0);K}7(a===10)a=E("W, 7a[7A=7z]").M;7(T.4L.M!=a){3z(18.3R,0);K}E.21()})()}E.16.1b(1e,"3U",E.21)}E.R(("7y,7x,3U,7w,5d,4H,4V,7v,"+"7G,7u,7t,4P,4O,7s,2k,"+"58,7K,7q,7p,3a").23(","),J(i,b){E.1n[b]=J(a){K a?6.2j(b,a):6.1N(b)}});L I=J(a,c){L b=a.4S;2b(b&&b!=c)1S{b=b.1a}1X(3a){b=c}K b==c};E(1e).2j("4H",J(){E("*").1b(T).3w()});E.1n.1s({3U:J(g,d,c){7(E.1q(g))K 6.2j("3U",g);L e=g.1f(" ");7(e>=0){L i=g.2K(e,g.M);g=g.2K(0,e)}c=c||J(){};L f="4Q";7(d)7(E.1q(d)){c=d;d=V}N{d=E.3m(d);f="61"}L h=6;E.3P({1c:g,U:f,1H:"3q",O:d,1y:J(a,b){7(b=="1W"||b=="5U")h.3q(i?E("<1x/>").3t(a.4b.1r(/<1m(.|\\s)*?\\/1m>/g,"")).2s(i):a.4b);h.R(c,[a.4b,b,a])}});K 6},7n:J(){K E.3m(6.5T())},5T:J(){K 6.2c(J(){K E.12(6,"3u")?E.2I(6.7m):6}).1E(J(){K 6.31&&!6.2Y&&(6.3k||/2k|6h/i.17(6.12)||/1u|1Z|3I/i.17(6.U))}).2c(J(i,c){L b=E(6).5O();K b==V?V:b.1k==1M?E.2c(b,J(a,i){K{31:c.31,1A:a}}):{31:c.31,1A:b}}).22()}});E.R("5S,6d,5R,6D,5Q,6m".23(","),J(i,o){E.1n[o]=J(f){K 6.2j(o,f)}});L B=(1B 3v).3L();E.1s({22:J(d,b,a,c){7(E.1q(b)){a=b;b=V}K E.3P({U:"4Q",1c:d,O:b,1W:a,1H:c})},7l:J(b,a){K E.22(b,V,a,"1m")},7k:J(c,b,a){K E.22(c,b,a,"3i")},7i:J(d,b,a,c){7(E.1q(b)){a=b;b={}}K E.3P({U:"61",1c:d,O:b,1W:a,1H:c})},85:J(a){E.1s(E.4I,a)},4I:{2a:P,U:"4Q",2U:0,5P:"4o/x-7h-3u-7g",5N:P,3l:P,O:V,6p:V,3I:V,49:{3M:"4o/3M, 1u/3M",3q:"1u/3q",1m:"1u/4m, 4o/4m",3i:"4o/3i, 1u/4m",1u:"1u/a7",4G:"*/*"}},4F:{},3P:J(s){L f,2W=/=\\?(&|$)/g,1z,O;s=E.1s(P,s,E.1s(P,{},E.4I,s));7(s.O&&s.5N&&1o s.O!="25")s.O=E.3m(s.O);7(s.1H=="4E"){7(s.U.2h()=="22"){7(!s.1c.1D(2W))s.1c+=(s.1c.1D(/\\?/)?"&":"?")+(s.4E||"7d")+"=?"}N 7(!s.O||!s.O.1D(2W))s.O=(s.O?s.O+"&":"")+(s.4E||"7d")+"=?";s.1H="3i"}7(s.1H=="3i"&&(s.O&&s.O.1D(2W)||s.1c.1D(2W))){f="4E"+B++;7(s.O)s.O=(s.O+"").1r(2W,"="+f+"$1");s.1c=s.1c.1r(2W,"="+f+"$1");s.1H="1m";1e[f]=J(a){O=a;1W();1y();1e[f]=10;1S{2V 1e[f]}1X(e){}7(h)h.34(g)}}7(s.1H=="1m"&&s.1T==V)s.1T=S;7(s.1T===S&&s.U.2h()=="22"){L i=(1B 3v()).3L();L j=s.1c.1r(/(\\?|&)4r=.*?(&|$)/,"$a4="+i+"$2");s.1c=j+((j==s.1c)?(s.1c.1D(/\\?/)?"&":"?")+"4r="+i:"")}7(s.O&&s.U.2h()=="22"){s.1c+=(s.1c.1D(/\\?/)?"&":"?")+s.O;s.O=V}7(s.2a&&!E.5H++)E.16.1N("5S");7((!s.1c.1f("a3")||!s.1c.1f("//"))&&s.1H=="1m"&&s.U.2h()=="22"){L h=T.3S("6f")[0];L g=T.3s("1m");g.3Q=s.1c;7(s.7c)g.a2=s.7c;7(!f){L l=S;g.9Z=g.9Y=J(){7(!l&&(!6.39||6.39=="5V"||6.39=="1y")){l=P;1W();1y();h.34(g)}}}h.38(g);K 10}L m=S;L k=1e.78?1B 78("9X.9V"):1B 76();k.9T(s.U,s.1c,s.3l,s.6p,s.3I);1S{7(s.O)k.4C("9R-9Q",s.5P);7(s.5C)k.4C("9O-5A-9N",E.4F[s.1c]||"9L, 9K 9I 9H 5z:5z:5z 9F");k.4C("X-9C-9A","76");k.4C("9z",s.1H&&s.49[s.1H]?s.49[s.1H]+", */*":s.49.4G)}1X(e){}7(s.6Y)s.6Y(k);7(s.2a)E.16.1N("6m",[k,s]);L c=J(a){7(!m&&k&&(k.39==4||a=="2U")){m=P;7(d){6I(d);d=V}1z=a=="2U"&&"2U"||!E.6X(k)&&"3a"||s.5C&&E.6J(k,s.1c)&&"5U"||"1W";7(1z=="1W"){1S{O=E.6W(k,s.1H)}1X(e){1z="5x"}}7(1z=="1W"){L b;1S{b=k.5q("6U-5A")}1X(e){}7(s.5C&&b)E.4F[s.1c]=b;7(!f)1W()}N E.5v(s,k,1z);1y();7(s.3l)k=V}};7(s.3l){L d=53(c,13);7(s.2U>0)3z(J(){7(k){k.9t();7(!m)c("2U")}},s.2U)}1S{k.9s(s.O)}1X(e){E.5v(s,k,V,e)}7(!s.3l)c();J 1W(){7(s.1W)s.1W(O,1z);7(s.2a)E.16.1N("5Q",[k,s])}J 1y(){7(s.1y)s.1y(k,1z);7(s.2a)E.16.1N("5R",[k,s]);7(s.2a&&!--E.5H)E.16.1N("6d")}K k},5v:J(s,a,b,e){7(s.3a)s.3a(a,b,e);7(s.2a)E.16.1N("6D",[a,s,e])},5H:0,6X:J(r){1S{K!r.1z&&9q.9p=="59:"||(r.1z>=6T&&r.1z<9n)||r.1z==6R||r.1z==9l||E.14.2d&&r.1z==10}1X(e){}K S},6J:J(a,c){1S{L b=a.5q("6U-5A");K a.1z==6R||b==E.4F[c]||E.14.2d&&a.1z==10}1X(e){}K S},6W:J(r,b){L c=r.5q("9k-U");L d=b=="3M"||!b&&c&&c.1f("3M")>=0;L a=d?r.9j:r.4b;7(d&&a.1F.28=="5x")6Q"5x";7(b=="1m")E.5g(a);7(b=="3i")a=6c("("+a+")");K a},3m:J(a){L s=[];7(a.1k==1M||a.5h)E.R(a,J(){s.1g(3r(6.31)+"="+3r(6.1A))});N Q(L j 1p a)7(a[j]&&a[j].1k==1M)E.R(a[j],J(){s.1g(3r(j)+"="+3r(6))});N s.1g(3r(j)+"="+3r(a[j]));K s.6a("&").1r(/%20/g,"+")}});E.1n.1s({1G:J(c,b){K c?6.2e({1R:"1G",27:"1G",1w:"1G"},c,b):6.1E(":1Z").R(J(){6.W.19=6.5s||"";7(E.1j(6,"19")=="2H"){L a=E("<"+6.28+" />").6y("1h");6.W.19=a.1j("19");7(6.W.19=="2H")6.W.19="3D";a.1V()}}).3h()},1I:J(b,a){K b?6.2e({1R:"1I",27:"1I",1w:"1I"},b,a):6.1E(":4d").R(J(){6.5s=6.5s||E.1j(6,"19");6.W.19="2H"}).3h()},6N:E.1n.2g,2g:J(a,b){K E.1q(a)&&E.1q(b)?6.6N(a,b):a?6.2e({1R:"2g",27:"2g",1w:"2g"},a,b):6.R(J(){E(6)[E(6).3H(":1Z")?"1G":"1I"]()})},9f:J(b,a){K 6.2e({1R:"1G"},b,a)},9d:J(b,a){K 6.2e({1R:"1I"},b,a)},9c:J(b,a){K 6.2e({1R:"2g"},b,a)},9a:J(b,a){K 6.2e({1w:"1G"},b,a)},99:J(b,a){K 6.2e({1w:"1I"},b,a)},97:J(c,a,b){K 6.2e({1w:a},c,b)},2e:J(l,k,j,h){L i=E.6P(k,j,h);K 6[i.2P===S?"R":"2P"](J(){7(6.15!=1)K S;L g=E.1s({},i);L f=E(6).3H(":1Z"),4A=6;Q(L p 1p l){7(l[p]=="1I"&&f||l[p]=="1G"&&!f)K E.1q(g.1y)&&g.1y.1i(6);7(p=="1R"||p=="27"){g.19=E.1j(6,"19");g.32=6.W.32}}7(g.32!=V)6.W.32="1Z";g.40=E.1s({},l);E.R(l,J(c,a){L e=1B E.2t(4A,g,c);7(/2g|1G|1I/.17(a))e[a=="2g"?f?"1G":"1I":a](l);N{L b=a.3X().1D(/^([+-]=)?([\\d+-.]+)(.*)$/),1Y=e.2m(P)||0;7(b){L d=2M(b[2]),2A=b[3]||"2S";7(2A!="2S"){4A.W[c]=(d||1)+2A;1Y=((d||1)/e.2m(P))*1Y;4A.W[c]=1Y+2A}7(b[1])d=((b[1]=="-="?-1:1)*d)+1Y;e.45(1Y,d,2A)}N e.45(1Y,a,"")}});K P})},2P:J(a,b){7(E.1q(a)||(a&&a.1k==1M)){b=a;a="2t"}7(!a||(1o a=="25"&&!b))K A(6[0],a);K 6.R(J(){7(b.1k==1M)A(6,a,b);N{A(6,a).1g(b);7(A(6,a).M==1)b.1i(6)}})},94:J(b,c){L a=E.3G;7(b)6.2P([]);6.R(J(){Q(L i=a.M-1;i>=0;i--)7(a[i].Y==6){7(c)a[i](P);a.72(i,1)}});7(!c)6.5p();K 6}});L A=J(b,c,a){7(!b)K 10;c=c||"2t";L q=E.O(b,c+"2P");7(!q||a)q=E.O(b,c+"2P",a?E.2I(a):[]);K q};E.1n.5p=J(a){a=a||"2t";K 6.R(J(){L q=A(6,a);q.4l();7(q.M)q[0].1i(6)})};E.1s({6P:J(b,a,c){L d=b&&b.1k==92?b:{1y:c||!c&&a||E.1q(b)&&b,2u:b,3Z:c&&a||a&&a.1k!=91&&a};d.2u=(d.2u&&d.2u.1k==51?d.2u:{90:8Z,9D:6T}[d.2u])||8X;d.5y=d.1y;d.1y=J(){7(d.2P!==S)E(6).5p();7(E.1q(d.5y))d.5y.1i(6)};K d},3Z:{70:J(p,n,b,a){K b+a*p},5j:J(p,n,b,a){K((-24.8V(p*24.8U)/2)+0.5)*a+b}},3G:[],3W:V,2t:J(b,c,a){6.11=c;6.Y=b;6.1l=a;7(!c.47)c.47={}}});E.2t.2l={4y:J(){7(6.11.30)6.11.30.1i(6.Y,[6.2J,6]);(E.2t.30[6.1l]||E.2t.30.4G)(6);7(6.1l=="1R"||6.1l=="27")6.Y.W.19="3D"},2m:J(a){7(6.Y[6.1l]!=V&&6.Y.W[6.1l]==V)K 6.Y[6.1l];L r=2M(E.1j(6.Y,6.1l,a));K r&&r>-8Q?r:2M(E.2o(6.Y,6.1l))||0},45:J(c,b,d){6.5B=(1B 3v()).3L();6.1Y=c;6.3h=b;6.2A=d||6.2A||"2S";6.2J=6.1Y;6.4B=6.4w=0;6.4y();L e=6;J t(a){K e.30(a)}t.Y=6.Y;E.3G.1g(t);7(E.3W==V){E.3W=53(J(){L a=E.3G;Q(L i=0;i<a.M;i++)7(!a[i]())a.72(i--,1);7(!a.M){6I(E.3W);E.3W=V}},13)}},1G:J(){6.11.47[6.1l]=E.1J(6.Y.W,6.1l);6.11.1G=P;6.45(0,6.2m());7(6.1l=="27"||6.1l=="1R")6.Y.W[6.1l]="8N";E(6.Y).1G()},1I:J(){6.11.47[6.1l]=E.1J(6.Y.W,6.1l);6.11.1I=P;6.45(6.2m(),0)},30:J(a){L t=(1B 3v()).3L();7(a||t>6.11.2u+6.5B){6.2J=6.3h;6.4B=6.4w=1;6.4y();6.11.40[6.1l]=P;L b=P;Q(L i 1p 6.11.40)7(6.11.40[i]!==P)b=S;7(b){7(6.11.19!=V){6.Y.W.32=6.11.32;6.Y.W.19=6.11.19;7(E.1j(6.Y,"19")=="2H")6.Y.W.19="3D"}7(6.11.1I)6.Y.W.19="2H";7(6.11.1I||6.11.1G)Q(L p 1p 6.11.40)E.1J(6.Y.W,p,6.11.47[p])}7(b&&E.1q(6.11.1y))6.11.1y.1i(6.Y);K S}N{L n=t-6.5B;6.4w=n/6.11.2u;6.4B=E.3Z[6.11.3Z||(E.3Z.5j?"5j":"70")](6.4w,n,0,1,6.11.2u);6.2J=6.1Y+((6.3h-6.1Y)*6.4B);6.4y()}K P}};E.2t.30={2v:J(a){a.Y.2v=a.2J},2x:J(a){a.Y.2x=a.2J},1w:J(a){E.1J(a.Y.W,"1w",a.2J)},4G:J(a){a.Y.W[a.1l]=a.2J+a.2A}};E.1n.5L=J(){L b=0,3b=0,Y=6[0],5l;7(Y)8M(E.14){L d=Y.1a,41=Y,1K=Y.1K,1L=Y.2i,5D=2d&&4s(5K)<8J&&!/a1/i.17(v),2T=E.1j(Y,"43")=="2T";7(Y.6G){L c=Y.6G();1b(c.26+24.2f(1L.1F.2v,1L.1h.2v),c.3b+24.2f(1L.1F.2x,1L.1h.2x));1b(-1L.1F.62,-1L.1F.60)}N{1b(Y.5G,Y.5F);2b(1K){1b(1K.5G,1K.5F);7(48&&!/^t(8H|d|h)$/i.17(1K.28)||2d&&!5D)2N(1K);7(!2T&&E.1j(1K,"43")=="2T")2T=P;41=/^1h$/i.17(1K.28)?41:1K;1K=1K.1K}2b(d&&d.28&&!/^1h|3q$/i.17(d.28)){7(!/^8G|1O.*$/i.17(E.1j(d,"19")))1b(-d.2v,-d.2x);7(48&&E.1j(d,"32")!="4d")2N(d);d=d.1a}7((5D&&(2T||E.1j(41,"43")=="4W"))||(48&&E.1j(41,"43")!="4W"))1b(-1L.1h.5G,-1L.1h.5F);7(2T)1b(24.2f(1L.1F.2v,1L.1h.2v),24.2f(1L.1F.2x,1L.1h.2x))}5l={3b:3b,26:b}}J 2N(a){1b(E.2o(a,"a8",P),E.2o(a,"a9",P))}J 1b(l,t){b+=4s(l)||0;3b+=4s(t)||0}K 5l}})();',62,631,'||||||this|if||||||||||||||||||||||||||||||||||||||function|return|var|length|else|data|true|for|each|false|document|type|null|style||elem||undefined|options|nodeName||browser|nodeType|event|test|arguments|display|parentNode|add|url|msie|window|indexOf|push|body|apply|css|constructor|prop|script|fn|typeof|in|isFunction|replace|extend|className|text|handle|opacity|div|complete|status|value|new|firstChild|match|filter|documentElement|show|dataType|hide|attr|offsetParent|doc|Array|trigger|table|call|break|height|try|cache|tbody|remove|success|catch|start|hidden||ready|get|split|Math|string|left|width|tagName|ret|global|while|map|safari|animate|max|toggle|toLowerCase|ownerDocument|bind|select|prototype|cur||curCSS|selected|handler|done|find|fx|duration|scrollLeft|id|scrollTop|special|opera|unit|nextSibling|stack|guid|toUpperCase|pushStack|button|none|makeArray|now|slice|target|parseFloat|border|exec|queue|isReady|events|px|fixed|timeout|delete|jsre|one|disabled|nth|step|name|overflow|inArray|removeChild|removeData|preventDefault|merge|appendChild|readyState|error|top|which|innerHTML|multiFilter|rl|trim|end|json|first|checked|async|param|elems|insertBefore|childNodes|html|encodeURIComponent|createElement|append|form|Date|unbind|color|grep|setTimeout|readyList|mouseleave|mouseenter|block|isXMLDoc|addEventListener|timers|is|password|last|runtimeStyle|getTime|xml|jQuery|domManip|ajax|src|callee|getElementsByTagName|selectedIndex|load|object|timerId|toString|has|easing|curAnim|offsetChild|args|position|stopPropagation|custom|props|orig|mozilla|accepts|clean|responseText|defaultView|visible|String|charCode|float|teardown|on|setup|nodeIndex|shift|javascript|currentStyle|application|child|RegExp|_|parseInt|previousSibling|dir|tr|state|empty|update|getAttribute|self|pos|setRequestHeader|input|jsonp|lastModified|_default|unload|ajaxSettings|unshift|getComputedStyle|styleSheets|getPropertyValue|lastToggle|mouseout|mouseover|GET|andSelf|relatedTarget|init|visibility|click|absolute|index|container|fix|outline|Number|removeAttribute|setInterval|prevObject|classFilter|not|unique|submit|file|after|windowData|deep|scroll|client|triggered|globalEval|jquery|sibling|swing|clone|results|wrapAll|triggerHandler|lastChild|dequeue|getResponseHeader|createTextNode|oldblock|checkbox|radio|handleError|fromElement|parsererror|old|00|Modified|startTime|ifModified|safari2|getWH|offsetTop|offsetLeft|active|values|getElementById|version|offset|bindReady|processData|val|contentType|ajaxSuccess|ajaxComplete|ajaxStart|serializeArray|notmodified|loaded|DOMContentLoaded|Width|ctrlKey|keyCode|clientTop|POST|clientLeft|clientX|pageX|exclusive|detachEvent|removeEventListener|swap|cloneNode|join|attachEvent|eval|ajaxStop|substr|head|parse|textarea|reset|image|zoom|odd|ajaxSend|even|before|username|prepend|expr|quickClass|uuid|quickID|quickChild|continue|textContent|appendTo|contents|evalScript|parent|defaultValue|ajaxError|setArray|compatMode|getBoundingClientRect|styleFloat|clearInterval|httpNotModified|nodeValue|100|alpha|_toggle|href|speed|throw|304|replaceWith|200|Last|colgroup|httpData|httpSuccess|beforeSend|eq|linear|concat|splice|fieldset|multiple|cssFloat|XMLHttpRequest|webkit|ActiveXObject|CSS1Compat|link|metaKey|scriptCharset|callback|col|pixelLeft|urlencoded|www|post|hasClass|getJSON|getScript|elements|serialize|black|keyup|keypress|solid|change|mousemove|mouseup|dblclick|resize|focus|blur|stylesheet|rel|doScroll|round|hover|padding|offsetHeight|mousedown|offsetWidth|Bottom|Top|keydown|clientY|Right|pageY|Left|toElement|srcElement|cancelBubble|returnValue|charAt|0n|substring|animated|header|noConflict|line|enabled|innerText|contains|only|weight|ajaxSetup|font|size|gt|lt|uFFFF|u0128|417|Boolean|inner|Height|toggleClass|removeClass|addClass|removeAttr|replaceAll|insertAfter|prependTo|contentWindow|contentDocument|wrap|iframe|children|siblings|prevAll|nextAll|prev|wrapInner|next|parents|maxLength|maxlength|readOnly|readonly|reverse|class|htmlFor|inline|able|boxModel|522|setData|compatible|with|1px|ie|getData|10000|ra|it|rv|PI|cos|userAgent|400|navigator|600|slow|Function|Object|array|stop|ig|NaN|fadeTo|option|fadeOut|fadeIn|setAttribute|slideToggle|slideUp|changed|slideDown|be|can|property|responseXML|content|1223|getAttributeNode|300|method|protocol|location|action|send|abort|cssText|th|td|cap|specified|Accept|With|colg|Requested|fast|tfoot|GMT|thead|1970|Jan|attributes|01|Thu|leg|Since|If|opt|Type|Content|embed|open|area|XMLHTTP|hr|Microsoft|onreadystatechange|onload|meta|adobeair|charset|http|1_|img|br|plain|borderLeftWidth|borderTopWidth|abbr'.split('|'),0,{}))
\ No newline at end of file
diff --git a/wui/src/public/javascripts/jquery.livequery.js b/wui/src/public/javascripts/jquery.livequery.js
new file mode 100644
index 0000000..dfed9fe
--- /dev/null
+++ b/wui/src/public/javascripts/jquery.livequery.js
@@ -0,0 +1,250 @@
+/* Copyright (c) 2007 Brandon Aaron (brandon aaron gmail com || http://brandonaaron.net)
+ * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 
+ * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
+ *
+ * Version: @VERSION
+ * Requires jQuery 1.1.3+
+ * Docs: http://docs.jquery.com/Plugins/livequery
+ */
+
+(function($) {
+	
+$.extend($.fn, {
+	livequery: function(type, fn, fn2) {
+		var self = this, q;
+		
+		// Handle different call patterns
+		if ($.isFunction(type))
+			fn2 = fn, fn = type, type = undefined;
+			
+		// See if Live Query already exists
+		$.each( $.livequery.queries, function(i, query) {
+			if ( self.selector == query.selector && self.context == query.context &&
+				type == query.type && (!fn || fn.$lqguid == query.fn.$lqguid) && (!fn2 || fn2.$lqguid == query.fn2.$lqguid) )
+					// Found the query, exit the each loop
+					return (q = query) && false;
+		});
+		
+		// Create new Live Query if it wasn't found
+		q = q || new $.livequery(this.selector, this.context, type, fn, fn2);
+		
+		// Make sure it is running
+		q.stopped = false;
+		
+		// Run it
+		$.livequery.run( q.id );
+		
+		// Contnue the chain
+		return this;
+	},
+	
+	expire: function(type, fn, fn2) {
+		var self = this;
+		
+		// Handle different call patterns
+		if ($.isFunction(type))
+			fn2 = fn, fn = type, type = undefined;
+			
+		// Find the Live Query based on arguments and stop it
+		$.each( $.livequery.queries, function(i, query) {
+			if ( self.selector == query.selector && self.context == query.context && 
+				(!type || type == query.type) && (!fn || fn.$lqguid == query.fn.$lqguid) && (!fn2 || fn2.$lqguid == query.fn2.$lqguid) && !this.stopped )
+					$.livequery.stop(query.id);
+		});
+		
+		// Continue the chain
+		return this;
+	}
+});
+
+$.livequery = function(selector, context, type, fn, fn2) {
+	this.selector = selector;
+	this.context  = context || document;
+	this.type     = type;
+	this.fn       = fn;
+	this.fn2      = fn2;
+	this.elements = [];
+	this.stopped  = false;
+	
+	// The id is the index of the Live Query in $.livequery.queries
+	this.id = $.livequery.queries.push(this)-1;
+	
+	// Mark the functions for matching later on
+	fn.$lqguid = fn.$lqguid || $.livequery.guid++;
+	if (fn2) fn2.$lqguid = fn2.$lqguid || $.livequery.guid++;
+	
+	// Return the Live Query
+	return this;
+};
+
+$.livequery.prototype = {
+	stop: function() {
+		var query = this;
+		
+		if ( this.type )
+			// Unbind all bound events
+			this.elements.unbind(this.type, this.fn);
+		else if (this.fn2)
+			// Call the second function for all matched elements
+			this.elements.each(function(i, el) {
+				query.fn2.apply(el);
+			});
+			
+		// Clear out matched elements
+		this.elements = [];
+		
+		// Stop the Live Query from running until restarted
+		this.stopped = true;
+	},
+	
+	run: function() {
+		// Short-circuit if stopped
+		if ( this.stopped ) return;
+		var query = this;
+		
+		var oEls = this.elements,
+			els  = $(this.selector, this.context),
+			nEls = els.not(oEls);
+		
+		// Set elements to the latest set of matched elements
+		this.elements = els;
+		
+		if (this.type) {
+			// Bind events to newly matched elements
+			nEls.bind(this.type, this.fn);
+			
+			// Unbind events to elements no longer matched
+			if (oEls.length > 0)
+				$.each(oEls, function(i, el) {
+					if ( $.inArray(el, els) < 0 )
+						$.event.remove(el, query.type, query.fn);
+				});
+		}
+		else {
+			// Call the first function for newly matched elements
+			nEls.each(function() {
+				query.fn.apply(this);
+			});
+			
+			// Call the second function for elements no longer matched
+			if ( this.fn2 && oEls.length > 0 )
+				$.each(oEls, function(i, el) {
+					if ( $.inArray(el, els) < 0 )
+						query.fn2.apply(el);
+				});
+		}
+	}
+};
+
+$.extend($.livequery, {
+	guid: 0,
+	queries: [],
+	queue: [],
+	running: false,
+	timeout: null,
+	
+	checkQueue: function() {
+		if ( $.livequery.running && $.livequery.queue.length ) {
+			var length = $.livequery.queue.length;
+			// Run each Live Query currently in the queue
+			while ( length-- )
+				$.livequery.queries[ $.livequery.queue.shift() ].run();
+		}
+	},
+	
+	pause: function() {
+		// Don't run anymore Live Queries until restarted
+		$.livequery.running = false;
+	},
+	
+	play: function() {
+		// Restart Live Queries
+		$.livequery.running = true;
+		// Request a run of the Live Queries
+		$.livequery.run();
+	},
+	
+	registerPlugin: function() {
+		$.each( arguments, function(i,n) {
+			// Short-circuit if the method doesn't exist
+			if (!$.fn[n]) return;
+			
+			// Save a reference to the original method
+			var old = $.fn[n];
+			
+			// Create a new method
+			$.fn[n] = function() {
+				// Call the original method
+				var r = old.apply(this, arguments);
+				
+				// Request a run of the Live Queries
+				$.livequery.run();
+				
+				// Return the original methods result
+				return r;
+			}
+		});
+	},
+	
+	run: function(id) {
+		if (id != undefined) {
+			// Put the particular Live Query in the queue if it doesn't already exist
+			if ( $.inArray(id, $.livequery.queue) < 0 )
+				$.livequery.queue.push( id );
+		}
+		else
+			// Put each Live Query in the queue if it doesn't already exist
+			$.each( $.livequery.queries, function(id) {
+				if ( $.inArray(id, $.livequery.queue) < 0 )
+					$.livequery.queue.push( id );
+			});
+		
+		// Clear timeout if it already exists
+		if ($.livequery.timeout) clearTimeout($.livequery.timeout);
+		// Create a timeout to check the queue and actually run the Live Queries
+		$.livequery.timeout = setTimeout($.livequery.checkQueue, 20);
+	},
+	
+	stop: function(id) {
+		if (id != undefined)
+			// Stop are particular Live Query
+			$.livequery.queries[ id ].stop();
+		else
+			// Stop all Live Queries
+			$.each( $.livequery.queries, function(id) {
+				$.livequery.queries[ id ].stop();
+			});
+	}
+});
+
+// Register core DOM manipulation methods
+$.livequery.registerPlugin('append', 'prepend', 'after', 'before', 'wrap', 'attr', 'removeAttr', 'addClass', 'removeClass', 'toggleClass', 'empty', 'remove');
+
+// Run Live Queries when the Document is ready
+$(function() { $.livequery.play(); });
+
+
+// Save a reference to the original init method
+var init = $.prototype.init;
+
+// Create a new init method that exposes two new properties: selector and context
+$.prototype.init = function(a,c) {
+	// Call the original init and save the result
+	var r = init.apply(this, arguments);
+	
+	// Copy over properties if they exist already
+	if (a && a.selector)
+		r.context = a.context, r.selector = a.selector;
+		
+	// Set properties
+	if ( typeof a == 'string' )
+		r.context = c || document, r.selector = a;
+	
+	// Return the result
+	return r;
+};
+
+// Give the init function the jQuery prototype for later instantiation (needed after Rev 4091)
+$.prototype.init.prototype = $.prototype;
+	
+})(jQuery);
\ No newline at end of file
diff --git a/wui/src/vendor/plugins/will_paginate/LICENSE b/wui/src/vendor/plugins/will_paginate/LICENSE
new file mode 100644
index 0000000..96a48cb
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/LICENSE
@@ -0,0 +1,18 @@
+Copyright (c) 2007 PJ Hyett and Mislav Marohnić
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of 
+this software and associated documentation files (the "Software"), to deal in 
+the Software without restriction, including without limitation the rights to 
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 
+the Software, and to permit persons to whom the Software is furnished to do so, 
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all 
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/wui/src/vendor/plugins/will_paginate/README b/wui/src/vendor/plugins/will_paginate/README
new file mode 100644
index 0000000..162c189
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/README
@@ -0,0 +1,180 @@
+= WillPaginate
+
+Pagination is just limiting the number of records displayed. Why should you let
+it get in your way while developing, then? This plugin makes magic happen. Did
+you ever want to be able to do just this on a model:
+
+  Post.paginate :page => 1, :order => 'created_at DESC'
+
+... and then render the page links with a single view helper? Well, now you
+can.
+
+Ryan Bates made an awesome screencast[http://railscasts.com/episodes/51],
+check it out.
+
+Your mind reels with questions? Join our Google
+group[http://groups.google.com/group/will_paginate].
+
+== Installation
+
+Will Paginate officially supports Rails versions 1.2.6 and 2.0.x.
+
+Previously, the plugin was available on the following SVN location:
+
+  svn://errtheblog.com/svn/plugins/will_paginate
+
+In February 2008, it moved to GitHub[http://github.com/mislav/will_paginate/tree]
+to be tracked with git. The SVN repo continued to have updates, but not
+forever. Therefore you should switch to using the gem:
+
+  gem install will_paginate --no-ri
+
+After that, you can remove the plugin from your applications and add
+a simple require to the end of config/environment.rb:
+
+  require 'will_paginate'
+
+That's it, just remember to install the gem on all machines that
+you are deploying to.
+
+The second option is to download and extract the tarball from GitHub. Here is the
+link for downloading the current state of the master branch:
+http://github.com/mislav/will_paginate/tarball/master
+
+Extract it to <tt>vendor/plugins</tt>. The directory will have a default name
+like "mislav-will_paginate-master"; you can rename it to "will_paginate" for
+simplicity.
+
+== Example usage
+
+Use a paginate finder in the controller:
+
+    @posts = Post.paginate_by_board_id @board.id, :page => params[:page], :order => 'updated_at DESC'
+
+Yeah, +paginate+ works just like +find+ -- it just doesn't fetch all the
+records.  Don't forget to tell it which page you want, or it will complain!
+Read more on WillPaginate::Finder::ClassMethods.
+
+Render the posts in your view like you would normally do. When you need to render
+pagination, just stick this in:
+
+    <%= will_paginate @posts %>
+
+You're done. (Copy and paste the example fancy CSS styles from the bottom.) You
+can find the option list at WillPaginate::ViewHelpers.
+
+How does it know how much items to fetch per page? It asks your model by calling
+its <tt>per_page</tt> class method. You can define it like this:
+
+    class Post < ActiveRecord::Base
+      cattr_reader :per_page
+      @@per_page = 50
+    end
+
+... or like this:
+
+    class Post < ActiveRecord::Base
+      def self.per_page
+        50
+      end
+    end
+
+... or don't worry about it at all. WillPaginate defines it to be <b>30</b> by default.
+But you can always specify the count explicitly when calling +paginate+:
+
+    @posts = Post.paginate :page => params[:page], :per_page => 50
+
+The +paginate+ finder wraps the original finder and returns your resultset that now has
+some new properties. You can use the collection as you would with any ActiveRecord
+resultset. WillPaginate view helpers also need that object to be able to render pagination:
+
+    <ol>
+      <% for post in @posts -%>
+        <li>Render `post` in some nice way.</li>
+      <% end -%>
+    </ol>
+
+    <p>Now let's render us some pagination!</p>
+    <%= will_paginate @posts %>
+
+More detailed documentation:
+
+* WillPaginate::Finder::ClassMethods for pagination on your models;
+* WillPaginate::ViewHelpers for your views.
+
+== Oh noes, a bug!
+
+Tell us what happened so we can fix it, quick! Issues are filed on the Lighthouse project:
+http://err.lighthouseapp.com/projects/466-plugins/tickets?q=tagged:will_paginate
+
+Steps to make an awesome bug report:
+
+1. Run <tt>rake test</tt> in the <i>will_paginate</i> directory. (You will need SQLite3.)
+   Copy the output if there are failing tests.
+2. Register on Lighthouse to create a new ticket.
+3. Write a descriptive, short title. Provide as much info as you can in the body.
+   Assign the ticket to Mislav and tag it with meaningful tags, <tt>"will_paginate"</tt>
+   being among them.
+4. Yay! You will be notified on updates automatically.
+
+Here is an example of a great bug report and patch:
+http://err.lighthouseapp.com/projects/466/tickets/172-total_entries-ignored-in-paginate_by_sql
+
+== Authors, credits, contact
+
+Want to discuss, request features, ask questions? Join the Google group:
+http://groups.google.com/group/will_paginate
+
+Authors::                Mislav Marohnić, PJ Hyett
+Original announcement::  http://errtheblog.com/post/929 
+Original PHP source::    http://www.strangerstudios.com/sandbox/pagination/diggstyle.php
+
+All these people helped making will_paginate what it is now with their code
+contributions or simply awesome ideas:
+
+Chris Wanstrath, Dr. Nic Williams, K. Adam Christensen, Mike Garey, Bence
+Golda, Matt Aimonetti, Charles Brian Quinn, Desi McAdam, James Coglan, Matijs
+van Zuijlen, Maria, Brendan Ribera, Todd Willey, Bryan Helmkamp, Jan Berkel.
+
+== Usable pagination in the UI
+
+Copy the following CSS into your stylesheet for a good start:
+
+  .pagination {
+    padding: 3px;
+    margin: 3px;
+  }
+  .pagination a {
+    padding: 2px 5px 2px 5px;
+    margin: 2px;
+    border: 1px solid #aaaadd;
+    text-decoration: none;
+    color: #000099;
+  }
+  .pagination a:hover, .pagination a:active {
+    border: 1px solid #000099;
+    color: #000;
+  }
+  .pagination span.current {
+    padding: 2px 5px 2px 5px;
+    margin: 2px;
+    border: 1px solid #000099;
+    font-weight: bold;
+    background-color: #000099;
+    color: #FFF;
+  }
+  .pagination span.disabled {
+    padding: 2px 5px 2px 5px;
+    margin: 2px;
+    border: 1px solid #eee;
+    color: #ddd;
+  }
+
+More reading about pagination as design pattern:
+
+* Pagination 101:
+  http://kurafire.net/log/archive/2007/06/22/pagination-101
+* Pagination gallery:
+  http://www.smashingmagazine.com/2007/11/16/pagination-gallery-examples-and-good-practices/
+* Pagination on Yahoo Design Pattern Library:
+  http://developer.yahoo.com/ypatterns/parent.php?pattern=pagination
diff --git a/wui/src/vendor/plugins/will_paginate/Rakefile b/wui/src/vendor/plugins/will_paginate/Rakefile
new file mode 100644
index 0000000..e21df94
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/Rakefile
@@ -0,0 +1,65 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the will_paginate plugin.'
+Rake::TestTask.new(:test) do |t|
+  t.pattern = 'test/**/*_test.rb'
+  t.verbose = true
+end
+
+# I want to specify environment variables at call time
+class EnvTestTask < Rake::TestTask
+  attr_accessor :env
+
+  def ruby(*args)
+    env.each { |key, value| ENV[key] = value } if env
+    super
+    env.keys.each { |key| ENV.delete key } if env
+  end
+end
+
+for configuration in %w( sqlite3 mysql postgres )
+  EnvTestTask.new("test_#{configuration}") do |t|
+    t.pattern = 'test/finder_test.rb'
+    t.verbose = true
+    t.env = { 'DB' => configuration }
+  end
+end
+
+task :test_databases => %w(test_mysql test_sqlite3 test_postgres)
+
+desc %{Test everything on SQLite3, MySQL and PostgreSQL}
+task :test_full => %w(test test_mysql test_postgres)
+
+desc %{Test everything with Rails 1.2.x and 2.0.x gems}
+task :test_all do
+  all = Rake::Task['test_full']
+  ENV['RAILS_VERSION'] = '~>1.2.6'
+  all.invoke 
+  # reset the invoked flag
+  %w( test_full test test_mysql test_postgres ).each do |name|
+    Rake::Task[name].instance_variable_set '@already_invoked', false
+  end
+  # do it again
+  ENV['RAILS_VERSION'] = '~>2.0.2'
+  all.invoke 
+end
+
+desc 'Generate RDoc documentation for the will_paginate plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+  files = ['README', 'LICENSE', 'lib/**/*.rb']
+  rdoc.rdoc_files.add(files)
+  rdoc.main = "README" # page to start on
+  rdoc.title = "will_paginate"
+  
+  templates = %w[/Users/chris/ruby/projects/err/rock/template.rb /var/www/rock/template.rb]
+  rdoc.template = templates.find { |t| File.exists? t }
+  
+  rdoc.rdoc_dir = 'doc' # rdoc output folder
+  rdoc.options << '--inline-source'
+  rdoc.options << '--charset=UTF-8'
+end
diff --git a/wui/src/vendor/plugins/will_paginate/init.rb b/wui/src/vendor/plugins/will_paginate/init.rb
new file mode 100644
index 0000000..dc0572d
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/init.rb
@@ -0,0 +1,2 @@
+require 'will_paginate'
+WillPaginate.enable
diff --git a/wui/src/vendor/plugins/will_paginate/lib/will_paginate.rb b/wui/src/vendor/plugins/will_paginate/lib/will_paginate.rb
new file mode 100644
index 0000000..c0a5ce1
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/lib/will_paginate.rb
@@ -0,0 +1,65 @@
+require 'active_support'
+
+# = You *will* paginate!
+#
+# First read about WillPaginate::Finder::ClassMethods, then see
+# WillPaginate::ViewHelpers. The magical array you're handling in-between is
+# WillPaginate::Collection.
+#
+# Happy paginating!
+module WillPaginate
+  class << self
+    # shortcut for <tt>enable_actionpack; enable_activerecord</tt>
+    def enable
+      enable_actionpack
+      enable_activerecord
+    end
+    
+    # mixes in WillPaginate::ViewHelpers in ActionView::Base
+    def enable_actionpack
+      return if ActionView::Base.instance_methods.include? 'will_paginate'
+      require 'will_paginate/view_helpers'
+      ActionView::Base.class_eval { include ViewHelpers }
+
+      if ActionController::Base.respond_to? :rescue_responses
+        ActionController::Base.rescue_responses['WillPaginate::InvalidPage'] = :not_found
+      end
+    end
+    
+    # mixes in WillPaginate::Finder in ActiveRecord::Base and classes that deal
+    # with associations
+    def enable_activerecord
+      return if ActiveRecord::Base.respond_to? :paginate
+      require 'will_paginate/finder'
+      ActiveRecord::Base.class_eval { include Finder }
+
+      associations = ActiveRecord::Associations
+      collection = associations::AssociationCollection
+      
+      # to support paginating finders on associations, we have to mix in the
+      # method_missing magic from WillPaginate::Finder::ClassMethods to AssociationProxy
+      # subclasses, but in a different way for Rails 1.2.x and 2.0
+      (collection.instance_methods.include?(:create!) ?
+        collection : collection.subclasses.map(&:constantize)
+      ).push(associations::HasManyThroughAssociation).each do |klass|
+        klass.class_eval do
+          include Finder::ClassMethods
+          alias_method_chain :method_missing, :paginate
+        end
+      end
+    end
+  end
+
+  module Deprecation #:nodoc:
+    extend ActiveSupport::Deprecation
+
+    def self.warn(message, callstack = caller)
+      message = 'WillPaginate: ' + message.strip.gsub(/ {3,}/, ' ')
+      behavior.call(message, callstack) if behavior && !silenced?
+    end
+
+    def self.silenced?
+      ActiveSupport::Deprecation.silenced?
+    end
+  end
+end
diff --git a/wui/src/vendor/plugins/will_paginate/lib/will_paginate/collection.rb b/wui/src/vendor/plugins/will_paginate/lib/will_paginate/collection.rb
new file mode 100644
index 0000000..f796131
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/lib/will_paginate/collection.rb
@@ -0,0 +1,132 @@
+require 'will_paginate'
+
+module WillPaginate
+  # = OMG, invalid page number!
+  # This is an ArgumentError raised in case a page was requested that is either
+  # zero or negative number. You should decide how do deal with such errors in
+  # the controller.
+  #
+  # This error is *not* raised when a page further than the last page is
+  # requested. Use <tt>WillPaginate::Collection#out_of_bounds?</tt> method to
+  # check for those cases and manually deal with them as you see fit.
+  class InvalidPage < ArgumentError
+    def initialize(page, page_num)
+      super "#{page.inspect} given as value, which translates to '#{page_num}' as page number"
+    end
+  end
+  
+  # Arrays returned from paginating finds are, in fact, instances of this.
+  # You may think of WillPaginate::Collection as an ordinary array with some
+  # extra properties. Those properties are used by view helpers to generate
+  # correct page links.
+  #
+  # WillPaginate::Collection also assists in rolling out your own pagination
+  # solutions: see +create+.
+  #
+  class Collection < Array
+    attr_reader :current_page, :per_page, :total_entries
+
+    # Arguments to this constructor are the current page number, per-page limit
+    # and the total number of entries. The last argument is optional because it
+    # is best to do lazy counting; in other words, count *conditionally* after
+    # populating the collection using the +replace+ method.
+    #
+    def initialize(page, per_page, total = nil)
+      @current_page = page.to_i
+      raise InvalidPage.new(page, @current_page) if @current_page < 1
+      @per_page     = per_page.to_i
+      raise ArgumentError, "`per_page` setting cannot be less than 1 (#{ per_page} given)" if @per_page < 1
+      
+      self.total_entries = total if total
+    end
+
+    # Just like +new+, but yields the object after instantiation and returns it
+    # afterwards. This is very useful for manual pagination:
+    #
+    #   @entries = WillPaginate::Collection.create(1, 10) do |pager|
+    #     result = Post.find(:all, :limit => pager.per_page, :offset => pager.offset)
+    #     # inject the result array into the paginated collection:
+    #     pager.replace(result)
+    #
+    #     unless pager.total_entries
+    #       # the pager didn't manage to guess the total count, do it manually
+    #       pager.total_entries = Post.count
+    #     end
+    #   end
+    #
+    # The possibilities with this are endless. For another example, here is how
+    # WillPaginate used to define pagination for Array instances:
+    #
+    #   Array.class_eval do
+    #     def paginate(page = 1, per_page = 15)
+    #       WillPaginate::Collection.create(page, per_page, size) do |pager|
+    #         pager.replace self[pager.offset, pager.per_page].to_a
+    #       end
+    #     end
+    #   end
+    #
+    def self.create(page, per_page, total = nil, &block)
+      pager = new(page, per_page, total)
+      yield pager
+      pager
+    end
+
+    # The total number of pages.
+    def page_count
+      @total_pages
+    end
+
+    # Helper method that is true when someone tries to fetch a page with a
+    # larger number than the last page. Can be used in combination with flashes
+    # and redirecting.
+    def out_of_bounds?
+      current_page > page_count
+    end
+
+    # Current offset of the paginated collection. If we're on the first page,
+    # it is always 0. If we're on the 2nd page and there are 30 entries per page,
+    # the offset is 30. This property is useful if you want to render ordinals
+    # besides your records: simply start with offset + 1.
+    #
+    def offset
+      (current_page - 1) * per_page
+    end
+
+    # current_page - 1 or nil if there is no previous page
+    def previous_page
+      current_page > 1 ? (current_page - 1) : nil
+    end
+
+    # current_page + 1 or nil if there is no next page
+    def next_page
+      current_page < page_count ? (current_page + 1) : nil
+    end
+
+    def total_entries=(number)
+      @total_entries = number.to_i
+      @total_pages   = (@total_entries / per_page.to_f).ceil
+    end
+
+    # This is a magic wrapper for the original Array#replace method. It serves
+    # for populating the paginated collection after initialization.
+    #
+    # Why magic? Because it tries to guess the total number of entries judging
+    # by the size of given array. If it is shorter than +per_page+ limit, then we
+    # know we're on the last page. This trick is very useful for avoiding
+    # unnecessary hits to the database to do the counting after we fetched the
+    # data for the current page.
+    #
+    # However, after using +replace+ you should always test the value of
+    # +total_entries+ and set it to a proper value if it's +nil+. See the example
+    # in +create+.
+    def replace(array)
+      returning super do
+        # The collection is shorter then page limit? Rejoice, because
+        # then we know that we are on the last page!
+        if total_entries.nil? and length > 0 and length < per_page
+          self.total_entries = offset + length
+        end
+      end
+    end
+  end
+end
diff --git a/wui/src/vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb b/wui/src/vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb
new file mode 100644
index 0000000..461153f
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb
@@ -0,0 +1,80 @@
+require 'will_paginate'
+require 'set'
+
+unless Hash.instance_methods.include? 'except'
+  Hash.class_eval do
+    # Returns a new hash without the given keys.
+    def except(*keys)
+      rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
+      reject { |key,| rejected.include?(key) }
+    end
+ 
+    # Replaces the hash without only the given keys.
+    def except!(*keys)
+      replace(except(*keys))
+    end
+  end
+end
+
+unless Hash.instance_methods.include? 'slice'
+  Hash.class_eval do
+    # Returns a new hash with only the given keys.
+    def slice(*keys)
+      allowed = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
+      reject { |key,| !allowed.include?(key) }
+    end
+
+    # Replaces the hash with only the given keys.
+    def slice!(*keys)
+      replace(slice(*keys))
+    end
+  end
+end
+
+unless Hash.instance_methods.include? 'rec_merge!'
+  Hash.class_eval do
+    # Same as Hash#merge!, but recursively merges sub-hashes
+    # (stolen from Haml)
+    def rec_merge!(other)
+      other.each do |key, other_value|
+        value = self[key]
+        if value.is_a?(Hash) and other_value.is_a?(Hash)
+          value.rec_merge! other_value
+        else
+          self[key] = other_value
+        end
+      end
+      self
+    end
+  end
+end
+
+require 'will_paginate/collection'
+
+unless Array.instance_methods.include? 'paginate'
+  # http://www.desimcadam.com/archives/8
+  Array.class_eval do
+    def paginate(options_or_page = {}, per_page = nil)
+      if options_or_page.nil? or Fixnum === options_or_page
+        if defined? WillPaginate::Deprecation
+          WillPaginate::Deprecation.warn <<-DEPR
+            Array#paginate now conforms to the main, ActiveRecord::Base#paginate API.  You should \
+            call it with a parameters hash (:page, :per_page).  The old API (numbers as arguments) \
+            has been deprecated and is going to be unsupported in future versions of will_paginate.
+          DEPR
+        end
+        page = options_or_page
+        options = {}
+      else
+        options = options_or_page
+        page = options[:page]
+        raise ArgumentError, "wrong number of arguments (1 hash or 2 Fixnums expected)" if per_page
+        per_page = options[:per_page]
+      end
+
+      WillPaginate::Collection.create(page || 1, per_page || 30, options[:total_entries] || size) do |pager|
+        pager.replace self[pager.offset, pager.per_page].to_a
+      end
+    end
+  end
+end
diff --git a/wui/src/vendor/plugins/will_paginate/lib/will_paginate/finder.rb b/wui/src/vendor/plugins/will_paginate/lib/will_paginate/finder.rb
new file mode 100644
index 0000000..5d2d73c
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/lib/will_paginate/finder.rb
@@ -0,0 +1,214 @@
+require 'will_paginate/core_ext'
+
+module WillPaginate
+  # A mixin for ActiveRecord::Base. Provides +per_page+ class method
+  # and makes +paginate+ finders possible with some method_missing magic.
+  #
+  # Find out more in WillPaginate::Finder::ClassMethods
+  #
+  module Finder
+    def self.included(base)
+      base.extend ClassMethods
+      class << base
+        alias_method_chain :method_missing, :paginate
+        # alias_method_chain :find_every,     :paginate
+        define_method(:per_page) { 30 } unless respond_to?(:per_page)
+      end
+    end
+
+    # = Paginating finders for ActiveRecord models
+    # 
+    # WillPaginate adds +paginate+ and +per_page+ methods to ActiveRecord::Base
+    # class methods and associations. It also hooks into +method_missing+ to
+    # intercept pagination calls to dynamic finders such as
+    # +paginate_by_user_id+ and translate them to ordinary finders
+    # (+find_all_by_user_id+ in this case).
+    # 
+    # In short, paginating finders are equivalent to ActiveRecord finders; the
+    # only difference is that we start with "paginate" instead of "find" and
+    # that <tt>:page</tt> is required parameter:
+    #
+    #   @posts = Post.paginate :all, :page => params[:page], :order => 'created_at DESC'
+    # 
+    # In paginating finders, "all" is implicit. There is no sense in paginating
+    # a single record, right? So, you can drop the <tt>:all</tt> argument:
+    # 
+    #   Post.paginate(...)              =>  Post.find :all
+    #   Post.paginate_all_by_something  =>  Post.find_all_by_something
+    #   Post.paginate_by_something      =>  Post.find_all_by_something
+    #
+    # == The importance of the <tt>:order</tt> parameter
+    #
+    # In ActiveRecord finders, <tt>:order</tt> parameter specifies columns for
+    # the <tt>ORDER BY</tt> clause in SQL. It is important to have it, since
+    # pagination only makes sense with ordered sets. Without the <tt>ORDER
+    # BY</tt> clause, databases aren't required to do consistent ordering when
+    # performing <tt>SELECT</tt> queries; this is especially true for
+    # PostgreSQL.
+    #
+    # Therefore, make sure you are doing ordering on a column that makes the
+    # most sense in the current context. Make that obvious to the user, also.
+    # For perfomance reasons you will also want to add an index to that column.
+    module ClassMethods
+      # This is the main paginating finder.
+      #
+      # == Special parameters for paginating finders
+      # * <tt>:page</tt> -- REQUIRED, but defaults to 1 if false or nil
+      # * <tt>:per_page</tt> -- defaults to <tt>CurrentModel.per_page</tt> (which is 30 if not overridden)
+      # * <tt>:total_entries</tt> -- use only if you manually count total entries
+      # * <tt>:count</tt> -- additional options that are passed on to +count+
+      # * <tt>:finder</tt> -- name of the ActiveRecord finder used (default: "find")
+      #
+      # All other options (+conditions+, +order+, ...) are forwarded to +find+
+      # and +count+ calls.
+      def paginate(*args, &block)
+        options = args.pop
+        page, per_page, total_entries = wp_parse_options(options)
+        finder = (options[:finder] || 'find').to_s
+
+        if finder == 'find'
+          # an array of IDs may have been given:
+          total_entries ||= (Array === args.first and args.first.size)
+          # :all is implicit
+          args.unshift(:all) if args.empty?
+        end
+
+        WillPaginate::Collection.create(page, per_page, total_entries) do |pager|
+          count_options = options.except :page, :per_page, :total_entries, :finder
+          find_options = count_options.except(:count).update(:offset => pager.offset, :limit => pager.per_page) 
+          
+          args << find_options
+          # @options_from_last_find = nil
+          pager.replace send(finder, *args, &block)
+          
+          # magic counting for user convenience:
+          pager.total_entries = wp_count(count_options, args, finder) unless pager.total_entries
+        end
+      end
+      
+      # Wraps +find_by_sql+ by simply adding LIMIT and OFFSET to your SQL string
+      # based on the params otherwise used by paginating finds: +page+ and
+      # +per_page+.
+      #
+      # Example:
+      # 
+      #   @developers = Developer.paginate_by_sql ['select * from developers where salary > ?', 80000],
+      #                          :page => params[:page], :per_page => 3
+      #
+      # A query for counting rows will automatically be generated if you don't
+      # supply <tt>:total_entries</tt>. If you experience problems with this
+      # generated SQL, you might want to perform the count manually in your
+      # application.
+      # 
+      def paginate_by_sql(sql, options)
+        WillPaginate::Collection.create(*wp_parse_options(options)) do |pager|
+          query = sanitize_sql(sql)
+          original_query = query.dup
+          # add limit, offset
+          add_limit! query, :offset => pager.offset, :limit => pager.per_page
+          # perfom the find
+          pager.replace find_by_sql(query)
+          
+          unless pager.total_entries
+            count_query = original_query.sub /\bORDER\s+BY\s+[\w`,\s]+$/mi, ''
+            count_query = "SELECT COUNT(*) FROM (#{count_query})"
+            
+            unless ['oracle', 'oci'].include?(self.connection.adapter_name.downcase)
+              count_query << ' AS count_table'
+            end
+            # perform the count query
+            pager.total_entries = count_by_sql(count_query)
+          end
+        end
+      end
+
+      def respond_to?(method, include_priv = false) #:nodoc:
+        case method.to_sym
+        when :paginate, :paginate_by_sql
+          true
+        else
+          super(method.to_s.sub(/^paginate/, 'find'), include_priv)
+        end
+      end
+
+    protected
+      
+      def method_missing_with_paginate(method, *args, &block) #:nodoc:
+        # did somebody tried to paginate? if not, let them be
+        unless method.to_s.index('paginate') == 0
+          return method_missing_without_paginate(method, *args, &block) 
+        end
+        
+        # paginate finders are really just find_* with limit and offset
+        finder = method.to_s.sub('paginate', 'find')
+        finder.sub!('find', 'find_all') if finder.index('find_by_') == 0
+        
+        options = args.pop
+        raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
+        options = options.dup
+        options[:finder] = finder
+        args << options
+        
+        paginate(*args, &block)
+      end
+
+      # Does the not-so-trivial job of finding out the total number of entries
+      # in the database. It relies on the ActiveRecord +count+ method.
+      def wp_count(options, args, finder)
+        excludees = [:count, :order, :limit, :offset, :readonly]
+        unless options[:select] and options[:select] =~ /^\s*DISTINCT\b/i
+          excludees << :select # only exclude the select param if it doesn't begin with DISTINCT
+        end
+        # count expects (almost) the same options as find
+        count_options = options.except *excludees
+
+        # merge the hash found in :count
+        # this allows you to specify :select, :order, or anything else just for the count query
+        count_options.update options[:count] if options[:count]
+
+        # we may have to scope ...
+        counter = Proc.new { count(count_options) }
+
+        # we may be in a model or an association proxy!
+        klass = (@owner and @reflection) ? @reflection.klass : self
+
+        count = if finder.index('find_') == 0 and klass.respond_to?(scoper = finder.sub('find', 'with'))
+                  # scope_out adds a 'with_finder' method which acts like with_scope, if it's present
+                  # then execute the count with the scoping provided by the with_finder
+                  send(scoper, &counter)
+                elsif match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(finder)
+                  # extract conditions from calls like "paginate_by_foo_and_bar"
+                  attribute_names = extract_attribute_names_from_match(match)
+                  conditions = construct_attributes_from_arguments(attribute_names, args)
+                  with_scope(:find => { :conditions => conditions }, &counter)
+                else
+                  counter.call
+                end
+
+        count.respond_to?(:length) ? count.length : count
+      end
+
+      def wp_parse_options(options) #:nodoc:
+        raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
+        options = options.symbolize_keys
+        raise ArgumentError, ':page parameter required' unless options.key? :page
+        
+        if options[:count] and options[:total_entries]
+          raise ArgumentError, ':count and :total_entries are mutually exclusive'
+        end
+
+        page     = options[:page] || 1
+        per_page = options[:per_page] || self.per_page
+        total    = options[:total_entries]
+        [page, per_page, total]
+      end
+
+    private
+
+      # def find_every_with_paginate(options)
+      #   @options_from_last_find = options
+      #   find_every_without_paginate(options)
+      # end
+    end
+  end
+end
diff --git a/wui/src/vendor/plugins/will_paginate/lib/will_paginate/view_helpers.rb b/wui/src/vendor/plugins/will_paginate/lib/will_paginate/view_helpers.rb
new file mode 100644
index 0000000..34e2ff0
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/lib/will_paginate/view_helpers.rb
@@ -0,0 +1,226 @@
+require 'will_paginate/core_ext'
+
+module WillPaginate
+  # = Will Paginate view helpers
+  #
+  # Currently there is only one view helper: +will_paginate+. It renders the
+  # pagination links for the given collection. The helper itself is lightweight
+  # and serves only as a wrapper around link renderer instantiation; the
+  # renderer then does all the hard work of generating the HTML.
+  # 
+  # == Global options for helpers
+  #
+  # Options for pagination helpers are optional and get their default values from the
+  # WillPaginate::ViewHelpers.pagination_options hash. You can write to this hash to
+  # override default options on the global level:
+  #
+  #   WillPaginate::ViewHelpers.pagination_options[:prev_label] = 'Previous page'
+  #
+  # By putting this into your environment.rb you can easily translate link texts to previous
+  # and next pages, as well as override some other defaults to your liking.
+  module ViewHelpers
+    # default options that can be overridden on the global level
+    @@pagination_options = {
+      :class        => 'pagination',
+      :prev_label   => '&laquo; Previous',
+      :next_label   => 'Next &raquo;',
+      :inner_window => 4, # links around the current page
+      :outer_window => 1, # links around beginning and end
+      :separator    => ' ', # single space is friendly to spiders and non-graphic browsers
+      :param_name   => :page,
+      :params       => nil,
+      :renderer     => 'WillPaginate::LinkRenderer',
+      :page_links   => true,
+      :container    => true
+    }
+    mattr_reader :pagination_options
+
+    # Renders Digg/Flickr-style pagination for a WillPaginate::Collection
+    # object. Nil is returned if there is only one page in total; no point in
+    # rendering the pagination in that case...
+    # 
+    # ==== Options
+    # * <tt>:class</tt> -- CSS class name for the generated DIV (default: "pagination")
+    # * <tt>:prev_label</tt> -- default: "« Previous"
+    # * <tt>:next_label</tt> -- default: "Next »"
+    # * <tt>:inner_window</tt> -- how many links are shown around the current page (default: 4)
+    # * <tt>:outer_window</tt> -- how many links are around the first and the last page (default: 1)
+    # * <tt>:separator</tt> -- string separator for page HTML elements (default: single space)
+    # * <tt>:param_name</tt> -- parameter name for page number in URLs (default: <tt>:page</tt>)
+    # * <tt>:params</tt> -- additional parameters when generating pagination links
+    #   (eg. <tt>:controller => "foo", :action => nil</tt>)
+    # * <tt>:renderer</tt> -- class name of the link renderer (default: WillPaginate::LinkRenderer)
+    # * <tt>:page_links</tt> -- when false, only previous/next links are rendered (default: true)
+    # * <tt>:container</tt> -- toggles rendering of the DIV container for pagination links, set to
+    #   false only when you are rendering your own pagination markup (default: true)
+    # * <tt>:id</tt> -- HTML ID for the container (default: nil). Pass +true+ to have the ID automatically
+    #   generated from the class name of objects in collection: for example, paginating
+    #   ArticleComment models would yield an ID of "article_comments_pagination".
+    #
+    # All options beside listed ones are passed as HTML attributes to the container
+    # element for pagination links (the DIV). For example:
+    # 
+    #   <%= will_paginate @posts, :id => 'wp_posts' %>
+    #
+    # ... will result in:
+    #
+    #   <div class="pagination" id="wp_posts"> ... </div>
+    #
+    # ==== Using the helper without arguments
+    # If the helper is called without passing in the collection object, it will
+    # try to read from the instance variable inferred by the controller name.
+    # For example, calling +will_paginate+ while the current controller is
+    # PostsController will result in trying to read from the <tt>@posts</tt>
+    # variable. Example:
+    #
+    #   <%= will_paginate :id => true %>
+    #
+    # ... will result in <tt>@post</tt> collection getting paginated:
+    #
+    #   <div class="pagination" id="posts_pagination"> ... </div>
+    #
+    def will_paginate(collection = nil, options = {})
+      options, collection = collection, nil if collection.is_a? Hash
+      unless collection or !controller
+        collection_name = "@#{controller.controller_name}"
+        collection = instance_variable_get(collection_name)
+        raise ArgumentError, "The #{collection_name} variable appears to be empty. Did you " +
+          "forget to specify the collection object for will_paginate?" unless collection
+      end
+      # early exit if there is nothing to render
+      return nil unless collection.page_count > 1
+      options = options.symbolize_keys.reverse_merge WillPaginate::ViewHelpers.pagination_options
+      # create the renderer instance
+      renderer_class = options[:renderer].to_s.constantize
+      renderer = renderer_class.new collection, options, self
+      # render HTML for pagination
+      renderer.to_html
+    end
+
+    # Renders a helpful message with numbers of displayed vs. total entries.
+    # You can use this as a blueprint for your own, similar helpers.
+    #
+    #   <%= page_entries_info @posts %>
+    #   #-> Displaying entries 6 - 10 of 26 in total
+    def page_entries_info(collection)
+      %{Displaying entries <b>%d&nbsp;-&nbsp;%d</b> of <b>%d</b> in total} % [
+        collection.offset + 1,
+        collection.offset + collection.length,
+        collection.total_entries
+      ]
+    end
+  end
+
+  # This class does the heavy lifting of actually building the pagination
+  # links. It is used by +will_paginate+ helper internally.
+  class LinkRenderer
+
+    def initialize(collection, options, template)
+      @collection = collection
+      @options    = options
+      @template   = template
+    end
+
+    def to_html
+      links = @options[:page_links] ? windowed_links : []
+      # previous/next buttons
+      links.unshift page_link_or_span(@collection.previous_page, 'disabled', @options[:prev_label])
+      links.push    page_link_or_span(@collection.next_page,     'disabled', @options[:next_label])
+      
+      html = links.join(@options[:separator])
+      @options[:container] ? @template.content_tag(:div, html, html_attributes) : html
+    end
+
+    def html_attributes
+      return @html_attributes if @html_attributes
+      @html_attributes = @options.except *(WillPaginate::ViewHelpers.pagination_options.keys - [:class])
+      # pagination of Post models will have the ID of "posts_pagination"
+      if @options[:container] and @options[:id] === true
+        @html_attributes[:id] = @collection.first.class.name.underscore.pluralize + '_pagination'
+      end
+      @html_attributes
+    end
+    
+  protected
+
+    def gap_marker; '...'; end
+    
+    def windowed_links
+      prev = nil
+
+      visible_page_numbers.inject [] do |links, n|
+        # detect gaps:
+        links << gap_marker if prev and n > prev + 1
+        links << page_link_or_span(n)
+        prev = n
+        links
+      end
+    end
+
+    def visible_page_numbers
+      inner_window, outer_window = @options[:inner_window].to_i, @options[:outer_window].to_i
+      window_from = current_page - inner_window
+      window_to = current_page + inner_window
+      
+      # adjust lower or upper limit if other is out of bounds
+      if window_to > total_pages
+        window_from -= window_to - total_pages
+        window_to = total_pages
+      elsif window_from < 1
+        window_to += 1 - window_from
+        window_from = 1
+      end
+      
+      visible   = (1..total_pages).to_a
+      left_gap  = (2 + outer_window)...window_from
+      right_gap = (window_to + 1)...(total_pages - outer_window)
+      visible  -= left_gap.to_a  if left_gap.last - left_gap.first > 1
+      visible  -= right_gap.to_a if right_gap.last - right_gap.first > 1
+
+      visible
+    end
+    
+    def page_link_or_span(page, span_class = 'current', text = nil)
+      text ||= page.to_s
+      if page and page != current_page
+        @template.link_to text, url_options(page), :rel => rel_value(page)
+      else
+        @template.content_tag :span, text, :class => span_class
+      end
+    end
+
+    def url_options(page)
+      options = { param_name => page }
+      # page links should preserve GET parameters
+      options = params.merge(options) if @template.request.get?
+      options.rec_merge!(@options[:params]) if @options[:params]
+      return options
+    end
+
+  private
+
+    def rel_value(page)
+      case page
+      when @collection.previous_page; 'prev' + (page == 1 ? ' start' : '')
+      when @collection.next_page; 'next'
+      when 1; 'start'
+      end
+    end
+
+    def current_page
+      @collection.current_page
+    end
+
+    def total_pages
+      @collection.page_count
+    end
+
+    def param_name
+      @param_name ||= @options[:param_name].to_sym
+    end
+
+    def params
+      @params ||= @template.params.to_hash.symbolize_keys
+    end
+  end
+end
diff --git a/wui/src/vendor/plugins/will_paginate/test/array_pagination_test.rb b/wui/src/vendor/plugins/will_paginate/test/array_pagination_test.rb
new file mode 100644
index 0000000..0aad4c8
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/array_pagination_test.rb
@@ -0,0 +1,131 @@
+require File.dirname(__FILE__) + '/helper'
+require 'will_paginate/core_ext'
+
+class ArrayPaginationTest < Test::Unit::TestCase
+  def test_simple
+    collection = ('a'..'e').to_a
+    
+    [{ :page => 1,  :per_page => 3,  :expected => %w( a b c ) },
+     { :page => 2,  :per_page => 3,  :expected => %w( d e ) },
+     { :page => 1,  :per_page => 5,  :expected => %w( a b c d e ) },
+     { :page => 3,  :per_page => 5,  :expected => [] },
+    ].
+    each do |conditions|
+      assert_equal conditions[:expected], collection.paginate(conditions.slice(:page, :per_page))
+    end
+  end
+
+  def test_defaults
+    result = (1..50).to_a.paginate
+    assert_equal 1, result.current_page
+    assert_equal 30, result.size
+  end
+
+  def test_deprecated_api
+    assert_deprecated 'paginate API' do
+      result = (1..50).to_a.paginate(2, 10)
+      assert_equal 2, result.current_page
+      assert_equal (11..20).to_a, result
+      assert_equal 50, result.total_entries
+    end
+    
+    assert_deprecated { [].paginate nil }
+  end
+
+  def test_total_entries_has_precedence
+    result = %w(a b c).paginate :total_entries => 5
+    assert_equal 5, result.total_entries
+  end
+
+  def test_argument_error_with_params_and_another_argument
+    assert_raise ArgumentError do
+      [].paginate({}, 5)
+    end
+  end
+
+  def test_paginated_collection
+    entries = %w(a b c)
+    collection = create(2, 3, 10) do |pager|
+      assert_equal entries, pager.replace(entries)
+    end
+
+    assert_equal entries, collection
+    assert_respond_to_all collection, %w(page_count each offset size current_page per_page total_entries)
+    assert_kind_of Array, collection
+    assert_instance_of Array, collection.entries
+    assert_equal 3, collection.offset
+    assert_equal 4, collection.page_count
+    assert !collection.out_of_bounds?
+  end
+
+  def test_out_of_bounds
+    entries = create(2, 3, 2){}
+    assert entries.out_of_bounds?
+    
+    entries = create(1, 3, 2){}
+    assert !entries.out_of_bounds?
+  end
+
+  def test_guessing_total_count
+    entries = create do |pager|
+      # collection is shorter than limit
+      pager.replace array
+    end
+    assert_equal 8, entries.total_entries
+    
+    entries = create(2, 5, 10) do |pager|
+      # collection is shorter than limit, but we have an explicit count
+      pager.replace array
+    end
+    assert_equal 10, entries.total_entries
+    
+    entries = create do |pager|
+      # collection is the same as limit; we can't guess
+      pager.replace array(5)
+    end
+    assert_equal nil, entries.total_entries
+    
+    entries = create do |pager|
+      # collection is empty; we can't guess
+      pager.replace array(0)
+    end
+    assert_equal nil, entries.total_entries
+  end
+
+  def test_invalid_page
+    bad_input = [0, -1, nil, '', 'Schnitzel']
+
+    bad_input.each do |bad|
+      assert_raise(WillPaginate::InvalidPage) { create(bad) }
+    end
+  end
+
+  def test_invalid_per_page_setting
+    assert_raise(ArgumentError) { create(1, -1) }
+  end
+
+  private
+    def create(page = 2, limit = 5, total = nil, &block)
+      if block_given?
+        WillPaginate::Collection.create(page, limit, total, &block)
+      else
+        WillPaginate::Collection.new(page, limit, total)
+      end
+    end
+
+    def array(size = 3)
+      Array.new(size)
+    end
+    
+    def collect_deprecations
+      old_behavior = WillPaginate::Deprecation.behavior
+      deprecations = []
+      WillPaginate::Deprecation.behavior = Proc.new do |message, callstack|
+        deprecations << message
+      end
+      result = yield
+      [result, deprecations]
+    ensure
+      WillPaginate::Deprecation.behavior = old_behavior
+    end
+end
diff --git a/wui/src/vendor/plugins/will_paginate/test/boot.rb b/wui/src/vendor/plugins/will_paginate/test/boot.rb
new file mode 100644
index 0000000..f1bd72b
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/boot.rb
@@ -0,0 +1,23 @@
+plugin_root = File.join(File.dirname(__FILE__), '..')
+version = ENV['RAILS_VERSION']
+version = nil if version and version == ""
+
+# first look for a symlink to a copy of the framework
+if !version and framework_root = ["#{plugin_root}/rails", "#{plugin_root}/../../rails"].find { |p| File.directory? p }
+  puts "found framework root: #{framework_root}"
+  # this allows for a plugin to be tested outside of an app and without Rails gems
+  $:.unshift "#{framework_root}/activesupport/lib", "#{framework_root}/activerecord/lib", "#{framework_root}/actionpack/lib"
+else
+  # simply use installed gems if available
+  puts "using Rails#{version ? ' ' + version : nil} gems"
+  require 'rubygems'
+  
+  if version
+    gem 'rails', version
+  else
+    gem 'actionpack'
+    gem 'activerecord'
+  end
+end
+
+$:.unshift "#{plugin_root}/lib"
diff --git a/wui/src/vendor/plugins/will_paginate/test/console b/wui/src/vendor/plugins/will_paginate/test/console
new file mode 100755
index 0000000..53b8de4
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/console
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby
+irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
+libs = []
+dirname = File.dirname(__FILE__)
+
+libs << 'irb/completion'
+libs << File.join(dirname, 'lib', 'load_fixtures')
+
+exec "#{irb}#{libs.map{ |l| " -r #{l}" }.join} --simple-prompt"
diff --git a/wui/src/vendor/plugins/will_paginate/test/database.yml b/wui/src/vendor/plugins/will_paginate/test/database.yml
new file mode 100644
index 0000000..7ef1e73
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/database.yml
@@ -0,0 +1,22 @@
+sqlite3:
+  database: ":memory:"
+  adapter: sqlite3
+  timeout: 500
+
+sqlite2:
+  database: ":memory:"
+  adapter: sqlite2
+
+mysql:
+  adapter: mysql
+  username: rails
+  password: mislav
+  encoding: utf8
+  database: will_paginate_unittest
+
+postgres:
+  adapter: postgresql
+  username: mislav
+  password: mislav
+  database: will_paginate_unittest
+  min_messages: warning
diff --git a/wui/src/vendor/plugins/will_paginate/test/finder_test.rb b/wui/src/vendor/plugins/will_paginate/test/finder_test.rb
new file mode 100644
index 0000000..b272341
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/finder_test.rb
@@ -0,0 +1,322 @@
+require File.dirname(__FILE__) + '/helper'
+require File.dirname(__FILE__) + '/lib/activerecord_test_case'
+
+require 'will_paginate'
+WillPaginate.enable_activerecord
+
+class FinderTest < ActiveRecordTestCase
+  fixtures :topics, :replies, :users, :projects, :developers_projects
+
+  def test_new_methods_presence
+    assert_respond_to_all Topic, %w(per_page paginate paginate_by_sql)
+  end
+  
+  def test_simple_paginate
+    entries = Topic.paginate :page => nil
+    assert_equal 1, entries.current_page
+    assert_nil entries.previous_page
+    assert_nil entries.next_page
+    assert_equal 1, entries.page_count
+    assert_equal 4, entries.size
+    
+    entries = Topic.paginate :page => 2
+    assert_equal 2, entries.current_page
+    assert_equal 1, entries.previous_page
+    assert_equal 1, entries.page_count
+    assert entries.empty?
+  end
+
+  def test_parameter_api
+    # :page parameter in options is required!
+    assert_raise(ArgumentError){ Topic.paginate }
+    assert_raise(ArgumentError){ Topic.paginate({}) }
+    
+    # explicit :all should not break anything
+    assert_equal Topic.paginate(:page => nil), Topic.paginate(:all, :page => 1)
+
+    # :count could be nil and we should still not cry
+    assert_nothing_raised { Topic.paginate :page => 1, :count => nil }
+  end
+  
+  def test_paginate_with_per_page
+    entries = Topic.paginate :page => 1, :per_page => 1
+    assert_equal 1, entries.size
+    assert_equal 4, entries.page_count
+
+    # Developer class has explicit per_page at 10
+    entries = Developer.paginate :page => 1
+    assert_equal 10, entries.size
+    assert_equal 2, entries.page_count
+
+    entries = Developer.paginate :page => 1, :per_page => 5
+    assert_equal 11, entries.total_entries
+    assert_equal 5, entries.size
+    assert_equal 3, entries.page_count
+  end
+  
+  def test_paginate_with_order
+    entries = Topic.paginate :page => 1, :order => 'created_at desc'
+    expected = [topics(:futurama), topics(:harvey_birdman), topics(:rails), topics(:ar)].reverse
+    assert_equal expected, entries.to_a
+    assert_equal 1, entries.page_count
+  end
+  
+  def test_paginate_with_conditions
+    entries = Topic.paginate :page => 1, :conditions => ["created_at > ?", 30.minutes.ago]
+    expected = [topics(:rails), topics(:ar)]
+    assert_equal expected, entries.to_a
+    assert_equal 1, entries.page_count
+  end
+
+  def test_paginate_with_include_and_conditions
+    entries = Topic.paginate \
+      :page     => 1, 
+      :include  => :replies,  
+      :conditions => "replies.content LIKE 'Bird%' ", 
+      :per_page => 10
+
+    expected = Topic.find :all, 
+      :include => 'replies', 
+      :conditions => "replies.content LIKE 'Bird%' ", 
+      :limit   => 10
+
+    assert_equal expected, entries.to_a
+    assert_equal 1, entries.total_entries
+  end
+  
+  def test_paginate_with_include_and_order
+    entries = Topic.paginate \
+      :page     => 1, 
+      :include  => :replies,  
+      :order    => 'replies.created_at asc, topics.created_at asc', 
+      :per_page => 10
+
+    expected = Topic.find :all, 
+      :include => 'replies', 
+      :order   => 'replies.created_at asc, topics.created_at asc', 
+      :limit   => 10
+
+    assert_equal expected, entries.to_a
+    assert_equal 4, entries.total_entries
+  end
+
+  def test_paginate_associations_with_include
+    entries, project = nil, projects(:active_record)
+
+    assert_nothing_raised "THIS IS A BUG in Rails 1.2.3 that was fixed in [7326]. " +
+        "Please upgrade to the 1-2-stable branch or edge Rails." do
+      entries = project.topics.paginate \
+        :page     => 1, 
+        :include  => :replies,  
+        :conditions => "replies.content LIKE 'Nice%' ", 
+        :per_page => 10
+    end
+
+    expected = Topic.find :all, 
+      :include => 'replies', 
+      :conditions => "project_id = #{project.id} AND replies.content LIKE 'Nice%' ", 
+      :limit   => 10
+
+    assert_equal expected, entries.to_a
+  end
+
+  def test_paginate_associations
+    dhh = users :david
+    expected_name_ordered = [projects(:action_controller), projects(:active_record)]
+    expected_id_ordered   = [projects(:active_record), projects(:action_controller)]
+
+    # with association-specified order
+    entries = dhh.projects.paginate(:page => 1)
+    assert_equal expected_name_ordered, entries
+    assert_equal 2, entries.total_entries
+
+    # with explicit order
+    entries = dhh.projects.paginate(:page => 1, :order => 'projects.id')
+    assert_equal expected_id_ordered, entries
+    assert_equal 2, entries.total_entries
+
+    assert_nothing_raised { dhh.projects.find(:all, :order => 'projects.id', :limit => 4) }
+    entries = dhh.projects.paginate(:page => 1, :order => 'projects.id', :per_page => 4)
+    assert_equal expected_id_ordered, entries
+
+    # has_many with implicit order
+    topic = Topic.find(1)
+    expected = [replies(:spam), replies(:witty_retort)]
+    assert_equal expected.map(&:id).sort, topic.replies.paginate(:page => 1).map(&:id).sort
+    assert_equal expected.reverse, topic.replies.paginate(:page => 1, :order => 'replies.id ASC')
+  end
+
+  def test_paginate_association_extension
+    project = Project.find(:first)
+    entries = project.replies.paginate_recent :page => 1
+    assert_equal [replies(:brave)], entries
+  end
+  
+  def test_paginate_with_joins
+    entries = Developer.paginate :page => 1,
+                        :joins => 'LEFT JOIN developers_projects ON users.id = developers_projects.developer_id',
+                        :conditions => 'project_id = 1'        
+    assert_equal 2, entries.size
+    developer_names = entries.map { |d| d.name }
+    assert developer_names.include?('David')
+    assert developer_names.include?('Jamis')
+
+    expected = entries.to_a
+    entries = Developer.paginate :page => 1,
+                        :joins => 'LEFT JOIN developers_projects ON users.id = developers_projects.developer_id',
+                        :conditions => 'project_id = 1', :count => { :select => "users.id" }
+    assert_equal expected, entries.to_a
+  end
+
+  def test_paginate_with_group
+    entries = Developer.paginate :page => 1, :per_page => 10,
+                                 :group => 'salary', :select => 'salary', :order => 'salary'
+    expected = [ users(:david), users(:jamis), users(:dev_10), users(:poor_jamis) ].map(&:salary).sort
+    assert_equal expected, entries.map(&:salary)
+  end
+
+  def test_paginate_with_dynamic_finder
+    expected = [replies(:witty_retort), replies(:spam)]
+    assert_equal expected, Reply.paginate_by_topic_id(1, :page => 1)
+
+    entries = Developer.paginate :conditions => { :salary => 100000 }, :page => 1, :per_page => 5
+    assert_equal 8, entries.total_entries
+    assert_equal entries, Developer.paginate_by_salary(100000, :page => 1, :per_page => 5)
+
+    # dynamic finder + conditions
+    entries = Developer.paginate_by_salary(100000, :page => 1,
+                                           :conditions => ['id > ?', 6])
+    assert_equal 4, entries.total_entries
+    assert_equal (7..10).to_a, entries.map(&:id)
+
+    assert_raises NoMethodError do
+      Developer.paginate_by_inexistent_attribute 100000, :page => 1
+    end
+  end
+
+  def test_scoped_paginate
+    entries = Developer.with_poor_ones { Developer.paginate :page => 1 }
+
+    assert_equal 2, entries.size
+    assert_equal 2, entries.total_entries
+  end
+
+  def test_readonly
+    assert_nothing_raised { Developer.paginate :readonly => true, :page => 1 }
+  end
+
+  # this functionality is temporarily removed
+  def xtest_pagination_defines_method
+    pager = "paginate_by_created_at"
+    assert !User.methods.include?(pager), "User methods should not include `#{pager}` method"
+    # paginate!
+    assert 0, User.send(pager, nil, :page => 1).total_entries
+    # the paging finder should now be defined
+    assert User.methods.include?(pager), "`#{pager}` method should be defined on User"
+  end
+
+  # Is this Rails 2.0? Find out by testing find_all which was removed in [6998]
+  unless Developer.respond_to? :find_all
+    def test_paginate_array_of_ids
+      # AR finders also accept arrays of IDs
+      # (this was broken in Rails before [6912])
+      entries = Developer.paginate((1..8).to_a, :per_page => 3, :page => 2, :order => 'id')
+      assert_equal (4..6).to_a, entries.map(&:id)
+      assert_equal 8, entries.total_entries
+    end
+  end
+
+  uses_mocha 'internals' do
+    def test_implicit_all_with_dynamic_finders
+      Topic.expects(:find_all_by_foo).returns([])
+      Topic.expects(:count).returns(0)
+      Topic.paginate_by_foo :page => 1
+    end
+    
+    def test_guessing_the_total_count
+      Topic.expects(:find).returns(Array.new(2))
+      Topic.expects(:count).never
+      
+      entries = Topic.paginate :page => 2, :per_page => 4
+      assert_equal 6, entries.total_entries
+    end
+    
+    def test_extra_parameters_stay_untouched
+      Topic.expects(:find).with(:all, {:foo => 'bar', :limit => 4, :offset => 0 }).returns(Array.new(5))
+      Topic.expects(:count).with({:foo => 'bar'}).returns(1)
+
+      Topic.paginate :foo => 'bar', :page => 1, :per_page => 4
+    end
+
+    def test_count_skips_select
+      Developer.stubs(:find).returns([])
+      Developer.expects(:count).with({}).returns(0)
+      Developer.paginate :select => 'salary', :page => 1
+    end
+
+    def test_count_select_when_distinct
+      Developer.stubs(:find).returns([])
+      Developer.expects(:count).with(:select => 'DISTINCT salary').returns(0)
+      Developer.paginate :select => 'DISTINCT salary', :page => 1
+    end
+
+    def test_should_use_scoped_finders_if_present
+      # scope-out compatibility
+      Topic.expects(:find_best).returns(Array.new(5))
+      Topic.expects(:with_best).returns(1)
+      
+      Topic.paginate_best :page => 1, :per_page => 4
+    end
+
+    def test_paginate_by_sql
+      assert_respond_to Developer, :paginate_by_sql
+      Developer.expects(:find_by_sql).with(regexp_matches(/sql LIMIT 3(,| OFFSET) 3/)).returns([])
+      Developer.expects(:count_by_sql).with('SELECT COUNT(*) FROM (sql) AS count_table').returns(0)
+      
+      entries = Developer.paginate_by_sql 'sql', :page => 2, :per_page => 3
+    end
+
+    def test_paginate_by_sql_respects_total_entries_setting
+      Developer.expects(:find_by_sql).returns([])
+      Developer.expects(:count_by_sql).never
+      
+      entries = Developer.paginate_by_sql 'sql', :page => 1, :total_entries => 999
+      assert_equal 999, entries.total_entries
+    end
+
+    def test_paginate_by_sql_strips_order_by_when_counting
+      Developer.expects(:find_by_sql).returns([])
+      Developer.expects(:count_by_sql).with("SELECT COUNT(*) FROM (sql\n ) AS count_table").returns(0)
+      
+      entries = Developer.paginate_by_sql "sql\n ORDER\nby foo, bar, `baz` ASC", :page => 1
+    end
+
+    # TODO: counts are still wrong
+    def test_ability_to_use_with_custom_finders
+      # acts_as_taggable defines find_tagged_with(tag, options)
+      Topic.expects(:find_tagged_with).with('will_paginate', :offset => 0, :limit => 5).returns([])
+      Topic.expects(:count).with({}).returns(0)
+      
+      Topic.paginate_tagged_with 'will_paginate', :page => 1, :per_page => 5
+    end
+    
+    def test_array_argument_doesnt_eliminate_count
+      ids = (1..8).to_a
+      Developer.expects(:find_all_by_id).returns([])
+      Developer.expects(:count).returns(0)
+      
+      Developer.paginate_by_id(ids, :per_page => 3, :page => 2, :order => 'id')
+    end
+
+    def test_paginating_finder_doesnt_mangle_options
+      Developer.expects(:find).returns([])
+      Developer.expects(:count).returns(0)
+      options = { :page => 1 }
+      options.expects(:delete).never
+      options_before = options.dup
+      
+      Developer.paginate(options)
+      assert_equal options, options_before
+    end
+  end
+end
diff --git a/wui/src/vendor/plugins/will_paginate/test/fixtures/admin.rb b/wui/src/vendor/plugins/will_paginate/test/fixtures/admin.rb
new file mode 100644
index 0000000..1d5e7f3
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/fixtures/admin.rb
@@ -0,0 +1,3 @@
+class Admin < User
+  has_many :companies, :finder_sql => 'SELECT * FROM companies'
+end
diff --git a/wui/src/vendor/plugins/will_paginate/test/fixtures/developer.rb b/wui/src/vendor/plugins/will_paginate/test/fixtures/developer.rb
new file mode 100644
index 0000000..6650a98
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/fixtures/developer.rb
@@ -0,0 +1,11 @@
+class Developer < User
+  has_and_belongs_to_many :projects, :include => :topics, :order => 'projects.name'
+
+  def self.with_poor_ones(&block)
+    with_scope :find => { :conditions => ['salary <= ?', 80000], :order => 'salary' } do
+      yield
+    end
+  end
+
+  def self.per_page() 10 end
+end
diff --git a/wui/src/vendor/plugins/will_paginate/test/fixtures/developers_projects.yml b/wui/src/vendor/plugins/will_paginate/test/fixtures/developers_projects.yml
new file mode 100644
index 0000000..cee359c
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/fixtures/developers_projects.yml
@@ -0,0 +1,13 @@
+david_action_controller:
+  developer_id: 1
+  project_id: 2
+  joined_on: 2004-10-10
+
+david_active_record:
+  developer_id: 1
+  project_id: 1
+  joined_on: 2004-10-10
+
+jamis_active_record:
+  developer_id: 2
+  project_id: 1
\ No newline at end of file
diff --git a/wui/src/vendor/plugins/will_paginate/test/fixtures/project.rb b/wui/src/vendor/plugins/will_paginate/test/fixtures/project.rb
new file mode 100644
index 0000000..0f85ef5
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/fixtures/project.rb
@@ -0,0 +1,15 @@
+class Project < ActiveRecord::Base
+  has_and_belongs_to_many :developers, :uniq => true
+  
+  has_many :topics
+    # :finder_sql  => 'SELECT * FROM topics WHERE (topics.project_id = #{id})',
+    # :counter_sql => 'SELECT COUNT(*) FROM topics WHERE (topics.project_id = #{id})'
+  
+  has_many :replies, :through => :topics do
+    def find_recent(params = {})
+      with_scope :find => { :conditions => ['replies.created_at > ?', 15.minutes.ago] } do
+        find :all, params
+      end
+    end
+  end
+end
diff --git a/wui/src/vendor/plugins/will_paginate/test/fixtures/projects.yml b/wui/src/vendor/plugins/will_paginate/test/fixtures/projects.yml
new file mode 100644
index 0000000..02800c7
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/fixtures/projects.yml
@@ -0,0 +1,7 @@
+action_controller:
+  id: 2
+  name: Active Controller
+
+active_record:
+  id: 1
+  name: Active Record
diff --git a/wui/src/vendor/plugins/will_paginate/test/fixtures/replies.yml b/wui/src/vendor/plugins/will_paginate/test/fixtures/replies.yml
new file mode 100644
index 0000000..9a83c00
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/fixtures/replies.yml
@@ -0,0 +1,29 @@
+witty_retort:
+  id: 1
+  topic_id: 1
+  content: Birdman is better!
+  created_at: <%= 6.hours.ago.to_s(:db) %>
+  
+another:
+  id: 2
+  topic_id: 2
+  content: Nuh uh!
+  created_at: <%= 1.hour.ago.to_s(:db) %>
+  
+spam:
+  id: 3
+  topic_id: 1
+  content: Nice site!
+  created_at: <%= 1.hour.ago.to_s(:db) %>
+
+decisive:
+  id: 4
+  topic_id: 4
+  content: "I'm getting to the bottom of this"
+  created_at: <%= 30.minutes.ago.to_s(:db) %>
+
+brave:
+  id: 5
+  topic_id: 4
+  content: "AR doesn't scare me a bit"
+  created_at: <%= 10.minutes.ago.to_s(:db) %>
diff --git a/wui/src/vendor/plugins/will_paginate/test/fixtures/reply.rb b/wui/src/vendor/plugins/will_paginate/test/fixtures/reply.rb
new file mode 100644
index 0000000..ea84042
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/fixtures/reply.rb
@@ -0,0 +1,5 @@
+class Reply < ActiveRecord::Base
+  belongs_to :topic, :include => [:replies]
+  
+  validates_presence_of :content
+end
diff --git a/wui/src/vendor/plugins/will_paginate/test/fixtures/schema.rb b/wui/src/vendor/plugins/will_paginate/test/fixtures/schema.rb
new file mode 100644
index 0000000..8831aad
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/fixtures/schema.rb
@@ -0,0 +1,38 @@
+ActiveRecord::Schema.define do
+
+  create_table "users", :force => true do |t|
+    t.column "name",       :text
+    t.column "salary",     :integer,  :default => 70000
+    t.column "created_at", :datetime
+    t.column "updated_at", :datetime
+    t.column "type",       :text
+  end
+
+  create_table "projects", :force => true do |t|
+    t.column "name", :text
+  end
+
+  create_table "developers_projects", :id => false, :force => true do |t|
+    t.column "developer_id", :integer, :null => false
+    t.column "project_id",   :integer, :null => false
+    t.column "joined_on",    :date
+    t.column "access_level", :integer, :default => 1
+  end
+
+  create_table "topics", :force => true do |t|
+    t.column "project_id", :integer
+    t.column "title",      :string
+    t.column "subtitle",   :string
+    t.column "content",    :text
+    t.column "created_at", :datetime
+    t.column "updated_at", :datetime
+  end
+
+  create_table "replies", :force => true do |t|
+    t.column "content",    :text
+    t.column "created_at", :datetime
+    t.column "updated_at", :datetime
+    t.column "topic_id",   :integer
+  end
+
+end
diff --git a/wui/src/vendor/plugins/will_paginate/test/fixtures/topic.rb b/wui/src/vendor/plugins/will_paginate/test/fixtures/topic.rb
new file mode 100644
index 0000000..12b8747
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/fixtures/topic.rb
@@ -0,0 +1,4 @@
+class Topic < ActiveRecord::Base
+  has_many :replies, :dependent => :destroy, :order => 'replies.created_at DESC'
+  belongs_to :project
+end
diff --git a/wui/src/vendor/plugins/will_paginate/test/fixtures/topics.yml b/wui/src/vendor/plugins/will_paginate/test/fixtures/topics.yml
new file mode 100644
index 0000000..0a26904
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/fixtures/topics.yml
@@ -0,0 +1,30 @@
+futurama:
+  id: 1
+  title: Isnt futurama awesome?
+  subtitle: It really is, isnt it.
+  content: I like futurama
+  created_at: <%= 1.day.ago.to_s(:db) %>
+  updated_at:
+  
+harvey_birdman:
+  id: 2
+  title: Harvey Birdman is the king of all men
+  subtitle: yup
+  content: He really is
+  created_at: <%= 2.hours.ago.to_s(:db) %>
+  updated_at:
+
+rails:
+  id: 3
+  project_id: 1
+  title: Rails is nice
+  subtitle: It makes me happy
+  content: except when I have to hack internals to fix pagination. even then really.
+  created_at: <%= 20.minutes.ago.to_s(:db) %>
+
+ar:
+  id: 4
+  project_id: 1
+  title: ActiveRecord sometimes freaks me out
+  content: "I mean, what's the deal with eager loading?"
+  created_at: <%= 15.minutes.ago.to_s(:db) %>
diff --git a/wui/src/vendor/plugins/will_paginate/test/fixtures/user.rb b/wui/src/vendor/plugins/will_paginate/test/fixtures/user.rb
new file mode 100644
index 0000000..4a57cf0
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/fixtures/user.rb
@@ -0,0 +1,2 @@
+class User < ActiveRecord::Base
+end
diff --git a/wui/src/vendor/plugins/will_paginate/test/fixtures/users.yml b/wui/src/vendor/plugins/will_paginate/test/fixtures/users.yml
new file mode 100644
index 0000000..ed2c03a
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/fixtures/users.yml
@@ -0,0 +1,35 @@
+david:
+  id: 1
+  name: David
+  salary: 80000
+  type: Developer
+
+jamis:
+  id: 2
+  name: Jamis
+  salary: 150000
+  type: Developer
+
+<% for digit in 3..10 %>
+dev_<%= digit %>:
+  id: <%= digit %>
+  name: fixture_<%= digit %>
+  salary: 100000
+  type: Developer
+<% end %>
+
+poor_jamis:
+  id: 11
+  name: Jamis
+  salary: 9000
+  type: Developer
+
+admin:
+  id: 12
+  name: admin
+  type: Admin
+
+goofy:
+  id: 13
+  name: Goofy
+  type: Admin
diff --git a/wui/src/vendor/plugins/will_paginate/test/helper.rb b/wui/src/vendor/plugins/will_paginate/test/helper.rb
new file mode 100644
index 0000000..b8683ce
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/helper.rb
@@ -0,0 +1,25 @@
+require 'test/unit'
+require 'rubygems'
+
+# gem install redgreen for colored test output
+begin require 'redgreen'; rescue LoadError; end
+
+require File.join(File.dirname(__FILE__), 'boot') unless defined?(ActiveRecord)
+
+class Test::Unit::TestCase
+  protected
+  def assert_respond_to_all object, methods
+    methods.each do |method|
+      [method.to_s, method.to_sym].each { |m| assert_respond_to object, m }
+    end
+  end
+end
+
+# Wrap tests that use Mocha and skip if unavailable.
+def uses_mocha(test_name)
+  require 'mocha' unless Object.const_defined?(:Mocha)
+rescue LoadError => load_error
+  $stderr.puts "Skipping #{test_name} tests. `gem install mocha` and try again."
+else
+  yield
+end
diff --git a/wui/src/vendor/plugins/will_paginate/test/lib/activerecord_test_case.rb b/wui/src/vendor/plugins/will_paginate/test/lib/activerecord_test_case.rb
new file mode 100644
index 0000000..a39b4a5
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/lib/activerecord_test_case.rb
@@ -0,0 +1,23 @@
+require File.join(File.dirname(__FILE__), 'activerecord_test_connector')
+
+class ActiveRecordTestCase < Test::Unit::TestCase
+  # Set our fixture path
+  if ActiveRecordTestConnector.able_to_connect
+    self.fixture_path = File.join(File.dirname(__FILE__), '..', 'fixtures')
+    self.use_transactional_fixtures = true
+  end
+
+  def self.fixtures(*args)
+    super if ActiveRecordTestConnector.connected
+  end
+
+  def run(*args)
+    super if ActiveRecordTestConnector.connected
+  end
+
+  # Default so Test::Unit::TestCase doesn't complain
+  def test_truth
+  end
+end
+
+ActiveRecordTestConnector.setup
diff --git a/wui/src/vendor/plugins/will_paginate/test/lib/activerecord_test_connector.rb b/wui/src/vendor/plugins/will_paginate/test/lib/activerecord_test_connector.rb
new file mode 100644
index 0000000..35fde8e
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/lib/activerecord_test_connector.rb
@@ -0,0 +1,60 @@
+require 'active_record'
+require 'active_record/version'
+require 'active_record/fixtures'
+
+class ActiveRecordTestConnector
+  cattr_accessor :able_to_connect
+  cattr_accessor :connected
+
+  # Set our defaults
+  self.connected = false
+  self.able_to_connect = true
+
+  def self.setup
+    unless self.connected || !self.able_to_connect
+      setup_connection
+      load_schema
+      # require_fixture_models
+      Dependencies.load_paths.unshift(File.dirname(__FILE__) + "/../fixtures")
+      self.connected = true
+    end
+  rescue Exception => e  # errors from ActiveRecord setup
+    $stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}"
+    #$stderr.puts "  #{e.backtrace.join("\n  ")}\n"
+    self.able_to_connect = false
+  end
+
+  private
+
+  def self.setup_connection
+    db = ENV['DB'].blank?? 'sqlite3' : ENV['DB']
+    
+    configurations = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'database.yml'))
+    raise "no configuration for '#{db}'" unless configurations.key? db
+    configuration = configurations[db]
+    
+    ActiveRecord::Base.logger = Logger.new(STDOUT) if $0 == 'irb'
+    puts "using #{configuration['adapter']} adapter" unless ENV['DB'].blank?
+    
+    ActiveRecord::Base.establish_connection(configuration)
+    ActiveRecord::Base.configurations = { db => configuration }
+    ActiveRecord::Base.connection
+
+    unless Object.const_defined?(:QUOTED_TYPE)
+      Object.send :const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')
+    end
+  end
+
+  def self.load_schema
+    ActiveRecord::Base.silence do
+      ActiveRecord::Migration.verbose = false
+      load File.dirname(__FILE__) + "/../fixtures/schema.rb"
+    end
+  end
+
+  def self.require_fixture_models
+    models = Dir.glob(File.dirname(__FILE__) + "/../fixtures/*.rb")
+    models = (models.grep(/user.rb/) + models).uniq
+    models.each { |f| require f }
+  end
+end
diff --git a/wui/src/vendor/plugins/will_paginate/test/lib/html_inner_text.rb b/wui/src/vendor/plugins/will_paginate/test/lib/html_inner_text.rb
new file mode 100644
index 0000000..7bb6246
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/lib/html_inner_text.rb
@@ -0,0 +1,21 @@
+require 'action_controller/test_process'
+
+module HTML
+  class Node
+    def inner_text
+      children.map(&:inner_text).join('')
+    end
+  end
+  
+  class Text
+    def inner_text
+      self.to_s
+    end
+  end
+
+  class Tag
+    def inner_text
+      childless?? '' : super
+    end
+  end
+end
diff --git a/wui/src/vendor/plugins/will_paginate/test/lib/load_fixtures.rb b/wui/src/vendor/plugins/will_paginate/test/lib/load_fixtures.rb
new file mode 100644
index 0000000..c9f4d1f
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/lib/load_fixtures.rb
@@ -0,0 +1,13 @@
+dirname = File.dirname(__FILE__)
+require File.join(dirname, '..', 'boot')
+require File.join(dirname, 'activerecord_test_connector')
+
+# setup the connection
+ActiveRecordTestConnector.setup
+
+# load all fixtures
+fixture_path = File.join(dirname, '..', 'fixtures')
+Fixtures.create_fixtures(fixture_path, ActiveRecord::Base.connection.tables)
+
+require 'will_paginate'
+WillPaginate.enable_activerecord
diff --git a/wui/src/vendor/plugins/will_paginate/test/pagination_test.rb b/wui/src/vendor/plugins/will_paginate/test/pagination_test.rb
new file mode 100644
index 0000000..f116391
--- /dev/null
+++ b/wui/src/vendor/plugins/will_paginate/test/pagination_test.rb
@@ -0,0 +1,272 @@
+require File.dirname(__FILE__) + '/helper'
+require 'action_controller'
+require File.dirname(__FILE__) + '/lib/html_inner_text'
+
+ActionController::Routing::Routes.draw do |map|
+  map.connect ':controller/:action/:id'
+end
+
+ActionController::Base.perform_caching = false
+
+require 'will_paginate'
+WillPaginate.enable_actionpack
+
+class PaginationTest < Test::Unit::TestCase
+  
+  class DevelopersController < ActionController::Base
+    def list_developers
+      @options = session[:wp] || {}
+      
+      @developers = (1..11).to_a.paginate(
+        :page => params[ options[:param_name] || :page] || 1,
+        :per_page => params[:per_page] || 4
+      )
+
+      render :inline => '<%= will_paginate @developers, @options %>'
+    end
+
+    def guess_collection_name
+      @developers = session[:wp]
+      @options    = session[:wp_options]
+      render :inline => '<%= will_paginate @options %>'
+    end
+
+    protected
+      def rescue_errors(e) raise e end
+      def rescue_action(e) raise e end
+  end
+  
+  def setup
+    @controller = DevelopersController.new
+    @request    = ActionController::TestRequest.new
+    @response   = ActionController::TestResponse.new
+    super
+  end
+
+  def test_will_paginate
+    get :list_developers
+
+    entries = assigns :developers
+    assert entries
+    assert_equal 4, entries.size
+
+    assert_select 'div.pagination', 1, 'no main DIV' do |pagination|
+      assert_select 'a[href]', 3 do |elements|
+        validate_page_numbers [2,3,2], elements
+        assert_select elements.last, ':last-child', "Next &raquo;"
+      end
+      assert_select 'span', 2
+      assert_select 'span.disabled:first-child', "&laquo; Previous"
+      assert_select 'span.current', entries.current_page.to_s
+      assert_equal '&laquo; Previous 1 2 3 Next &raquo;', pagination.first.inner_text
+    end
+  end
+
+  def test_will_paginate_with_options
+    get :list_developers, { :page => 2 }, :wp => {
+      :class => 'will_paginate', :prev_label => 'Prev', :next_label => 'Next'
+    }
+    assert_response :success
+    
+    entries = assigns :developers
+    assert entries
+    assert_equal 4, entries.size
+
+    assert_select 'div.will_paginate', 1, 'no main DIV' do
+      assert_select 'a[href]', 4 do |elements|
+        validate_page_numbers [1,1,3,3], elements
+        # test rel attribute values:
+        assert_select elements[1], 'a', '1' do |link|
+          assert_equal 'prev start', link.first['rel']
+        end
+        assert_select elements.first, 'a', "Prev" do |link|
+          assert_equal 'prev start', link.first['rel']
+        end
+        assert_select elements.last, 'a', "Next" do |link|
+          assert_equal 'next', link.first['rel']
+        end
+      end
+      assert_select 'span.current', entries.current_page.to_s
+    end
+  end
+
+  def test_will_paginate_without_container
+    get :list_developers, {}, :wp => { :container => false }
+    assert_select 'div.pagination', 0, 'no main DIV'
+    assert_select 'a[href]', 3
+  end
+
+  def test_will_paginate_without_page_links
+    get :list_developers, { :page => 2 }, :wp => { :page_links => false }
+    assert_select 'a[href]', 2 do |elements|
+      validate_page_numbers [1,3], elements
+    end
+  end
+  
+  def test_will_paginate_preserves_parameters_on_get
+    get :list_developers, :foo => { :bar => 'baz' }
+    assert_links_match /foo%5Bbar%5D=baz/
+  end
+  
+  def test_will_paginate_doesnt_preserve_parameters_on_post
+    post :list_developers, :foo => 'bar'
+    assert_no_links_match /foo=bar/
+  end
+  
+  def test_adding_additional_parameters
+    get :list_developers, {}, :wp => { :params => { :foo => 'bar' } }
+    assert_links_match /foo=bar/
+  end
+  
+  def test_removing_arbitrary_parameters
+    get :list_developers, { :foo => 'bar' }, :wp => { :params => { :foo => nil } }
+    assert_no_links_match /foo=bar/
+  end
+    
+  def test_adding_additional_route_parameters
+    get :list_developers, {}, :wp => { :params => { :controller => 'baz' } }
+    assert_links_match %r{\Wbaz/list_developers\W}
+  end
+  
+  def test_will_paginate_with_custom_page_param
+    get :list_developers, { :developers_page => 2 }, :wp => { :param_name => :developers_page }
+    assert_response :success
+    
+    entries = assigns :developers
+    assert entries
+    assert_equal 4, entries.size
+
+    assert_select 'div.pagination', 1, 'no main DIV' do
+      assert_select 'a[href]', 4 do |elements|
+        validate_page_numbers [1,1,3,3], elements, :developers_page
+      end
+      assert_select 'span.current', entries.current_page.to_s
+    end    
+  end
+
+  def test_will_paginate_windows
+    get :list_developers, { :page => 6, :per_page => 1 }, :wp => { :inner_window => 1 }
+    assert_response :success
+    
+    entries = assigns :developers
+    assert entries
+    assert_equal 1, entries.size
+
+    assert_select 'div.pagination', 1, 'no main DIV' do |pagination|
+      assert_select 'a[href]', 8 do |elements|
+        validate_page_numbers [5,1,2,5,7,10,11,7], elements
+        assert_select elements.first, 'a', "&laquo; Previous"
+        assert_select elements.last, 'a', "Next &raquo;"
+      end
+      assert_select 'span.current', entries.current_page.to_s
+      assert_equal '&laquo; Previous 1 2 ... 5 6 7 ... 10 11 Next &raquo;', pagination.first.inner_text
+    end
+  end
+
+  def test_will_paginate_eliminates_small_gaps
+    get :list_developers, { :page => 6, :per_page => 1 }, :wp => { :inner_window => 2 }
+    assert_response :success
+    
+    assert_select 'div.pagination', 1, 'no main DIV' do
+      assert_select 'a[href]', 12 do |elements|
+        validate_page_numbers [5,1,2,3,4,5,7,8,9,10,11,7], elements
+      end
+    end
+  end
+
+  def test_no_pagination
+    get :list_developers, :per_page => 12
+    entries = assigns :developers
+    assert_equal 1, entries.page_count
+    assert_equal 11, entries.size
+
+    assert_equal '', @response.body
+  end
+  
+  def test_faulty_input_raises_error
+    assert_raise WillPaginate::InvalidPage do
+      get :list_developers, :page => 'foo'
+    end
+  end
+
+  uses_mocha 'helper internals' do
+    def test_collection_name_can_be_guessed
+      collection = mock
+      collection.expects(:page_count).returns(1)
+      get :guess_collection_name, {}, :wp => collection
+    end
+  end
+  
+  def test_inferred_collection_name_raises_error_when_nil
+    ex = assert_raise ArgumentError do
+      get :guess_collection_name, {}, :wp => nil
+    end
+    assert ex.message.include?('@developers')
+  end
+
+  def test_setting_id_for_container
+    get :list_developers
+    assert_select 'div.pagination', 1 do |div|
+      assert_nil div.first['id']
+    end
+    # magic ID
+    get :list_developers, {}, :wp => { :id => true }
+    assert_select 'div.pagination', 1 do |div|
+      assert_equal 'fixnums_pagination', div.first['id']
+    end
+    # explicit ID
+    get :list_developers, {}, :wp => { :id => 'custom_id' }
+    assert_select 'div.pagination', 1 do |div|
+      assert_equal 'custom_id', div.first['id']
+    end
+  end
+
+  if ActionController::Base.respond_to? :rescue_responses
+    def test_rescue_response_hook_presence
+      assert_equal :not_found,
+        DevelopersController.rescue_responses['WillPaginate::InvalidPage']
+    end
+  end
+  
+protected
+
+  def validate_page_numbers expected, links, param_name = :page
+    param_pattern = /\W#{param_name}=([^&]*)/
+    
+    assert_equal(expected, links.map { |e|
+      e['href'] =~ param_pattern
+      $1 ? $1.to_i : $1
+    })
+  end
+
+  def assert_links_match pattern
+    assert_select 'div.pagination a[href]' do |elements|
+      elements.each do |el|
+        assert_match pattern, el['href']
+      end
+    end
+  end
+
+  def assert_no_links_match pattern
+    assert_select 'div.pagination a[href]' do |elements|
+      elements.each do |el|
+        assert_no_match pattern, el['href']
+      end
+    end
+  end
+end
+
+class ViewHelpersTest < Test::Unit::TestCase
+  include WillPaginate::ViewHelpers
+
+  def test_page_entries_info
+    arr = ('a'..'z').to_a
+    collection = arr.paginate :page => 2, :per_page => 5
+    assert_equal %{Displaying entries <b>6&nbsp;-&nbsp;10</b> of <b>26</b> in total},
+      page_entries_info(collection)
+    
+    collection = arr.paginate :page => 7, :per_page => 4
+    assert_equal %{Displaying entries <b>25&nbsp;-&nbsp;26</b> of <b>26</b> in total},
+      page_entries_info(collection)
+  end
+end

[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]