h4|1aTjw7qX_k~e{TWO7jqcekERN;Jyh%67)q4rKpL*CEYL;|#GY{B@5
zi52XoC?xsoorJKxsliugF#z38MJqrYCWV(t<=G&f;^Me13&AiI9{3jUZ$
zFM`*L(9qc^VMxkz1oaDH!1pcD^IXp>Z0Jb=_qs?Vsrs{mp<^{$N!EC9o+`CO-(o}E
zJ`y{*;9s|wr22-QoJ87y^~;)Q@b%P4UgSSsx>2$o@Vd{%Pk0@4qZ^fhB(vt$c1TG>
z*{Ad;foraENbld`=MCNm4?9kvlgK~&J>ialpJ7nua
zx0oRzwG5;}Qne)Fg(N3kf?JVmB;}y&5(0+~r*aL$0Zof8fe!AtHWH>A^1Y)@G@GsA
zup`R{Qg?{+MaxTq#2n{6w|)c&yaJ7{U4ngAH5v6I)*;@rEBE*ehIPBwKBQU)YKE8F0lR!Sm?sE4Xk-sj&E$|A-9n
dP56HS1^^A-61FoN)nxzx002ovPDHLkV1kw_Sd9Px
literal 0
HcmV?d00001
diff --git a/public/images/xfn-child.png b/public/images/xfn-child.png
new file mode 100644
index 0000000000000000000000000000000000000000..c7ac882e8e70d8b6b5ad3e81546711b2de01d3ba
GIT binary patch
literal 588
zcmV-S0<-;zP)5EPYy1nt1WLg)Mg
z1_l;>0wWuOPV8)*SQx`n@lhd$4g?Sql}Z^%T1u0&N=o9`#=e(dk)8^`bqcnK6W5A!{4Cxl4{I_KSA*LLMxBW%<(%^VbVJygN
zAH_PM*Ef6Y_Tb$bY_*dx%akak-P8rlUd$!*<+E8<*5TVWEU!Wo5BH>kZcICjpBx$J
zTh|I0`;}n?I;AHS)a-v`_7+$k<4(9aZq%65l<^Mp1t`&7^y-2-jmPV
zyjsF>@;Fm@Vg;E)77QsxqaP@@>j&I<^pSegADY(bMZ8$419kl0SLlaDrcT4$Ip_xp
zDMiE3T32cf@AmhmZ+d~EkQ+@v!U)uXrEF}snK@r%brUX}f?C51wAPg)FZ-xCeA(i-
a{@Nc%aL}HNs8S#R0000y{q`QY`6?zK#qG*KS<#k1zuAB}-f*
zN`mv#O3D+9QW+dm@{>{(JaZG%Q-e|yQz{EjrrH1%B?S0{xE{UoZr{1r_nv>%NIY=%
z_Qy;2K1H{kfB*THTKs;Ktiua;J)W@s-tRyEZa)6<>FaN&lH;#F{D|$i5Y~L||NsB{
z&cE4v?)AoF&jaetJb(LR*V$KVk365d?a{p#Un}R_xcTJkv17+hrfvNVv`e8R$S)YE
z1s`CDiIIK_RJ+2{#WAGfR?dV-zGed+m$@^JzVSYGLG)eyX)6V;pxON!zZ!MTUAQ&y
zM&Yt^j9kJ$rcMk0VS7(Fsn+1Lt?U2244FF39O*l~_3q@}`4ibUdH3JBj_YcKKIi|*
z{dekc^z63I!v}Bs?V7h@+cagqUFUUoCV83k?|oA&cx%&R!=6b~J*!rqx8+WonQ5k*
zbzx^e<1_W{zlsd)`y>B7-(Y5Fy5i}wT`#BIc5h&0xOrc=vXg`VB+$(ap00i_>zopr
E07Bv8X#fBK
literal 0
HcmV?d00001
diff --git a/public/images/xfn-colleague.png b/public/images/xfn-colleague.png
new file mode 100644
index 0000000000000000000000000000000000000000..d79e5256cc43a27c615a06bd95a05cfa1edf70ae
GIT binary patch
literal 413
zcmeAS@N?(olHy`uVBq!ia0vp^LO{&N!3-otS@yI7DVB6cUq=RpYd5a=M;HP5k|nMY
zCBgY=CFO}lsSJ)O`AMk?p1FzXsX?iUDV2pMQ*D5X5(0ceT<<>rdhYheV^`m)CG5ZX
z_{+P`KPRrguaS7*)rX%ZS%+iV&M(;c=+oEV7w>)wZ9ez^|NnjG-|Rd8dhfZ{`_8@o
z{pVk7$NAWf3r;1+4_$h@<;2Um+aEoD`=fHs&49Wy{&lC19Xpo&@0KLcE`^dHzhIzt
ze1IV)M*1yK?Q~BU$B>F!IRVjp%?3Q#o{D>3FEO~W`TPE-QVV^3X4T6?mOVN0bb)}V
zM#GW|6JiTKvb#2XoI0a)(zP@T!(9f&@vFB-#u!D-%v}4BUz|_8HnxgI-sOr#;qutW
zZqd8LPkT!3cz9~@Kaa;7F3P08Vz<9LeUo=`xYV9i>cXYnZT{dG2%YT_E
k%siX|n*0tY@)_9}o^0e*b>G1_3+O}!Pgg&ebxsLQ05S^AJOBUy
literal 0
HcmV?d00001
diff --git a/public/images/xfn-friend-met.png b/public/images/xfn-friend-met.png
new file mode 100644
index 0000000000000000000000000000000000000000..477f75178a3baaefa6ce105cfd47a8727129db68
GIT binary patch
literal 435
zcmeAS@N?(olHy`uVBq!ia0vp^;y}#D!3-on>y{q`QY`6?zK#qG*KS<#k1zuAB}-f*
zN`mv#O3D+9QW+dm@{>{(JaZG%Q-e|yQz{EjrrH1%B?S0{xW=@f*GN3@;@!_9SKgUq
z9$LBo+0Dmae*ONtaMxq?`2APze{w20{{R2~edk~AJ@&gSq_Fs6t
z@!0eG&%b^8`g`4x=TkS{-*V#Rt~0OJ9)7mz_>2ALUsuk#aqQSJN2Sjo2Pu>U`2_>b
z!3P*(Vx->!)von)aSW-rl{2B2uUUboC0b;wSLVf)zrX*zUlckgdXuAvzWkFak!e~+
z$4?}+N8XrnVu!=jL+`zmKSZTF?Gf8+t;dq0>)`R=*KzUdbCoU5v-evc@8y@AccCKY
zsxasIwfVl=9`?1LzmoVoZ~85jo38~dJ|Fuf)_ng{z_;lJrgLu!I4*WwWpC+ilF>Wy
z)UNr)>A$agZR-`;Q~&&reLIWy(lt6+$=z#K9nvz?)n{a5XmaFpW7k}M4CrtMPgg&e
IbxsLQ06e+iK>z>%
literal 0
HcmV?d00001
diff --git a/public/images/xfn-friend.png b/public/images/xfn-friend.png
new file mode 100644
index 0000000000000000000000000000000000000000..60563f678e6bcd8378de50605cdfeb747ce7feec
GIT binary patch
literal 413
zcmeAS@N?(olHy`uVBq!ia0vp^LO{&N!3-otS@yI7DVB6cUq=RpYd5a=M;HP5k|nMY
zCBgY=CFO}lsSJ)O`AMk?p1FzXsX?iUDV2pMQ*D5X5(0ceT<<>n8r^pO#rvNci3g5c
zez))Z8{>>aHy?jdi{HO;|Fd7e|9<-Vd;X3`SMGoM|NsBK^RM@wd%f@6>y-zd?Z5EG
zspR_niG+{9|rbuIIQ=X9>(SK3kbb7`fCXDaie$!F8gZro|c;8^GWvYF$7Lr%m~rFFO8
zJl8kT+i>Hn<(6;N4_R1Z&wO)uyoF=pKjE^}3G$X-xKg+l)ty+q!=Rz5toi$+qG$OF
mU!^>3IdMiqD7Kc{fMHi7kLCmOt<7uaS6Q-}yKD&c9Yq
z*uVGO>zj|i>^t{bBjG@7$N5iRf5&xRxcmHzdcuKw&%ZjAo>;i+@ux4pO|y?o*>Hc)
z+1Ih17fxP#ck{{DirF`uN{+8P^4uir$lAltjvYJpkgZ%4Xp3A)kY6y+7Tkd0jf6}b
zP-U~Hi(^Q|t(g8uJ{Cm|7tMgEx24~%{*EsWc-+qHsi@!T85ky*_D$zecZOPQ#)`L&
zG4lBg*N@FuaxO!o>`F{=!|eYPcINT=K6zck_TbV437x5Bd;A_hOl<$&cV=pz?CQf(
z`&aHh$5(K0W}Vfhr6m)8?afPG;Hs(Jz4YnH>Wxpteybl~IC_-1pFhsZ4(J>PPgg&e
IbxsLQ09&QMMgRZ+
literal 0
HcmV?d00001
diff --git a/public/images/xfn-parent.png b/public/images/xfn-parent.png
new file mode 100644
index 0000000000000000000000000000000000000000..a7137af51eaf4391024d79b23cc45c576aa66eb9
GIT binary patch
literal 605
zcmV-j0;2tiP)aiF_KW>R<*_ZhR1TFPXe&%!`tQ`<%v+t*_MdRB*IYh>695$
zaq-G??mcZI0vFCq<2vpj=KYK-PNz)5Q1b>5>xn8neD!tUR~J`fz@2uSXbWo$_e2CH
z_oWB-yqk>0_GgC?j3xp&IUSX9e99U4rU9uP|3G9VqvucGBSpOcUas_qF>DFw{|kV6
ztIO3JAGm%$8qKwvOB_8kLFvFa&MuqB@8H=-D2~I4gArElF0@ulg-rg37jX9S0^c@z
zBc-RT1m|auF_w;MD^G%x>hgMJqmfY6x!Rg{uHNoirNS7hN@o*-0CY=j{}A*%oJ>UY
rtEbe~JYQAkwzVR|HflxWe=Y5Ayj9d`&_!(500000NkvXXu0mjfi~6qxv++4$e0TL0U{7UI-%DPYA*f_w=-|wy!~N!XWnkOp@EK)!WjhsfD#siHkR^f
zDU_iQY2Li<(Ih2#0>;7>07cbrkO8UbaA{H2UPE64fbwYHeWL`azF>ftjfUCN{;h2-Aj
z{lem?W`!c2dCibd=D-halW9?HisU9}5?3jANVR?O9}*o}Kl58+$63+dN^0M^NTyun
z#3j}wC2#o0*cOHGW(C1=V|=?JmHTJah*Qa+V-fFUMxRPvuRgU+nZlBf>s0Dm(n#;Q
z7EljO%yk^wkt?)L)zzhuVQ#*xT|
zao@=mZd@y@rKJUhLTPAdXliQSxpN1NMr&zlX=`ii=;&ZD7%Uch_wHR?U0pprJ$-$B
z0|SG5_wE@Q8X6fH85}0Re%5
zfk8n*!NI{qA~7T+ghV2RhK7cPg^|hR@bK`6h=|C@ND74#6%`d79UT)B6B`>F7Z(>F
zAD@trkeHa5l$7-5&70)pWGa=Kl9G~|nwplDmY$xTk&%&^nMtG3va+(Wv$Jz@a^Aju
zo12@PmzVeM-Mjq!{DOjl!otF$qN4Zj-xn7bfB5jBq@<*@w6v_Oth~IuqN1X*vhw4{
zk5yGw)z#HCH8r)hwRLrMpFVx6udi=tXlQI~q|@mP2IKSR&rMBDOeT}XVl_86v)OE^
zDXp!o91f?gt*yPiy`!Up%jI@P
z%a@^{q2b}-k&zLBKrlKwIyN>oK0f~S>(_~iiOI>ysi~>y>FJr7nc3Odxw*Od`FWvG
zxUjIWxVX5qw6wguyt1;gy1Kfywzj^$zOk{fxw*NuwY9yyEfR@#c6P*K@$T;K-rnB+
z{{F$i!QtWI(b19g%&0%F^Q3=+ax*qCk;?xQfaz0?20+FHi`Fpp9JZ;j$J=SjaVa7o
z)6C)Ew?ZarPVl}D)4N1&=Z~LXJU*VrV>ZajwepV;zyV#PMDY@*@9e@Atp%+eRht|N(
ztHH$#BnE--l{FqOxyeD60a+EqPnYZ9wd#=Te=E8KBUxuHL!CV&(m{}D0tAWx?bqzx
T3td{aq*ei}mI1m%(<=BsHBQwB
literal 0
HcmV?d00001
diff --git a/public/images/xfn-spouse.png b/public/images/xfn-spouse.png
new file mode 100644
index 0000000000000000000000000000000000000000..484bb59db8dcdb20c7670328f2e7213cc04f4143
GIT binary patch
literal 853
zcmV-b1FHOqP)hLgzyG_PmY#<)|ak2;=hDz~J5OydGkqyzCARb0x+DTLA1{1c$h(b%7w3>FdH7O?A
zG|w4}DF&5NhdocW@IR)t`d37TML2t)D^gJLM0SH#
z>ak<#NK@3Y9LtA7{U<~aO0FTZSaM6Qm-9s0wNeb)mX7qa)LF5xO>HQEmNJ={CrEXf
z^&vm&Lw+ztGBZ!mQld5#AQraiX{obpOGlJqP~A)B7Irj-qMJ9`q|=l9_|s{&G|thu
z)n+zR;{I=nSX9&czDGlY&CK))&z?4L;`sOM+}XzbQkluQ;)3=(i3V*S>GTvgZyn*&
z&k8IqI3yF>Nu~U>?ke&1x9hm`OOa$^JFj1@GTvq+lS{#jCdj|h2>Q_
zcmA;aI{L9(x^zyAF(M-J!{vkWcXGcBTx^jOC-;hoh%rWnh6d!1Kle$0|L0OzUX^$v
zD;mHULpUQVLV`69|B*nl&8&0)ZNGFE-HCZ5STD
z1;D0FQBqTWEbBRKyIzr+@)3=02H^JXpXu%u@-ORgg26R-Jdu?>J8Bt!{D9f#XDAk{
zOpd=tEY``y#63RvD2v|*BX`2|_IyGvmt=R>G)uWvI@*V5ZU2zOWC6d{YH_7(qJ4e)
zSczX?V*ES@56x0vzernqg}WmzFS2Eh9&-qXN0^^?85^x~VE+JpeS4W-Dxaw`rIn&mae4G8!Sr;BrlxIlc6I}hO#VYA^OWZ1cj@TZgJs!xBK$RxcT29h
f{?-yvZ!6k25EN)n8Q*?p00000NkvXXu0mjfO4o@+
literal 0
HcmV?d00001
diff --git a/public/images/xfn-sweetheart-met.png b/public/images/xfn-sweetheart-met.png
new file mode 100644
index 0000000000000000000000000000000000000000..6982fa467d883e5f70f0599014a0540aa740d6b0
GIT binary patch
literal 402
zcmeAS@N?(olHy`uVBq!ia0vp^;y}#D!3-on>y{q`QY`6?zK#qG*KS<#k1zuAB}-f*
zN`mv#O3D+9QW+dm@{>{(JaZG%Q-e|yQz{EjrrH1%B?S0{xc;g-y7$~`r;_8UvHSiW
zc^1`r?!mLK8VLt}IIa8MaPn8-p&O6B{Qvap)7Re%cRl{$xB377|NG9r*?8>v-wUsQ
zMs7FBI`U`7qfN(OY&-d~V$O{}3$J&sxV3r4<+X>O{hojI*s){R%i>o6?NTTS@(Tv)
z#RnK-Vx->!)%JM0IEGZ*>Y4DG@2~=g+eN>GtJ^lctNZ--{-zy%e=q9X^yGQ+!k
zol<!)i!v#IEGZ*D(Sz;*KEMU!fR+YLAYxE&;R@1NQG>j+9jO5_~Mhqm04br
z*AtDMv@^x)mj!!GD^I)k-RYa3c2?sp&o1{<29@j**iQU>^`M0;#gzfx@Mbbd4zRTJQEMYjn&*r#{#~&0^P#k
M>FVdQ&MBb@01eN@qW}N^
literal 0
HcmV?d00001
diff --git a/public/javascripts/application.js b/public/javascripts/application.js
new file mode 100644
index 0000000..145b683
--- /dev/null
+++ b/public/javascripts/application.js
@@ -0,0 +1,10 @@
+// Place your application-specific JavaScript functions and classes here
+// This file is automatically included by javascript_include_tag :defaults
+
+
+function updateLocation(point) {
+ document.getElementById('photo_geo_lat').value = point.y;
+ document.getElementById('photo_geo_long').value = point.x;
+ map.clearOverlays();
+ map.addOverlay(new GMarker(new GLatLng(point.y, point.x)));
+}
\ No newline at end of file
diff --git a/public/javascripts/clusterer.js b/public/javascripts/clusterer.js
new file mode 100644
index 0000000..eeb6dd9
--- /dev/null
+++ b/public/javascripts/clusterer.js
@@ -0,0 +1,444 @@
+// Clusterer.js - marker clustering routines for Google Maps apps
+//
+// The original version of this code is available at:
+// http://www.acme.com/javascript/
+//
+// Copyright © 2005,2006 by Jef Poskanzer .
+// All rights reserved.
+//
+// Modified for inclusion into the YM4R library in accordance with the
+// following license:
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions
+// are met:
+// 1. Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+// SUCH DAMAGE.
+//
+// For commentary on this license please see http://www.acme.com/license.html
+
+
+// Constructor.
+Clusterer = function(markers,icon,maxVisibleMarkers,gridSize,minMarkersPerCluster,maxLinesPerInfoBox) {
+ this.markers = [];
+ if(markers){
+ for(var i =0 ; i< markers.length ; i++){
+ this.addMarker(markers[i]);
+ }
+ }
+ this.clusters = [];
+ this.timeout = null;
+
+ this.maxVisibleMarkers = maxVisibleMarkers || 150;
+ this.gridSize = gridSize || 5;
+ this.minMarkersPerCluster = minMarkersPerCluster || 5;
+ this.maxLinesPerInfoBox = maxLinesPerInfoBox || 10;
+
+ this.icon = icon || G_DEFAULT_ICON;
+}
+
+Clusterer.prototype = new GOverlay();
+
+Clusterer.prototype.initialize = function ( map ){
+ this.map = map;
+ this.currentZoomLevel = map.getZoom();
+
+ GEvent.addListener( map, 'zoomend', Clusterer.makeCaller( Clusterer.display, this ) );
+ GEvent.addListener( map, 'moveend', Clusterer.makeCaller( Clusterer.display, this ) );
+ GEvent.addListener( map, 'infowindowclose', Clusterer.makeCaller( Clusterer.popDown, this ) );
+ //Set map for each marker
+ for(var i = 0,len = this.markers.length ; i < len ; i++){
+ this.markers[i].setMap( map );
+ }
+ this.displayLater();
+}
+
+Clusterer.prototype.remove = function(){
+ for ( var i = 0; i < this.markers.length; ++i ){
+ this.removeMarker(this.markers[i]);
+ }
+}
+
+Clusterer.prototype.copy = function(){
+ return new Clusterer(this.markers,this.icon,this.maxVisibleMarkers,this.gridSize,this.minMarkersPerCluster,this.maxLinesPerInfoBox);
+}
+
+Clusterer.prototype.redraw = function(force){
+ this.displayLater();
+}
+
+// Call this to change the cluster icon.
+Clusterer.prototype.setIcon = function ( icon ){
+ this.icon = icon;
+}
+
+// Call this to add a marker.
+Clusterer.prototype.addMarker = function ( marker, description){
+ marker.onMap = false;
+ this.markers.push( marker );
+ marker.description = marker.description || description;
+ if(this.map != null){
+ marker.setMap(this.map);
+ this.displayLater();
+ }
+};
+
+
+// Call this to remove a marker.
+Clusterer.prototype.removeMarker = function ( marker ){
+ for ( var i = 0; i < this.markers.length; ++i )
+ if ( this.markers[i] == marker ){
+ if ( marker.onMap )
+ this.map.removeOverlay( marker );
+ for ( var j = 0; j < this.clusters.length; ++j ){
+ var cluster = this.clusters[j];
+ if ( cluster != null ){
+ for ( var k = 0; k < cluster.markers.length; ++k )
+ if ( cluster.markers[k] == marker ){
+ cluster.markers[k] = null;
+ --cluster.markerCount;
+ break;
+ }
+ if ( cluster.markerCount == 0 ){
+ this.clearCluster( cluster );
+ this.clusters[j] = null;
+ }
+ else if ( cluster == this.poppedUpCluster )
+ Clusterer.rePop( this );
+ }
+ }
+ this.markers[i] = null;
+ break;
+ }
+ this.displayLater();
+};
+
+Clusterer.prototype.displayLater = function (){
+ if ( this.timeout != null )
+ clearTimeout( this.timeout );
+ this.timeout = setTimeout( Clusterer.makeCaller( Clusterer.display, this ), 50 );
+};
+
+Clusterer.display = function ( clusterer ){
+ var i, j, marker, cluster, len, len2;
+
+ clearTimeout( clusterer.timeout );
+
+ var newZoomLevel = clusterer.map.getZoom();
+ if ( newZoomLevel != clusterer.currentZoomLevel ){
+ // When the zoom level changes, we have to remove all the clusters.
+ for ( i = 0 , len = clusterer.clusters.length; i < len; ++i ){
+ if ( clusterer.clusters[i] != null ){
+ clusterer.clearCluster( clusterer.clusters[i] );
+ clusterer.clusters[i] = null;
+ }
+ }
+ clusterer.clusters.length = 0;
+ clusterer.currentZoomLevel = newZoomLevel;
+ }
+
+ // Get the current bounds of the visible area.
+ var bounds = clusterer.map.getBounds();
+
+ // Expand the bounds a little, so things look smoother when scrolling
+ // by small amounts.
+ var sw = bounds.getSouthWest();
+ var ne = bounds.getNorthEast();
+ var dx = ne.lng() - sw.lng();
+ var dy = ne.lat() - sw.lat();
+ dx *= 0.10;
+ dy *= 0.10;
+ bounds = new GLatLngBounds(
+ new GLatLng( sw.lat() - dy, sw.lng() - dx ),
+ new GLatLng( ne.lat() + dy, ne.lng() + dx )
+ );
+
+ // Partition the markers into visible and non-visible lists.
+ var visibleMarkers = [];
+ var nonvisibleMarkers = [];
+ for ( i = 0, len = clusterer.markers.length ; i < len; ++i ){
+ marker = clusterer.markers[i];
+ if ( marker != null )
+ if ( bounds.contains( marker.getPoint() ) )
+ visibleMarkers.push( marker );
+ else
+ nonvisibleMarkers.push( marker );
+ }
+
+ // Take down the non-visible markers.
+ for ( i = 0, len = nonvisibleMarkers.length ; i < len; ++i ){
+ marker = nonvisibleMarkers[i];
+ if ( marker.onMap ){
+ clusterer.map.removeOverlay( marker );
+ marker.onMap = false;
+ }
+ }
+
+ // Take down the non-visible clusters.
+ for ( i = 0, len = clusterer.clusters.length ; i < len ; ++i ){
+ cluster = clusterer.clusters[i];
+ if ( cluster != null && ! bounds.contains( cluster.marker.getPoint() ) && cluster.onMap ){
+ clusterer.map.removeOverlay( cluster.marker );
+ cluster.onMap = false;
+ }
+ }
+
+ // Clustering! This is some complicated stuff. We have three goals
+ // here. One, limit the number of markers & clusters displayed, so the
+ // maps code doesn't slow to a crawl. Two, when possible keep existing
+ // clusters instead of replacing them with new ones, so that the app pans
+ // better. And three, of course, be CPU and memory efficient.
+ if ( visibleMarkers.length > clusterer.maxVisibleMarkers ){
+ // Add to the list of clusters by splitting up the current bounds
+ // into a grid.
+ var latRange = bounds.getNorthEast().lat() - bounds.getSouthWest().lat();
+ var latInc = latRange / clusterer.gridSize;
+ var lngInc = latInc / Math.cos( ( bounds.getNorthEast().lat() + bounds.getSouthWest().lat() ) / 2.0 * Math.PI / 180.0 );
+ for ( var lat = bounds.getSouthWest().lat(); lat <= bounds.getNorthEast().lat(); lat += latInc )
+ for ( var lng = bounds.getSouthWest().lng(); lng <= bounds.getNorthEast().lng(); lng += lngInc ){
+ cluster = new Object();
+ cluster.clusterer = clusterer;
+ cluster.bounds = new GLatLngBounds( new GLatLng( lat, lng ), new GLatLng( lat + latInc, lng + lngInc ) );
+ cluster.markers = [];
+ cluster.markerCount = 0;
+ cluster.onMap = false;
+ cluster.marker = null;
+ clusterer.clusters.push( cluster );
+ }
+
+ // Put all the unclustered visible markers into a cluster - the first
+ // one it fits in, which favors pre-existing clusters.
+ for ( i = 0, len = visibleMarkers.length ; i < len; ++i ){
+ marker = visibleMarkers[i];
+ if ( marker != null && ! marker.inCluster ){
+ for ( j = 0, len2 = clusterer.clusters.length ; j < len2 ; ++j ){
+ cluster = clusterer.clusters[j];
+ if ( cluster != null && cluster.bounds.contains( marker.getPoint() ) ){
+ cluster.markers.push( marker );
+ ++cluster.markerCount;
+ marker.inCluster = true;
+ }
+ }
+ }
+ }
+
+ // Get rid of any clusters containing only a few markers.
+ for ( i = 0, len = clusterer.clusters.length ; i < len ; ++i )
+ if ( clusterer.clusters[i] != null && clusterer.clusters[i].markerCount < clusterer.minMarkersPerCluster ){
+ clusterer.clearCluster( clusterer.clusters[i] );
+ clusterer.clusters[i] = null;
+ }
+
+ // Shrink the clusters list.
+ for ( i = clusterer.clusters.length - 1; i >= 0; --i )
+ if ( clusterer.clusters[i] != null )
+ break;
+ else
+ --clusterer.clusters.length;
+
+ // Ok, we have our clusters. Go through the markers in each
+ // cluster and remove them from the map if they are currently up.
+ for ( i = 0, len = clusterer.clusters.length ; i < len; ++i ){
+ cluster = clusterer.clusters[i];
+ if ( cluster != null ){
+ for ( j = 0 , len2 = cluster.markers.length ; j < len2; ++j ){
+ marker = cluster.markers[j];
+ if ( marker != null && marker.onMap ){
+ clusterer.map.removeOverlay( marker );
+ marker.onMap = false;
+ }
+ }
+ }
+ }
+
+ // Now make cluster-markers for any clusters that need one.
+ for ( i = 0, len = clusterer.clusters.length; i < len; ++i ){
+ cluster = clusterer.clusters[i];
+ if ( cluster != null && cluster.marker == null ){
+ // Figure out the average coordinates of the markers in this
+ // cluster.
+ var xTotal = 0.0, yTotal = 0.0;
+ for ( j = 0, len2 = cluster.markers.length; j < len2 ; ++j ){
+ marker = cluster.markers[j];
+ if ( marker != null ){
+ xTotal += ( + marker.getPoint().lng() );
+ yTotal += ( + marker.getPoint().lat() );
+ }
+ }
+ var location = new GLatLng( yTotal / cluster.markerCount, xTotal / cluster.markerCount );
+ marker = new GMarker( location, { icon: clusterer.icon } );
+ cluster.marker = marker;
+ GEvent.addListener( marker, 'click', Clusterer.makeCaller( Clusterer.popUp, cluster ) );
+ }
+ }
+ }
+
+ // Display the visible markers not already up and not in clusters.
+ for ( i = 0, len = visibleMarkers.length; i < len; ++i ){
+ marker = visibleMarkers[i];
+ if ( marker != null && ! marker.onMap && ! marker.inCluster )
+ {
+ clusterer.map.addOverlay( marker );
+ marker.addedToMap();
+ marker.onMap = true;
+ }
+ }
+
+ // Display the visible clusters not already up.
+ for ( i = 0, len = clusterer.clusters.length ; i < len; ++i ){
+ cluster = clusterer.clusters[i];
+ if ( cluster != null && ! cluster.onMap && bounds.contains( cluster.marker.getPoint() )){
+ clusterer.map.addOverlay( cluster.marker );
+ cluster.onMap = true;
+ }
+ }
+
+ // In case a cluster is currently popped-up, re-pop to get any new
+ // markers into the infobox.
+ Clusterer.rePop( clusterer );
+};
+
+
+Clusterer.popUp = function ( cluster ){
+ var clusterer = cluster.clusterer;
+ var html = '';
+ var n = 0;
+ for ( var i = 0 , len = cluster.markers.length; i < len; ++i )
+ {
+ var marker = cluster.markers[i];
+ if ( marker != null )
+ {
+ ++n;
+ html += '';
+ if ( marker.getIcon().smallImage != null )
+ html += '';
+ else
+ html += '';
+ html += ' | ' + marker.description + ' |
';
+ if ( n == clusterer.maxLinesPerInfoBox - 1 && cluster.markerCount > clusterer.maxLinesPerInfoBox )
+ {
+ html += '...and ' + ( cluster.markerCount - n ) + ' more |
';
+ break;
+ }
+ }
+ }
+ html += '
';
+ clusterer.map.closeInfoWindow();
+ cluster.marker.openInfoWindowHtml( html );
+ clusterer.poppedUpCluster = cluster;
+};
+
+Clusterer.rePop = function ( clusterer ){
+ if ( clusterer.poppedUpCluster != null )
+ Clusterer.popUp( clusterer.poppedUpCluster );
+};
+
+Clusterer.popDown = function ( clusterer ){
+ clusterer.poppedUpCluster = null;
+};
+
+Clusterer.prototype.clearCluster = function ( cluster ){
+ var i, marker;
+
+ for ( i = 0; i < cluster.markers.length; ++i ){
+ if ( cluster.markers[i] != null ){
+ cluster.markers[i].inCluster = false;
+ cluster.markers[i] = null;
+ }
+ }
+
+ cluster.markers.length = 0;
+ cluster.markerCount = 0;
+
+ if ( cluster == this.poppedUpCluster )
+ this.map.closeInfoWindow();
+
+ if ( cluster.onMap )
+ {
+ this.map.removeOverlay( cluster.marker );
+ cluster.onMap = false;
+ }
+};
+
+// This returns a function closure that calls the given routine with the
+// specified arg.
+Clusterer.makeCaller = function ( func, arg ){
+ return function () { func( arg ); };
+};
+
+
+// Augment GMarker so it handles markers that have been created but
+// not yet addOverlayed.
+GMarker.prototype.setMap = function ( map ){
+ this.map = map;
+};
+
+GMarker.prototype.getMap = function (){
+ return this.map;
+}
+
+GMarker.prototype.addedToMap = function (){
+ this.map = null;
+};
+
+
+GMarker.prototype.origOpenInfoWindow = GMarker.prototype.openInfoWindow;
+GMarker.prototype.openInfoWindow = function ( node, opts ){
+ if ( this.map != null )
+ return this.map.openInfoWindow( this.getPoint(), node, opts );
+ else
+ return this.origOpenInfoWindow( node, opts );
+};
+
+GMarker.prototype.origOpenInfoWindowHtml = GMarker.prototype.openInfoWindowHtml;
+GMarker.prototype.openInfoWindowHtml = function ( html, opts ){
+ if ( this.map != null )
+ return this.map.openInfoWindowHtml( this.getPoint(), html, opts );
+ else
+ return this.origOpenInfoWindowHtml( html, opts );
+};
+
+GMarker.prototype.origOpenInfoWindowTabs = GMarker.prototype.openInfoWindowTabs;
+GMarker.prototype.openInfoWindowTabs = function ( tabNodes, opts ){
+ if ( this.map != null )
+ return this.map.openInfoWindowTabs( this.getPoint(), tabNodes, opts );
+ else
+ return this.origOpenInfoWindowTabs( tabNodes, opts );
+};
+
+GMarker.prototype.origOpenInfoWindowTabsHtml = GMarker.prototype.openInfoWindowTabsHtml;
+GMarker.prototype.openInfoWindowTabsHtml = function ( tabHtmls, opts ){
+ if ( this.map != null )
+ return this.map.openInfoWindowTabsHtml( this.getPoint(), tabHtmls, opts );
+ else
+ return this.origOpenInfoWindowTabsHtml( tabHtmls, opts );
+};
+
+GMarker.prototype.origShowMapBlowup = GMarker.prototype.showMapBlowup;
+GMarker.prototype.showMapBlowup = function ( opts ){
+ if ( this.map != null )
+ return this.map.showMapBlowup( this.getPoint(), opts );
+ else
+ return this.origShowMapBlowup( opts );
+};
+
+
+function addDescriptionToMarker(marker, description){
+ marker.description = description;
+ return marker;
+}
diff --git a/public/javascripts/controls.js b/public/javascripts/controls.js
new file mode 100644
index 0000000..8c273f8
--- /dev/null
+++ b/public/javascripts/controls.js
@@ -0,0 +1,833 @@
+// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005, 2006 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+// (c) 2005, 2006 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+// Richard Livsey
+// Rahul Bhargava
+// Rob Wills
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+// Autocompleter.Base handles all the autocompletion functionality
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least,
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method
+// should get the text for which to provide autocompletion by
+// invoking this.getToken(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: [',', '\n'] } which
+// enables autocompletion on multiple tokens. This is most
+// useful when one of the tokens is \n (a newline), as it
+// allows smart autocompletion after linebreaks.
+
+if(typeof Effect == 'undefined')
+ throw("controls.js requires including script.aculo.us' effects.js library");
+
+var Autocompleter = {}
+Autocompleter.Base = function() {};
+Autocompleter.Base.prototype = {
+ baseInitialize: function(element, update, options) {
+ this.element = $(element);
+ this.update = $(update);
+ this.hasFocus = false;
+ this.changed = false;
+ this.active = false;
+ this.index = 0;
+ this.entryCount = 0;
+
+ if(this.setOptions)
+ this.setOptions(options);
+ else
+ this.options = options || {};
+
+ this.options.paramName = this.options.paramName || this.element.name;
+ this.options.tokens = this.options.tokens || [];
+ this.options.frequency = this.options.frequency || 0.4;
+ this.options.minChars = this.options.minChars || 1;
+ this.options.onShow = this.options.onShow ||
+ function(element, update){
+ if(!update.style.position || update.style.position=='absolute') {
+ update.style.position = 'absolute';
+ Position.clone(element, update, {
+ setHeight: false,
+ offsetTop: element.offsetHeight
+ });
+ }
+ Effect.Appear(update,{duration:0.15});
+ };
+ this.options.onHide = this.options.onHide ||
+ function(element, update){ new Effect.Fade(update,{duration:0.15}) };
+
+ if(typeof(this.options.tokens) == 'string')
+ this.options.tokens = new Array(this.options.tokens);
+
+ this.observer = null;
+
+ this.element.setAttribute('autocomplete','off');
+
+ Element.hide(this.update);
+
+ Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
+ Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
+ },
+
+ show: function() {
+ if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+ if(!this.iefix &&
+ (navigator.appVersion.indexOf('MSIE')>0) &&
+ (navigator.userAgent.indexOf('Opera')<0) &&
+ (Element.getStyle(this.update, 'position')=='absolute')) {
+ new Insertion.After(this.update,
+ '');
+ this.iefix = $(this.update.id+'_iefix');
+ }
+ if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+ },
+
+ fixIEOverlapping: function() {
+ Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
+ this.iefix.style.zIndex = 1;
+ this.update.style.zIndex = 2;
+ Element.show(this.iefix);
+ },
+
+ hide: function() {
+ this.stopIndicator();
+ if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
+ if(this.iefix) Element.hide(this.iefix);
+ },
+
+ startIndicator: function() {
+ if(this.options.indicator) Element.show(this.options.indicator);
+ },
+
+ stopIndicator: function() {
+ if(this.options.indicator) Element.hide(this.options.indicator);
+ },
+
+ onKeyPress: function(event) {
+ if(this.active)
+ switch(event.keyCode) {
+ case Event.KEY_TAB:
+ case Event.KEY_RETURN:
+ this.selectEntry();
+ Event.stop(event);
+ case Event.KEY_ESC:
+ this.hide();
+ this.active = false;
+ Event.stop(event);
+ return;
+ case Event.KEY_LEFT:
+ case Event.KEY_RIGHT:
+ return;
+ case Event.KEY_UP:
+ this.markPrevious();
+ this.render();
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+ return;
+ case Event.KEY_DOWN:
+ this.markNext();
+ this.render();
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+ return;
+ }
+ else
+ if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
+ (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return;
+
+ this.changed = true;
+ this.hasFocus = true;
+
+ if(this.observer) clearTimeout(this.observer);
+ this.observer =
+ setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+ },
+
+ activate: function() {
+ this.changed = false;
+ this.hasFocus = true;
+ this.getUpdatedChoices();
+ },
+
+ onHover: function(event) {
+ var element = Event.findElement(event, 'LI');
+ if(this.index != element.autocompleteIndex)
+ {
+ this.index = element.autocompleteIndex;
+ this.render();
+ }
+ Event.stop(event);
+ },
+
+ onClick: function(event) {
+ var element = Event.findElement(event, 'LI');
+ this.index = element.autocompleteIndex;
+ this.selectEntry();
+ this.hide();
+ },
+
+ onBlur: function(event) {
+ // needed to make click events working
+ setTimeout(this.hide.bind(this), 250);
+ this.hasFocus = false;
+ this.active = false;
+ },
+
+ render: function() {
+ if(this.entryCount > 0) {
+ for (var i = 0; i < this.entryCount; i++)
+ this.index==i ?
+ Element.addClassName(this.getEntry(i),"selected") :
+ Element.removeClassName(this.getEntry(i),"selected");
+
+ if(this.hasFocus) {
+ this.show();
+ this.active = true;
+ }
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ },
+
+ markPrevious: function() {
+ if(this.index > 0) this.index--
+ else this.index = this.entryCount-1;
+ this.getEntry(this.index).scrollIntoView(true);
+ },
+
+ markNext: function() {
+ if(this.index < this.entryCount-1) this.index++
+ else this.index = 0;
+ this.getEntry(this.index).scrollIntoView(false);
+ },
+
+ getEntry: function(index) {
+ return this.update.firstChild.childNodes[index];
+ },
+
+ getCurrentEntry: function() {
+ return this.getEntry(this.index);
+ },
+
+ selectEntry: function() {
+ this.active = false;
+ this.updateElement(this.getCurrentEntry());
+ },
+
+ updateElement: function(selectedElement) {
+ if (this.options.updateElement) {
+ this.options.updateElement(selectedElement);
+ return;
+ }
+ var value = '';
+ if (this.options.select) {
+ var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
+ if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
+ } else
+ value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+
+ var lastTokenPos = this.findLastToken();
+ if (lastTokenPos != -1) {
+ var newValue = this.element.value.substr(0, lastTokenPos + 1);
+ var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
+ if (whitespace)
+ newValue += whitespace[0];
+ this.element.value = newValue + value;
+ } else {
+ this.element.value = value;
+ }
+ this.element.focus();
+
+ if (this.options.afterUpdateElement)
+ this.options.afterUpdateElement(this.element, selectedElement);
+ },
+
+ updateChoices: function(choices) {
+ if(!this.changed && this.hasFocus) {
+ this.update.innerHTML = choices;
+ Element.cleanWhitespace(this.update);
+ Element.cleanWhitespace(this.update.down());
+
+ if(this.update.firstChild && this.update.down().childNodes) {
+ this.entryCount =
+ this.update.down().childNodes.length;
+ for (var i = 0; i < this.entryCount; i++) {
+ var entry = this.getEntry(i);
+ entry.autocompleteIndex = i;
+ this.addObservers(entry);
+ }
+ } else {
+ this.entryCount = 0;
+ }
+
+ this.stopIndicator();
+ this.index = 0;
+
+ if(this.entryCount==1 && this.options.autoSelect) {
+ this.selectEntry();
+ this.hide();
+ } else {
+ this.render();
+ }
+ }
+ },
+
+ addObservers: function(element) {
+ Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+ Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+ },
+
+ onObserverEvent: function() {
+ this.changed = false;
+ if(this.getToken().length>=this.options.minChars) {
+ this.startIndicator();
+ this.getUpdatedChoices();
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ },
+
+ getToken: function() {
+ var tokenPos = this.findLastToken();
+ if (tokenPos != -1)
+ var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
+ else
+ var ret = this.element.value;
+
+ return /\n/.test(ret) ? '' : ret;
+ },
+
+ findLastToken: function() {
+ var lastTokenPos = -1;
+
+ for (var i=0; i lastTokenPos)
+ lastTokenPos = thisTokenPos;
+ }
+ return lastTokenPos;
+ }
+}
+
+Ajax.Autocompleter = Class.create();
+Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
+ initialize: function(element, update, url, options) {
+ this.baseInitialize(element, update, options);
+ this.options.asynchronous = true;
+ this.options.onComplete = this.onComplete.bind(this);
+ this.options.defaultParams = this.options.parameters || null;
+ this.url = url;
+ },
+
+ getUpdatedChoices: function() {
+ entry = encodeURIComponent(this.options.paramName) + '=' +
+ encodeURIComponent(this.getToken());
+
+ this.options.parameters = this.options.callback ?
+ this.options.callback(this.element, entry) : entry;
+
+ if(this.options.defaultParams)
+ this.options.parameters += '&' + this.options.defaultParams;
+
+ new Ajax.Request(this.url, this.options);
+ },
+
+ onComplete: function(request) {
+ this.updateChoices(request.responseText);
+ }
+
+});
+
+// The local array autocompleter. Used when you'd prefer to
+// inject an array of autocompletion options into the page, rather
+// than sending out Ajax queries, which can be quite slow sometimes.
+//
+// The constructor takes four parameters. The first two are, as usual,
+// the id of the monitored textbox, and id of the autocompletion menu.
+// The third is the array you want to autocomplete from, and the fourth
+// is the options block.
+//
+// Extra local autocompletion options:
+// - choices - How many autocompletion choices to offer
+//
+// - partialSearch - If false, the autocompleter will match entered
+// text only at the beginning of strings in the
+// autocomplete array. Defaults to true, which will
+// match text at the beginning of any *word* in the
+// strings in the autocomplete array. If you want to
+// search anywhere in the string, additionally set
+// the option fullSearch to true (default: off).
+//
+// - fullSsearch - Search anywhere in autocomplete array strings.
+//
+// - partialChars - How many characters to enter before triggering
+// a partial match (unlike minChars, which defines
+// how many characters are required to do any match
+// at all). Defaults to 2.
+//
+// - ignoreCase - Whether to ignore case when autocompleting.
+// Defaults to true.
+//
+// It's possible to pass in a custom function as the 'selector'
+// option, if you prefer to write your own autocompletion logic.
+// In that case, the other options above will not apply unless
+// you support them.
+
+Autocompleter.Local = Class.create();
+Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
+ initialize: function(element, update, array, options) {
+ this.baseInitialize(element, update, options);
+ this.options.array = array;
+ },
+
+ getUpdatedChoices: function() {
+ this.updateChoices(this.options.selector(this));
+ },
+
+ setOptions: function(options) {
+ this.options = Object.extend({
+ choices: 10,
+ partialSearch: true,
+ partialChars: 2,
+ ignoreCase: true,
+ fullSearch: false,
+ selector: function(instance) {
+ var ret = []; // Beginning matches
+ var partial = []; // Inside matches
+ var entry = instance.getToken();
+ var count = 0;
+
+ for (var i = 0; i < instance.options.array.length &&
+ ret.length < instance.options.choices ; i++) {
+
+ var elem = instance.options.array[i];
+ var foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase()) :
+ elem.indexOf(entry);
+
+ while (foundPos != -1) {
+ if (foundPos == 0 && elem.length != entry.length) {
+ ret.push("" + elem.substr(0, entry.length) + "" +
+ elem.substr(entry.length) + "");
+ break;
+ } else if (entry.length >= instance.options.partialChars &&
+ instance.options.partialSearch && foundPos != -1) {
+ if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+ partial.push("" + elem.substr(0, foundPos) + "" +
+ elem.substr(foundPos, entry.length) + "" + elem.substr(
+ foundPos + entry.length) + "");
+ break;
+ }
+ }
+
+ foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
+ elem.indexOf(entry, foundPos + 1);
+
+ }
+ }
+ if (partial.length)
+ ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
+ return "";
+ }
+ }, options || {});
+ }
+});
+
+// AJAX in-place editor
+//
+// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
+
+// Use this if you notice weird scrolling problems on some browsers,
+// the DOM might be a bit confused when this gets called so do this
+// waits 1 ms (with setTimeout) until it does the activation
+Field.scrollFreeActivate = function(field) {
+ setTimeout(function() {
+ Field.activate(field);
+ }, 1);
+}
+
+Ajax.InPlaceEditor = Class.create();
+Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
+Ajax.InPlaceEditor.prototype = {
+ initialize: function(element, url, options) {
+ this.url = url;
+ this.element = $(element);
+
+ this.options = Object.extend({
+ paramName: "value",
+ okButton: true,
+ okText: "ok",
+ cancelLink: true,
+ cancelText: "cancel",
+ savingText: "Saving...",
+ clickToEditText: "Click to edit",
+ okText: "ok",
+ rows: 1,
+ onComplete: function(transport, element) {
+ new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
+ },
+ onFailure: function(transport) {
+ alert("Error communicating with the server: " + transport.responseText.stripTags());
+ },
+ callback: function(form) {
+ return Form.serialize(form);
+ },
+ handleLineBreaks: true,
+ loadingText: 'Loading...',
+ savingClassName: 'inplaceeditor-saving',
+ loadingClassName: 'inplaceeditor-loading',
+ formClassName: 'inplaceeditor-form',
+ highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
+ highlightendcolor: "#FFFFFF",
+ externalControl: null,
+ submitOnBlur: false,
+ ajaxOptions: {},
+ evalScripts: false
+ }, options || {});
+
+ if(!this.options.formId && this.element.id) {
+ this.options.formId = this.element.id + "-inplaceeditor";
+ if ($(this.options.formId)) {
+ // there's already a form with that name, don't specify an id
+ this.options.formId = null;
+ }
+ }
+
+ if (this.options.externalControl) {
+ this.options.externalControl = $(this.options.externalControl);
+ }
+
+ this.originalBackground = Element.getStyle(this.element, 'background-color');
+ if (!this.originalBackground) {
+ this.originalBackground = "transparent";
+ }
+
+ this.element.title = this.options.clickToEditText;
+
+ this.onclickListener = this.enterEditMode.bindAsEventListener(this);
+ this.mouseoverListener = this.enterHover.bindAsEventListener(this);
+ this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
+ Event.observe(this.element, 'click', this.onclickListener);
+ Event.observe(this.element, 'mouseover', this.mouseoverListener);
+ Event.observe(this.element, 'mouseout', this.mouseoutListener);
+ if (this.options.externalControl) {
+ Event.observe(this.options.externalControl, 'click', this.onclickListener);
+ Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
+ Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
+ }
+ },
+ enterEditMode: function(evt) {
+ if (this.saving) return;
+ if (this.editing) return;
+ this.editing = true;
+ this.onEnterEditMode();
+ if (this.options.externalControl) {
+ Element.hide(this.options.externalControl);
+ }
+ Element.hide(this.element);
+ this.createForm();
+ this.element.parentNode.insertBefore(this.form, this.element);
+ if (!this.options.loadTextURL) Field.scrollFreeActivate(this.editField);
+ // stop the event to avoid a page refresh in Safari
+ if (evt) {
+ Event.stop(evt);
+ }
+ return false;
+ },
+ createForm: function() {
+ this.form = document.createElement("form");
+ this.form.id = this.options.formId;
+ Element.addClassName(this.form, this.options.formClassName)
+ this.form.onsubmit = this.onSubmit.bind(this);
+
+ this.createEditField();
+
+ if (this.options.textarea) {
+ var br = document.createElement("br");
+ this.form.appendChild(br);
+ }
+
+ if (this.options.okButton) {
+ okButton = document.createElement("input");
+ okButton.type = "submit";
+ okButton.value = this.options.okText;
+ okButton.className = 'editor_ok_button';
+ this.form.appendChild(okButton);
+ }
+
+ if (this.options.cancelLink) {
+ cancelLink = document.createElement("a");
+ cancelLink.href = "#";
+ cancelLink.appendChild(document.createTextNode(this.options.cancelText));
+ cancelLink.onclick = this.onclickCancel.bind(this);
+ cancelLink.className = 'editor_cancel';
+ this.form.appendChild(cancelLink);
+ }
+ },
+ hasHTMLLineBreaks: function(string) {
+ if (!this.options.handleLineBreaks) return false;
+ return string.match(/
/i);
+ },
+ convertHTMLLineBreaks: function(string) {
+ return string.replace(/
/gi, "\n").replace(/
/gi, "\n").replace(/<\/p>/gi, "\n").replace(//gi, "");
+ },
+ createEditField: function() {
+ var text;
+ if(this.options.loadTextURL) {
+ text = this.options.loadingText;
+ } else {
+ text = this.getText();
+ }
+
+ var obj = this;
+
+ if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
+ this.options.textarea = false;
+ var textField = document.createElement("input");
+ textField.obj = this;
+ textField.type = "text";
+ textField.name = this.options.paramName;
+ textField.value = text;
+ textField.style.backgroundColor = this.options.highlightcolor;
+ textField.className = 'editor_field';
+ var size = this.options.size || this.options.cols || 0;
+ if (size != 0) textField.size = size;
+ if (this.options.submitOnBlur)
+ textField.onblur = this.onSubmit.bind(this);
+ this.editField = textField;
+ } else {
+ this.options.textarea = true;
+ var textArea = document.createElement("textarea");
+ textArea.obj = this;
+ textArea.name = this.options.paramName;
+ textArea.value = this.convertHTMLLineBreaks(text);
+ textArea.rows = this.options.rows;
+ textArea.cols = this.options.cols || 40;
+ textArea.className = 'editor_field';
+ if (this.options.submitOnBlur)
+ textArea.onblur = this.onSubmit.bind(this);
+ this.editField = textArea;
+ }
+
+ if(this.options.loadTextURL) {
+ this.loadExternalText();
+ }
+ this.form.appendChild(this.editField);
+ },
+ getText: function() {
+ return this.element.innerHTML;
+ },
+ loadExternalText: function() {
+ Element.addClassName(this.form, this.options.loadingClassName);
+ this.editField.disabled = true;
+ new Ajax.Request(
+ this.options.loadTextURL,
+ Object.extend({
+ asynchronous: true,
+ onComplete: this.onLoadedExternalText.bind(this)
+ }, this.options.ajaxOptions)
+ );
+ },
+ onLoadedExternalText: function(transport) {
+ Element.removeClassName(this.form, this.options.loadingClassName);
+ this.editField.disabled = false;
+ this.editField.value = transport.responseText.stripTags();
+ Field.scrollFreeActivate(this.editField);
+ },
+ onclickCancel: function() {
+ this.onComplete();
+ this.leaveEditMode();
+ return false;
+ },
+ onFailure: function(transport) {
+ this.options.onFailure(transport);
+ if (this.oldInnerHTML) {
+ this.element.innerHTML = this.oldInnerHTML;
+ this.oldInnerHTML = null;
+ }
+ return false;
+ },
+ onSubmit: function() {
+ // onLoading resets these so we need to save them away for the Ajax call
+ var form = this.form;
+ var value = this.editField.value;
+
+ // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
+ // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
+ // to be displayed indefinitely
+ this.onLoading();
+
+ if (this.options.evalScripts) {
+ new Ajax.Request(
+ this.url, Object.extend({
+ parameters: this.options.callback(form, value),
+ onComplete: this.onComplete.bind(this),
+ onFailure: this.onFailure.bind(this),
+ asynchronous:true,
+ evalScripts:true
+ }, this.options.ajaxOptions));
+ } else {
+ new Ajax.Updater(
+ { success: this.element,
+ // don't update on failure (this could be an option)
+ failure: null },
+ this.url, Object.extend({
+ parameters: this.options.callback(form, value),
+ onComplete: this.onComplete.bind(this),
+ onFailure: this.onFailure.bind(this)
+ }, this.options.ajaxOptions));
+ }
+ // stop the event to avoid a page refresh in Safari
+ if (arguments.length > 1) {
+ Event.stop(arguments[0]);
+ }
+ return false;
+ },
+ onLoading: function() {
+ this.saving = true;
+ this.removeForm();
+ this.leaveHover();
+ this.showSaving();
+ },
+ showSaving: function() {
+ this.oldInnerHTML = this.element.innerHTML;
+ this.element.innerHTML = this.options.savingText;
+ Element.addClassName(this.element, this.options.savingClassName);
+ this.element.style.backgroundColor = this.originalBackground;
+ Element.show(this.element);
+ },
+ removeForm: function() {
+ if(this.form) {
+ if (this.form.parentNode) Element.remove(this.form);
+ this.form = null;
+ }
+ },
+ enterHover: function() {
+ if (this.saving) return;
+ this.element.style.backgroundColor = this.options.highlightcolor;
+ if (this.effect) {
+ this.effect.cancel();
+ }
+ Element.addClassName(this.element, this.options.hoverClassName)
+ },
+ leaveHover: function() {
+ if (this.options.backgroundColor) {
+ this.element.style.backgroundColor = this.oldBackground;
+ }
+ Element.removeClassName(this.element, this.options.hoverClassName)
+ if (this.saving) return;
+ this.effect = new Effect.Highlight(this.element, {
+ startcolor: this.options.highlightcolor,
+ endcolor: this.options.highlightendcolor,
+ restorecolor: this.originalBackground
+ });
+ },
+ leaveEditMode: function() {
+ Element.removeClassName(this.element, this.options.savingClassName);
+ this.removeForm();
+ this.leaveHover();
+ this.element.style.backgroundColor = this.originalBackground;
+ Element.show(this.element);
+ if (this.options.externalControl) {
+ Element.show(this.options.externalControl);
+ }
+ this.editing = false;
+ this.saving = false;
+ this.oldInnerHTML = null;
+ this.onLeaveEditMode();
+ },
+ onComplete: function(transport) {
+ this.leaveEditMode();
+ this.options.onComplete.bind(this)(transport, this.element);
+ },
+ onEnterEditMode: function() {},
+ onLeaveEditMode: function() {},
+ dispose: function() {
+ if (this.oldInnerHTML) {
+ this.element.innerHTML = this.oldInnerHTML;
+ }
+ this.leaveEditMode();
+ Event.stopObserving(this.element, 'click', this.onclickListener);
+ Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
+ Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
+ if (this.options.externalControl) {
+ Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
+ Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
+ Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
+ }
+ }
+};
+
+Ajax.InPlaceCollectionEditor = Class.create();
+Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
+Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
+ createEditField: function() {
+ if (!this.cached_selectTag) {
+ var selectTag = document.createElement("select");
+ var collection = this.options.collection || [];
+ var optionTag;
+ collection.each(function(e,i) {
+ optionTag = document.createElement("option");
+ optionTag.value = (e instanceof Array) ? e[0] : e;
+ if((typeof this.options.value == 'undefined') &&
+ ((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true;
+ if(this.options.value==optionTag.value) optionTag.selected = true;
+ optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
+ selectTag.appendChild(optionTag);
+ }.bind(this));
+ this.cached_selectTag = selectTag;
+ }
+
+ this.editField = this.cached_selectTag;
+ if(this.options.loadTextURL) this.loadExternalText();
+ this.form.appendChild(this.editField);
+ this.options.callback = function(form, value) {
+ return "value=" + encodeURIComponent(value);
+ }
+ }
+});
+
+// Delayed observer, like Form.Element.Observer,
+// but waits for delay after last key input
+// Ideal for live-search fields
+
+Form.Element.DelayedObserver = Class.create();
+Form.Element.DelayedObserver.prototype = {
+ initialize: function(element, delay, callback) {
+ this.delay = delay || 0.5;
+ this.element = $(element);
+ this.callback = callback;
+ this.timer = null;
+ this.lastValue = $F(this.element);
+ Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
+ },
+ delayedListener: function(event) {
+ if(this.lastValue == $F(this.element)) return;
+ if(this.timer) clearTimeout(this.timer);
+ this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
+ this.lastValue = $F(this.element);
+ },
+ onTimerEvent: function() {
+ this.timer = null;
+ this.callback(this.element, $F(this.element));
+ }
+};
diff --git a/public/javascripts/dragdrop.js b/public/javascripts/dragdrop.js
new file mode 100644
index 0000000..c71ddb8
--- /dev/null
+++ b/public/javascripts/dragdrop.js
@@ -0,0 +1,942 @@
+// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005, 2006 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+if(typeof Effect == 'undefined')
+ throw("dragdrop.js requires including script.aculo.us' effects.js library");
+
+var Droppables = {
+ drops: [],
+
+ remove: function(element) {
+ this.drops = this.drops.reject(function(d) { return d.element==$(element) });
+ },
+
+ add: function(element) {
+ element = $(element);
+ var options = Object.extend({
+ greedy: true,
+ hoverclass: null,
+ tree: false
+ }, arguments[1] || {});
+
+ // cache containers
+ if(options.containment) {
+ options._containers = [];
+ var containment = options.containment;
+ if((typeof containment == 'object') &&
+ (containment.constructor == Array)) {
+ containment.each( function(c) { options._containers.push($(c)) });
+ } else {
+ options._containers.push($(containment));
+ }
+ }
+
+ if(options.accept) options.accept = [options.accept].flatten();
+
+ Element.makePositioned(element); // fix IE
+ options.element = element;
+
+ this.drops.push(options);
+ },
+
+ findDeepestChild: function(drops) {
+ deepest = drops[0];
+
+ for (i = 1; i < drops.length; ++i)
+ if (Element.isParent(drops[i].element, deepest.element))
+ deepest = drops[i];
+
+ return deepest;
+ },
+
+ isContained: function(element, drop) {
+ var containmentNode;
+ if(drop.tree) {
+ containmentNode = element.treeNode;
+ } else {
+ containmentNode = element.parentNode;
+ }
+ return drop._containers.detect(function(c) { return containmentNode == c });
+ },
+
+ isAffected: function(point, element, drop) {
+ return (
+ (drop.element!=element) &&
+ ((!drop._containers) ||
+ this.isContained(element, drop)) &&
+ ((!drop.accept) ||
+ (Element.classNames(element).detect(
+ function(v) { return drop.accept.include(v) } ) )) &&
+ Position.within(drop.element, point[0], point[1]) );
+ },
+
+ deactivate: function(drop) {
+ if(drop.hoverclass)
+ Element.removeClassName(drop.element, drop.hoverclass);
+ this.last_active = null;
+ },
+
+ activate: function(drop) {
+ if(drop.hoverclass)
+ Element.addClassName(drop.element, drop.hoverclass);
+ this.last_active = drop;
+ },
+
+ show: function(point, element) {
+ if(!this.drops.length) return;
+ var affected = [];
+
+ if(this.last_active) this.deactivate(this.last_active);
+ this.drops.each( function(drop) {
+ if(Droppables.isAffected(point, element, drop))
+ affected.push(drop);
+ });
+
+ if(affected.length>0) {
+ drop = Droppables.findDeepestChild(affected);
+ Position.within(drop.element, point[0], point[1]);
+ if(drop.onHover)
+ drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
+
+ Droppables.activate(drop);
+ }
+ },
+
+ fire: function(event, element) {
+ if(!this.last_active) return;
+ Position.prepare();
+
+ if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
+ if (this.last_active.onDrop)
+ this.last_active.onDrop(element, this.last_active.element, event);
+ },
+
+ reset: function() {
+ if(this.last_active)
+ this.deactivate(this.last_active);
+ }
+}
+
+var Draggables = {
+ drags: [],
+ observers: [],
+
+ register: function(draggable) {
+ if(this.drags.length == 0) {
+ this.eventMouseUp = this.endDrag.bindAsEventListener(this);
+ this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
+ this.eventKeypress = this.keyPress.bindAsEventListener(this);
+
+ Event.observe(document, "mouseup", this.eventMouseUp);
+ Event.observe(document, "mousemove", this.eventMouseMove);
+ Event.observe(document, "keypress", this.eventKeypress);
+ }
+ this.drags.push(draggable);
+ },
+
+ unregister: function(draggable) {
+ this.drags = this.drags.reject(function(d) { return d==draggable });
+ if(this.drags.length == 0) {
+ Event.stopObserving(document, "mouseup", this.eventMouseUp);
+ Event.stopObserving(document, "mousemove", this.eventMouseMove);
+ Event.stopObserving(document, "keypress", this.eventKeypress);
+ }
+ },
+
+ activate: function(draggable) {
+ if(draggable.options.delay) {
+ this._timeout = setTimeout(function() {
+ Draggables._timeout = null;
+ window.focus();
+ Draggables.activeDraggable = draggable;
+ }.bind(this), draggable.options.delay);
+ } else {
+ window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
+ this.activeDraggable = draggable;
+ }
+ },
+
+ deactivate: function() {
+ this.activeDraggable = null;
+ },
+
+ updateDrag: function(event) {
+ if(!this.activeDraggable) return;
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ // Mozilla-based browsers fire successive mousemove events with
+ // the same coordinates, prevent needless redrawing (moz bug?)
+ if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
+ this._lastPointer = pointer;
+
+ this.activeDraggable.updateDrag(event, pointer);
+ },
+
+ endDrag: function(event) {
+ if(this._timeout) {
+ clearTimeout(this._timeout);
+ this._timeout = null;
+ }
+ if(!this.activeDraggable) return;
+ this._lastPointer = null;
+ this.activeDraggable.endDrag(event);
+ this.activeDraggable = null;
+ },
+
+ keyPress: function(event) {
+ if(this.activeDraggable)
+ this.activeDraggable.keyPress(event);
+ },
+
+ addObserver: function(observer) {
+ this.observers.push(observer);
+ this._cacheObserverCallbacks();
+ },
+
+ removeObserver: function(element) { // element instead of observer fixes mem leaks
+ this.observers = this.observers.reject( function(o) { return o.element==element });
+ this._cacheObserverCallbacks();
+ },
+
+ notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
+ if(this[eventName+'Count'] > 0)
+ this.observers.each( function(o) {
+ if(o[eventName]) o[eventName](eventName, draggable, event);
+ });
+ if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
+ },
+
+ _cacheObserverCallbacks: function() {
+ ['onStart','onEnd','onDrag'].each( function(eventName) {
+ Draggables[eventName+'Count'] = Draggables.observers.select(
+ function(o) { return o[eventName]; }
+ ).length;
+ });
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Draggable = Class.create();
+Draggable._dragging = {};
+
+Draggable.prototype = {
+ initialize: function(element) {
+ var defaults = {
+ handle: false,
+ reverteffect: function(element, top_offset, left_offset) {
+ var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
+ new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
+ queue: {scope:'_draggable', position:'end'}
+ });
+ },
+ endeffect: function(element) {
+ var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
+ new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
+ queue: {scope:'_draggable', position:'end'},
+ afterFinish: function(){
+ Draggable._dragging[element] = false
+ }
+ });
+ },
+ zindex: 1000,
+ revert: false,
+ scroll: false,
+ scrollSensitivity: 20,
+ scrollSpeed: 15,
+ snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] }
+ delay: 0
+ };
+
+ if(!arguments[1] || typeof arguments[1].endeffect == 'undefined')
+ Object.extend(defaults, {
+ starteffect: function(element) {
+ element._opacity = Element.getOpacity(element);
+ Draggable._dragging[element] = true;
+ new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
+ }
+ });
+
+ var options = Object.extend(defaults, arguments[1] || {});
+
+ this.element = $(element);
+
+ if(options.handle && (typeof options.handle == 'string'))
+ this.handle = this.element.down('.'+options.handle, 0);
+
+ if(!this.handle) this.handle = $(options.handle);
+ if(!this.handle) this.handle = this.element;
+
+ if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
+ options.scroll = $(options.scroll);
+ this._isScrollChild = Element.childOf(this.element, options.scroll);
+ }
+
+ Element.makePositioned(this.element); // fix IE
+
+ this.delta = this.currentDelta();
+ this.options = options;
+ this.dragging = false;
+
+ this.eventMouseDown = this.initDrag.bindAsEventListener(this);
+ Event.observe(this.handle, "mousedown", this.eventMouseDown);
+
+ Draggables.register(this);
+ },
+
+ destroy: function() {
+ Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
+ Draggables.unregister(this);
+ },
+
+ currentDelta: function() {
+ return([
+ parseInt(Element.getStyle(this.element,'left') || '0'),
+ parseInt(Element.getStyle(this.element,'top') || '0')]);
+ },
+
+ initDrag: function(event) {
+ if(typeof Draggable._dragging[this.element] != 'undefined' &&
+ Draggable._dragging[this.element]) return;
+ if(Event.isLeftClick(event)) {
+ // abort on form elements, fixes a Firefox issue
+ var src = Event.element(event);
+ if(src.tagName && (
+ src.tagName=='INPUT' ||
+ src.tagName=='SELECT' ||
+ src.tagName=='OPTION' ||
+ src.tagName=='BUTTON' ||
+ src.tagName=='TEXTAREA')) return;
+
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ var pos = Position.cumulativeOffset(this.element);
+ this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
+
+ Draggables.activate(this);
+ Event.stop(event);
+ }
+ },
+
+ startDrag: function(event) {
+ this.dragging = true;
+
+ if(this.options.zindex) {
+ this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
+ this.element.style.zIndex = this.options.zindex;
+ }
+
+ if(this.options.ghosting) {
+ this._clone = this.element.cloneNode(true);
+ Position.absolutize(this.element);
+ this.element.parentNode.insertBefore(this._clone, this.element);
+ }
+
+ if(this.options.scroll) {
+ if (this.options.scroll == window) {
+ var where = this._getWindowScroll(this.options.scroll);
+ this.originalScrollLeft = where.left;
+ this.originalScrollTop = where.top;
+ } else {
+ this.originalScrollLeft = this.options.scroll.scrollLeft;
+ this.originalScrollTop = this.options.scroll.scrollTop;
+ }
+ }
+
+ Draggables.notify('onStart', this, event);
+
+ if(this.options.starteffect) this.options.starteffect(this.element);
+ },
+
+ updateDrag: function(event, pointer) {
+ if(!this.dragging) this.startDrag(event);
+ Position.prepare();
+ Droppables.show(pointer, this.element);
+ Draggables.notify('onDrag', this, event);
+
+ this.draw(pointer);
+ if(this.options.change) this.options.change(this);
+
+ if(this.options.scroll) {
+ this.stopScrolling();
+
+ var p;
+ if (this.options.scroll == window) {
+ with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
+ } else {
+ p = Position.page(this.options.scroll);
+ p[0] += this.options.scroll.scrollLeft + Position.deltaX;
+ p[1] += this.options.scroll.scrollTop + Position.deltaY;
+ p.push(p[0]+this.options.scroll.offsetWidth);
+ p.push(p[1]+this.options.scroll.offsetHeight);
+ }
+ var speed = [0,0];
+ if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
+ if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
+ if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
+ if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
+ this.startScrolling(speed);
+ }
+
+ // fix AppleWebKit rendering
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+
+ Event.stop(event);
+ },
+
+ finishDrag: function(event, success) {
+ this.dragging = false;
+
+ if(this.options.ghosting) {
+ Position.relativize(this.element);
+ Element.remove(this._clone);
+ this._clone = null;
+ }
+
+ if(success) Droppables.fire(event, this.element);
+ Draggables.notify('onEnd', this, event);
+
+ var revert = this.options.revert;
+ if(revert && typeof revert == 'function') revert = revert(this.element);
+
+ var d = this.currentDelta();
+ if(revert && this.options.reverteffect) {
+ this.options.reverteffect(this.element,
+ d[1]-this.delta[1], d[0]-this.delta[0]);
+ } else {
+ this.delta = d;
+ }
+
+ if(this.options.zindex)
+ this.element.style.zIndex = this.originalZ;
+
+ if(this.options.endeffect)
+ this.options.endeffect(this.element);
+
+ Draggables.deactivate(this);
+ Droppables.reset();
+ },
+
+ keyPress: function(event) {
+ if(event.keyCode!=Event.KEY_ESC) return;
+ this.finishDrag(event, false);
+ Event.stop(event);
+ },
+
+ endDrag: function(event) {
+ if(!this.dragging) return;
+ this.stopScrolling();
+ this.finishDrag(event, true);
+ Event.stop(event);
+ },
+
+ draw: function(point) {
+ var pos = Position.cumulativeOffset(this.element);
+ if(this.options.ghosting) {
+ var r = Position.realOffset(this.element);
+ pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
+ }
+
+ var d = this.currentDelta();
+ pos[0] -= d[0]; pos[1] -= d[1];
+
+ if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
+ pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
+ pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
+ }
+
+ var p = [0,1].map(function(i){
+ return (point[i]-pos[i]-this.offset[i])
+ }.bind(this));
+
+ if(this.options.snap) {
+ if(typeof this.options.snap == 'function') {
+ p = this.options.snap(p[0],p[1],this);
+ } else {
+ if(this.options.snap instanceof Array) {
+ p = p.map( function(v, i) {
+ return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
+ } else {
+ p = p.map( function(v) {
+ return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
+ }
+ }}
+
+ var style = this.element.style;
+ if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+ style.left = p[0] + "px";
+ if((!this.options.constraint) || (this.options.constraint=='vertical'))
+ style.top = p[1] + "px";
+
+ if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
+ },
+
+ stopScrolling: function() {
+ if(this.scrollInterval) {
+ clearInterval(this.scrollInterval);
+ this.scrollInterval = null;
+ Draggables._lastScrollPointer = null;
+ }
+ },
+
+ startScrolling: function(speed) {
+ if(!(speed[0] || speed[1])) return;
+ this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
+ this.lastScrolled = new Date();
+ this.scrollInterval = setInterval(this.scroll.bind(this), 10);
+ },
+
+ scroll: function() {
+ var current = new Date();
+ var delta = current - this.lastScrolled;
+ this.lastScrolled = current;
+ if(this.options.scroll == window) {
+ with (this._getWindowScroll(this.options.scroll)) {
+ if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
+ var d = delta / 1000;
+ this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
+ }
+ }
+ } else {
+ this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
+ this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
+ }
+
+ Position.prepare();
+ Droppables.show(Draggables._lastPointer, this.element);
+ Draggables.notify('onDrag', this);
+ if (this._isScrollChild) {
+ Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
+ Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
+ Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
+ if (Draggables._lastScrollPointer[0] < 0)
+ Draggables._lastScrollPointer[0] = 0;
+ if (Draggables._lastScrollPointer[1] < 0)
+ Draggables._lastScrollPointer[1] = 0;
+ this.draw(Draggables._lastScrollPointer);
+ }
+
+ if(this.options.change) this.options.change(this);
+ },
+
+ _getWindowScroll: function(w) {
+ var T, L, W, H;
+ with (w.document) {
+ if (w.document.documentElement && documentElement.scrollTop) {
+ T = documentElement.scrollTop;
+ L = documentElement.scrollLeft;
+ } else if (w.document.body) {
+ T = body.scrollTop;
+ L = body.scrollLeft;
+ }
+ if (w.innerWidth) {
+ W = w.innerWidth;
+ H = w.innerHeight;
+ } else if (w.document.documentElement && documentElement.clientWidth) {
+ W = documentElement.clientWidth;
+ H = documentElement.clientHeight;
+ } else {
+ W = body.offsetWidth;
+ H = body.offsetHeight
+ }
+ }
+ return { top: T, left: L, width: W, height: H };
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var SortableObserver = Class.create();
+SortableObserver.prototype = {
+ initialize: function(element, observer) {
+ this.element = $(element);
+ this.observer = observer;
+ this.lastValue = Sortable.serialize(this.element);
+ },
+
+ onStart: function() {
+ this.lastValue = Sortable.serialize(this.element);
+ },
+
+ onEnd: function() {
+ Sortable.unmark();
+ if(this.lastValue != Sortable.serialize(this.element))
+ this.observer(this.element)
+ }
+}
+
+var Sortable = {
+ SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
+
+ sortables: {},
+
+ _findRootElement: function(element) {
+ while (element.tagName != "BODY") {
+ if(element.id && Sortable.sortables[element.id]) return element;
+ element = element.parentNode;
+ }
+ },
+
+ options: function(element) {
+ element = Sortable._findRootElement($(element));
+ if(!element) return;
+ return Sortable.sortables[element.id];
+ },
+
+ destroy: function(element){
+ var s = Sortable.options(element);
+
+ if(s) {
+ Draggables.removeObserver(s.element);
+ s.droppables.each(function(d){ Droppables.remove(d) });
+ s.draggables.invoke('destroy');
+
+ delete Sortable.sortables[s.element.id];
+ }
+ },
+
+ create: function(element) {
+ element = $(element);
+ var options = Object.extend({
+ element: element,
+ tag: 'li', // assumes li children, override with tag: 'tagname'
+ dropOnEmpty: false,
+ tree: false,
+ treeTag: 'ul',
+ overlap: 'vertical', // one of 'vertical', 'horizontal'
+ constraint: 'vertical', // one of 'vertical', 'horizontal', false
+ containment: element, // also takes array of elements (or id's); or false
+ handle: false, // or a CSS class
+ only: false,
+ delay: 0,
+ hoverclass: null,
+ ghosting: false,
+ scroll: false,
+ scrollSensitivity: 20,
+ scrollSpeed: 15,
+ format: this.SERIALIZE_RULE,
+ onChange: Prototype.emptyFunction,
+ onUpdate: Prototype.emptyFunction
+ }, arguments[1] || {});
+
+ // clear any old sortable with same element
+ this.destroy(element);
+
+ // build options for the draggables
+ var options_for_draggable = {
+ revert: true,
+ scroll: options.scroll,
+ scrollSpeed: options.scrollSpeed,
+ scrollSensitivity: options.scrollSensitivity,
+ delay: options.delay,
+ ghosting: options.ghosting,
+ constraint: options.constraint,
+ handle: options.handle };
+
+ if(options.starteffect)
+ options_for_draggable.starteffect = options.starteffect;
+
+ if(options.reverteffect)
+ options_for_draggable.reverteffect = options.reverteffect;
+ else
+ if(options.ghosting) options_for_draggable.reverteffect = function(element) {
+ element.style.top = 0;
+ element.style.left = 0;
+ };
+
+ if(options.endeffect)
+ options_for_draggable.endeffect = options.endeffect;
+
+ if(options.zindex)
+ options_for_draggable.zindex = options.zindex;
+
+ // build options for the droppables
+ var options_for_droppable = {
+ overlap: options.overlap,
+ containment: options.containment,
+ tree: options.tree,
+ hoverclass: options.hoverclass,
+ onHover: Sortable.onHover
+ }
+
+ var options_for_tree = {
+ onHover: Sortable.onEmptyHover,
+ overlap: options.overlap,
+ containment: options.containment,
+ hoverclass: options.hoverclass
+ }
+
+ // fix for gecko engine
+ Element.cleanWhitespace(element);
+
+ options.draggables = [];
+ options.droppables = [];
+
+ // drop on empty handling
+ if(options.dropOnEmpty || options.tree) {
+ Droppables.add(element, options_for_tree);
+ options.droppables.push(element);
+ }
+
+ (this.findElements(element, options) || []).each( function(e) {
+ // handles are per-draggable
+ var handle = options.handle ?
+ $(e).down('.'+options.handle,0) : e;
+ options.draggables.push(
+ new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
+ Droppables.add(e, options_for_droppable);
+ if(options.tree) e.treeNode = element;
+ options.droppables.push(e);
+ });
+
+ if(options.tree) {
+ (Sortable.findTreeElements(element, options) || []).each( function(e) {
+ Droppables.add(e, options_for_tree);
+ e.treeNode = element;
+ options.droppables.push(e);
+ });
+ }
+
+ // keep reference
+ this.sortables[element.id] = options;
+
+ // for onupdate
+ Draggables.addObserver(new SortableObserver(element, options.onUpdate));
+
+ },
+
+ // return all suitable-for-sortable elements in a guaranteed order
+ findElements: function(element, options) {
+ return Element.findChildren(
+ element, options.only, options.tree ? true : false, options.tag);
+ },
+
+ findTreeElements: function(element, options) {
+ return Element.findChildren(
+ element, options.only, options.tree ? true : false, options.treeTag);
+ },
+
+ onHover: function(element, dropon, overlap) {
+ if(Element.isParent(dropon, element)) return;
+
+ if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
+ return;
+ } else if(overlap>0.5) {
+ Sortable.mark(dropon, 'before');
+ if(dropon.previousSibling != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, dropon);
+ if(dropon.parentNode!=oldParentNode)
+ Sortable.options(oldParentNode).onChange(element);
+ Sortable.options(dropon.parentNode).onChange(element);
+ }
+ } else {
+ Sortable.mark(dropon, 'after');
+ var nextElement = dropon.nextSibling || null;
+ if(nextElement != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, nextElement);
+ if(dropon.parentNode!=oldParentNode)
+ Sortable.options(oldParentNode).onChange(element);
+ Sortable.options(dropon.parentNode).onChange(element);
+ }
+ }
+ },
+
+ onEmptyHover: function(element, dropon, overlap) {
+ var oldParentNode = element.parentNode;
+ var droponOptions = Sortable.options(dropon);
+
+ if(!Element.isParent(dropon, element)) {
+ var index;
+
+ var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
+ var child = null;
+
+ if(children) {
+ var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
+
+ for (index = 0; index < children.length; index += 1) {
+ if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
+ offset -= Element.offsetSize (children[index], droponOptions.overlap);
+ } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
+ child = index + 1 < children.length ? children[index + 1] : null;
+ break;
+ } else {
+ child = children[index];
+ break;
+ }
+ }
+ }
+
+ dropon.insertBefore(element, child);
+
+ Sortable.options(oldParentNode).onChange(element);
+ droponOptions.onChange(element);
+ }
+ },
+
+ unmark: function() {
+ if(Sortable._marker) Sortable._marker.hide();
+ },
+
+ mark: function(dropon, position) {
+ // mark on ghosting only
+ var sortable = Sortable.options(dropon.parentNode);
+ if(sortable && !sortable.ghosting) return;
+
+ if(!Sortable._marker) {
+ Sortable._marker =
+ ($('dropmarker') || Element.extend(document.createElement('DIV'))).
+ hide().addClassName('dropmarker').setStyle({position:'absolute'});
+ document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
+ }
+ var offsets = Position.cumulativeOffset(dropon);
+ Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
+
+ if(position=='after')
+ if(sortable.overlap == 'horizontal')
+ Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
+ else
+ Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
+
+ Sortable._marker.show();
+ },
+
+ _tree: function(element, options, parent) {
+ var children = Sortable.findElements(element, options) || [];
+
+ for (var i = 0; i < children.length; ++i) {
+ var match = children[i].id.match(options.format);
+
+ if (!match) continue;
+
+ var child = {
+ id: encodeURIComponent(match ? match[1] : null),
+ element: element,
+ parent: parent,
+ children: [],
+ position: parent.children.length,
+ container: $(children[i]).down(options.treeTag)
+ }
+
+ /* Get the element containing the children and recurse over it */
+ if (child.container)
+ this._tree(child.container, options, child)
+
+ parent.children.push (child);
+ }
+
+ return parent;
+ },
+
+ tree: function(element) {
+ element = $(element);
+ var sortableOptions = this.options(element);
+ var options = Object.extend({
+ tag: sortableOptions.tag,
+ treeTag: sortableOptions.treeTag,
+ only: sortableOptions.only,
+ name: element.id,
+ format: sortableOptions.format
+ }, arguments[1] || {});
+
+ var root = {
+ id: null,
+ parent: null,
+ children: [],
+ container: element,
+ position: 0
+ }
+
+ return Sortable._tree(element, options, root);
+ },
+
+ /* Construct a [i] index for a particular node */
+ _constructIndex: function(node) {
+ var index = '';
+ do {
+ if (node.id) index = '[' + node.position + ']' + index;
+ } while ((node = node.parent) != null);
+ return index;
+ },
+
+ sequence: function(element) {
+ element = $(element);
+ var options = Object.extend(this.options(element), arguments[1] || {});
+
+ return $(this.findElements(element, options) || []).map( function(item) {
+ return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
+ });
+ },
+
+ setSequence: function(element, new_sequence) {
+ element = $(element);
+ var options = Object.extend(this.options(element), arguments[2] || {});
+
+ var nodeMap = {};
+ this.findElements(element, options).each( function(n) {
+ if (n.id.match(options.format))
+ nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
+ n.parentNode.removeChild(n);
+ });
+
+ new_sequence.each(function(ident) {
+ var n = nodeMap[ident];
+ if (n) {
+ n[1].appendChild(n[0]);
+ delete nodeMap[ident];
+ }
+ });
+ },
+
+ serialize: function(element) {
+ element = $(element);
+ var options = Object.extend(Sortable.options(element), arguments[1] || {});
+ var name = encodeURIComponent(
+ (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
+
+ if (options.tree) {
+ return Sortable.tree(element, arguments[1]).children.map( function (item) {
+ return [name + Sortable._constructIndex(item) + "[id]=" +
+ encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
+ }).flatten().join('&');
+ } else {
+ return Sortable.sequence(element, arguments[1]).map( function(item) {
+ return name + "[]=" + encodeURIComponent(item);
+ }).join('&');
+ }
+ }
+}
+
+// Returns true if child is contained within element
+Element.isParent = function(child, element) {
+ if (!child.parentNode || child == element) return false;
+ if (child.parentNode == element) return true;
+ return Element.isParent(child.parentNode, element);
+}
+
+Element.findChildren = function(element, only, recursive, tagName) {
+ if(!element.hasChildNodes()) return null;
+ tagName = tagName.toUpperCase();
+ if(only) only = [only].flatten();
+ var elements = [];
+ $A(element.childNodes).each( function(e) {
+ if(e.tagName && e.tagName.toUpperCase()==tagName &&
+ (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
+ elements.push(e);
+ if(recursive) {
+ var grandchildren = Element.findChildren(e, only, recursive, tagName);
+ if(grandchildren) elements.push(grandchildren);
+ }
+ });
+
+ return (elements.length>0 ? elements.flatten() : []);
+}
+
+Element.offsetSize = function (element, type) {
+ return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
+}
diff --git a/public/javascripts/effects.js b/public/javascripts/effects.js
new file mode 100644
index 0000000..3b02eda
--- /dev/null
+++ b/public/javascripts/effects.js
@@ -0,0 +1,1088 @@
+// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Contributors:
+// Justin Palmer (http://encytemedia.com/)
+// Mark Pilgrim (http://diveintomark.org/)
+// Martin Bialasinki
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+// converts rgb() and #xxx to #xxxxxx format,
+// returns self (or first argument) if not convertable
+String.prototype.parseColor = function() {
+ var color = '#';
+ if(this.slice(0,4) == 'rgb(') {
+ var cols = this.slice(4,this.length-1).split(',');
+ var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
+ } else {
+ if(this.slice(0,1) == '#') {
+ if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
+ if(this.length==7) color = this.toLowerCase();
+ }
+ }
+ return(color.length==7 ? color : (arguments[0] || this));
+}
+
+/*--------------------------------------------------------------------------*/
+
+Element.collectTextNodes = function(element) {
+ return $A($(element).childNodes).collect( function(node) {
+ return (node.nodeType==3 ? node.nodeValue :
+ (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
+ }).flatten().join('');
+}
+
+Element.collectTextNodesIgnoreClass = function(element, className) {
+ return $A($(element).childNodes).collect( function(node) {
+ return (node.nodeType==3 ? node.nodeValue :
+ ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
+ Element.collectTextNodesIgnoreClass(node, className) : ''));
+ }).flatten().join('');
+}
+
+Element.setContentZoom = function(element, percent) {
+ element = $(element);
+ element.setStyle({fontSize: (percent/100) + 'em'});
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+ return element;
+}
+
+Element.getOpacity = function(element){
+ element = $(element);
+ var opacity;
+ if (opacity = element.getStyle('opacity'))
+ return parseFloat(opacity);
+ if (opacity = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+ if(opacity[1]) return parseFloat(opacity[1]) / 100;
+ return 1.0;
+}
+
+Element.setOpacity = function(element, value){
+ element= $(element);
+ if (value == 1){
+ element.setStyle({ opacity:
+ (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ?
+ 0.999999 : 1.0 });
+ if(/MSIE/.test(navigator.userAgent) && !window.opera)
+ element.setStyle({filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});
+ } else {
+ if(value < 0.00001) value = 0;
+ element.setStyle({opacity: value});
+ if(/MSIE/.test(navigator.userAgent) && !window.opera)
+ element.setStyle(
+ { filter: element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
+ 'alpha(opacity='+value*100+')' });
+ }
+ return element;
+}
+
+Element.getInlineOpacity = function(element){
+ return $(element).style.opacity || '';
+}
+
+Element.forceRerendering = function(element) {
+ try {
+ element = $(element);
+ var n = document.createTextNode(' ');
+ element.appendChild(n);
+ element.removeChild(n);
+ } catch(e) { }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Array.prototype.call = function() {
+ var args = arguments;
+ this.each(function(f){ f.apply(this, args) });
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Effect = {
+ _elementDoesNotExistError: {
+ name: 'ElementDoesNotExistError',
+ message: 'The specified DOM element does not exist, but is required for this effect to operate'
+ },
+ tagifyText: function(element) {
+ if(typeof Builder == 'undefined')
+ throw("Effect.tagifyText requires including script.aculo.us' builder.js library");
+
+ var tagifyStyle = 'position:relative';
+ if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1';
+
+ element = $(element);
+ $A(element.childNodes).each( function(child) {
+ if(child.nodeType==3) {
+ child.nodeValue.toArray().each( function(character) {
+ element.insertBefore(
+ Builder.node('span',{style: tagifyStyle},
+ character == ' ' ? String.fromCharCode(160) : character),
+ child);
+ });
+ Element.remove(child);
+ }
+ });
+ },
+ multiple: function(element, effect) {
+ var elements;
+ if(((typeof element == 'object') ||
+ (typeof element == 'function')) &&
+ (element.length))
+ elements = element;
+ else
+ elements = $(element).childNodes;
+
+ var options = Object.extend({
+ speed: 0.1,
+ delay: 0.0
+ }, arguments[2] || {});
+ var masterDelay = options.delay;
+
+ $A(elements).each( function(element, index) {
+ new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
+ });
+ },
+ PAIRS: {
+ 'slide': ['SlideDown','SlideUp'],
+ 'blind': ['BlindDown','BlindUp'],
+ 'appear': ['Appear','Fade']
+ },
+ toggle: function(element, effect) {
+ element = $(element);
+ effect = (effect || 'appear').toLowerCase();
+ var options = Object.extend({
+ queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
+ }, arguments[2] || {});
+ Effect[element.visible() ?
+ Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
+ }
+};
+
+var Effect2 = Effect; // deprecated
+
+/* ------------- transitions ------------- */
+
+Effect.Transitions = {
+ linear: Prototype.K,
+ sinoidal: function(pos) {
+ return (-Math.cos(pos*Math.PI)/2) + 0.5;
+ },
+ reverse: function(pos) {
+ return 1-pos;
+ },
+ flicker: function(pos) {
+ return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
+ },
+ wobble: function(pos) {
+ return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
+ },
+ pulse: function(pos, pulses) {
+ pulses = pulses || 5;
+ return (
+ Math.round((pos % (1/pulses)) * pulses) == 0 ?
+ ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) :
+ 1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2))
+ );
+ },
+ none: function(pos) {
+ return 0;
+ },
+ full: function(pos) {
+ return 1;
+ }
+};
+
+/* ------------- core effects ------------- */
+
+Effect.ScopedQueue = Class.create();
+Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
+ initialize: function() {
+ this.effects = [];
+ this.interval = null;
+ },
+ _each: function(iterator) {
+ this.effects._each(iterator);
+ },
+ add: function(effect) {
+ var timestamp = new Date().getTime();
+
+ var position = (typeof effect.options.queue == 'string') ?
+ effect.options.queue : effect.options.queue.position;
+
+ switch(position) {
+ case 'front':
+ // move unstarted effects after this effect
+ this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
+ e.startOn += effect.finishOn;
+ e.finishOn += effect.finishOn;
+ });
+ break;
+ case 'with-last':
+ timestamp = this.effects.pluck('startOn').max() || timestamp;
+ break;
+ case 'end':
+ // start effect after last queued effect has finished
+ timestamp = this.effects.pluck('finishOn').max() || timestamp;
+ break;
+ }
+
+ effect.startOn += timestamp;
+ effect.finishOn += timestamp;
+
+ if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
+ this.effects.push(effect);
+
+ if(!this.interval)
+ this.interval = setInterval(this.loop.bind(this), 40);
+ },
+ remove: function(effect) {
+ this.effects = this.effects.reject(function(e) { return e==effect });
+ if(this.effects.length == 0) {
+ clearInterval(this.interval);
+ this.interval = null;
+ }
+ },
+ loop: function() {
+ var timePos = new Date().getTime();
+ this.effects.invoke('loop', timePos);
+ }
+});
+
+Effect.Queues = {
+ instances: $H(),
+ get: function(queueName) {
+ if(typeof queueName != 'string') return queueName;
+
+ if(!this.instances[queueName])
+ this.instances[queueName] = new Effect.ScopedQueue();
+
+ return this.instances[queueName];
+ }
+}
+Effect.Queue = Effect.Queues.get('global');
+
+Effect.DefaultOptions = {
+ transition: Effect.Transitions.sinoidal,
+ duration: 1.0, // seconds
+ fps: 25.0, // max. 25fps due to Effect.Queue implementation
+ sync: false, // true for combining
+ from: 0.0,
+ to: 1.0,
+ delay: 0.0,
+ queue: 'parallel'
+}
+
+Effect.Base = function() {};
+Effect.Base.prototype = {
+ position: null,
+ start: function(options) {
+ this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
+ this.currentFrame = 0;
+ this.state = 'idle';
+ this.startOn = this.options.delay*1000;
+ this.finishOn = this.startOn + (this.options.duration*1000);
+ this.event('beforeStart');
+ if(!this.options.sync)
+ Effect.Queues.get(typeof this.options.queue == 'string' ?
+ 'global' : this.options.queue.scope).add(this);
+ },
+ loop: function(timePos) {
+ if(timePos >= this.startOn) {
+ if(timePos >= this.finishOn) {
+ this.render(1.0);
+ this.cancel();
+ this.event('beforeFinish');
+ if(this.finish) this.finish();
+ this.event('afterFinish');
+ return;
+ }
+ var pos = (timePos - this.startOn) / (this.finishOn - this.startOn);
+ var frame = Math.round(pos * this.options.fps * this.options.duration);
+ if(frame > this.currentFrame) {
+ this.render(pos);
+ this.currentFrame = frame;
+ }
+ }
+ },
+ render: function(pos) {
+ if(this.state == 'idle') {
+ this.state = 'running';
+ this.event('beforeSetup');
+ if(this.setup) this.setup();
+ this.event('afterSetup');
+ }
+ if(this.state == 'running') {
+ if(this.options.transition) pos = this.options.transition(pos);
+ pos *= (this.options.to-this.options.from);
+ pos += this.options.from;
+ this.position = pos;
+ this.event('beforeUpdate');
+ if(this.update) this.update(pos);
+ this.event('afterUpdate');
+ }
+ },
+ cancel: function() {
+ if(!this.options.sync)
+ Effect.Queues.get(typeof this.options.queue == 'string' ?
+ 'global' : this.options.queue.scope).remove(this);
+ this.state = 'finished';
+ },
+ event: function(eventName) {
+ if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
+ if(this.options[eventName]) this.options[eventName](this);
+ },
+ inspect: function() {
+ return '#';
+ }
+}
+
+Effect.Parallel = Class.create();
+Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
+ initialize: function(effects) {
+ this.effects = effects || [];
+ this.start(arguments[1]);
+ },
+ update: function(position) {
+ this.effects.invoke('render', position);
+ },
+ finish: function(position) {
+ this.effects.each( function(effect) {
+ effect.render(1.0);
+ effect.cancel();
+ effect.event('beforeFinish');
+ if(effect.finish) effect.finish(position);
+ effect.event('afterFinish');
+ });
+ }
+});
+
+Effect.Event = Class.create();
+Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), {
+ initialize: function() {
+ var options = Object.extend({
+ duration: 0
+ }, arguments[0] || {});
+ this.start(options);
+ },
+ update: Prototype.emptyFunction
+});
+
+Effect.Opacity = Class.create();
+Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ if(!this.element) throw(Effect._elementDoesNotExistError);
+ // make this work on IE on elements without 'layout'
+ if(/MSIE/.test(navigator.userAgent) && !window.opera && (!this.element.currentStyle.hasLayout))
+ this.element.setStyle({zoom: 1});
+ var options = Object.extend({
+ from: this.element.getOpacity() || 0.0,
+ to: 1.0
+ }, arguments[1] || {});
+ this.start(options);
+ },
+ update: function(position) {
+ this.element.setOpacity(position);
+ }
+});
+
+Effect.Move = Class.create();
+Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ if(!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ x: 0,
+ y: 0,
+ mode: 'relative'
+ }, arguments[1] || {});
+ this.start(options);
+ },
+ setup: function() {
+ // Bug in Opera: Opera returns the "real" position of a static element or
+ // relative element that does not have top/left explicitly set.
+ // ==> Always set top and left for position relative elements in your stylesheets
+ // (to 0 if you do not need them)
+ this.element.makePositioned();
+ this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
+ this.originalTop = parseFloat(this.element.getStyle('top') || '0');
+ if(this.options.mode == 'absolute') {
+ // absolute movement, so we need to calc deltaX and deltaY
+ this.options.x = this.options.x - this.originalLeft;
+ this.options.y = this.options.y - this.originalTop;
+ }
+ },
+ update: function(position) {
+ this.element.setStyle({
+ left: Math.round(this.options.x * position + this.originalLeft) + 'px',
+ top: Math.round(this.options.y * position + this.originalTop) + 'px'
+ });
+ }
+});
+
+// for backwards compatibility
+Effect.MoveBy = function(element, toTop, toLeft) {
+ return new Effect.Move(element,
+ Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
+};
+
+Effect.Scale = Class.create();
+Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
+ initialize: function(element, percent) {
+ this.element = $(element);
+ if(!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ scaleX: true,
+ scaleY: true,
+ scaleContent: true,
+ scaleFromCenter: false,
+ scaleMode: 'box', // 'box' or 'contents' or {} with provided values
+ scaleFrom: 100.0,
+ scaleTo: percent
+ }, arguments[2] || {});
+ this.start(options);
+ },
+ setup: function() {
+ this.restoreAfterFinish = this.options.restoreAfterFinish || false;
+ this.elementPositioning = this.element.getStyle('position');
+
+ this.originalStyle = {};
+ ['top','left','width','height','fontSize'].each( function(k) {
+ this.originalStyle[k] = this.element.style[k];
+ }.bind(this));
+
+ this.originalTop = this.element.offsetTop;
+ this.originalLeft = this.element.offsetLeft;
+
+ var fontSize = this.element.getStyle('font-size') || '100%';
+ ['em','px','%','pt'].each( function(fontSizeType) {
+ if(fontSize.indexOf(fontSizeType)>0) {
+ this.fontSize = parseFloat(fontSize);
+ this.fontSizeType = fontSizeType;
+ }
+ }.bind(this));
+
+ this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
+
+ this.dims = null;
+ if(this.options.scaleMode=='box')
+ this.dims = [this.element.offsetHeight, this.element.offsetWidth];
+ if(/^content/.test(this.options.scaleMode))
+ this.dims = [this.element.scrollHeight, this.element.scrollWidth];
+ if(!this.dims)
+ this.dims = [this.options.scaleMode.originalHeight,
+ this.options.scaleMode.originalWidth];
+ },
+ update: function(position) {
+ var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
+ if(this.options.scaleContent && this.fontSize)
+ this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
+ this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
+ },
+ finish: function(position) {
+ if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
+ },
+ setDimensions: function(height, width) {
+ var d = {};
+ if(this.options.scaleX) d.width = Math.round(width) + 'px';
+ if(this.options.scaleY) d.height = Math.round(height) + 'px';
+ if(this.options.scaleFromCenter) {
+ var topd = (height - this.dims[0])/2;
+ var leftd = (width - this.dims[1])/2;
+ if(this.elementPositioning == 'absolute') {
+ if(this.options.scaleY) d.top = this.originalTop-topd + 'px';
+ if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
+ } else {
+ if(this.options.scaleY) d.top = -topd + 'px';
+ if(this.options.scaleX) d.left = -leftd + 'px';
+ }
+ }
+ this.element.setStyle(d);
+ }
+});
+
+Effect.Highlight = Class.create();
+Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ if(!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
+ this.start(options);
+ },
+ setup: function() {
+ // Prevent executing on elements not in the layout flow
+ if(this.element.getStyle('display')=='none') { this.cancel(); return; }
+ // Disable background image during the effect
+ this.oldStyle = {
+ backgroundImage: this.element.getStyle('background-image') };
+ this.element.setStyle({backgroundImage: 'none'});
+ if(!this.options.endcolor)
+ this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
+ if(!this.options.restorecolor)
+ this.options.restorecolor = this.element.getStyle('background-color');
+ // init color calculations
+ this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
+ this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
+ },
+ update: function(position) {
+ this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
+ return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
+ },
+ finish: function() {
+ this.element.setStyle(Object.extend(this.oldStyle, {
+ backgroundColor: this.options.restorecolor
+ }));
+ }
+});
+
+Effect.ScrollTo = Class.create();
+Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ this.start(arguments[1] || {});
+ },
+ setup: function() {
+ Position.prepare();
+ var offsets = Position.cumulativeOffset(this.element);
+ if(this.options.offset) offsets[1] += this.options.offset;
+ var max = window.innerHeight ?
+ window.height - window.innerHeight :
+ document.body.scrollHeight -
+ (document.documentElement.clientHeight ?
+ document.documentElement.clientHeight : document.body.clientHeight);
+ this.scrollStart = Position.deltaY;
+ this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
+ },
+ update: function(position) {
+ Position.prepare();
+ window.scrollTo(Position.deltaX,
+ this.scrollStart + (position*this.delta));
+ }
+});
+
+/* ------------- combination effects ------------- */
+
+Effect.Fade = function(element) {
+ element = $(element);
+ var oldOpacity = element.getInlineOpacity();
+ var options = Object.extend({
+ from: element.getOpacity() || 1.0,
+ to: 0.0,
+ afterFinishInternal: function(effect) {
+ if(effect.options.to!=0) return;
+ effect.element.hide().setStyle({opacity: oldOpacity});
+ }}, arguments[1] || {});
+ return new Effect.Opacity(element,options);
+}
+
+Effect.Appear = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
+ to: 1.0,
+ // force Safari to render floated elements properly
+ afterFinishInternal: function(effect) {
+ effect.element.forceRerendering();
+ },
+ beforeSetup: function(effect) {
+ effect.element.setOpacity(effect.options.from).show();
+ }}, arguments[1] || {});
+ return new Effect.Opacity(element,options);
+}
+
+Effect.Puff = function(element) {
+ element = $(element);
+ var oldStyle = {
+ opacity: element.getInlineOpacity(),
+ position: element.getStyle('position'),
+ top: element.style.top,
+ left: element.style.left,
+ width: element.style.width,
+ height: element.style.height
+ };
+ return new Effect.Parallel(
+ [ new Effect.Scale(element, 200,
+ { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
+ Object.extend({ duration: 1.0,
+ beforeSetupInternal: function(effect) {
+ Position.absolutize(effect.effects[0].element)
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().setStyle(oldStyle); }
+ }, arguments[1] || {})
+ );
+}
+
+Effect.BlindUp = function(element) {
+ element = $(element);
+ element.makeClipping();
+ return new Effect.Scale(element, 0,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ restoreAfterFinish: true,
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping();
+ }
+ }, arguments[1] || {})
+ );
+}
+
+Effect.BlindDown = function(element) {
+ element = $(element);
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, 100, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ scaleFrom: 0,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makeClipping().setStyle({height: '0px'}).show();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.undoClipping();
+ }
+ }, arguments[1] || {}));
+}
+
+Effect.SwitchOff = function(element) {
+ element = $(element);
+ var oldOpacity = element.getInlineOpacity();
+ return new Effect.Appear(element, Object.extend({
+ duration: 0.4,
+ from: 0,
+ transition: Effect.Transitions.flicker,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(effect.element, 1, {
+ duration: 0.3, scaleFromCenter: true,
+ scaleX: false, scaleContent: false, restoreAfterFinish: true,
+ beforeSetup: function(effect) {
+ effect.element.makePositioned().makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
+ }
+ })
+ }
+ }, arguments[1] || {}));
+}
+
+Effect.DropOut = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.getStyle('top'),
+ left: element.getStyle('left'),
+ opacity: element.getInlineOpacity() };
+ return new Effect.Parallel(
+ [ new Effect.Move(element, {x: 0, y: 100, sync: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
+ Object.extend(
+ { duration: 0.5,
+ beforeSetup: function(effect) {
+ effect.effects[0].element.makePositioned();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
+ }
+ }, arguments[1] || {}));
+}
+
+Effect.Shake = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.getStyle('top'),
+ left: element.getStyle('left') };
+ return new Effect.Move(element,
+ { x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+ effect.element.undoPositioned().setStyle(oldStyle);
+ }}) }}) }}) }}) }}) }});
+}
+
+Effect.SlideDown = function(element) {
+ element = $(element).cleanWhitespace();
+ // SlideDown need to have the content of the element wrapped in a container element with fixed height!
+ var oldInnerBottom = element.down().getStyle('bottom');
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, 100, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ scaleFrom: window.opera ? 0 : 1,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makePositioned();
+ effect.element.down().makePositioned();
+ if(window.opera) effect.element.setStyle({top: ''});
+ effect.element.makeClipping().setStyle({height: '0px'}).show();
+ },
+ afterUpdateInternal: function(effect) {
+ effect.element.down().setStyle({bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' });
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.undoClipping().undoPositioned();
+ effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
+ }, arguments[1] || {})
+ );
+}
+
+Effect.SlideUp = function(element) {
+ element = $(element).cleanWhitespace();
+ var oldInnerBottom = element.down().getStyle('bottom');
+ return new Effect.Scale(element, window.opera ? 0 : 1,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ scaleMode: 'box',
+ scaleFrom: 100,
+ restoreAfterFinish: true,
+ beforeStartInternal: function(effect) {
+ effect.element.makePositioned();
+ effect.element.down().makePositioned();
+ if(window.opera) effect.element.setStyle({top: ''});
+ effect.element.makeClipping().show();
+ },
+ afterUpdateInternal: function(effect) {
+ effect.element.down().setStyle({bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' });
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom});
+ effect.element.down().undoPositioned();
+ }
+ }, arguments[1] || {})
+ );
+}
+
+// Bug in opera makes the TD containing this element expand for a instance after finish
+Effect.Squish = function(element) {
+ return new Effect.Scale(element, window.opera ? 1 : 0, {
+ restoreAfterFinish: true,
+ beforeSetup: function(effect) {
+ effect.element.makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping();
+ }
+ });
+}
+
+Effect.Grow = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransition: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.full
+ }, arguments[1] || {});
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: element.getInlineOpacity() };
+
+ var dims = element.getDimensions();
+ var initialMoveX, initialMoveY;
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ initialMoveX = initialMoveY = moveX = moveY = 0;
+ break;
+ case 'top-right':
+ initialMoveX = dims.width;
+ initialMoveY = moveY = 0;
+ moveX = -dims.width;
+ break;
+ case 'bottom-left':
+ initialMoveX = moveX = 0;
+ initialMoveY = dims.height;
+ moveY = -dims.height;
+ break;
+ case 'bottom-right':
+ initialMoveX = dims.width;
+ initialMoveY = dims.height;
+ moveX = -dims.width;
+ moveY = -dims.height;
+ break;
+ case 'center':
+ initialMoveX = dims.width / 2;
+ initialMoveY = dims.height / 2;
+ moveX = -dims.width / 2;
+ moveY = -dims.height / 2;
+ break;
+ }
+
+ return new Effect.Move(element, {
+ x: initialMoveX,
+ y: initialMoveY,
+ duration: 0.01,
+ beforeSetup: function(effect) {
+ effect.element.hide().makeClipping().makePositioned();
+ },
+ afterFinishInternal: function(effect) {
+ new Effect.Parallel(
+ [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
+ new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
+ new Effect.Scale(effect.element, 100, {
+ scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
+ sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
+ ], Object.extend({
+ beforeSetup: function(effect) {
+ effect.effects[0].element.setStyle({height: '0px'}).show();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
+ }
+ }, options)
+ )
+ }
+ });
+}
+
+Effect.Shrink = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransition: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.none
+ }, arguments[1] || {});
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: element.getInlineOpacity() };
+
+ var dims = element.getDimensions();
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ moveX = moveY = 0;
+ break;
+ case 'top-right':
+ moveX = dims.width;
+ moveY = 0;
+ break;
+ case 'bottom-left':
+ moveX = 0;
+ moveY = dims.height;
+ break;
+ case 'bottom-right':
+ moveX = dims.width;
+ moveY = dims.height;
+ break;
+ case 'center':
+ moveX = dims.width / 2;
+ moveY = dims.height / 2;
+ break;
+ }
+
+ return new Effect.Parallel(
+ [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
+ new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
+ new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
+ ], Object.extend({
+ beforeStartInternal: function(effect) {
+ effect.effects[0].element.makePositioned().makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
+ }, options)
+ );
+}
+
+Effect.Pulsate = function(element) {
+ element = $(element);
+ var options = arguments[1] || {};
+ var oldOpacity = element.getInlineOpacity();
+ var transition = options.transition || Effect.Transitions.sinoidal;
+ var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) };
+ reverser.bind(transition);
+ return new Effect.Opacity(element,
+ Object.extend(Object.extend({ duration: 2.0, from: 0,
+ afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
+ }, options), {transition: reverser}));
+}
+
+Effect.Fold = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ width: element.style.width,
+ height: element.style.height };
+ element.makeClipping();
+ return new Effect.Scale(element, 5, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(element, 1, {
+ scaleContent: false,
+ scaleY: false,
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().setStyle(oldStyle);
+ } });
+ }}, arguments[1] || {}));
+};
+
+Effect.Morph = Class.create();
+Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ if(!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ style: ''
+ }, arguments[1] || {});
+ this.start(options);
+ },
+ setup: function(){
+ function parseColor(color){
+ if(!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
+ color = color.parseColor();
+ return $R(0,2).map(function(i){
+ return parseInt( color.slice(i*2+1,i*2+3), 16 )
+ });
+ }
+ this.transforms = this.options.style.parseStyle().map(function(property){
+ var originalValue = this.element.getStyle(property[0]);
+ return $H({
+ style: property[0],
+ originalValue: property[1].unit=='color' ?
+ parseColor(originalValue) : parseFloat(originalValue || 0),
+ targetValue: property[1].unit=='color' ?
+ parseColor(property[1].value) : property[1].value,
+ unit: property[1].unit
+ });
+ }.bind(this)).reject(function(transform){
+ return (
+ (transform.originalValue == transform.targetValue) ||
+ (
+ transform.unit != 'color' &&
+ (isNaN(transform.originalValue) || isNaN(transform.targetValue))
+ )
+ )
+ });
+ },
+ update: function(position) {
+ var style = $H(), value = null;
+ this.transforms.each(function(transform){
+ value = transform.unit=='color' ?
+ $R(0,2).inject('#',function(m,v,i){
+ return m+(Math.round(transform.originalValue[i]+
+ (transform.targetValue[i] - transform.originalValue[i])*position)).toColorPart() }) :
+ transform.originalValue + Math.round(
+ ((transform.targetValue - transform.originalValue) * position) * 1000)/1000 + transform.unit;
+ style[transform.style] = value;
+ });
+ this.element.setStyle(style);
+ }
+});
+
+Effect.Transform = Class.create();
+Object.extend(Effect.Transform.prototype, {
+ initialize: function(tracks){
+ this.tracks = [];
+ this.options = arguments[1] || {};
+ this.addTracks(tracks);
+ },
+ addTracks: function(tracks){
+ tracks.each(function(track){
+ var data = $H(track).values().first();
+ this.tracks.push($H({
+ ids: $H(track).keys().first(),
+ effect: Effect.Morph,
+ options: { style: data }
+ }));
+ }.bind(this));
+ return this;
+ },
+ play: function(){
+ return new Effect.Parallel(
+ this.tracks.map(function(track){
+ var elements = [$(track.ids) || $$(track.ids)].flatten();
+ return elements.map(function(e){ return new track.effect(e, Object.extend({ sync:true }, track.options)) });
+ }).flatten(),
+ this.options
+ );
+ }
+});
+
+Element.CSS_PROPERTIES = ['azimuth', 'backgroundAttachment', 'backgroundColor', 'backgroundImage',
+ 'backgroundPosition', 'backgroundRepeat', 'borderBottomColor', 'borderBottomStyle',
+ 'borderBottomWidth', 'borderCollapse', 'borderLeftColor', 'borderLeftStyle', 'borderLeftWidth',
+ 'borderRightColor', 'borderRightStyle', 'borderRightWidth', 'borderSpacing', 'borderTopColor',
+ 'borderTopStyle', 'borderTopWidth', 'bottom', 'captionSide', 'clear', 'clip', 'color', 'content',
+ 'counterIncrement', 'counterReset', 'cssFloat', 'cueAfter', 'cueBefore', 'cursor', 'direction',
+ 'display', 'elevation', 'emptyCells', 'fontFamily', 'fontSize', 'fontSizeAdjust', 'fontStretch',
+ 'fontStyle', 'fontVariant', 'fontWeight', 'height', 'left', 'letterSpacing', 'lineHeight',
+ 'listStyleImage', 'listStylePosition', 'listStyleType', 'marginBottom', 'marginLeft', 'marginRight',
+ 'marginTop', 'markerOffset', 'marks', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'opacity',
+ 'orphans', 'outlineColor', 'outlineOffset', 'outlineStyle', 'outlineWidth', 'overflowX', 'overflowY',
+ 'paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop', 'page', 'pageBreakAfter', 'pageBreakBefore',
+ 'pageBreakInside', 'pauseAfter', 'pauseBefore', 'pitch', 'pitchRange', 'position', 'quotes',
+ 'richness', 'right', 'size', 'speakHeader', 'speakNumeral', 'speakPunctuation', 'speechRate', 'stress',
+ 'tableLayout', 'textAlign', 'textDecoration', 'textIndent', 'textShadow', 'textTransform', 'top',
+ 'unicodeBidi', 'verticalAlign', 'visibility', 'voiceFamily', 'volume', 'whiteSpace', 'widows',
+ 'width', 'wordSpacing', 'zIndex'];
+
+Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
+
+String.prototype.parseStyle = function(){
+ var element = Element.extend(document.createElement('div'));
+ element.innerHTML = '';
+ var style = element.down().style, styleRules = $H();
+
+ Element.CSS_PROPERTIES.each(function(property){
+ if(style[property]) styleRules[property] = style[property];
+ });
+
+ var result = $H();
+
+ styleRules.each(function(pair){
+ var property = pair[0], value = pair[1], unit = null;
+
+ if(value.parseColor('#zzzzzz') != '#zzzzzz') {
+ value = value.parseColor();
+ unit = 'color';
+ } else if(Element.CSS_LENGTH.test(value))
+ var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/),
+ value = parseFloat(components[1]), unit = (components.length == 3) ? components[2] : null;
+
+ result[property.underscore().dasherize()] = $H({ value:value, unit:unit });
+ }.bind(this));
+
+ return result;
+};
+
+Element.morph = function(element, style) {
+ new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {}));
+ return element;
+};
+
+['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
+ 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each(
+ function(f) { Element.Methods[f] = Element[f]; }
+);
+
+Element.Methods.visualEffect = function(element, effect, options) {
+ s = effect.gsub(/_/, '-').camelize();
+ effect_class = s.charAt(0).toUpperCase() + s.substring(1);
+ new Effect[effect_class](element, options);
+ return $(element);
+};
+
+Element.addMethods();
\ No newline at end of file
diff --git a/public/javascripts/geoRssOverlay.js b/public/javascripts/geoRssOverlay.js
new file mode 100644
index 0000000..315c26d
--- /dev/null
+++ b/public/javascripts/geoRssOverlay.js
@@ -0,0 +1,194 @@
+// GeoRssOverlay: GMaps API extension to display a group of markers from
+// a RSS feed
+//
+// Copyright 2006 Mikel Maron (email: mikel_maron yahoo com)
+//
+// The original version of this code is called MGeoRSS and can be found
+// at the following address:
+// http://brainoff.com/gmaps/mgeorss.html
+//
+// Modified by Andrew Turner to add support for the GeoRss Simple vocabulary
+//
+// Modified and bundled with YM4R in accordance with the following
+// license:
+//
+// This work is public domain
+
+function GeoRssOverlay(rssurl,icon,proxyurl,options){
+ this.rssurl = rssurl;
+ this.icon = icon;
+ this.proxyurl = proxyurl;
+ if(options['visible'] == undefined)
+ this.visible = true;
+ else
+ this.visible = options['visible'];
+ this.listDiv = options['listDiv']; //ID of the item list DIV
+ this.contentDiv = options['contentDiv']; //ID of the content DIV
+ this.listItemClass = options['listItemClass']; //Class of the list item DIV
+ this.limitItems = options['limit']; //Maximum number of displayed entries
+ this.request = false;
+ this.markers = [];
+}
+
+GeoRssOverlay.prototype = new GOverlay();
+
+GeoRssOverlay.prototype.initialize=function(map) {
+ this.map = map;
+ this.load();
+}
+
+GeoRssOverlay.prototype.redraw = function(force){
+ //nothing to do : the markers are already taken care of
+}
+
+GeoRssOverlay.prototype.remove = function(){
+ for(var i= 0, len = this.markers.length ; i< len; i++){
+ this.map.removeOverlay(this.markers[i]);
+ }
+}
+
+GeoRssOverlay.prototype.showHide=function() {
+ if (this.visible) {
+ for (var i=0;i" + title + "
" + description;
+
+ if(this.contentDiv == undefined){
+ GEvent.addListener(marker, "click", function() {
+ marker.openInfoWindowHtml(html);
+ });
+ }else{
+ var contentDiv = this.contentDiv;
+ GEvent.addListener(marker, "click", function() {
+ document.getElementById(contentDiv).innerHTML = html;
+ });
+ }
+
+ if(this.listDiv != undefined){
+ var a = document.createElement('a');
+ a.innerHTML = title;
+ a.setAttribute("href","#");
+ var georss = this;
+ a.onclick = function(){
+ georss.showMarker(index);
+ return false;
+ };
+ var div = document.createElement('div');
+ if(this.listItemClass != undefined){
+ div.setAttribute("class",this.listItemClass);
+ }
+ div.appendChild(a);
+ document.getElementById(this.listDiv).appendChild(div);
+ }
+
+ return marker;
+}
diff --git a/public/javascripts/markerGroup.js b/public/javascripts/markerGroup.js
new file mode 100644
index 0000000..02fe624
--- /dev/null
+++ b/public/javascripts/markerGroup.js
@@ -0,0 +1,114 @@
+function GMarkerGroup(active, markers, markersById) {
+ this.active = active;
+ this.markers = markers || new Array();
+ this.markersById = markersById || new Object();
+}
+
+GMarkerGroup.prototype = new GOverlay();
+
+GMarkerGroup.prototype.initialize = function(map) {
+ this.map = map;
+
+ if(this.active){
+ for(var i = 0 , len = this.markers.length; i < len; i++) {
+ this.map.addOverlay(this.markers[i]);
+ }
+ for(var id in this.markersById){
+ this.map.addOverlay(this.markersById[id]);
+ }
+ }
+}
+
+//If not already done (ie if not inactive) remove all the markers from the map
+GMarkerGroup.prototype.remove = function() {
+ this.deactivate();
+}
+
+GMarkerGroup.prototype.redraw = function(force){
+ //Nothing to do : markers are already taken care of
+}
+
+//Copy the data to a new Marker Group
+GMarkerGroup.prototype.copy = function() {
+ var overlay = new GMarkerGroup(this.active);
+ overlay.markers = this.markers; //Need to do deep copy
+ overlay.markersById = this.markersById; //Need to do deep copy
+ return overlay;
+}
+
+//Inactivate the Marker group and clear the internal content
+GMarkerGroup.prototype.clear = function(){
+ //deactivate the map first (which removes the markers from the map)
+ this.deactivate();
+ //Clear the internal content
+ this.markers = new Array();
+ this.markersById = new Object();
+}
+
+//Add a marker to the GMarkerGroup ; Adds it now to the map if the GMarkerGroup is active
+GMarkerGroup.prototype.addMarker = function(marker,id){
+ if(id == undefined){
+ this.markers.push(marker);
+ }else{
+ this.markersById[id] = marker;
+ }
+ if(this.active && this.map != undefined ){
+ this.map.addOverlay(marker);
+ }
+}
+
+//Open the info window (or info window tabs) of a marker
+GMarkerGroup.prototype.showMarker = function(id){
+ var marker = this.markersById[id];
+ if(marker != undefined){
+ GEvent.trigger(marker,"click");
+ }
+}
+
+//Activate (or deactivate depending on the argument) the GMarkerGroup
+GMarkerGroup.prototype.activate = function(active){
+ active = (active == undefined) ? true : active;
+ if(!active){
+ if(this.active){
+ if(this.map != undefined){
+ for(var i = 0 , len = this.markers.length; i < len; i++){
+ this.map.removeOverlay(this.markers[i])
+ }
+ for(var id in this.markersById){
+ this.map.removeOverlay(this.markersById[id]);
+ }
+ }
+ this.active = false;
+ }
+ }else{
+ if(!this.active){
+ if(this.map != undefined){
+ for(var i = 0 , len = this.markers.length; i < len; i++){
+ this.map.addOverlay(this.markers[i]);
+ }
+ for(var id in this.markersById){
+ this.map.addOverlay(this.markersById[id]);
+ }
+ }
+ this.active = true;
+ }
+ }
+}
+
+GMarkerGroup.prototype.centerAndZoomOnMarkers = function() {
+ if(this.map != undefined){
+ //merge markers and markersById
+ var tmpMarkers = this.markers.slice();
+ for (var id in this.markersById){
+ tmpMarkers.push(this.markersById[id]);
+ }
+ if(tmpMarkers.length > 0){
+ this.map.centerAndZoomOnMarkers(tmpMarkers);
+ }
+ }
+}
+
+//Deactivate the Group Overlay (convenience method)
+GMarkerGroup.prototype.deactivate = function(){
+ this.activate(false);
+}
diff --git a/public/javascripts/prototype.js b/public/javascripts/prototype.js
new file mode 100644
index 0000000..5058221
--- /dev/null
+++ b/public/javascripts/prototype.js
@@ -0,0 +1,2515 @@
+/* Prototype JavaScript framework, version 1.5.0
+ * (c) 2005-2007 Sam Stephenson
+ *
+ * Prototype is freely distributable under the terms of an MIT-style license.
+ * For details, see the Prototype web site: http://prototype.conio.net/
+ *
+/*--------------------------------------------------------------------------*/
+
+var Prototype = {
+ Version: '1.5.0',
+ BrowserFeatures: {
+ XPath: !!document.evaluate
+ },
+
+ ScriptFragment: '(?:)((\n|\r|.)*?)(?:<\/script>)',
+ emptyFunction: function() {},
+ K: function(x) { return x }
+}
+
+var Class = {
+ create: function() {
+ return function() {
+ this.initialize.apply(this, arguments);
+ }
+ }
+}
+
+var Abstract = new Object();
+
+Object.extend = function(destination, source) {
+ for (var property in source) {
+ destination[property] = source[property];
+ }
+ return destination;
+}
+
+Object.extend(Object, {
+ inspect: function(object) {
+ try {
+ if (object === undefined) return 'undefined';
+ if (object === null) return 'null';
+ return object.inspect ? object.inspect() : object.toString();
+ } catch (e) {
+ if (e instanceof RangeError) return '...';
+ throw e;
+ }
+ },
+
+ keys: function(object) {
+ var keys = [];
+ for (var property in object)
+ keys.push(property);
+ return keys;
+ },
+
+ values: function(object) {
+ var values = [];
+ for (var property in object)
+ values.push(object[property]);
+ return values;
+ },
+
+ clone: function(object) {
+ return Object.extend({}, object);
+ }
+});
+
+Function.prototype.bind = function() {
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function() {
+ return __method.apply(object, args.concat($A(arguments)));
+ }
+}
+
+Function.prototype.bindAsEventListener = function(object) {
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function(event) {
+ return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments)));
+ }
+}
+
+Object.extend(Number.prototype, {
+ toColorPart: function() {
+ var digits = this.toString(16);
+ if (this < 16) return '0' + digits;
+ return digits;
+ },
+
+ succ: function() {
+ return this + 1;
+ },
+
+ times: function(iterator) {
+ $R(0, this, true).each(iterator);
+ return this;
+ }
+});
+
+var Try = {
+ these: function() {
+ var returnValue;
+
+ for (var i = 0, length = arguments.length; i < length; i++) {
+ var lambda = arguments[i];
+ try {
+ returnValue = lambda();
+ break;
+ } catch (e) {}
+ }
+
+ return returnValue;
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create();
+PeriodicalExecuter.prototype = {
+ initialize: function(callback, frequency) {
+ this.callback = callback;
+ this.frequency = frequency;
+ this.currentlyExecuting = false;
+
+ this.registerCallback();
+ },
+
+ registerCallback: function() {
+ this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+ },
+
+ stop: function() {
+ if (!this.timer) return;
+ clearInterval(this.timer);
+ this.timer = null;
+ },
+
+ onTimerEvent: function() {
+ if (!this.currentlyExecuting) {
+ try {
+ this.currentlyExecuting = true;
+ this.callback(this);
+ } finally {
+ this.currentlyExecuting = false;
+ }
+ }
+ }
+}
+String.interpret = function(value){
+ return value == null ? '' : String(value);
+}
+
+Object.extend(String.prototype, {
+ gsub: function(pattern, replacement) {
+ var result = '', source = this, match;
+ replacement = arguments.callee.prepareReplacement(replacement);
+
+ while (source.length > 0) {
+ if (match = source.match(pattern)) {
+ result += source.slice(0, match.index);
+ result += String.interpret(replacement(match));
+ source = source.slice(match.index + match[0].length);
+ } else {
+ result += source, source = '';
+ }
+ }
+ return result;
+ },
+
+ sub: function(pattern, replacement, count) {
+ replacement = this.gsub.prepareReplacement(replacement);
+ count = count === undefined ? 1 : count;
+
+ return this.gsub(pattern, function(match) {
+ if (--count < 0) return match[0];
+ return replacement(match);
+ });
+ },
+
+ scan: function(pattern, iterator) {
+ this.gsub(pattern, iterator);
+ return this;
+ },
+
+ truncate: function(length, truncation) {
+ length = length || 30;
+ truncation = truncation === undefined ? '...' : truncation;
+ return this.length > length ?
+ this.slice(0, length - truncation.length) + truncation : this;
+ },
+
+ strip: function() {
+ return this.replace(/^\s+/, '').replace(/\s+$/, '');
+ },
+
+ stripTags: function() {
+ return this.replace(/<\/?[^>]+>/gi, '');
+ },
+
+ stripScripts: function() {
+ return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+ },
+
+ extractScripts: function() {
+ var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+ var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+ return (this.match(matchAll) || []).map(function(scriptTag) {
+ return (scriptTag.match(matchOne) || ['', ''])[1];
+ });
+ },
+
+ evalScripts: function() {
+ return this.extractScripts().map(function(script) { return eval(script) });
+ },
+
+ escapeHTML: function() {
+ var div = document.createElement('div');
+ var text = document.createTextNode(this);
+ div.appendChild(text);
+ return div.innerHTML;
+ },
+
+ unescapeHTML: function() {
+ var div = document.createElement('div');
+ div.innerHTML = this.stripTags();
+ return div.childNodes[0] ? (div.childNodes.length > 1 ?
+ $A(div.childNodes).inject('',function(memo,node){ return memo+node.nodeValue }) :
+ div.childNodes[0].nodeValue) : '';
+ },
+
+ toQueryParams: function(separator) {
+ var match = this.strip().match(/([^?#]*)(#.*)?$/);
+ if (!match) return {};
+
+ return match[1].split(separator || '&').inject({}, function(hash, pair) {
+ if ((pair = pair.split('='))[0]) {
+ var name = decodeURIComponent(pair[0]);
+ var value = pair[1] ? decodeURIComponent(pair[1]) : undefined;
+
+ if (hash[name] !== undefined) {
+ if (hash[name].constructor != Array)
+ hash[name] = [hash[name]];
+ if (value) hash[name].push(value);
+ }
+ else hash[name] = value;
+ }
+ return hash;
+ });
+ },
+
+ toArray: function() {
+ return this.split('');
+ },
+
+ succ: function() {
+ return this.slice(0, this.length - 1) +
+ String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+ },
+
+ camelize: function() {
+ var parts = this.split('-'), len = parts.length;
+ if (len == 1) return parts[0];
+
+ var camelized = this.charAt(0) == '-'
+ ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
+ : parts[0];
+
+ for (var i = 1; i < len; i++)
+ camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
+
+ return camelized;
+ },
+
+ capitalize: function(){
+ return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+ },
+
+ underscore: function() {
+ return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
+ },
+
+ dasherize: function() {
+ return this.gsub(/_/,'-');
+ },
+
+ inspect: function(useDoubleQuotes) {
+ var escapedString = this.replace(/\\/g, '\\\\');
+ if (useDoubleQuotes)
+ return '"' + escapedString.replace(/"/g, '\\"') + '"';
+ else
+ return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+ }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+ if (typeof replacement == 'function') return replacement;
+ var template = new Template(replacement);
+ return function(match) { return template.evaluate(match) };
+}
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+var Template = Class.create();
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+Template.prototype = {
+ initialize: function(template, pattern) {
+ this.template = template.toString();
+ this.pattern = pattern || Template.Pattern;
+ },
+
+ evaluate: function(object) {
+ return this.template.gsub(this.pattern, function(match) {
+ var before = match[1];
+ if (before == '\\') return match[2];
+ return before + String.interpret(object[match[3]]);
+ });
+ }
+}
+
+var $break = new Object();
+var $continue = new Object();
+
+var Enumerable = {
+ each: function(iterator) {
+ var index = 0;
+ try {
+ this._each(function(value) {
+ try {
+ iterator(value, index++);
+ } catch (e) {
+ if (e != $continue) throw e;
+ }
+ });
+ } catch (e) {
+ if (e != $break) throw e;
+ }
+ return this;
+ },
+
+ eachSlice: function(number, iterator) {
+ var index = -number, slices = [], array = this.toArray();
+ while ((index += number) < array.length)
+ slices.push(array.slice(index, index+number));
+ return slices.map(iterator);
+ },
+
+ all: function(iterator) {
+ var result = true;
+ this.each(function(value, index) {
+ result = result && !!(iterator || Prototype.K)(value, index);
+ if (!result) throw $break;
+ });
+ return result;
+ },
+
+ any: function(iterator) {
+ var result = false;
+ this.each(function(value, index) {
+ if (result = !!(iterator || Prototype.K)(value, index))
+ throw $break;
+ });
+ return result;
+ },
+
+ collect: function(iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ results.push((iterator || Prototype.K)(value, index));
+ });
+ return results;
+ },
+
+ detect: function(iterator) {
+ var result;
+ this.each(function(value, index) {
+ if (iterator(value, index)) {
+ result = value;
+ throw $break;
+ }
+ });
+ return result;
+ },
+
+ findAll: function(iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ if (iterator(value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ grep: function(pattern, iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ var stringValue = value.toString();
+ if (stringValue.match(pattern))
+ results.push((iterator || Prototype.K)(value, index));
+ })
+ return results;
+ },
+
+ include: function(object) {
+ var found = false;
+ this.each(function(value) {
+ if (value == object) {
+ found = true;
+ throw $break;
+ }
+ });
+ return found;
+ },
+
+ inGroupsOf: function(number, fillWith) {
+ fillWith = fillWith === undefined ? null : fillWith;
+ return this.eachSlice(number, function(slice) {
+ while(slice.length < number) slice.push(fillWith);
+ return slice;
+ });
+ },
+
+ inject: function(memo, iterator) {
+ this.each(function(value, index) {
+ memo = iterator(memo, value, index);
+ });
+ return memo;
+ },
+
+ invoke: function(method) {
+ var args = $A(arguments).slice(1);
+ return this.map(function(value) {
+ return value[method].apply(value, args);
+ });
+ },
+
+ max: function(iterator) {
+ var result;
+ this.each(function(value, index) {
+ value = (iterator || Prototype.K)(value, index);
+ if (result == undefined || value >= result)
+ result = value;
+ });
+ return result;
+ },
+
+ min: function(iterator) {
+ var result;
+ this.each(function(value, index) {
+ value = (iterator || Prototype.K)(value, index);
+ if (result == undefined || value < result)
+ result = value;
+ });
+ return result;
+ },
+
+ partition: function(iterator) {
+ var trues = [], falses = [];
+ this.each(function(value, index) {
+ ((iterator || Prototype.K)(value, index) ?
+ trues : falses).push(value);
+ });
+ return [trues, falses];
+ },
+
+ pluck: function(property) {
+ var results = [];
+ this.each(function(value, index) {
+ results.push(value[property]);
+ });
+ return results;
+ },
+
+ reject: function(iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ if (!iterator(value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ sortBy: function(iterator) {
+ return this.map(function(value, index) {
+ return {value: value, criteria: iterator(value, index)};
+ }).sort(function(left, right) {
+ var a = left.criteria, b = right.criteria;
+ return a < b ? -1 : a > b ? 1 : 0;
+ }).pluck('value');
+ },
+
+ toArray: function() {
+ return this.map();
+ },
+
+ zip: function() {
+ var iterator = Prototype.K, args = $A(arguments);
+ if (typeof args.last() == 'function')
+ iterator = args.pop();
+
+ var collections = [this].concat(args).map($A);
+ return this.map(function(value, index) {
+ return iterator(collections.pluck(index));
+ });
+ },
+
+ size: function() {
+ return this.toArray().length;
+ },
+
+ inspect: function() {
+ return '#';
+ }
+}
+
+Object.extend(Enumerable, {
+ map: Enumerable.collect,
+ find: Enumerable.detect,
+ select: Enumerable.findAll,
+ member: Enumerable.include,
+ entries: Enumerable.toArray
+});
+var $A = Array.from = function(iterable) {
+ if (!iterable) return [];
+ if (iterable.toArray) {
+ return iterable.toArray();
+ } else {
+ var results = [];
+ for (var i = 0, length = iterable.length; i < length; i++)
+ results.push(iterable[i]);
+ return results;
+ }
+}
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse)
+ Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+ _each: function(iterator) {
+ for (var i = 0, length = this.length; i < length; i++)
+ iterator(this[i]);
+ },
+
+ clear: function() {
+ this.length = 0;
+ return this;
+ },
+
+ first: function() {
+ return this[0];
+ },
+
+ last: function() {
+ return this[this.length - 1];
+ },
+
+ compact: function() {
+ return this.select(function(value) {
+ return value != null;
+ });
+ },
+
+ flatten: function() {
+ return this.inject([], function(array, value) {
+ return array.concat(value && value.constructor == Array ?
+ value.flatten() : [value]);
+ });
+ },
+
+ without: function() {
+ var values = $A(arguments);
+ return this.select(function(value) {
+ return !values.include(value);
+ });
+ },
+
+ indexOf: function(object) {
+ for (var i = 0, length = this.length; i < length; i++)
+ if (this[i] == object) return i;
+ return -1;
+ },
+
+ reverse: function(inline) {
+ return (inline !== false ? this : this.toArray())._reverse();
+ },
+
+ reduce: function() {
+ return this.length > 1 ? this : this[0];
+ },
+
+ uniq: function() {
+ return this.inject([], function(array, value) {
+ return array.include(value) ? array : array.concat([value]);
+ });
+ },
+
+ clone: function() {
+ return [].concat(this);
+ },
+
+ size: function() {
+ return this.length;
+ },
+
+ inspect: function() {
+ return '[' + this.map(Object.inspect).join(', ') + ']';
+ }
+});
+
+Array.prototype.toArray = Array.prototype.clone;
+
+function $w(string){
+ string = string.strip();
+ return string ? string.split(/\s+/) : [];
+}
+
+if(window.opera){
+ Array.prototype.concat = function(){
+ var array = [];
+ for(var i = 0, length = this.length; i < length; i++) array.push(this[i]);
+ for(var i = 0, length = arguments.length; i < length; i++) {
+ if(arguments[i].constructor == Array) {
+ for(var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
+ array.push(arguments[i][j]);
+ } else {
+ array.push(arguments[i]);
+ }
+ }
+ return array;
+ }
+}
+var Hash = function(obj) {
+ Object.extend(this, obj || {});
+};
+
+Object.extend(Hash, {
+ toQueryString: function(obj) {
+ var parts = [];
+
+ this.prototype._each.call(obj, function(pair) {
+ if (!pair.key) return;
+
+ if (pair.value && pair.value.constructor == Array) {
+ var values = pair.value.compact();
+ if (values.length < 2) pair.value = values.reduce();
+ else {
+ key = encodeURIComponent(pair.key);
+ values.each(function(value) {
+ value = value != undefined ? encodeURIComponent(value) : '';
+ parts.push(key + '=' + encodeURIComponent(value));
+ });
+ return;
+ }
+ }
+ if (pair.value == undefined) pair[1] = '';
+ parts.push(pair.map(encodeURIComponent).join('='));
+ });
+
+ return parts.join('&');
+ }
+});
+
+Object.extend(Hash.prototype, Enumerable);
+Object.extend(Hash.prototype, {
+ _each: function(iterator) {
+ for (var key in this) {
+ var value = this[key];
+ if (value && value == Hash.prototype[key]) continue;
+
+ var pair = [key, value];
+ pair.key = key;
+ pair.value = value;
+ iterator(pair);
+ }
+ },
+
+ keys: function() {
+ return this.pluck('key');
+ },
+
+ values: function() {
+ return this.pluck('value');
+ },
+
+ merge: function(hash) {
+ return $H(hash).inject(this, function(mergedHash, pair) {
+ mergedHash[pair.key] = pair.value;
+ return mergedHash;
+ });
+ },
+
+ remove: function() {
+ var result;
+ for(var i = 0, length = arguments.length; i < length; i++) {
+ var value = this[arguments[i]];
+ if (value !== undefined){
+ if (result === undefined) result = value;
+ else {
+ if (result.constructor != Array) result = [result];
+ result.push(value)
+ }
+ }
+ delete this[arguments[i]];
+ }
+ return result;
+ },
+
+ toQueryString: function() {
+ return Hash.toQueryString(this);
+ },
+
+ inspect: function() {
+ return '#';
+ }
+});
+
+function $H(object) {
+ if (object && object.constructor == Hash) return object;
+ return new Hash(object);
+};
+ObjectRange = Class.create();
+Object.extend(ObjectRange.prototype, Enumerable);
+Object.extend(ObjectRange.prototype, {
+ initialize: function(start, end, exclusive) {
+ this.start = start;
+ this.end = end;
+ this.exclusive = exclusive;
+ },
+
+ _each: function(iterator) {
+ var value = this.start;
+ while (this.include(value)) {
+ iterator(value);
+ value = value.succ();
+ }
+ },
+
+ include: function(value) {
+ if (value < this.start)
+ return false;
+ if (this.exclusive)
+ return value < this.end;
+ return value <= this.end;
+ }
+});
+
+var $R = function(start, end, exclusive) {
+ return new ObjectRange(start, end, exclusive);
+}
+
+var Ajax = {
+ getTransport: function() {
+ return Try.these(
+ function() {return new XMLHttpRequest()},
+ function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+ function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+ ) || false;
+ },
+
+ activeRequestCount: 0
+}
+
+Ajax.Responders = {
+ responders: [],
+
+ _each: function(iterator) {
+ this.responders._each(iterator);
+ },
+
+ register: function(responder) {
+ if (!this.include(responder))
+ this.responders.push(responder);
+ },
+
+ unregister: function(responder) {
+ this.responders = this.responders.without(responder);
+ },
+
+ dispatch: function(callback, request, transport, json) {
+ this.each(function(responder) {
+ if (typeof responder[callback] == 'function') {
+ try {
+ responder[callback].apply(responder, [request, transport, json]);
+ } catch (e) {}
+ }
+ });
+ }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+ onCreate: function() {
+ Ajax.activeRequestCount++;
+ },
+ onComplete: function() {
+ Ajax.activeRequestCount--;
+ }
+});
+
+Ajax.Base = function() {};
+Ajax.Base.prototype = {
+ setOptions: function(options) {
+ this.options = {
+ method: 'post',
+ asynchronous: true,
+ contentType: 'application/x-www-form-urlencoded',
+ encoding: 'UTF-8',
+ parameters: ''
+ }
+ Object.extend(this.options, options || {});
+
+ this.options.method = this.options.method.toLowerCase();
+ if (typeof this.options.parameters == 'string')
+ this.options.parameters = this.options.parameters.toQueryParams();
+ }
+}
+
+Ajax.Request = Class.create();
+Ajax.Request.Events =
+ ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+ _complete: false,
+
+ initialize: function(url, options) {
+ this.transport = Ajax.getTransport();
+ this.setOptions(options);
+ this.request(url);
+ },
+
+ request: function(url) {
+ this.url = url;
+ this.method = this.options.method;
+ var params = this.options.parameters;
+
+ if (!['get', 'post'].include(this.method)) {
+ // simulate other verbs over post
+ params['_method'] = this.method;
+ this.method = 'post';
+ }
+
+ params = Hash.toQueryString(params);
+ if (params && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_='
+
+ // when GET, append parameters to URL
+ if (this.method == 'get' && params)
+ this.url += (this.url.indexOf('?') > -1 ? '&' : '?') + params;
+
+ try {
+ Ajax.Responders.dispatch('onCreate', this, this.transport);
+
+ this.transport.open(this.method.toUpperCase(), this.url,
+ this.options.asynchronous);
+
+ if (this.options.asynchronous)
+ setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);
+
+ this.transport.onreadystatechange = this.onStateChange.bind(this);
+ this.setRequestHeaders();
+
+ var body = this.method == 'post' ? (this.options.postBody || params) : null;
+
+ this.transport.send(body);
+
+ /* Force Firefox to handle ready state 4 for synchronous requests */
+ if (!this.options.asynchronous && this.transport.overrideMimeType)
+ this.onStateChange();
+
+ }
+ catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ onStateChange: function() {
+ var readyState = this.transport.readyState;
+ if (readyState > 1 && !((readyState == 4) && this._complete))
+ this.respondToReadyState(this.transport.readyState);
+ },
+
+ setRequestHeaders: function() {
+ var headers = {
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'X-Prototype-Version': Prototype.Version,
+ 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+ };
+
+ if (this.method == 'post') {
+ headers['Content-type'] = this.options.contentType +
+ (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+ /* Force "Connection: close" for older Mozilla browsers to work
+ * around a bug where XMLHttpRequest sends an incorrect
+ * Content-length header. See Mozilla Bugzilla #246651.
+ */
+ if (this.transport.overrideMimeType &&
+ (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
+ headers['Connection'] = 'close';
+ }
+
+ // user-defined headers
+ if (typeof this.options.requestHeaders == 'object') {
+ var extras = this.options.requestHeaders;
+
+ if (typeof extras.push == 'function')
+ for (var i = 0, length = extras.length; i < length; i += 2)
+ headers[extras[i]] = extras[i+1];
+ else
+ $H(extras).each(function(pair) { headers[pair.key] = pair.value });
+ }
+
+ for (var name in headers)
+ this.transport.setRequestHeader(name, headers[name]);
+ },
+
+ success: function() {
+ return !this.transport.status
+ || (this.transport.status >= 200 && this.transport.status < 300);
+ },
+
+ respondToReadyState: function(readyState) {
+ var state = Ajax.Request.Events[readyState];
+ var transport = this.transport, json = this.evalJSON();
+
+ if (state == 'Complete') {
+ try {
+ this._complete = true;
+ (this.options['on' + this.transport.status]
+ || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+ || Prototype.emptyFunction)(transport, json);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ if ((this.getHeader('Content-type') || 'text/javascript').strip().
+ match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
+ this.evalResponse();
+ }
+
+ try {
+ (this.options['on' + state] || Prototype.emptyFunction)(transport, json);
+ Ajax.Responders.dispatch('on' + state, this, transport, json);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ if (state == 'Complete') {
+ // avoid memory leak in MSIE: clean up
+ this.transport.onreadystatechange = Prototype.emptyFunction;
+ }
+ },
+
+ getHeader: function(name) {
+ try {
+ return this.transport.getResponseHeader(name);
+ } catch (e) { return null }
+ },
+
+ evalJSON: function() {
+ try {
+ var json = this.getHeader('X-JSON');
+ return json ? eval('(' + json + ')') : null;
+ } catch (e) { return null }
+ },
+
+ evalResponse: function() {
+ try {
+ return eval(this.transport.responseText);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ dispatchException: function(exception) {
+ (this.options.onException || Prototype.emptyFunction)(this, exception);
+ Ajax.Responders.dispatch('onException', this, exception);
+ }
+});
+
+Ajax.Updater = Class.create();
+
+Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
+ initialize: function(container, url, options) {
+ this.container = {
+ success: (container.success || container),
+ failure: (container.failure || (container.success ? null : container))
+ }
+
+ this.transport = Ajax.getTransport();
+ this.setOptions(options);
+
+ var onComplete = this.options.onComplete || Prototype.emptyFunction;
+ this.options.onComplete = (function(transport, param) {
+ this.updateContent();
+ onComplete(transport, param);
+ }).bind(this);
+
+ this.request(url);
+ },
+
+ updateContent: function() {
+ var receiver = this.container[this.success() ? 'success' : 'failure'];
+ var response = this.transport.responseText;
+
+ if (!this.options.evalScripts) response = response.stripScripts();
+
+ if (receiver = $(receiver)) {
+ if (this.options.insertion)
+ new this.options.insertion(receiver, response);
+ else
+ receiver.update(response);
+ }
+
+ if (this.success()) {
+ if (this.onComplete)
+ setTimeout(this.onComplete.bind(this), 10);
+ }
+ }
+});
+
+Ajax.PeriodicalUpdater = Class.create();
+Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
+ initialize: function(container, url, options) {
+ this.setOptions(options);
+ this.onComplete = this.options.onComplete;
+
+ this.frequency = (this.options.frequency || 2);
+ this.decay = (this.options.decay || 1);
+
+ this.updater = {};
+ this.container = container;
+ this.url = url;
+
+ this.start();
+ },
+
+ start: function() {
+ this.options.onComplete = this.updateComplete.bind(this);
+ this.onTimerEvent();
+ },
+
+ stop: function() {
+ this.updater.options.onComplete = undefined;
+ clearTimeout(this.timer);
+ (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+ },
+
+ updateComplete: function(request) {
+ if (this.options.decay) {
+ this.decay = (request.responseText == this.lastText ?
+ this.decay * this.options.decay : 1);
+
+ this.lastText = request.responseText;
+ }
+ this.timer = setTimeout(this.onTimerEvent.bind(this),
+ this.decay * this.frequency * 1000);
+ },
+
+ onTimerEvent: function() {
+ this.updater = new Ajax.Updater(this.container, this.url, this.options);
+ }
+});
+function $(element) {
+ if (arguments.length > 1) {
+ for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+ elements.push($(arguments[i]));
+ return elements;
+ }
+ if (typeof element == 'string')
+ element = document.getElementById(element);
+ return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+ document._getElementsByXPath = function(expression, parentElement) {
+ var results = [];
+ var query = document.evaluate(expression, $(parentElement) || document,
+ null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+ for (var i = 0, length = query.snapshotLength; i < length; i++)
+ results.push(query.snapshotItem(i));
+ return results;
+ };
+}
+
+document.getElementsByClassName = function(className, parentElement) {
+ if (Prototype.BrowserFeatures.XPath) {
+ var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]";
+ return document._getElementsByXPath(q, parentElement);
+ } else {
+ var children = ($(parentElement) || document.body).getElementsByTagName('*');
+ var elements = [], child;
+ for (var i = 0, length = children.length; i < length; i++) {
+ child = children[i];
+ if (Element.hasClassName(child, className))
+ elements.push(Element.extend(child));
+ }
+ return elements;
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Element)
+ var Element = new Object();
+
+Element.extend = function(element) {
+ if (!element || _nativeExtensions || element.nodeType == 3) return element;
+
+ if (!element._extended && element.tagName && element != window) {
+ var methods = Object.clone(Element.Methods), cache = Element.extend.cache;
+
+ if (element.tagName == 'FORM')
+ Object.extend(methods, Form.Methods);
+ if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName))
+ Object.extend(methods, Form.Element.Methods);
+
+ Object.extend(methods, Element.Methods.Simulated);
+
+ for (var property in methods) {
+ var value = methods[property];
+ if (typeof value == 'function' && !(property in element))
+ element[property] = cache.findOrStore(value);
+ }
+ }
+
+ element._extended = true;
+ return element;
+};
+
+Element.extend.cache = {
+ findOrStore: function(value) {
+ return this[value] = this[value] || function() {
+ return value.apply(null, [this].concat($A(arguments)));
+ }
+ }
+};
+
+Element.Methods = {
+ visible: function(element) {
+ return $(element).style.display != 'none';
+ },
+
+ toggle: function(element) {
+ element = $(element);
+ Element[Element.visible(element) ? 'hide' : 'show'](element);
+ return element;
+ },
+
+ hide: function(element) {
+ $(element).style.display = 'none';
+ return element;
+ },
+
+ show: function(element) {
+ $(element).style.display = '';
+ return element;
+ },
+
+ remove: function(element) {
+ element = $(element);
+ element.parentNode.removeChild(element);
+ return element;
+ },
+
+ update: function(element, html) {
+ html = typeof html == 'undefined' ? '' : html.toString();
+ $(element).innerHTML = html.stripScripts();
+ setTimeout(function() {html.evalScripts()}, 10);
+ return element;
+ },
+
+ replace: function(element, html) {
+ element = $(element);
+ html = typeof html == 'undefined' ? '' : html.toString();
+ if (element.outerHTML) {
+ element.outerHTML = html.stripScripts();
+ } else {
+ var range = element.ownerDocument.createRange();
+ range.selectNodeContents(element);
+ element.parentNode.replaceChild(
+ range.createContextualFragment(html.stripScripts()), element);
+ }
+ setTimeout(function() {html.evalScripts()}, 10);
+ return element;
+ },
+
+ inspect: function(element) {
+ element = $(element);
+ var result = '<' + element.tagName.toLowerCase();
+ $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+ var property = pair.first(), attribute = pair.last();
+ var value = (element[property] || '').toString();
+ if (value) result += ' ' + attribute + '=' + value.inspect(true);
+ });
+ return result + '>';
+ },
+
+ recursivelyCollect: function(element, property) {
+ element = $(element);
+ var elements = [];
+ while (element = element[property])
+ if (element.nodeType == 1)
+ elements.push(Element.extend(element));
+ return elements;
+ },
+
+ ancestors: function(element) {
+ return $(element).recursivelyCollect('parentNode');
+ },
+
+ descendants: function(element) {
+ return $A($(element).getElementsByTagName('*'));
+ },
+
+ immediateDescendants: function(element) {
+ if (!(element = $(element).firstChild)) return [];
+ while (element && element.nodeType != 1) element = element.nextSibling;
+ if (element) return [element].concat($(element).nextSiblings());
+ return [];
+ },
+
+ previousSiblings: function(element) {
+ return $(element).recursivelyCollect('previousSibling');
+ },
+
+ nextSiblings: function(element) {
+ return $(element).recursivelyCollect('nextSibling');
+ },
+
+ siblings: function(element) {
+ element = $(element);
+ return element.previousSiblings().reverse().concat(element.nextSiblings());
+ },
+
+ match: function(element, selector) {
+ if (typeof selector == 'string')
+ selector = new Selector(selector);
+ return selector.match($(element));
+ },
+
+ up: function(element, expression, index) {
+ return Selector.findElement($(element).ancestors(), expression, index);
+ },
+
+ down: function(element, expression, index) {
+ return Selector.findElement($(element).descendants(), expression, index);
+ },
+
+ previous: function(element, expression, index) {
+ return Selector.findElement($(element).previousSiblings(), expression, index);
+ },
+
+ next: function(element, expression, index) {
+ return Selector.findElement($(element).nextSiblings(), expression, index);
+ },
+
+ getElementsBySelector: function() {
+ var args = $A(arguments), element = $(args.shift());
+ return Selector.findChildElements(element, args);
+ },
+
+ getElementsByClassName: function(element, className) {
+ return document.getElementsByClassName(className, element);
+ },
+
+ readAttribute: function(element, name) {
+ element = $(element);
+ if (document.all && !window.opera) {
+ var t = Element._attributeTranslations;
+ if (t.values[name]) return t.values[name](element, name);
+ if (t.names[name]) name = t.names[name];
+ var attribute = element.attributes[name];
+ if(attribute) return attribute.nodeValue;
+ }
+ return element.getAttribute(name);
+ },
+
+ getHeight: function(element) {
+ return $(element).getDimensions().height;
+ },
+
+ getWidth: function(element) {
+ return $(element).getDimensions().width;
+ },
+
+ classNames: function(element) {
+ return new Element.ClassNames(element);
+ },
+
+ hasClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ var elementClassName = element.className;
+ if (elementClassName.length == 0) return false;
+ if (elementClassName == className ||
+ elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
+ return true;
+ return false;
+ },
+
+ addClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ Element.classNames(element).add(className);
+ return element;
+ },
+
+ removeClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ Element.classNames(element).remove(className);
+ return element;
+ },
+
+ toggleClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className);
+ return element;
+ },
+
+ observe: function() {
+ Event.observe.apply(Event, arguments);
+ return $A(arguments).first();
+ },
+
+ stopObserving: function() {
+ Event.stopObserving.apply(Event, arguments);
+ return $A(arguments).first();
+ },
+
+ // removes whitespace-only text node children
+ cleanWhitespace: function(element) {
+ element = $(element);
+ var node = element.firstChild;
+ while (node) {
+ var nextNode = node.nextSibling;
+ if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+ element.removeChild(node);
+ node = nextNode;
+ }
+ return element;
+ },
+
+ empty: function(element) {
+ return $(element).innerHTML.match(/^\s*$/);
+ },
+
+ descendantOf: function(element, ancestor) {
+ element = $(element), ancestor = $(ancestor);
+ while (element = element.parentNode)
+ if (element == ancestor) return true;
+ return false;
+ },
+
+ scrollTo: function(element) {
+ element = $(element);
+ var pos = Position.cumulativeOffset(element);
+ window.scrollTo(pos[0], pos[1]);
+ return element;
+ },
+
+ getStyle: function(element, style) {
+ element = $(element);
+ if (['float','cssFloat'].include(style))
+ style = (typeof element.style.styleFloat != 'undefined' ? 'styleFloat' : 'cssFloat');
+ style = style.camelize();
+ var value = element.style[style];
+ if (!value) {
+ if (document.defaultView && document.defaultView.getComputedStyle) {
+ var css = document.defaultView.getComputedStyle(element, null);
+ value = css ? css[style] : null;
+ } else if (element.currentStyle) {
+ value = element.currentStyle[style];
+ }
+ }
+
+ if((value == 'auto') && ['width','height'].include(style) && (element.getStyle('display') != 'none'))
+ value = element['offset'+style.capitalize()] + 'px';
+
+ if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
+ if (Element.getStyle(element, 'position') == 'static') value = 'auto';
+ if(style == 'opacity') {
+ if(value) return parseFloat(value);
+ if(value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+ if(value[1]) return parseFloat(value[1]) / 100;
+ return 1.0;
+ }
+ return value == 'auto' ? null : value;
+ },
+
+ setStyle: function(element, style) {
+ element = $(element);
+ for (var name in style) {
+ var value = style[name];
+ if(name == 'opacity') {
+ if (value == 1) {
+ value = (/Gecko/.test(navigator.userAgent) &&
+ !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 0.999999 : 1.0;
+ if(/MSIE/.test(navigator.userAgent) && !window.opera)
+ element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'');
+ } else if(value == '') {
+ if(/MSIE/.test(navigator.userAgent) && !window.opera)
+ element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'');
+ } else {
+ if(value < 0.00001) value = 0;
+ if(/MSIE/.test(navigator.userAgent) && !window.opera)
+ element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
+ 'alpha(opacity='+value*100+')';
+ }
+ } else if(['float','cssFloat'].include(name)) name = (typeof element.style.styleFloat != 'undefined') ? 'styleFloat' : 'cssFloat';
+ element.style[name.camelize()] = value;
+ }
+ return element;
+ },
+
+ getDimensions: function(element) {
+ element = $(element);
+ var display = $(element).getStyle('display');
+ if (display != 'none' && display != null) // Safari bug
+ return {width: element.offsetWidth, height: element.offsetHeight};
+
+ // All *Width and *Height properties give 0 on elements with display none,
+ // so enable the element temporarily
+ var els = element.style;
+ var originalVisibility = els.visibility;
+ var originalPosition = els.position;
+ var originalDisplay = els.display;
+ els.visibility = 'hidden';
+ els.position = 'absolute';
+ els.display = 'block';
+ var originalWidth = element.clientWidth;
+ var originalHeight = element.clientHeight;
+ els.display = originalDisplay;
+ els.position = originalPosition;
+ els.visibility = originalVisibility;
+ return {width: originalWidth, height: originalHeight};
+ },
+
+ makePositioned: function(element) {
+ element = $(element);
+ var pos = Element.getStyle(element, 'position');
+ if (pos == 'static' || !pos) {
+ element._madePositioned = true;
+ element.style.position = 'relative';
+ // Opera returns the offset relative to the positioning context, when an
+ // element is position relative but top and left have not been defined
+ if (window.opera) {
+ element.style.top = 0;
+ element.style.left = 0;
+ }
+ }
+ return element;
+ },
+
+ undoPositioned: function(element) {
+ element = $(element);
+ if (element._madePositioned) {
+ element._madePositioned = undefined;
+ element.style.position =
+ element.style.top =
+ element.style.left =
+ element.style.bottom =
+ element.style.right = '';
+ }
+ return element;
+ },
+
+ makeClipping: function(element) {
+ element = $(element);
+ if (element._overflow) return element;
+ element._overflow = element.style.overflow || 'auto';
+ if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+ element.style.overflow = 'hidden';
+ return element;
+ },
+
+ undoClipping: function(element) {
+ element = $(element);
+ if (!element._overflow) return element;
+ element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+ element._overflow = null;
+ return element;
+ }
+};
+
+Object.extend(Element.Methods, {childOf: Element.Methods.descendantOf});
+
+Element._attributeTranslations = {};
+
+Element._attributeTranslations.names = {
+ colspan: "colSpan",
+ rowspan: "rowSpan",
+ valign: "vAlign",
+ datetime: "dateTime",
+ accesskey: "accessKey",
+ tabindex: "tabIndex",
+ enctype: "encType",
+ maxlength: "maxLength",
+ readonly: "readOnly",
+ longdesc: "longDesc"
+};
+
+Element._attributeTranslations.values = {
+ _getAttr: function(element, attribute) {
+ return element.getAttribute(attribute, 2);
+ },
+
+ _flag: function(element, attribute) {
+ return $(element).hasAttribute(attribute) ? attribute : null;
+ },
+
+ style: function(element) {
+ return element.style.cssText.toLowerCase();
+ },
+
+ title: function(element) {
+ var node = element.getAttributeNode('title');
+ return node.specified ? node.nodeValue : null;
+ }
+};
+
+Object.extend(Element._attributeTranslations.values, {
+ href: Element._attributeTranslations.values._getAttr,
+ src: Element._attributeTranslations.values._getAttr,
+ disabled: Element._attributeTranslations.values._flag,
+ checked: Element._attributeTranslations.values._flag,
+ readonly: Element._attributeTranslations.values._flag,
+ multiple: Element._attributeTranslations.values._flag
+});
+
+Element.Methods.Simulated = {
+ hasAttribute: function(element, attribute) {
+ var t = Element._attributeTranslations;
+ attribute = t.names[attribute] || attribute;
+ return $(element).getAttributeNode(attribute).specified;
+ }
+};
+
+// IE is missing .innerHTML support for TABLE-related elements
+if (document.all && !window.opera){
+ Element.Methods.update = function(element, html) {
+ element = $(element);
+ html = typeof html == 'undefined' ? '' : html.toString();
+ var tagName = element.tagName.toUpperCase();
+ if (['THEAD','TBODY','TR','TD'].include(tagName)) {
+ var div = document.createElement('div');
+ switch (tagName) {
+ case 'THEAD':
+ case 'TBODY':
+ div.innerHTML = '' + html.stripScripts() + '
';
+ depth = 2;
+ break;
+ case 'TR':
+ div.innerHTML = '' + html.stripScripts() + '
';
+ depth = 3;
+ break;
+ case 'TD':
+ div.innerHTML = '' + html.stripScripts() + ' |
';
+ depth = 4;
+ }
+ $A(element.childNodes).each(function(node){
+ element.removeChild(node)
+ });
+ depth.times(function(){ div = div.firstChild });
+
+ $A(div.childNodes).each(
+ function(node){ element.appendChild(node) });
+ } else {
+ element.innerHTML = html.stripScripts();
+ }
+ setTimeout(function() {html.evalScripts()}, 10);
+ return element;
+ }
+};
+
+Object.extend(Element, Element.Methods);
+
+var _nativeExtensions = false;
+
+if(/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+ ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) {
+ var className = 'HTML' + tag + 'Element';
+ if(window[className]) return;
+ var klass = window[className] = {};
+ klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__;
+ });
+
+Element.addMethods = function(methods) {
+ Object.extend(Element.Methods, methods || {});
+
+ function copy(methods, destination, onlyIfAbsent) {
+ onlyIfAbsent = onlyIfAbsent || false;
+ var cache = Element.extend.cache;
+ for (var property in methods) {
+ var value = methods[property];
+ if (!onlyIfAbsent || !(property in destination))
+ destination[property] = cache.findOrStore(value);
+ }
+ }
+
+ if (typeof HTMLElement != 'undefined') {
+ copy(Element.Methods, HTMLElement.prototype);
+ copy(Element.Methods.Simulated, HTMLElement.prototype, true);
+ copy(Form.Methods, HTMLFormElement.prototype);
+ [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) {
+ copy(Form.Element.Methods, klass.prototype);
+ });
+ _nativeExtensions = true;
+ }
+}
+
+var Toggle = new Object();
+Toggle.display = Element.toggle;
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.Insertion = function(adjacency) {
+ this.adjacency = adjacency;
+}
+
+Abstract.Insertion.prototype = {
+ initialize: function(element, content) {
+ this.element = $(element);
+ this.content = content.stripScripts();
+
+ if (this.adjacency && this.element.insertAdjacentHTML) {
+ try {
+ this.element.insertAdjacentHTML(this.adjacency, this.content);
+ } catch (e) {
+ var tagName = this.element.tagName.toUpperCase();
+ if (['TBODY', 'TR'].include(tagName)) {
+ this.insertContent(this.contentFromAnonymousTable());
+ } else {
+ throw e;
+ }
+ }
+ } else {
+ this.range = this.element.ownerDocument.createRange();
+ if (this.initializeRange) this.initializeRange();
+ this.insertContent([this.range.createContextualFragment(this.content)]);
+ }
+
+ setTimeout(function() {content.evalScripts()}, 10);
+ },
+
+ contentFromAnonymousTable: function() {
+ var div = document.createElement('div');
+ div.innerHTML = '';
+ return $A(div.childNodes[0].childNodes[0].childNodes);
+ }
+}
+
+var Insertion = new Object();
+
+Insertion.Before = Class.create();
+Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
+ initializeRange: function() {
+ this.range.setStartBefore(this.element);
+ },
+
+ insertContent: function(fragments) {
+ fragments.each((function(fragment) {
+ this.element.parentNode.insertBefore(fragment, this.element);
+ }).bind(this));
+ }
+});
+
+Insertion.Top = Class.create();
+Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
+ initializeRange: function() {
+ this.range.selectNodeContents(this.element);
+ this.range.collapse(true);
+ },
+
+ insertContent: function(fragments) {
+ fragments.reverse(false).each((function(fragment) {
+ this.element.insertBefore(fragment, this.element.firstChild);
+ }).bind(this));
+ }
+});
+
+Insertion.Bottom = Class.create();
+Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
+ initializeRange: function() {
+ this.range.selectNodeContents(this.element);
+ this.range.collapse(this.element);
+ },
+
+ insertContent: function(fragments) {
+ fragments.each((function(fragment) {
+ this.element.appendChild(fragment);
+ }).bind(this));
+ }
+});
+
+Insertion.After = Class.create();
+Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
+ initializeRange: function() {
+ this.range.setStartAfter(this.element);
+ },
+
+ insertContent: function(fragments) {
+ fragments.each((function(fragment) {
+ this.element.parentNode.insertBefore(fragment,
+ this.element.nextSibling);
+ }).bind(this));
+ }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+ initialize: function(element) {
+ this.element = $(element);
+ },
+
+ _each: function(iterator) {
+ this.element.className.split(/\s+/).select(function(name) {
+ return name.length > 0;
+ })._each(iterator);
+ },
+
+ set: function(className) {
+ this.element.className = className;
+ },
+
+ add: function(classNameToAdd) {
+ if (this.include(classNameToAdd)) return;
+ this.set($A(this).concat(classNameToAdd).join(' '));
+ },
+
+ remove: function(classNameToRemove) {
+ if (!this.include(classNameToRemove)) return;
+ this.set($A(this).without(classNameToRemove).join(' '));
+ },
+
+ toString: function() {
+ return $A(this).join(' ');
+ }
+};
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+var Selector = Class.create();
+Selector.prototype = {
+ initialize: function(expression) {
+ this.params = {classNames: []};
+ this.expression = expression.toString().strip();
+ this.parseExpression();
+ this.compileMatcher();
+ },
+
+ parseExpression: function() {
+ function abort(message) { throw 'Parse error in selector: ' + message; }
+
+ if (this.expression == '') abort('empty expression');
+
+ var params = this.params, expr = this.expression, match, modifier, clause, rest;
+ while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) {
+ params.attributes = params.attributes || [];
+ params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''});
+ expr = match[1];
+ }
+
+ if (expr == '*') return this.params.wildcard = true;
+
+ while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) {
+ modifier = match[1], clause = match[2], rest = match[3];
+ switch (modifier) {
+ case '#': params.id = clause; break;
+ case '.': params.classNames.push(clause); break;
+ case '':
+ case undefined: params.tagName = clause.toUpperCase(); break;
+ default: abort(expr.inspect());
+ }
+ expr = rest;
+ }
+
+ if (expr.length > 0) abort(expr.inspect());
+ },
+
+ buildMatchExpression: function() {
+ var params = this.params, conditions = [], clause;
+
+ if (params.wildcard)
+ conditions.push('true');
+ if (clause = params.id)
+ conditions.push('element.readAttribute("id") == ' + clause.inspect());
+ if (clause = params.tagName)
+ conditions.push('element.tagName.toUpperCase() == ' + clause.inspect());
+ if ((clause = params.classNames).length > 0)
+ for (var i = 0, length = clause.length; i < length; i++)
+ conditions.push('element.hasClassName(' + clause[i].inspect() + ')');
+ if (clause = params.attributes) {
+ clause.each(function(attribute) {
+ var value = 'element.readAttribute(' + attribute.name.inspect() + ')';
+ var splitValueBy = function(delimiter) {
+ return value + ' && ' + value + '.split(' + delimiter.inspect() + ')';
+ }
+
+ switch (attribute.operator) {
+ case '=': conditions.push(value + ' == ' + attribute.value.inspect()); break;
+ case '~=': conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break;
+ case '|=': conditions.push(
+ splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect()
+ ); break;
+ case '!=': conditions.push(value + ' != ' + attribute.value.inspect()); break;
+ case '':
+ case undefined: conditions.push('element.hasAttribute(' + attribute.name.inspect() + ')'); break;
+ default: throw 'Unknown operator ' + attribute.operator + ' in selector';
+ }
+ });
+ }
+
+ return conditions.join(' && ');
+ },
+
+ compileMatcher: function() {
+ this.match = new Function('element', 'if (!element.tagName) return false; \
+ element = $(element); \
+ return ' + this.buildMatchExpression());
+ },
+
+ findElements: function(scope) {
+ var element;
+
+ if (element = $(this.params.id))
+ if (this.match(element))
+ if (!scope || Element.childOf(element, scope))
+ return [element];
+
+ scope = (scope || document).getElementsByTagName(this.params.tagName || '*');
+
+ var results = [];
+ for (var i = 0, length = scope.length; i < length; i++)
+ if (this.match(element = scope[i]))
+ results.push(Element.extend(element));
+
+ return results;
+ },
+
+ toString: function() {
+ return this.expression;
+ }
+}
+
+Object.extend(Selector, {
+ matchElements: function(elements, expression) {
+ var selector = new Selector(expression);
+ return elements.select(selector.match.bind(selector)).map(Element.extend);
+ },
+
+ findElement: function(elements, expression, index) {
+ if (typeof expression == 'number') index = expression, expression = false;
+ return Selector.matchElements(elements, expression || '*')[index || 0];
+ },
+
+ findChildElements: function(element, expressions) {
+ return expressions.map(function(expression) {
+ return expression.match(/[^\s"]+(?:"[^"]*"[^\s"]+)*/g).inject([null], function(results, expr) {
+ var selector = new Selector(expr);
+ return results.inject([], function(elements, result) {
+ return elements.concat(selector.findElements(result || element));
+ });
+ });
+ }).flatten();
+ }
+});
+
+function $$() {
+ return Selector.findChildElements(document, $A(arguments));
+}
+var Form = {
+ reset: function(form) {
+ $(form).reset();
+ return form;
+ },
+
+ serializeElements: function(elements, getHash) {
+ var data = elements.inject({}, function(result, element) {
+ if (!element.disabled && element.name) {
+ var key = element.name, value = $(element).getValue();
+ if (value != undefined) {
+ if (result[key]) {
+ if (result[key].constructor != Array) result[key] = [result[key]];
+ result[key].push(value);
+ }
+ else result[key] = value;
+ }
+ }
+ return result;
+ });
+
+ return getHash ? data : Hash.toQueryString(data);
+ }
+};
+
+Form.Methods = {
+ serialize: function(form, getHash) {
+ return Form.serializeElements(Form.getElements(form), getHash);
+ },
+
+ getElements: function(form) {
+ return $A($(form).getElementsByTagName('*')).inject([],
+ function(elements, child) {
+ if (Form.Element.Serializers[child.tagName.toLowerCase()])
+ elements.push(Element.extend(child));
+ return elements;
+ }
+ );
+ },
+
+ getInputs: function(form, typeName, name) {
+ form = $(form);
+ var inputs = form.getElementsByTagName('input');
+
+ if (!typeName && !name) return $A(inputs).map(Element.extend);
+
+ for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
+ var input = inputs[i];
+ if ((typeName && input.type != typeName) || (name && input.name != name))
+ continue;
+ matchingInputs.push(Element.extend(input));
+ }
+
+ return matchingInputs;
+ },
+
+ disable: function(form) {
+ form = $(form);
+ form.getElements().each(function(element) {
+ element.blur();
+ element.disabled = 'true';
+ });
+ return form;
+ },
+
+ enable: function(form) {
+ form = $(form);
+ form.getElements().each(function(element) {
+ element.disabled = '';
+ });
+ return form;
+ },
+
+ findFirstElement: function(form) {
+ return $(form).getElements().find(function(element) {
+ return element.type != 'hidden' && !element.disabled &&
+ ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+ });
+ },
+
+ focusFirstElement: function(form) {
+ form = $(form);
+ form.findFirstElement().activate();
+ return form;
+ }
+}
+
+Object.extend(Form, Form.Methods);
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element = {
+ focus: function(element) {
+ $(element).focus();
+ return element;
+ },
+
+ select: function(element) {
+ $(element).select();
+ return element;
+ }
+}
+
+Form.Element.Methods = {
+ serialize: function(element) {
+ element = $(element);
+ if (!element.disabled && element.name) {
+ var value = element.getValue();
+ if (value != undefined) {
+ var pair = {};
+ pair[element.name] = value;
+ return Hash.toQueryString(pair);
+ }
+ }
+ return '';
+ },
+
+ getValue: function(element) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ return Form.Element.Serializers[method](element);
+ },
+
+ clear: function(element) {
+ $(element).value = '';
+ return element;
+ },
+
+ present: function(element) {
+ return $(element).value != '';
+ },
+
+ activate: function(element) {
+ element = $(element);
+ element.focus();
+ if (element.select && ( element.tagName.toLowerCase() != 'input' ||
+ !['button', 'reset', 'submit'].include(element.type) ) )
+ element.select();
+ return element;
+ },
+
+ disable: function(element) {
+ element = $(element);
+ element.disabled = true;
+ return element;
+ },
+
+ enable: function(element) {
+ element = $(element);
+ element.blur();
+ element.disabled = false;
+ return element;
+ }
+}
+
+Object.extend(Form.Element, Form.Element.Methods);
+var Field = Form.Element;
+var $F = Form.Element.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+ input: function(element) {
+ switch (element.type.toLowerCase()) {
+ case 'checkbox':
+ case 'radio':
+ return Form.Element.Serializers.inputSelector(element);
+ default:
+ return Form.Element.Serializers.textarea(element);
+ }
+ },
+
+ inputSelector: function(element) {
+ return element.checked ? element.value : null;
+ },
+
+ textarea: function(element) {
+ return element.value;
+ },
+
+ select: function(element) {
+ return this[element.type == 'select-one' ?
+ 'selectOne' : 'selectMany'](element);
+ },
+
+ selectOne: function(element) {
+ var index = element.selectedIndex;
+ return index >= 0 ? this.optionValue(element.options[index]) : null;
+ },
+
+ selectMany: function(element) {
+ var values, length = element.length;
+ if (!length) return null;
+
+ for (var i = 0, values = []; i < length; i++) {
+ var opt = element.options[i];
+ if (opt.selected) values.push(this.optionValue(opt));
+ }
+ return values;
+ },
+
+ optionValue: function(opt) {
+ // extend element because hasAttribute may not be native
+ return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = function() {}
+Abstract.TimedObserver.prototype = {
+ initialize: function(element, frequency, callback) {
+ this.frequency = frequency;
+ this.element = $(element);
+ this.callback = callback;
+
+ this.lastValue = this.getValue();
+ this.registerCallback();
+ },
+
+ registerCallback: function() {
+ setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+ },
+
+ onTimerEvent: function() {
+ var value = this.getValue();
+ var changed = ('string' == typeof this.lastValue && 'string' == typeof value
+ ? this.lastValue != value : String(this.lastValue) != String(value));
+ if (changed) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ }
+}
+
+Form.Element.Observer = Class.create();
+Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.Observer = Class.create();
+Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = function() {}
+Abstract.EventObserver.prototype = {
+ initialize: function(element, callback) {
+ this.element = $(element);
+ this.callback = callback;
+
+ this.lastValue = this.getValue();
+ if (this.element.tagName.toLowerCase() == 'form')
+ this.registerFormCallbacks();
+ else
+ this.registerCallback(this.element);
+ },
+
+ onElementEvent: function() {
+ var value = this.getValue();
+ if (this.lastValue != value) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ },
+
+ registerFormCallbacks: function() {
+ Form.getElements(this.element).each(this.registerCallback.bind(this));
+ },
+
+ registerCallback: function(element) {
+ if (element.type) {
+ switch (element.type.toLowerCase()) {
+ case 'checkbox':
+ case 'radio':
+ Event.observe(element, 'click', this.onElementEvent.bind(this));
+ break;
+ default:
+ Event.observe(element, 'change', this.onElementEvent.bind(this));
+ break;
+ }
+ }
+ }
+}
+
+Form.Element.EventObserver = Class.create();
+Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.EventObserver = Class.create();
+Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+if (!window.Event) {
+ var Event = new Object();
+}
+
+Object.extend(Event, {
+ KEY_BACKSPACE: 8,
+ KEY_TAB: 9,
+ KEY_RETURN: 13,
+ KEY_ESC: 27,
+ KEY_LEFT: 37,
+ KEY_UP: 38,
+ KEY_RIGHT: 39,
+ KEY_DOWN: 40,
+ KEY_DELETE: 46,
+ KEY_HOME: 36,
+ KEY_END: 35,
+ KEY_PAGEUP: 33,
+ KEY_PAGEDOWN: 34,
+
+ element: function(event) {
+ return event.target || event.srcElement;
+ },
+
+ isLeftClick: function(event) {
+ return (((event.which) && (event.which == 1)) ||
+ ((event.button) && (event.button == 1)));
+ },
+
+ pointerX: function(event) {
+ return event.pageX || (event.clientX +
+ (document.documentElement.scrollLeft || document.body.scrollLeft));
+ },
+
+ pointerY: function(event) {
+ return event.pageY || (event.clientY +
+ (document.documentElement.scrollTop || document.body.scrollTop));
+ },
+
+ stop: function(event) {
+ if (event.preventDefault) {
+ event.preventDefault();
+ event.stopPropagation();
+ } else {
+ event.returnValue = false;
+ event.cancelBubble = true;
+ }
+ },
+
+ // find the first node with the given tagName, starting from the
+ // node the event was triggered on; traverses the DOM upwards
+ findElement: function(event, tagName) {
+ var element = Event.element(event);
+ while (element.parentNode && (!element.tagName ||
+ (element.tagName.toUpperCase() != tagName.toUpperCase())))
+ element = element.parentNode;
+ return element;
+ },
+
+ observers: false,
+
+ _observeAndCache: function(element, name, observer, useCapture) {
+ if (!this.observers) this.observers = [];
+ if (element.addEventListener) {
+ this.observers.push([element, name, observer, useCapture]);
+ element.addEventListener(name, observer, useCapture);
+ } else if (element.attachEvent) {
+ this.observers.push([element, name, observer, useCapture]);
+ element.attachEvent('on' + name, observer);
+ }
+ },
+
+ unloadCache: function() {
+ if (!Event.observers) return;
+ for (var i = 0, length = Event.observers.length; i < length; i++) {
+ Event.stopObserving.apply(this, Event.observers[i]);
+ Event.observers[i][0] = null;
+ }
+ Event.observers = false;
+ },
+
+ observe: function(element, name, observer, useCapture) {
+ element = $(element);
+ useCapture = useCapture || false;
+
+ if (name == 'keypress' &&
+ (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+ || element.attachEvent))
+ name = 'keydown';
+
+ Event._observeAndCache(element, name, observer, useCapture);
+ },
+
+ stopObserving: function(element, name, observer, useCapture) {
+ element = $(element);
+ useCapture = useCapture || false;
+
+ if (name == 'keypress' &&
+ (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+ || element.detachEvent))
+ name = 'keydown';
+
+ if (element.removeEventListener) {
+ element.removeEventListener(name, observer, useCapture);
+ } else if (element.detachEvent) {
+ try {
+ element.detachEvent('on' + name, observer);
+ } catch (e) {}
+ }
+ }
+});
+
+/* prevent memory leaks in IE */
+if (navigator.appVersion.match(/\bMSIE\b/))
+ Event.observe(window, 'unload', Event.unloadCache, false);
+var Position = {
+ // set to true if needed, warning: firefox performance problems
+ // NOT neeeded for page scrolling, only if draggable contained in
+ // scrollable elements
+ includeScrollOffsets: false,
+
+ // must be called before calling withinIncludingScrolloffset, every time the
+ // page is scrolled
+ prepare: function() {
+ this.deltaX = window.pageXOffset
+ || document.documentElement.scrollLeft
+ || document.body.scrollLeft
+ || 0;
+ this.deltaY = window.pageYOffset
+ || document.documentElement.scrollTop
+ || document.body.scrollTop
+ || 0;
+ },
+
+ realOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.scrollTop || 0;
+ valueL += element.scrollLeft || 0;
+ element = element.parentNode;
+ } while (element);
+ return [valueL, valueT];
+ },
+
+ cumulativeOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ } while (element);
+ return [valueL, valueT];
+ },
+
+ positionedOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ if (element) {
+ if(element.tagName=='BODY') break;
+ var p = Element.getStyle(element, 'position');
+ if (p == 'relative' || p == 'absolute') break;
+ }
+ } while (element);
+ return [valueL, valueT];
+ },
+
+ offsetParent: function(element) {
+ if (element.offsetParent) return element.offsetParent;
+ if (element == document.body) return element;
+
+ while ((element = element.parentNode) && element != document.body)
+ if (Element.getStyle(element, 'position') != 'static')
+ return element;
+
+ return document.body;
+ },
+
+ // caches x/y coordinate pair to use with overlap
+ within: function(element, x, y) {
+ if (this.includeScrollOffsets)
+ return this.withinIncludingScrolloffsets(element, x, y);
+ this.xcomp = x;
+ this.ycomp = y;
+ this.offset = this.cumulativeOffset(element);
+
+ return (y >= this.offset[1] &&
+ y < this.offset[1] + element.offsetHeight &&
+ x >= this.offset[0] &&
+ x < this.offset[0] + element.offsetWidth);
+ },
+
+ withinIncludingScrolloffsets: function(element, x, y) {
+ var offsetcache = this.realOffset(element);
+
+ this.xcomp = x + offsetcache[0] - this.deltaX;
+ this.ycomp = y + offsetcache[1] - this.deltaY;
+ this.offset = this.cumulativeOffset(element);
+
+ return (this.ycomp >= this.offset[1] &&
+ this.ycomp < this.offset[1] + element.offsetHeight &&
+ this.xcomp >= this.offset[0] &&
+ this.xcomp < this.offset[0] + element.offsetWidth);
+ },
+
+ // within must be called directly before
+ overlap: function(mode, element) {
+ if (!mode) return 0;
+ if (mode == 'vertical')
+ return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+ element.offsetHeight;
+ if (mode == 'horizontal')
+ return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+ element.offsetWidth;
+ },
+
+ page: function(forElement) {
+ var valueT = 0, valueL = 0;
+
+ var element = forElement;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+
+ // Safari fix
+ if (element.offsetParent==document.body)
+ if (Element.getStyle(element,'position')=='absolute') break;
+
+ } while (element = element.offsetParent);
+
+ element = forElement;
+ do {
+ if (!window.opera || element.tagName=='BODY') {
+ valueT -= element.scrollTop || 0;
+ valueL -= element.scrollLeft || 0;
+ }
+ } while (element = element.parentNode);
+
+ return [valueL, valueT];
+ },
+
+ clone: function(source, target) {
+ var options = Object.extend({
+ setLeft: true,
+ setTop: true,
+ setWidth: true,
+ setHeight: true,
+ offsetTop: 0,
+ offsetLeft: 0
+ }, arguments[2] || {})
+
+ // find page position of source
+ source = $(source);
+ var p = Position.page(source);
+
+ // find coordinate system to use
+ target = $(target);
+ var delta = [0, 0];
+ var parent = null;
+ // delta [0,0] will do fine with position: fixed elements,
+ // position:absolute needs offsetParent deltas
+ if (Element.getStyle(target,'position') == 'absolute') {
+ parent = Position.offsetParent(target);
+ delta = Position.page(parent);
+ }
+
+ // correct by body offsets (fixes Safari)
+ if (parent == document.body) {
+ delta[0] -= document.body.offsetLeft;
+ delta[1] -= document.body.offsetTop;
+ }
+
+ // set position
+ if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px';
+ if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px';
+ if(options.setWidth) target.style.width = source.offsetWidth + 'px';
+ if(options.setHeight) target.style.height = source.offsetHeight + 'px';
+ },
+
+ absolutize: function(element) {
+ element = $(element);
+ if (element.style.position == 'absolute') return;
+ Position.prepare();
+
+ var offsets = Position.positionedOffset(element);
+ var top = offsets[1];
+ var left = offsets[0];
+ var width = element.clientWidth;
+ var height = element.clientHeight;
+
+ element._originalLeft = left - parseFloat(element.style.left || 0);
+ element._originalTop = top - parseFloat(element.style.top || 0);
+ element._originalWidth = element.style.width;
+ element._originalHeight = element.style.height;
+
+ element.style.position = 'absolute';
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ element.style.width = width + 'px';
+ element.style.height = height + 'px';
+ },
+
+ relativize: function(element) {
+ element = $(element);
+ if (element.style.position == 'relative') return;
+ Position.prepare();
+
+ element.style.position = 'relative';
+ var top = parseFloat(element.style.top || 0) - (element._originalTop || 0);
+ var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ element.style.height = element._originalHeight;
+ element.style.width = element._originalWidth;
+ }
+}
+
+// Safari returns margins on body which is incorrect if the child is absolutely
+// positioned. For performance reasons, redefine Position.cumulativeOffset for
+// KHTML/WebKit only.
+if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
+ Position.cumulativeOffset = function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ if (element.offsetParent == document.body)
+ if (Element.getStyle(element, 'position') == 'absolute') break;
+
+ element = element.offsetParent;
+ } while (element);
+
+ return [valueL, valueT];
+ }
+}
+
+Element.addMethods();
\ No newline at end of file
diff --git a/public/javascripts/wms-gs.js b/public/javascripts/wms-gs.js
new file mode 100644
index 0000000..c67146b
--- /dev/null
+++ b/public/javascripts/wms-gs.js
@@ -0,0 +1,69 @@
+/*
+ * Call generic wms service for GoogleMaps v2
+ * John Deck, UC Berkeley
+ * Inspiration & Code from:
+ * Mike Williams http://www.econym.demon.co.uk/googlemaps2/ V2 Reference & custommap code
+ * Brian Flood http://www.spatialdatalogic.com/cs/blogs/brian_flood/archive/2005/07/11/39.aspx V1 WMS code
+ * Kyle Mulka http://blog.kylemulka.com/?p=287 V1 WMS code modifications
+ * http://search.cpan.org/src/RRWO/GPS-Lowrance-0.31/lib/Geo/Coordinates/MercatorMeters.pm
+ *
+ * Modified by Chris Holmes, TOPP to work by default with GeoServer.
+ *
+ * Bundled with YM4R with John Deck's permission.
+ * Slightly modified to fit YM4R.
+ * See johndeck.blogspot.com for the original version and for examples and instructions of how to use it.
+ */
+
+var WGS84_SEMI_MAJOR_AXIS = 6378137.0; //equatorial radius
+var WGS84_ECCENTRICITY = 0.0818191913108718138;
+var DEG2RAD=0.0174532922519943;
+var PI=3.14159267;
+
+function dd2MercMetersLng(p_lng) {
+ return WGS84_SEMI_MAJOR_AXIS * (p_lng*DEG2RAD);
+}
+
+function dd2MercMetersLat(p_lat) {
+ var lat_rad = p_lat * DEG2RAD;
+ return WGS84_SEMI_MAJOR_AXIS * Math.log(Math.tan((lat_rad + PI / 2) / 2) * Math.pow( ((1 - WGS84_ECCENTRICITY * Math.sin(lat_rad)) / (1 + WGS84_ECCENTRICITY * Math.sin(lat_rad))), (WGS84_ECCENTRICITY/2)));
+}
+
+function addWMSPropertiesToLayer(tile_layer,base_url,layers,styles,format,merc_proj,use_geo){
+ tile_layer.format = format;
+ tile_layer.baseURL = base_url;
+ tile_layer.styles = styles;
+ tile_layer.layers = layers;
+ tile_layer.mercatorEpsg = merc_proj;
+ tile_layer.useGeographic = use_geo;
+ return tile_layer;
+}
+
+getTileUrlForWMS=function(a,b,c) {
+ var lULP = new GPoint(a.x*256,(a.y+1)*256);
+ var lLRP = new GPoint((a.x+1)*256,a.y*256);
+ var lUL = G_NORMAL_MAP.getProjection().fromPixelToLatLng(lULP,b,c);
+ var lLR = G_NORMAL_MAP.getProjection().fromPixelToLatLng(lLRP,b,c);
+
+ if (this.useGeographic){
+ var lBbox=lUL.x+","+lUL.y+","+lLR.x+","+lLR.y;
+ var lSRS="EPSG:4326";
+ }else{
+ var lBbox=dd2MercMetersLng(lUL.x)+","+dd2MercMetersLat(lUL.y)+","+dd2MercMetersLng(lLR.x)+","+dd2MercMetersLat(lLR.y);
+ var lSRS="EPSG:" + this.mercatorEpsg;
+ }
+ var lURL=this.baseURL;
+ lURL+="?REQUEST=GetMap";
+ lURL+="&SERVICE=WMS";
+ lURL+="&VERSION=1.1.1";
+ lURL+="&LAYERS="+this.layers;
+ lURL+="&STYLES="+this.styles;
+ lURL+="&FORMAT=image/"+this.format;
+ lURL+="&BGCOLOR=0xFFFFFF";
+ lURL+="&TRANSPARENT=TRUE";
+ lURL+="&SRS="+lSRS;
+ lURL+="&BBOX="+lBbox;
+ lURL+="&WIDTH=256";
+ lURL+="&HEIGHT=256";
+ lURL+="&reaspect=false";
+ return lURL;
+}
diff --git a/public/javascripts/ym4r-gm.js b/public/javascripts/ym4r-gm.js
new file mode 100644
index 0000000..1c768df
--- /dev/null
+++ b/public/javascripts/ym4r-gm.js
@@ -0,0 +1,117 @@
+// JS helper functions for YM4R
+
+function addInfoWindowToMarker(marker,info,options){
+ GEvent.addListener(marker, "click", function() {marker.openInfoWindowHtml(info,options);});
+ return marker;
+}
+
+function addInfoWindowTabsToMarker(marker,info,options){
+ GEvent.addListener(marker, "click", function() {marker.openInfoWindowTabsHtml(info,options);});
+ return marker;
+}
+
+function addPropertiesToLayer(layer,getTile,copyright,opacity,isPng){
+ layer.getTileUrl = getTile;
+ layer.getCopyright = copyright;
+ layer.getOpacity = opacity;
+ layer.isPng = isPng;
+ return layer;
+}
+
+function addOptionsToIcon(icon,options){
+ for(var k in options){
+ icon[k] = options[k];
+ }
+ return icon;
+}
+
+function addCodeToFunction(func,code){
+ if(func == undefined)
+ return code;
+ else{
+ return function(){
+ func();
+ code();
+ }
+ }
+}
+
+function addGeocodingToMarker(marker,address){
+ marker.orig_initialize = marker.initialize;
+ orig_redraw = marker.redraw;
+ marker.redraw = function(force){}; //empty the redraw method so no error when called by addOverlay.
+ marker.initialize = function(map){
+ new GClientGeocoder().getLatLng(address,
+ function(latlng){
+ if(latlng){
+ marker.redraw = orig_redraw;
+ marker.orig_initialize(map); //init before setting point
+ marker.setPoint(latlng);
+ }//do nothing
+ });
+ };
+ return marker;
+}
+
+
+
+GMap2.prototype.centerAndZoomOnMarkers = function(markers) {
+ var bounds = new GLatLngBounds(markers[0].getPoint(),
+ markers[0].getPoint());
+ for (var i=1, len = markers.length ; i
+ updated_at: <%= Time.now.to_s :db %>
+ published_at: <%= Time.now.to_s :db %>
+ category_id: 1
+some_gossip:
+ id: 2
+ user_id: 1
+ title: Rails Updated
+ synopsis: A new update to Rails was released
+ body: Time to update, folks!
+ published: true
+ created_at: <%= Time.now.to_s :db %>
+ updated_at: <%= Time.now.to_s :db %>
+ published_at: <%= Time.now.to_s :db %>
+ category_id: 2
diff --git a/test/fixtures/categories.yml b/test/fixtures/categories.yml
new file mode 100644
index 0000000..2681aea
--- /dev/null
+++ b/test/fixtures/categories.yml
@@ -0,0 +1,6 @@
+site_news:
+ id: 1
+ name: Site News
+gossip:
+ id: 2
+ name: Rails News
diff --git a/test/fixtures/comments.yml b/test/fixtures/comments.yml
new file mode 100644
index 0000000..a4c1fec
--- /dev/null
+++ b/test/fixtures/comments.yml
@@ -0,0 +1,6 @@
+valid_comment:
+ id: 1
+ entry_id: 1
+ user_id: 2
+ body: a quick comment
+ created_at: <%= 1.days.ago.to_s(:db) %>
diff --git a/test/fixtures/emails.yml b/test/fixtures/emails.yml
new file mode 100644
index 0000000..b49c4eb
--- /dev/null
+++ b/test/fixtures/emails.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+one:
+ id: 1
+two:
+ id: 2
diff --git a/test/fixtures/entries.yml b/test/fixtures/entries.yml
new file mode 100644
index 0000000..474353c
--- /dev/null
+++ b/test/fixtures/entries.yml
@@ -0,0 +1,7 @@
+valid_entry:
+ id: 1
+ user_id: 1
+ title: first post
+ body: blah blah
+ created_at: <%= 1.days.ago.to_s(:db) %>
+ updated_at: <%= 1.days.ago.to_s(:db) %>
diff --git a/test/fixtures/forums.yml b/test/fixtures/forums.yml
new file mode 100644
index 0000000..6259550
--- /dev/null
+++ b/test/fixtures/forums.yml
@@ -0,0 +1,4 @@
+valid_forum:
+ id: 1
+ name: Forum 1
+ description: Just a test forum
diff --git a/test/fixtures/friendships.yml b/test/fixtures/friendships.yml
new file mode 100644
index 0000000..9cf1af9
--- /dev/null
+++ b/test/fixtures/friendships.yml
@@ -0,0 +1,5 @@
+valid_friendship:
+ id: 1
+ user_id: 1
+ friend_id: 2
+ xfn_met: true
\ No newline at end of file
diff --git a/test/fixtures/newsletters.yml b/test/fixtures/newsletters.yml
new file mode 100644
index 0000000..39eb3a9
--- /dev/null
+++ b/test/fixtures/newsletters.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+one:
+ id: 1
+ subject: this is a newsletter
+ body: it's very interesting
diff --git a/test/fixtures/pages.yml b/test/fixtures/pages.yml
new file mode 100644
index 0000000..532ad00
--- /dev/null
+++ b/test/fixtures/pages.yml
@@ -0,0 +1,14 @@
+valid_page:
+ id: 1
+ title: Welcome Page
+ permalink: welcome-page
+ body: Welcome to RailsCoders
+invalid_page_short_title:
+ id: 2
+ title: a
+ permalink: a
+ body: The title is shorter than 3 character
+valid_with_auto_permalink:
+ id: 3
+ title: Another Page, but without a permalink
+ body: No permalink is given so should be automatically generated
diff --git a/test/fixtures/photos.yml b/test/fixtures/photos.yml
new file mode 100644
index 0000000..df03fbb
--- /dev/null
+++ b/test/fixtures/photos.yml
@@ -0,0 +1,28 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+parent_photo:
+ id: 1
+ user_id: 1
+ title: a test photo
+ body: just a test
+ content_type: image/jpeg
+ filename: testimage.jpg
+ size: 1000
+ width: 640
+ height: 480
+ created_at: <%= 1.days.ago.to_s(:db) %>
+thumb_photo:
+ id: 2
+ parent_id: 1
+ width: 160
+ height: 120
+ filename: testimage_thumb.jpg
+ thumbnail: thumb
+ created_at: <%= 1.days.ago.to_s(:db) %>
+tiny_photo:
+ id: 3
+ parent_id: 1
+ width: 80
+ height: 80
+ filename: testimage_tiny.jpg
+ thumbnail: tiny
+ created_at: <%= 1.days.ago.to_s(:db) %>
\ No newline at end of file
diff --git a/test/fixtures/posts.yml b/test/fixtures/posts.yml
new file mode 100644
index 0000000..1e54f37
--- /dev/null
+++ b/test/fixtures/posts.yml
@@ -0,0 +1,6 @@
+valid_post:
+ id: 1
+ topic_id: 1
+ user_id: 1
+ body: we have a forum
+ created_at: <%= 1.days.ago.to_s(:db) %>
diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml
new file mode 100644
index 0000000..66ae780
--- /dev/null
+++ b/test/fixtures/roles.yml
@@ -0,0 +1,9 @@
+admin:
+ id: 1
+ name: Administrator
+editor:
+ id: 2
+ name: Editor
+moderator:
+ id: 3
+ name: Moderator
diff --git a/test/fixtures/roles_users.yml b/test/fixtures/roles_users.yml
new file mode 100644
index 0000000..3d0a1bc
--- /dev/null
+++ b/test/fixtures/roles_users.yml
@@ -0,0 +1,9 @@
+admin:
+ role_id: 1
+ user_id: 2
+editor:
+ role_id: 2
+ user_id: 3
+moderator:
+ role_id: 3
+ user_id: 4
diff --git a/test/fixtures/topics.yml b/test/fixtures/topics.yml
new file mode 100644
index 0000000..5171c97
--- /dev/null
+++ b/test/fixtures/topics.yml
@@ -0,0 +1,6 @@
+valid_topic:
+ id: 1
+ forum_id: 1
+ user_id: 1
+ name: we have a forum
+ created_at: <%= 1.days.ago.to_s(:db) %>
diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml
new file mode 100644
index 0000000..ce9028b
--- /dev/null
+++ b/test/fixtures/users.yml
@@ -0,0 +1,26 @@
+valid_user:
+ id: 1
+ username: joe
+ email: joe@example.com
+ hashed_password: 5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5
+ # clear password = 12345
+ profile: Just a regular Joe
+ created_at: <%= 1.days.ago.to_s(:db) %>
+admin_user:
+ id: 2
+ username: admin
+ email: admin@example.com
+ hashed_password: 5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5 # pw = 12345
+ created_at: <%= 1.days.ago.to_s(:db) %>
+editor_user:
+ id: 3
+ username: editor
+ email: editor@example.com
+ hashed_password: 5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5 # pw = 12345
+ created_at: <%= 1.days.ago.to_s(:db) %>
+moderator_user:
+ id: 4
+ username: moderator
+ email: moderator@example.com
+ hashed_password: 5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5
+ created_at: <%= 1.days.ago.to_s(:db) %>
diff --git a/test/fixtures/usertemplates.yml b/test/fixtures/usertemplates.yml
new file mode 100644
index 0000000..b7e1192
--- /dev/null
+++ b/test/fixtures/usertemplates.yml
@@ -0,0 +1,15 @@
+valid_blog_index_for_joe:
+ id: 1
+ user_id: 1
+ name: blog_index
+ body: a template
+valid_blog_entry_for_joe:
+ id: 2
+ user_id: 1
+ name: blog_entry
+ body: a template
+valid_blog_index_for_admin:
+ id: 3
+ user_id: 2
+ name: blog_index
+ body: my template
\ No newline at end of file
diff --git a/test/functional/account_controller_test.rb b/test/functional/account_controller_test.rb
new file mode 100644
index 0000000..67f2daf
--- /dev/null
+++ b/test/functional/account_controller_test.rb
@@ -0,0 +1,35 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'account_controller'
+
+# Re-raise errors caught by the controller.
+class AccountController; def rescue_action(e) raise e end; end
+
+class AccountControllerTest < Test::Unit::TestCase
+ fixtures :users
+
+ def setup
+ @controller = AccountController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_valid_login_and_redirect
+ post :authenticate, :user => {:username => 'joe', :password => '12345'}
+ assert session[:user]
+ assert_response :redirect
+ end
+ def test_invalid_login
+ post :authenticate, :user => {:username => 'joe', :password => 'abc'}
+ assert !session[:user]
+ assert_response :redirect
+ assert_redirected_to :action => 'login'
+ assert flash.has_key?(:error)
+ end
+ def test_logout
+ post :authenticate, :user => {:username => 'joe', :password => '12345'}
+ assert session[:user]
+ post :logout
+ assert !session[:user]
+ assert_response :redirect
+ end
+end
diff --git a/test/functional/articles_controller_test.rb b/test/functional/articles_controller_test.rb
new file mode 100644
index 0000000..14cc5d9
--- /dev/null
+++ b/test/functional/articles_controller_test.rb
@@ -0,0 +1,53 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'articles_controller'
+
+class ArticlesController; def rescue_action(e) raise e end; end
+
+class ArticlesControllerTest < Test::Unit::TestCase
+ fixtures :articles, :users, :roles, :roles_users
+
+ def setup
+ @controller = ArticlesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:articles)
+ end
+
+ def test_index_as_xml
+ @request.env['HTTP_ACCEPT'] = 'application/xml'
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:articles)
+ end
+
+ def test_show
+ get :show, :id => 1
+ assert_response :success
+ assert_not_nil assigns(:article)
+ end
+
+ def test_create_article_with_http_auth_and_xml
+ old_count = Article.count
+ @request.env['HTTP_ACCEPT'] = 'application/xml'
+ @request.env['Authorization'] = 'Basic ' + Base64::b64encode('editor:12345')
+
+ post :create, :article => { :title => 'New article', :synopsis => 'Just a test',
+ :body => 'Nothing to see here', :published => true }
+
+ assert_response :success
+ assert_equal old_count + 1, Article.count
+ assert_not_nil assigns(:article)
+ end
+
+ def test_rest_routing
+ with_options :controller => 'articles' do |test|
+ test.assert_routing 'articles', :action => 'index'
+ test.assert_routing 'articles/1', :action => 'show', :id => '1'
+ end
+ end
+end
diff --git a/test/functional/backend_api_test.rb b/test/functional/backend_api_test.rb
new file mode 100644
index 0000000..5e14475
--- /dev/null
+++ b/test/functional/backend_api_test.rb
@@ -0,0 +1,46 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'backend_controller'
+
+class BackendController; def rescue_action(e) raise e end; end
+
+class BackendControllerApiTest < Test::Unit::TestCase
+ fixtures :users, :entries
+
+ def setup
+ @controller = BackendController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_get_users_blogs
+ blogs = invoke_layered :blogger, :getUsersBlogs, '', 'joe', '12345'
+ assert_equal '1', blogs[0]['blogId']
+ end
+
+ def test_get_post
+ entry = invoke_layered :blogger, :getPost, '', '1', 'joe', '12345'
+ assert_equal '1', entry['postId']
+ end
+
+ def test_get_recent_posts
+ entries = invoke_layered :blogger, :getRecentPosts, '', '1', 'joe', '12345', '1'
+ assert_equal 1, entries.size
+ assert_equal '1', entries[0]['postId']
+ end
+
+ def test_new_post
+ blogs = invoke_layered :blogger, :getUsersBlogs, '', 'joe', '12345'
+ new_post = invoke_layered :blogger, :newPost, '', blogs[0]['blogId'],
+ 'joe', '12345', 'New Post', true
+ assert new_post.is_a?(Integer)
+ end
+
+ def test_new_and_edit_post
+ blogs = invoke_layered :blogger, :getUsersBlogs, '', 'joe', '12345'
+ new_post = invoke_layered :blogger, :newPost, '', blogs[0]['blogId'],
+ 'joe', '12345','New Post', true
+ result = invoke_layered :blogger, :editPost, '', new_post, 'joe', '12345',
+ 'Edited Post', true
+ assert_equal true, result
+ end
+end
diff --git a/test/functional/blogs_controller_test.rb b/test/functional/blogs_controller_test.rb
new file mode 100644
index 0000000..89ae8ce
--- /dev/null
+++ b/test/functional/blogs_controller_test.rb
@@ -0,0 +1,18 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'blogs_controller'
+
+# Re-raise errors caught by the controller.
+class BlogsController; def rescue_action(e) raise e end; end
+
+class BlogsControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = BlogsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/functional/categories_controller_test.rb b/test/functional/categories_controller_test.rb
new file mode 100644
index 0000000..7b7a8bc
--- /dev/null
+++ b/test/functional/categories_controller_test.rb
@@ -0,0 +1,18 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'categories_controller'
+
+# Re-raise errors caught by the controller.
+class CategoriesController; def rescue_action(e) raise e end; end
+
+class CategoriesControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = CategoriesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/functional/comments_controller_test.rb b/test/functional/comments_controller_test.rb
new file mode 100644
index 0000000..c37ef96
--- /dev/null
+++ b/test/functional/comments_controller_test.rb
@@ -0,0 +1,42 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'comments_controller'
+
+# Re-raise errors caught by the controller.
+class CommentsController; def rescue_action(e) raise e end; end
+
+class CommentsControllerTest < Test::Unit::TestCase
+ fixtures :comments, :users, :entries
+
+ def setup
+ @controller = CommentsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_should_create_comment
+ login_as(:valid_user)
+ old_count = Comment.count
+ post :create,{:user_id => 1, :entry_id => 1,
+ :comment => {:body => 'that is great'}}
+ assert_equal old_count+1, Comment.count
+ assert_redirected_to entry_path(:user_id => 1, :id => 1)
+ end
+
+ def test_should_destroy_comment
+ login_as(:valid_user)
+ old_count = Comment.count
+ delete :destroy, :user_id => 1, :entry_id => 1, :id => 1
+ assert_equal old_count-1, Comment.count
+ assert_redirected_to entry_path(:user_id => 1, :id => 1)
+ end
+
+ def test_send_notify_email
+ num_deliveries = ActionMailer::Base.deliveries.size
+
+ login_as(:valid_user)
+ post :create,{:user_id => 1, :entry_id => 1,
+ :comment => {:body => 'that is great'}}
+
+ assert_equal num_deliveries + 1, ActionMailer::Base.deliveries.size
+ end
+end
diff --git a/test/functional/entries_controller_test.rb b/test/functional/entries_controller_test.rb
new file mode 100644
index 0000000..2037d81
--- /dev/null
+++ b/test/functional/entries_controller_test.rb
@@ -0,0 +1,61 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'entries_controller'
+
+# Re-raise errors caught by the controller.
+class EntriesController; def rescue_action(e) raise e end; end
+
+class EntriesControllerTest < Test::Unit::TestCase
+ fixtures :entries, :users
+
+ def setup
+ @controller = EntriesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_should_get_index
+ get :index, {:user_id => 1}
+ assert_response :success
+ assert assigns(:entries)
+ end
+
+ def test_should_get_new
+ login_as(:valid_user)
+ get :new, {:user_id => 1}
+ assert_response :success
+ end
+
+ def test_should_create_entry
+ login_as(:valid_user)
+ old_count = Entry.count
+ post :create, :entry => {:title => 'test entry', :body => 'a blog entry'}
+ assert_equal old_count+1, Entry.count
+ assert_redirected_to entry_path(:user_id => 1, :id => assigns(:entry))
+ end
+
+ def test_should_show_entry
+ get :show, {:user_id => 1, :id => 1}
+ assert_response :success
+ end
+
+ def test_should_get_edit
+ login_as(:valid_user)
+ get :edit, {:user_id => 1, :id => 1}
+ assert_response :success
+ end
+
+ def test_should_update_entry
+ login_as(:valid_user)
+ put :update, {:user_id => 1, :id => 1,
+ :entry => {:title => 'test entry', :body => 'a blog entry'} }
+ assert_redirected_to entry_path(:user_id => 1, :id => 1)
+ end
+
+ def test_should_destroy_entry
+ login_as(:valid_user)
+ old_count = Entry.count
+ delete :destroy, {:user_id => 1, :id => 1}
+ assert_equal old_count-1, Entry.count
+ assert_redirected_to entries_path
+ end
+end
diff --git a/test/functional/forums_controller_test.rb b/test/functional/forums_controller_test.rb
new file mode 100644
index 0000000..ade7a67
--- /dev/null
+++ b/test/functional/forums_controller_test.rb
@@ -0,0 +1,61 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'forums_controller'
+
+# Re-raise errors caught by the controller.
+class ForumsController; def rescue_action(e) raise e end; end
+
+class ForumsControllerTest < Test::Unit::TestCase
+ fixtures :forums, :users, :roles, :roles_users
+
+ def setup
+ @controller = ForumsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_should_get_index
+ get :index
+ assert_response :success
+ assert assigns(:forums)
+ end
+
+ def test_should_get_new
+ login_as(:moderator_user)
+ get :new
+ assert_response :success
+ end
+
+ def test_should_create_forum
+ login_as(:moderator_user)
+ old_count = Forum.count
+ post :create, :forum => { :name => 'testing', :description => 'just a test'}
+ assert_equal old_count+1, Forum.count
+ assert_redirected_to forums_path
+ end
+
+ def test_should_show_forum
+ get :show, :id => 1
+ assert_redirected_to :controller => 'topics', :action => 'index', :forum_id => 1
+ end
+
+ def test_should_get_edit
+ login_as(:moderator_user)
+ get :edit, :id => 1
+ assert_response :success
+ end
+
+ def test_should_update_forum
+ login_as(:moderator_user)
+ put :update, :id => 1, :forum => { :name => 'testing', :description => 'a test'}
+ assert_redirected_to forum_path(assigns(:forum))
+ end
+
+ def test_should_destroy_forum
+ login_as(:moderator_user)
+ old_count = Forum.count
+ delete :destroy, :id => 1
+ assert_equal old_count-1, Forum.count
+
+ assert_redirected_to forums_path
+ end
+end
diff --git a/test/functional/friends_controller_test.rb b/test/functional/friends_controller_test.rb
new file mode 100644
index 0000000..172d523
--- /dev/null
+++ b/test/functional/friends_controller_test.rb
@@ -0,0 +1,64 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'friends_controller'
+
+# Re-raise errors caught by the controller.
+class FriendsController; def rescue_action(e) raise e end; end
+
+class FriendsControllerTest < Test::Unit::TestCase
+ fixtures :friendships, :users, :roles, :roles_users
+
+ def setup
+ @controller = FriendsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_should_get_index
+ get :index, {:user_id => 1}
+ assert_response :success
+ assert assigns(:user)
+ end
+
+ def test_should_get_new
+ login_as(:valid_user)
+ get :new, {:user_id => 1, :friend_id => 3}
+ assert_response :success
+ end
+
+ def test_should_create_friendship
+ login_as(:valid_user)
+ old_count = Friendship.count
+ post :create, {:user_id => 1, :friend_id => 3, :friendship => { :xfn_met => true } }
+ assert_equal old_count + 1, Friendship.count
+ assert_redirected_to friends_path(:user_id => 1)
+ end
+
+ def test_should_get_edit
+ login_as(:valid_user)
+ get :edit, :user_id => 1, :id => 2
+ assert_response :success
+ end
+
+ def test_should_update_friendship
+ login_as(:valid_user)
+
+ get :index, {:user_id => 1}
+ assert_select "a#friend-2[rel~=crush]", false
+
+ put :update, {:user_id => 1, :id => 2, :friendship => { :xfn_crush => true} }
+ assert_redirected_to friends_path(:user_id => 1)
+
+ get :index, {:user_id => 1}
+ assert_response :success
+ assert_select "a#friend-2[rel~=crush]", true
+ end
+
+ def test_should_destroy_friendship
+ login_as(:valid_user)
+ old_count = Friendship.count
+ delete :destroy, :user_id => 1, :id => 2
+ assert_equal old_count - 1, Friendship.count
+ assert_redirected_to friends_path(:user_id => 1)
+ end
+
+end
diff --git a/test/functional/newsletters_controller_test.rb b/test/functional/newsletters_controller_test.rb
new file mode 100644
index 0000000..c6ca28b
--- /dev/null
+++ b/test/functional/newsletters_controller_test.rb
@@ -0,0 +1,64 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'newsletters_controller'
+
+# Re-raise errors caught by the controller.
+class NewslettersController; def rescue_action(e) raise e end; end
+
+class NewslettersControllerTest < Test::Unit::TestCase
+ fixtures :newsletters, :users, :roles, :roles_users
+
+ def setup
+ @controller = NewslettersController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_should_get_index
+ login_as(:admin_user)
+ get :index
+ assert_response :success
+ assert assigns(:newsletters)
+ end
+
+ def test_should_get_new
+ login_as(:admin_user)
+ get :new
+ assert_response :success
+ end
+
+ def test_should_create_newsletter
+ login_as(:admin_user)
+ old_count = Newsletter.count
+ post :create, :newsletter => { :subject => 'new newsletter', :body => 'interesting news goes here' }
+ assert_equal old_count+1, Newsletter.count
+
+ assert_redirected_to newsletter_path(assigns(:newsletter))
+ end
+
+ def test_should_show_newsletter
+ login_as(:admin_user)
+ get :show, :id => 1
+ assert_response :success
+ end
+
+ def test_should_get_edit
+ login_as(:admin_user)
+ get :edit, :id => 1
+ assert_response :success
+ end
+
+ def test_should_update_newsletter
+ login_as(:admin_user)
+ put :update, :id => 1, :newsletter => { :subject => 'new newsletter', :body => 'interesting news goes here' }
+ assert_redirected_to newsletter_path(assigns(:newsletter))
+ end
+
+ def test_should_destroy_newsletter
+ login_as(:admin_user)
+ old_count = Newsletter.count
+ delete :destroy, :id => 1
+ assert_equal old_count-1, Newsletter.count
+
+ assert_redirected_to newsletters_path
+ end
+end
diff --git a/test/functional/pages_controller_test.rb b/test/functional/pages_controller_test.rb
new file mode 100644
index 0000000..79e3581
--- /dev/null
+++ b/test/functional/pages_controller_test.rb
@@ -0,0 +1,18 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'pages_controller'
+
+# Re-raise errors caught by the controller.
+class PagesController; def rescue_action(e) raise e end; end
+
+class PagesControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = PagesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/functional/photos_controller_test.rb b/test/functional/photos_controller_test.rb
new file mode 100644
index 0000000..3f72c13
--- /dev/null
+++ b/test/functional/photos_controller_test.rb
@@ -0,0 +1,18 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'photos_controller'
+
+# Re-raise errors caught by the controller.
+class PhotosController; def rescue_action(e) raise e end; end
+
+class PhotosControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = PhotosController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/functional/posts_controller_test.rb b/test/functional/posts_controller_test.rb
new file mode 100644
index 0000000..3448005
--- /dev/null
+++ b/test/functional/posts_controller_test.rb
@@ -0,0 +1,62 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'posts_controller'
+
+# Re-raise errors caught by the controller.
+class PostsController; def rescue_action(e) raise e end; end
+
+class PostsControllerTest < Test::Unit::TestCase
+ fixtures :posts, :topics, :forums, :users, :roles, :roles_users
+
+ def setup
+ @controller = PostsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_should_get_index
+ get :index, {:forum_id => 1, :topic_id => 1}
+ assert_response :success
+ assert assigns(:posts)
+ end
+
+ def test_should_get_new
+ login_as(:valid_user)
+ get :new, {:forum_id => 1, :topic_id => 1}
+ assert_response :success
+ end
+
+ def test_should_create_post
+ login_as(:valid_user)
+ old_count = Post.count
+ post :create, {:forum_id => 1, :topic_id => 1,
+ :post => { :body => 'test message' } }
+ assert_equal old_count+1, Post.count
+ assert_redirected_to posts_path(:forum_id => 1, :topic_id => 1)
+ end
+
+ def test_should_show_post
+ get :show, {:id => 1, :forum_id => 1, :topic_id => 1}
+ assert_response :success
+ end
+
+ def test_should_get_edit
+ login_as(:moderator_user)
+ get :edit, :id => 1
+ assert_response :success
+ end
+
+ def test_should_update_post
+ login_as(:moderator_user)
+ put :update, {:forum_id => 1, :topic_id => 1, :id => 1,
+ :post => { :body => 'test message'} }
+ assert_redirected_to posts_path(:forum_id => 1, :topic_id => 1)
+ end
+
+ def test_should_destroy_post
+ login_as(:moderator_user)
+ old_count = Post.count
+ delete :destroy, :id => 1, :forum_id => 1, :topic_id => 1
+ assert_equal old_count-1, Post.count
+ assert_redirected_to posts_path(:forum_id => 1, :topic_id => 1)
+ end
+end
diff --git a/test/functional/tags_controller_test.rb b/test/functional/tags_controller_test.rb
new file mode 100644
index 0000000..5656ff2
--- /dev/null
+++ b/test/functional/tags_controller_test.rb
@@ -0,0 +1,18 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'tags_controller'
+
+# Re-raise errors caught by the controller.
+class TagsController; def rescue_action(e) raise e end; end
+
+class TagsControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = TagsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/functional/topics_controller_test.rb b/test/functional/topics_controller_test.rb
new file mode 100644
index 0000000..ae4dcb0
--- /dev/null
+++ b/test/functional/topics_controller_test.rb
@@ -0,0 +1,65 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'topics_controller'
+
+# Re-raise errors caught by the controller.
+class TopicsController; def rescue_action(e) raise e end; end
+
+class TopicsControllerTest < Test::Unit::TestCase
+ fixtures :topics, :forums, :users, :roles, :roles_users
+
+ def setup
+ @controller = TopicsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_should_get_index
+ get :index, {:forum_id => 1}
+ assert_response :success
+ assert assigns(:topics)
+ end
+
+ def test_should_get_new
+ login_as(:moderator_user)
+ get :new, {:forum_id => 1}
+ assert_response :success
+ end
+
+ def test_should_create_topic
+ login_as(:moderator_user)
+ old_count = Topic.count
+ post :create, {:forum_id => 1,
+ :topic => { :name => 'a test topic' },
+ :post => { :body => 'and the message'} }
+ assert_equal old_count+1, Topic.count
+ assert_redirected_to posts_path(:forum_id => 1, :topic_id => assigns(:topic))
+ end
+
+ def test_should_show_topic
+ get :show, { :id => 1, :forum_id => 1 }
+ assert_redirected_to :controller => 'posts', :action => 'index',
+ :forum_id => 1, :topic_id => 1
+ assert_redirected_to posts_path(:forum_id => 1, :topic_id => 1)
+ end
+
+ def test_should_get_edit
+ login_as(:moderator_user)
+ get :edit, { :id => 1, :forum_id => 1 }
+ assert_response :success
+ end
+
+ def test_should_update_topic
+ login_as(:moderator_user)
+ put :update, {:id => 1, :forum_id => 1, :topic => { :name => 'a test' } }
+ assert_redirected_to :controller => 'posts', :action => 'index',
+ :forum_id => 1, :topic_id => 1
+ end
+
+ def test_should_destroy_topic
+ login_as(:moderator_user)
+ old_count = Topic.count
+ delete :destroy, { :id => 1, :forum_id => 1 }
+ assert_equal old_count-1, Topic.count
+ assert_redirected_to topics_path(:forum_id => 1)
+ end
+end
diff --git a/test/functional/user_photos_controller_test.rb b/test/functional/user_photos_controller_test.rb
new file mode 100644
index 0000000..b78bfa7
--- /dev/null
+++ b/test/functional/user_photos_controller_test.rb
@@ -0,0 +1,90 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'user_photos_controller'
+
+# Re-raise errors caught by the controller.
+class UserPhotosController; def rescue_action(e) raise e end; end
+
+class UserPhotosControllerTest < Test::Unit::TestCase
+ fixtures :photos, :users
+
+ def setup
+ @controller = UserPhotosController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_should_get_index
+ get :index, {:user_id => 1}
+ assert_response :success
+ assert assigns(:photos)
+ end
+
+ def test_should_get_new
+ login_as(:valid_user)
+ get :new, {:user_id => 1}
+ assert_response :success
+ end
+
+ def test_should_create_photo
+ login_as(:valid_user)
+ old_count = Photo.count
+ image_file = File.join(RAILS_ROOT, 'public', 'images', 'rails.png')
+
+ post :create,
+ :photo => {:title => 'test photo',
+ :body => 'a test image',
+ :temp_path => image_file,
+ :content_type => 'image/png',
+ :filename => 'rails.png'}
+
+ assert_equal old_count+3, Photo.count
+ assert_redirected_to user_photos_path(:user_id => 1)
+ end
+
+ def test_should_show_photo
+ get :show, {:user_id => 1, :id => 1}
+ assert_response :success
+ end
+
+ def test_should_get_edit
+ login_as(:valid_user)
+ get :edit, {:user_id => 1, :id => 1}
+ assert_response :success
+ end
+
+ def test_should_update_photo
+ login_as(:valid_user)
+
+ # upload a test image
+ image_file = File.join(RAILS_ROOT, 'public', 'images', 'rails.png')
+ post :create,
+ :photo => {:title => 'test photo',
+ :body => 'a test image',
+ :temp_path => image_file,
+ :content_type => 'image/png',
+ :filename => 'rails.png'}
+
+ put :update, {:user_id => assigns['photo'].user_id, :id => assigns['photo'].id, :photo => {:body => 'this has been edited' }}
+ assert_redirected_to user_photo_path(:user_id => assigns['photo'].user_id, :id => assigns['photo'].id)
+ end
+
+ def test_should_destroy_photo
+ login_as(:valid_user)
+
+ # upload a test image
+ image_file = File.join(RAILS_ROOT, 'public', 'images', 'rails.png')
+ post :create,
+ :photo => {:title => 'test photo',
+ :body => 'a test image',
+ :temp_path => image_file,
+ :content_type => 'image/png',
+ :filename => 'rails.png'}
+
+
+ old_count = Photo.count
+ delete :destroy, {:user_id => assigns['photo'].user_id, :id => assigns['photo'].id}
+ assert_equal old_count-3, Photo.count
+
+ assert_redirected_to user_photos_path
+ end
+end
diff --git a/test/functional/user_tags_controller_test.rb b/test/functional/user_tags_controller_test.rb
new file mode 100644
index 0000000..3a3cfbb
--- /dev/null
+++ b/test/functional/user_tags_controller_test.rb
@@ -0,0 +1,18 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'user_tags_controller'
+
+# Re-raise errors caught by the controller.
+class UserTagsController; def rescue_action(e) raise e end; end
+
+class UserTagsControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = UserTagsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb
new file mode 100644
index 0000000..05ab9d8
--- /dev/null
+++ b/test/functional/users_controller_test.rb
@@ -0,0 +1,36 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'users_controller'
+
+# Re-raise errors caught by the controller.
+class UsersController; def rescue_action(e) raise e end; end
+
+class UsersControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = UsersController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_signup_page
+ get :new
+ assert_response :success
+ end
+ def test_valid_signup_and_redirect
+ post :create, :user => {:username => 'fred',
+ :email => 'fred@example.com',
+ :password => 'abc123',
+ :password_confirmation => 'abc123',
+ :profile => 'A regular guy'}
+ assert_response :redirect
+ end
+ def test_invalid_signup_dupe_username
+ post :create, :user => {:username => 'joe',
+ :email => 'fred@example.com',
+ :password => 'abc123',
+ :password_confirmation => 'abc123',
+ :profile => 'A regular guy'}
+ assert assigns(:user).errors.on(:username)
+ assert_response :success
+ assert_template 'users/new'
+ end
+end
diff --git a/test/functional/usertemplates_controller_test.rb b/test/functional/usertemplates_controller_test.rb
new file mode 100644
index 0000000..2717718
--- /dev/null
+++ b/test/functional/usertemplates_controller_test.rb
@@ -0,0 +1,48 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'usertemplates_controller'
+
+# Re-raise errors caught by the controller.
+class UsertemplatesController; def rescue_action(e) raise e end; end
+
+class UsertemplatesControllerTest < Test::Unit::TestCase
+ fixtures :usertemplates, :users
+
+ def setup
+ @controller = UsertemplatesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_should_get_index
+ login_as(:valid_user)
+ get :index
+ assert_response :success
+ assert assigns(:usertemplates)
+ end
+
+ def test_should_get_edit
+ login_as(:valid_user)
+ get :edit, :id => 1
+ assert_response :success
+ end
+
+ def test_should_update_usertemplate
+ login_as(:valid_user)
+ put :update, :id => 1, :usertemplate => { :body => 'a different template'}
+ assert_redirected_to usertemplates_path
+ end
+
+ def test_should_fail_get_edit_for_other_user
+ login_as(:valid_user)
+ get :edit, :id => 3
+ assert_response :redirect
+ assert_redirected_to :action => 'index'
+ end
+
+ def test_should_fail_update_for_other_user
+ login_as(:valid_user)
+ put :update, :id => 3, :usertemplate => { :body => 'a different template'}
+ assert_response :redirect
+ assert_redirected_to :action => 'index'
+ end
+end
diff --git a/test/integration/articles_stories_test.rb b/test/integration/articles_stories_test.rb
new file mode 100644
index 0000000..e795695
--- /dev/null
+++ b/test/integration/articles_stories_test.rb
@@ -0,0 +1,19 @@
+require "#{File.dirname(__FILE__)}/../test_helper"
+
+class ArticleStoriesTest < ActionController::IntegrationTest
+ fixtures :users, :articles, :categories
+
+ def test_view_all_articles
+ get articles_url
+ assert_response :success
+ assert_template 'articles/index'
+ assert_equal assigns['articles'].length, 2
+ end
+
+ def test_view_one_category
+ get category_articles_url(:category_id => 1)
+ assert_response :success
+ assert_template 'articles/index'
+ assert_equal assigns['articles'].length, 1
+ end
+end
diff --git a/test/integration/login_stories_test.rb b/test/integration/login_stories_test.rb
new file mode 100644
index 0000000..86ccb23
--- /dev/null
+++ b/test/integration/login_stories_test.rb
@@ -0,0 +1,34 @@
+require "#{File.dirname(__FILE__)}/../test_helper"
+
+class LoginStoriesTest < ActionController::IntegrationTest
+ fixtures :users, :pages
+
+ def test_valid_login
+ get edit_user_url(1)
+ assert_response :redirect
+ follow_redirect!
+ assert_response :success
+ assert_template 'account/login'
+
+ go_to_login
+
+ login :user => {:username => 'joe', :password => '12345'}
+
+ get edit_user_url(1)
+ assert_response :success
+ assert_template 'users/edit'
+ end
+
+ private
+
+ def go_to_login
+ get 'account/login'
+ assert_response :success
+ assert_template 'account/login'
+ end
+
+ def login(options)
+ post 'account/authenticate', options
+ assert_response :redirect
+ end
+end
diff --git a/test/integration/mobile_login_stories_test.rb b/test/integration/mobile_login_stories_test.rb
new file mode 100644
index 0000000..3f7b96f
--- /dev/null
+++ b/test/integration/mobile_login_stories_test.rb
@@ -0,0 +1,29 @@
+require "#{File.dirname(__FILE__)}/../test_helper"
+
+class MobileLoginStoriesTest < ActionController::IntegrationTest
+ fixtures :users, :pages
+
+ def test_valid_mobile_login
+ get 'mobile/login'
+ assert_response :success
+ assert_template 'mobile/account/login'
+
+ post 'mobile/account/authenticate', :user => {:username => 'joe', :password => '12345'}
+ assert_response :redirect
+ follow_redirect!
+ assert_response :success
+ assert_template 'mobile/pages/show'
+ end
+
+ def test_invalid_mobile_login
+ get 'mobile/login'
+ assert_response :success
+ assert_template 'mobile/account/login'
+
+ post 'mobile/account/authenticate', :user => {:username => 'joe', :password => 'wrong'}
+ assert_response :redirect
+ follow_redirect!
+ assert_response :success
+ assert_template 'mobile/account/login'
+ end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644
index 0000000..4acf2b9
--- /dev/null
+++ b/test/test_helper.rb
@@ -0,0 +1,31 @@
+ENV["RAILS_ENV"] = "test"
+require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
+require 'test_help'
+
+class Test::Unit::TestCase
+ # Transactional fixtures accelerate your tests by wrapping each test method
+ # in a transaction that's rolled back on completion. This ensures that the
+ # test database remains unchanged so your fixtures don't have to be reloaded
+ # between every test method. Fewer database queries means faster tests.
+ #
+ # Read Mike Clark's excellent walkthrough at
+ # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
+ #
+ # Every Active Record database supports transactions except MyISAM tables
+ # in MySQL. Turn off transactional fixtures in this case; however, if you
+ # don't care one way or the other, switching from MyISAM to InnoDB tables
+ # is recommended.
+ self.use_transactional_fixtures = true
+
+ # Instantiated fixtures are slow, but give you @david where otherwise you
+ # would need people(:david). If you don't want to migrate your existing
+ # test cases which use the @david style and don't mind the speed hit (each
+ # instantiated fixtures translates to a database query per test method),
+ # then set this back to true.
+ self.use_instantiated_fixtures = false
+
+ # Add more helper methods to be used by all tests here...
+ def login_as(user)
+ @request.session[:user] = users(user).id
+ end
+end
diff --git a/test/unit/article_test.rb b/test/unit/article_test.rb
new file mode 100644
index 0000000..813dcbc
--- /dev/null
+++ b/test/unit/article_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ArticleTest < Test::Unit::TestCase
+ fixtures :articles
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/unit/category_test.rb b/test/unit/category_test.rb
new file mode 100644
index 0000000..10aa26c
--- /dev/null
+++ b/test/unit/category_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class CategoryTest < Test::Unit::TestCase
+ fixtures :categories
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/unit/comment_test.rb b/test/unit/comment_test.rb
new file mode 100644
index 0000000..f3042e4
--- /dev/null
+++ b/test/unit/comment_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class CommentTest < Test::Unit::TestCase
+ fixtures :comments
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/unit/email_test.rb b/test/unit/email_test.rb
new file mode 100644
index 0000000..fcfa5aa
--- /dev/null
+++ b/test/unit/email_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class EmailTest < Test::Unit::TestCase
+ fixtures :emails
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/unit/entry_test.rb b/test/unit/entry_test.rb
new file mode 100644
index 0000000..3f193d0
--- /dev/null
+++ b/test/unit/entry_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class EntryTest < Test::Unit::TestCase
+ fixtures :entries
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/unit/forum_test.rb b/test/unit/forum_test.rb
new file mode 100644
index 0000000..91815de
--- /dev/null
+++ b/test/unit/forum_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ForumTest < Test::Unit::TestCase
+ fixtures :forums
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/unit/friendship_test.rb b/test/unit/friendship_test.rb
new file mode 100644
index 0000000..bfc3041
--- /dev/null
+++ b/test/unit/friendship_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class FriendshipTest < Test::Unit::TestCase
+ fixtures :friendships
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/unit/newsletter_test.rb b/test/unit/newsletter_test.rb
new file mode 100644
index 0000000..7c5c047
--- /dev/null
+++ b/test/unit/newsletter_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class NewsletterTest < Test::Unit::TestCase
+ fixtures :newsletters
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/unit/notifier_test.rb b/test/unit/notifier_test.rb
new file mode 100644
index 0000000..ce511ac
--- /dev/null
+++ b/test/unit/notifier_test.rb
@@ -0,0 +1,37 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class NotifierTest < Test::Unit::TestCase
+ FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
+ CHARSET = "utf-8"
+ fixtures :entries, :comments, :users
+
+ include ActionMailer::Quoting
+
+ def setup
+ ActionMailer::Base.delivery_method = :test
+ ActionMailer::Base.perform_deliveries = true
+ ActionMailer::Base.deliveries = []
+
+ @expected = TMail::Mail.new
+ @expected.set_content_type "text", "plain", { "charset" => CHARSET }
+ @expected.mime_version = '1.0'
+ end
+
+ def test_comment_notify
+ comment = Comment.find(1)
+ response = Notifier.create_new_comment_notification(comment)
+ assert_equal "A new comment has been left on your blog", response.subject
+ assert_match /Hi #{comment.entry.user.username}/, response.body
+ assert_match /The comment was left by '#{comment.user.username}' at #{comment.created_at.to_s(:short)}/, response.body
+ assert_match /go to http:\/\/railscoders.net\/users\/1\/entries\/1/, response.body
+ end
+
+ private
+ def read_fixture(action)
+ IO.readlines("#{FIXTURES_PATH}/notifier/#{action}")
+ end
+
+ def encode(subject)
+ quoted_printable(subject, CHARSET)
+ end
+end
diff --git a/test/unit/page_test.rb b/test/unit/page_test.rb
new file mode 100644
index 0000000..17cff64
--- /dev/null
+++ b/test/unit/page_test.rb
@@ -0,0 +1,25 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class PageTest < Test::Unit::TestCase
+ fixtures :pages
+
+ def test_invalid_if_any_field_empty
+ page = Page.new
+ assert !page.valid?
+ assert page.errors.invalid?(:title)
+ assert page.errors.invalid?(:body)
+ end
+ def test_valid_fields
+ page = pages(:valid_page)
+ assert page.valid?
+ end
+ def test_invalid_short_title
+ page = pages(:invalid_page_short_title)
+ assert !page.valid?
+ end
+
+ def test_auto_permalink
+ page = pages(:valid_with_auto_permalink)
+ assert page.valid?
+ end
+end
diff --git a/test/unit/photo_test.rb b/test/unit/photo_test.rb
new file mode 100644
index 0000000..5657d41
--- /dev/null
+++ b/test/unit/photo_test.rb
@@ -0,0 +1,47 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class PhotoTest < Test::Unit::TestCase
+ fixtures :photos, :users
+
+ def test_should_upload_photo_and_create_thumbnails
+ photo_object = upload_file 'rails.png', users(:valid_user)
+ assert_file_exists photo_object.id, "rails.png"
+ assert_file_exists photo_object.id, "rails_thumb.png"
+ assert_file_exists photo_object.id, "rails_tiny.png"
+ end
+
+ def test_should_delete_db_row_and_files
+ photo_object = upload_file 'rails.png', users(:valid_user)
+ photo_count = Photo.count
+
+ assert_file_exists photo_object.id, "rails.png"
+ Photo.destroy(photo_object.id)
+
+ assert_equal photo_count-3, Photo.count
+ assert_file_does_not_exist photo_object.id, "rails.png"
+ assert_file_does_not_exist photo_object.id, "rails_thumb.png"
+ assert_file_does_not_exist photo_object.id, "rails_tiny.png"
+ end
+
+ protected
+ def upload_file(image_file, user)
+ image_file = File.join(RAILS_ROOT, 'public', 'images', image_file)
+ photo = user.photos.create(:filename => image_file,
+ :content_type => 'image/png',
+ :temp_path => image_file)
+ assert_valid photo
+ photo
+ end
+
+ def assert_file_exists(photo_id, image_file)
+ file = File.join(RAILS_ROOT, 'public', 'photos',
+ "#{photo_id}", "#{image_file}")
+ assert File.file?(file), "File not found: #{image_file}"
+ end
+
+ def assert_file_does_not_exist(photo_id, image_file)
+ file = File.join(RAILS_ROOT, 'public', 'photos',
+ "#{photo_id}", "#{image_file}")
+ assert !File.file?(file)
+ end
+end
diff --git a/test/unit/post_test.rb b/test/unit/post_test.rb
new file mode 100644
index 0000000..b87ec67
--- /dev/null
+++ b/test/unit/post_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class PostTest < Test::Unit::TestCase
+ fixtures :posts
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/unit/role_test.rb b/test/unit/role_test.rb
new file mode 100644
index 0000000..05d6652
--- /dev/null
+++ b/test/unit/role_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RoleTest < Test::Unit::TestCase
+ fixtures :roles
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/unit/topic_test.rb b/test/unit/topic_test.rb
new file mode 100644
index 0000000..1fe7c24
--- /dev/null
+++ b/test/unit/topic_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class TopicTest < Test::Unit::TestCase
+ fixtures :topics
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb
new file mode 100644
index 0000000..405cfe9
--- /dev/null
+++ b/test/unit/user_test.rb
@@ -0,0 +1,18 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class UserTest < Test::Unit::TestCase
+ fixtures :users
+
+ def test_create_valid_user
+ user = User.new(:username => 'fred', :email => 'fred@example.com',
+ :password => 'abc123', :password_confirmation => 'abc123',
+ :profile => 'A regular guy')
+ assert user.save
+ end
+ def test_invalid_duplicate_username
+ user = User.new(:username => 'joe', :email => 'fred@example.com',
+ :password => 'abc123', :password_confirmation => 'abc123',
+ :profile => 'A regular guy')
+ assert !user.save
+ end
+end
diff --git a/test/unit/usertemplate_test.rb b/test/unit/usertemplate_test.rb
new file mode 100644
index 0000000..fcc7e7a
--- /dev/null
+++ b/test/unit/usertemplate_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class UsertemplateTest < Test::Unit::TestCase
+ fixtures :usertemplates
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG b/vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG
new file mode 100644
index 0000000..a12b709
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG
@@ -0,0 +1,52 @@
+[23 June 2007]
+
+* Add validation to Tag model.
+
+* find_options_for_tagged_with should always return a hash.
+
+* find_tagged_with passing in no tags should return an empty array.
+
+* Improve compatibility with PostgreSQL.
+
+[21 June 2007]
+
+* Remove extra .rb from generated migration file name.
+
+[15 June 2007]
+
+* Introduce TagList class.
+
+* Various cleanups and improvements.
+
+* Use TagList.delimiter now, not Tag.delimiter. Tag.delimiter will be removed at some stage.
+
+[11 June 2007]
+
+* Restructure the creation of the options for find_tagged_with [Thijs Cadier]
+
+* Add an example migration with a generator.
+
+* Add caching.
+
+* Fix compatibility with Ruby < 1.8.6
+
+[23 April 2007]
+
+* Make tag_list to respect Tag.delimiter
+
+[31 March 2007]
+
+* Add Tag.delimiter accessor to change how tags are parsed.
+* Fix :include => :tags when used with find_tagged_with
+
+[7 March 2007]
+
+* Fix tag_counts for SQLServer [Brad Young]
+
+[21 Feb 2007]
+
+* Use scoping instead of TagCountsExtension [Michael Schuerig]
+
+[7 Jan 2007]
+
+* Add :match_all to find_tagged_with [Michael Sheakoski]
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE b/vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE
new file mode 100644
index 0000000..602bda2
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2006 Jonathan Viney
+
+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/vendor/plugins/acts_as_taggable_on_steroids/README b/vendor/plugins/acts_as_taggable_on_steroids/README
new file mode 100644
index 0000000..670d13a
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/README
@@ -0,0 +1,116 @@
+= acts_as_taggable_on_steroids
+
+If you find this plugin useful, please consider a donation to show your support!
+
+ http://www.paypal.com/cgi-bin/webscr?cmd=_send-money
+
+ Email address: jonathan.viney@gmail.com
+
+== Instructions
+
+This plugin is based on acts_as_taggable by DHH but includes extras
+such as tests, smarter tag assignment, and tag cloud calculations.
+
+Thanks to www.fanacious.com for allowing this plugin to be released. Please check out
+their site to show your support.
+
+== Resources
+
+Install
+ * script/plugin install http://svn.viney.net.nz/things/rails/plugins/acts_as_taggable_on_steroids
+
+== Usage
+
+=== Prepare database
+
+Generate and apply the migration:
+
+ ruby script/generate acts_as_taggable_migration
+ rake db:migrate
+
+=== Basic tagging
+
+Using the examples from the tests, let's suppose we have users that have many posts and we want those
+posts to be able to be tagged by the user.
+
+As usual, we add +acts_as_taggable+ to the Post class:
+
+ class Post < ActiveRecord::Base
+ acts_as_taggable
+
+ belongs_to :user
+ end
+
+We can now use the tagging methods provided by acts_as_taggable, tag_list and tag_list=. Both these
+methods work like regular attribute accessors.
+
+ p = Post.find(:first)
+ p.tag_list.to_s # ""
+ p.tag_list = "Funny, Silly"
+ p.save
+ p.reload.tag_list.to_s # "Funny, Silly"
+
+You can also add or remove arrays of tags.
+
+ p.tag_list.add("Great", "Awful")
+ p.tag_list.remove("Funny")
+
+=== Finding tagged objects
+
+To retrieve objects tagged with a certain tag, use find_tagged_with.
+
+ Post.find_tagged_with('Funny, Silly')
+
+By default, find_tagged_with will find objects that have any of the given tags. To
+find only objects that are tagged with all the given tags, use match_all.
+
+ Post.find_tagged_with('Funny, Silly', :match_all => true)
+
+=== Tag cloud calculations
+
+To construct tag clouds, the frequency of each tag needs to be calculated.
+Because we specified +acts_as_taggable+ on the Post class, we can
+get a calculation of all the tag counts by using Post.tag_counts. But what if we wanted a tag count for
+an single user's posts? To achieve this we call tag_counts on the association:
+
+ User.find(:first).posts.tag_counts
+
+=== Caching
+
+It is useful to cache the list of tags to reduce the number of queries executed. To do this,
+add a column named cached_tag_list to the model which is being tagged.
+
+ class CachePostTagList < ActiveRecord::Migration
+ def self.up
+ # You should make sure that the column is long enough to hold
+ # the full tag list. In some situations the :text type may be more appropriate.
+ add_column :posts, :cached_tag_list, :string
+ end
+ end
+
+ class Post < ActiveRecord::Base
+ acts_as_taggable
+
+ # The caching column defaults to cached_tag_list, but can be changed:
+ #
+ # set_cached_tag_list_column_name "my_caching_column_name"
+ end
+
+The details of the caching are handled for you. Just continue to use the tag_list accessor as you normally would.
+Note that the cached tag list will not be updated if you directly create Tagging objects or manually append to the
+tags or taggings associations. To update the cached tag list you should call save_cached_tag_list manually.
+
+=== Delimiter
+
+If you want to change the delimiter used to parse and present tags, set TagList.delimiter.
+For example, to use spaces instead of commas, add the following to config/environment.rb:
+
+ TagList.delimiter = " "
+
+=== Other
+
+Problems, comments, and suggestions all welcome. jonathan.viney@gmail.com
+
+== Credits
+
+www.fanacious.com
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/Rakefile b/vendor/plugins/acts_as_taggable_on_steroids/Rakefile
new file mode 100644
index 0000000..d2c0003
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/Rakefile
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the acts_as_taggable_on_steroids plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the acts_as_taggable_on_steroids plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'Acts As Taggable On Steroids'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb
new file mode 100644
index 0000000..be9b39c
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb
@@ -0,0 +1,11 @@
+class ActsAsTaggableMigrationGenerator < Rails::Generator::Base
+ def manifest
+ record do |m|
+ m.migration_template 'migration.rb', 'db/migrate'
+ end
+ end
+
+ def file_name
+ "acts_as_taggable_migration"
+ end
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/templates/migration.rb b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/templates/migration.rb
new file mode 100644
index 0000000..ea0c2cc
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/templates/migration.rb
@@ -0,0 +1,26 @@
+class ActsAsTaggableMigration < ActiveRecord::Migration
+ def self.up
+ create_table :tags do |t|
+ t.column :name, :string
+ end
+
+ create_table :taggings do |t|
+ t.column :tag_id, :integer
+ t.column :taggable_id, :integer
+
+ # You should make sure that the column created is
+ # long enough to store the required class names.
+ t.column :taggable_type, :string
+
+ t.column :created_at, :datetime
+ end
+
+ add_index :taggings, :tag_id
+ add_index :taggings, [:taggable_id, :taggable_type]
+ end
+
+ def self.down
+ drop_table :taggings
+ drop_table :tags
+ end
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/init.rb b/vendor/plugins/acts_as_taggable_on_steroids/init.rb
new file mode 100644
index 0000000..5d3aa8e
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/init.rb
@@ -0,0 +1,4 @@
+require File.dirname(__FILE__) + '/lib/acts_as_taggable'
+
+require File.dirname(__FILE__) + '/lib/tagging'
+require File.dirname(__FILE__) + '/lib/tag'
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb
new file mode 100644
index 0000000..060c625
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb
@@ -0,0 +1,155 @@
+module ActiveRecord
+ module Acts #:nodoc:
+ module Taggable #:nodoc:
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def acts_as_taggable(options = {})
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
+ has_many :tags, :through => :taggings
+
+ before_save :save_cached_tag_list
+ after_save :save_tags
+
+ include ActiveRecord::Acts::Taggable::InstanceMethods
+ extend ActiveRecord::Acts::Taggable::SingletonMethods
+
+ alias_method :reload_without_tag_list, :reload
+ alias_method :reload, :reload_with_tag_list
+ end
+
+ def cached_tag_list_column_name
+ "cached_tag_list"
+ end
+
+ def set_cached_tag_list_column_name(value = nil, &block)
+ define_attr_method :cached_tag_list_column_name, value, &block
+ end
+ end
+
+ module SingletonMethods
+ # Pass either a tag string, or an array of strings or tags
+ #
+ # Options:
+ # :exclude - Find models that are not tagged with the given tags
+ # :match_all - Find models that match all of the gievn tags, not just one
+ # :conditions - A piece of SQL conditions to add to the query
+ def find_options_for_tagged_with(tags, options = {})
+ tags = TagList.from(tags).names if tags.is_a?(String)
+ tags.compact!
+ tags.map!(&:to_s)
+
+ return {} if tags.empty?
+
+ conditions = sanitize_sql(["#{table_name}_tags.name #{"NOT" if options.delete(:exclude)} IN (?)", tags])
+ conditions << " AND #{sanitize_sql(options.delete(:conditions))}" if options[:conditions]
+
+ group = "#{table_name}_taggings.taggable_id HAVING COUNT(#{table_name}_taggings.taggable_id) = #{tags.size}" if options.delete(:match_all)
+
+ { :select => "DISTINCT #{table_name}.*",
+ :joins => "LEFT OUTER JOIN taggings #{table_name}_taggings ON #{table_name}_taggings.taggable_id = #{table_name}.#{primary_key} AND #{table_name}_taggings.taggable_type = '#{name}' " +
+ "LEFT OUTER JOIN tags #{table_name}_tags ON #{table_name}_tags.id = #{table_name}_taggings.tag_id",
+ :conditions => conditions,
+ :group => group
+ }.update(options)
+ end
+
+ def find_tagged_with(*args)
+ options = find_options_for_tagged_with(*args)
+ options.blank? ? [] : find(:all, options)
+ end
+
+ # Options:
+ # :start_at - Restrict the tags to those created after a certain time
+ # :end_at - Restrict the tags to those created before a certain time
+ # :conditions - A piece of SQL conditions to add to the query
+ # :limit - The maximum number of tags to return
+ # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
+ # :at_least - Exclude tags with a frequency less than the given value
+ # :at_most - Exclude tags with a frequency greater then the given value
+ def tag_counts(options = {})
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit
+
+ scope = scope(:find)
+ start_at = sanitize_sql(['taggings.created_at >= ?', options[:start_at]]) if options[:start_at]
+ end_at = sanitize_sql(['taggings.created_at <= ?', options[:end_at]]) if options[:end_at]
+
+ conditions = [
+ "taggings.taggable_type = #{quote_value(name)}",
+ options[:conditions],
+ scope && scope[:conditions],
+ start_at,
+ end_at
+ ]
+ conditions = conditions.compact.join(' and ')
+
+ at_least = sanitize_sql(['COUNT(*) >= ?', options[:at_least]]) if options[:at_least]
+ at_most = sanitize_sql(['COUNT(*) <= ?', options[:at_most]]) if options[:at_most]
+ having = [at_least, at_most].compact.join(' and ')
+ group_by = 'tags.id, tags.name HAVING COUNT(*) > 0'
+ group_by << " AND #{having}" unless having.blank?
+
+ Tag.find(:all,
+ :select => 'tags.id, tags.name, COUNT(*) AS count',
+ :joins => "LEFT OUTER JOIN taggings ON tags.id = taggings.tag_id LEFT OUTER JOIN #{table_name} ON #{table_name}.#{primary_key} = taggings.taggable_id",
+ :conditions => conditions,
+ :group => group_by,
+ :order => options[:order],
+ :limit => options[:limit]
+ )
+ end
+ end
+
+ module InstanceMethods
+ def tag_list
+ if @tag_list
+ @tag_list
+ elsif caching_tag_list? and !send(self.class.cached_tag_list_column_name).nil?
+ @tag_list = TagList.from(send(self.class.cached_tag_list_column_name))
+ else
+ @tag_list = TagList.new(tags.map(&:name))
+ end
+ end
+
+ def tag_list=(value)
+ @tag_list = TagList.from(value)
+ end
+
+ def save_cached_tag_list
+ if caching_tag_list? and !tag_list.blank?
+ self[self.class.cached_tag_list_column_name] = tag_list.to_s
+ end
+ end
+
+ def save_tags
+ return unless @tag_list
+
+ new_tag_names = @tag_list.names - tags.map(&:name)
+ old_tags = tags.reject { |tag| @tag_list.names.include?(tag.name) }
+
+ self.class.transaction do
+ new_tag_names.each do |new_tag_name|
+ tags << Tag.find_or_create_by_name(new_tag_name)
+ end
+
+ tags.delete(*old_tags) if old_tags.any?
+ end
+ true
+ end
+
+ def reload_with_tag_list(*args)
+ @tag_list = nil
+ reload_without_tag_list(*args)
+ end
+
+ def caching_tag_list?
+ self.class.column_names.include?(self.class.cached_tag_list_column_name)
+ end
+ end
+ end
+ end
+end
+
+ActiveRecord::Base.send(:include, ActiveRecord::Acts::Taggable)
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb
new file mode 100644
index 0000000..852ee33
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb
@@ -0,0 +1,22 @@
+class Tag < ActiveRecord::Base
+ has_many :taggings
+
+ validates_presence_of :name
+ validates_uniqueness_of :name
+
+ class << self
+ delegate :delimiter, :delimeter=, :to => TagList
+ end
+
+ def ==(object)
+ super || (object.is_a?(Tag) && name == object.name)
+ end
+
+ def to_s
+ name
+ end
+
+ def count
+ read_attribute(:count).to_i
+ end
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb
new file mode 100644
index 0000000..01defe0
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb
@@ -0,0 +1,2 @@
+module TagCountsExtension
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb
new file mode 100644
index 0000000..08306e1
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb
@@ -0,0 +1,57 @@
+class TagList
+ cattr_accessor :delimiter
+ self.delimiter = ','
+
+ attr_reader :names
+
+ def initialize(*names)
+ @names = []
+ add(*names)
+ end
+
+ def add(*names)
+ names = names.flatten
+
+ # Strip whitespace and remove blank or duplicate tags
+ names.map!(&:strip)
+ names.reject!(&:blank?)
+
+ @names.concat(names)
+ @names.uniq!
+ end
+
+ def remove(*names)
+ names = names.flatten
+ @names.delete_if { |name| names.include?(name) }
+ end
+
+ def blank?
+ @names.empty?
+ end
+
+ def to_s
+ @names.map do |name|
+ name.include?(delimiter) ? "\"#{name}\"" : name
+ end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
+ end
+
+ def ==(other)
+ super || (other.is_a?(TagList) && other.names == @names)
+ end
+
+ class << self
+ def from(string)
+ new(parse(string))
+ end
+
+ def parse(string)
+ returning [] do |names|
+ string = string.to_s.dup
+ # Parse the quoted tags
+ string.gsub!(/"(.*?)"\s*#{delimiter}?\s*/) { names << $1; "" }
+
+ names.concat(string.split(delimiter))
+ end
+ end
+ end
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb
new file mode 100644
index 0000000..33daf86
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb
@@ -0,0 +1,4 @@
+class Tagging < ActiveRecord::Base
+ belongs_to :tag
+ belongs_to :taggable, :polymorphic => true
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb
new file mode 100644
index 0000000..9f5258e
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb
@@ -0,0 +1,82 @@
+require 'test/unit'
+
+begin
+ require File.dirname(__FILE__) + '/../../../../config/environment'
+rescue LoadError
+ require 'rubygems'
+ require_gem 'activerecord'
+ require_gem 'actionpack'
+end
+
+# Search for fixtures first
+fixture_path = File.dirname(__FILE__) + '/fixtures/'
+begin
+ Dependencies.load_paths.insert(0, fixture_path)
+rescue
+ $LOAD_PATH.unshift(fixture_path)
+end
+
+require 'active_record/fixtures'
+
+require File.dirname(__FILE__) + '/../lib/acts_as_taggable'
+require_dependency File.dirname(__FILE__) + '/../lib/tag_list'
+
+ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/debug.log')
+ActiveRecord::Base.configurations = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
+ActiveRecord::Base.establish_connection(ENV['DB'] || 'mysql')
+
+load(File.dirname(__FILE__) + '/schema.rb')
+
+Test::Unit::TestCase.fixture_path = fixture_path
+
+class Test::Unit::TestCase #:nodoc:
+ self.use_transactional_fixtures = true
+ self.use_instantiated_fixtures = false
+
+ def assert_equivalent(expected, actual, message = nil)
+ if expected.first.is_a?(ActiveRecord::Base)
+ assert_equal expected.sort_by(&:id), actual.sort_by(&:id), message
+ else
+ assert_equal expected.sort, actual.sort, message
+ end
+ end
+
+ def assert_tag_counts(tags, expected_values)
+ # Map the tag fixture names to real tag names
+ expected_values = expected_values.inject({}) do |hash, (tag, count)|
+ hash[tags(tag).name] = count
+ hash
+ end
+
+ tags.each do |tag|
+ value = expected_values.delete(tag.name)
+ assert_not_nil value, "Expected count for #{tag.name} was not provided" if value.nil?
+ assert_equal value, tag.count, "Expected value of #{value} for #{tag.name}, but was #{tag.count}"
+ end
+
+ unless expected_values.empty?
+ assert false, "The following tag counts were not present: #{expected_values.inspect}"
+ end
+ end
+
+ def assert_queries(num = 1)
+ $query_count = 0
+ yield
+ ensure
+ assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed."
+ end
+
+ def assert_no_queries(&block)
+ assert_queries(0, &block)
+ end
+end
+
+ActiveRecord::Base.connection.class.class_eval do
+ def execute_with_counting(sql, name = nil, &block)
+ $query_count ||= 0
+ $query_count += 1
+ execute_without_counting(sql, name, &block)
+ end
+
+ alias_method_chain :execute, :counting
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb
new file mode 100644
index 0000000..d6e5166
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb
@@ -0,0 +1,211 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+
+class ActsAsTaggableOnSteroidsTest < Test::Unit::TestCase
+ fixtures :tags, :taggings, :posts, :users, :photos
+
+ def test_find_tagged_with
+ assert_equivalent [posts(:jonathan_sky), posts(:sam_flowers)], Post.find_tagged_with('"Very good"')
+ assert_equal Post.find_tagged_with('"Very good"'), Post.find_tagged_with(['Very good'])
+ assert_equal Post.find_tagged_with('"Very good"'), Post.find_tagged_with([tags(:good)])
+
+ assert_equivalent [photos(:jonathan_dog), photos(:sam_flower), photos(:sam_sky)], Photo.find_tagged_with('Nature')
+ assert_equal Photo.find_tagged_with('Nature'), Photo.find_tagged_with(['Nature'])
+ assert_equal Photo.find_tagged_with('Nature'), Photo.find_tagged_with([tags(:nature)])
+
+ assert_equivalent [photos(:jonathan_bad_cat), photos(:jonathan_dog), photos(:jonathan_questioning_dog)], Photo.find_tagged_with('"Crazy animal" Bad')
+ assert_equal Photo.find_tagged_with('"Crazy animal" Bad'), Photo.find_tagged_with(['Crazy animal', 'Bad'])
+ assert_equal Photo.find_tagged_with('"Crazy animal" Bad'), Photo.find_tagged_with([tags(:animal), tags(:bad)])
+ end
+
+ def test_find_tagged_with_nothing
+ assert_equal [], Post.find_tagged_with("")
+ assert_equal [], Post.find_tagged_with([])
+ end
+
+ def test_find_tagged_with_nonexistant_tags
+ assert_equal [], Post.find_tagged_with('ABCDEFG')
+ assert_equal [], Photo.find_tagged_with(['HIJKLM'])
+ assert_equal [], Photo.find_tagged_with([Tag.new(:name => 'unsaved tag')])
+ end
+
+ def test_find_tagged_with_matching_all_tags
+ assert_equivalent [photos(:jonathan_dog)], Photo.find_tagged_with('Crazy animal, "Nature"', :match_all => true)
+ assert_equivalent [posts(:jonathan_sky), posts(:sam_flowers)], Post.find_tagged_with(['Very good', 'Nature'], :match_all => true)
+ end
+
+ def test_find_options_for_tagged_with_no_tags_returns_empty_hash
+ assert_equal Hash.new, Post.find_options_for_tagged_with("")
+ assert_equal Hash.new, Post.find_options_for_tagged_with([nil])
+ end
+
+ def test_include_tags_on_find_tagged_with
+ assert_nothing_raised do
+ Photo.find_tagged_with('Nature', :include => :tags)
+ Photo.find_tagged_with("Nature", :include => { :taggings => :tag })
+ end
+ end
+
+ def test_basic_tag_counts_on_class
+ assert_tag_counts Post.tag_counts, :good => 2, :nature => 5, :question => 1, :bad => 1
+ assert_tag_counts Photo.tag_counts, :good => 1, :nature => 3, :question => 1, :bad => 1, :animal => 3
+ end
+
+ def test_tag_counts_on_class_with_date_conditions
+ assert_tag_counts Post.tag_counts(:start_at => Date.new(2006, 8, 4)), :good => 1, :nature => 3, :question => 1, :bad => 1
+ assert_tag_counts Post.tag_counts(:end_at => Date.new(2006, 8, 6)), :good => 1, :nature => 4, :question => 1
+ assert_tag_counts Post.tag_counts(:start_at => Date.new(2006, 8, 5), :end_at => Date.new(2006, 8, 8)), :good => 1, :nature => 2, :bad => 1
+
+ assert_tag_counts Photo.tag_counts(:start_at => Date.new(2006, 8, 12), :end_at => Date.new(2006, 8, 17)), :good => 1, :nature => 1, :bad => 1, :question => 1, :animal => 2
+ end
+
+ def test_tag_counts_on_class_with_frequencies
+ assert_tag_counts Photo.tag_counts(:at_least => 2), :nature => 3, :animal => 3
+ assert_tag_counts Photo.tag_counts(:at_most => 2), :good => 1, :question => 1, :bad => 1
+ end
+
+ def test_tag_counts_with_limit
+ assert_equal 2, Photo.tag_counts(:limit => 2).size
+ assert_equal 1, Post.tag_counts(:at_least => 4, :limit => 2).size
+ end
+
+ def test_tag_counts_with_limit_and_order
+ assert_equal [tags(:nature), tags(:good)], Post.tag_counts(:order => 'count desc', :limit => 2)
+ end
+
+ def test_tag_counts_on_association
+ assert_tag_counts users(:jonathan).posts.tag_counts, :good => 1, :nature => 3, :question => 1
+ assert_tag_counts users(:sam).posts.tag_counts, :good => 1, :nature => 2, :bad => 1
+
+ assert_tag_counts users(:jonathan).photos.tag_counts, :animal => 3, :nature => 1, :question => 1, :bad => 1
+ assert_tag_counts users(:sam).photos.tag_counts, :nature => 2, :good => 1
+ end
+
+ def test_tag_counts_on_association_with_options
+ assert_equal [], users(:jonathan).posts.tag_counts(:conditions => '1=0')
+ assert_tag_counts users(:jonathan).posts.tag_counts(:at_most => 2), :good => 1, :question => 1
+ end
+
+ def test_tag_list_reader
+ assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names
+ assert_equivalent ["Bad", "Crazy animal"], photos(:jonathan_bad_cat).tag_list.names
+ end
+
+ def test_reassign_tag_list
+ assert_equivalent ["Nature", "Question"], posts(:jonathan_rain).tag_list.names
+ posts(:jonathan_rain).taggings.reload
+
+ # Only an update of the posts table should be executed
+ assert_queries 1 do
+ posts(:jonathan_rain).update_attributes!(:tag_list => posts(:jonathan_rain).tag_list.to_s)
+ end
+
+ assert_equivalent ["Nature", "Question"], posts(:jonathan_rain).tag_list.names
+ end
+
+ def test_new_tags
+ assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names
+ posts(:jonathan_sky).update_attributes!(:tag_list => "#{posts(:jonathan_sky).tag_list}, One, Two")
+ assert_equivalent ["Very good", "Nature", "One", "Two"], posts(:jonathan_sky).tag_list.names
+ end
+
+ def test_remove_tag
+ assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names
+ posts(:jonathan_sky).update_attributes!(:tag_list => "Nature")
+ assert_equivalent ["Nature"], posts(:jonathan_sky).tag_list.names
+ end
+
+ def test_remove_and_add_tag
+ assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names
+ posts(:jonathan_sky).update_attributes!(:tag_list => "Nature, Beautiful")
+ assert_equivalent ["Nature", "Beautiful"], posts(:jonathan_sky).tag_list.names
+ end
+
+ def test_tags_not_saved_if_validation_fails
+ assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names
+ assert !posts(:jonathan_sky).update_attributes(:tag_list => "One, Two", :text => "")
+ assert_equivalent ["Very good", "Nature"], Post.find(posts(:jonathan_sky).id).tag_list.names
+ end
+
+ def test_tag_list_accessors_on_new_record
+ p = Post.new(:text => 'Test')
+
+ assert p.tag_list.blank?
+ p.tag_list = "One, Two"
+ assert_equal "One, Two", p.tag_list.to_s
+ end
+
+ def test_clear_tag_list_with_nil
+ p = photos(:jonathan_questioning_dog)
+
+ assert !p.tag_list.blank?
+ assert p.update_attributes(:tag_list => nil)
+ assert p.tag_list.blank?
+
+ assert p.reload.tag_list.blank?
+ end
+
+ def test_clear_tag_list_with_string
+ p = photos(:jonathan_questioning_dog)
+
+ assert !p.tag_list.blank?
+ assert p.update_attributes(:tag_list => ' ')
+ assert p.tag_list.blank?
+
+ assert p.reload.tag_list.blank?
+ end
+
+ def test_tag_list_reset_on_reload
+ p = photos(:jonathan_questioning_dog)
+ assert !p.tag_list.blank?
+ p.tag_list = nil
+ assert p.tag_list.blank?
+ assert !p.reload.tag_list.blank?
+ end
+
+ def test_tag_list_populated_when_cache_nil
+ assert_nil posts(:jonathan_sky).cached_tag_list
+ posts(:jonathan_sky).save!
+ assert_equal posts(:jonathan_sky).tag_list.to_s, posts(:jonathan_sky).cached_tag_list
+ end
+
+ def test_cached_tag_list_used
+ posts(:jonathan_sky).save!
+ posts(:jonathan_sky).reload
+
+ assert_no_queries do
+ assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names
+ end
+ end
+
+ def test_cached_tag_list_not_used
+ # Load fixture and column information
+ posts(:jonathan_sky).taggings(:reload)
+
+ assert_queries 1 do
+ # Tags association will be loaded
+ posts(:jonathan_sky).tag_list
+ end
+ end
+
+ def test_cached_tag_list_updated
+ assert_nil posts(:jonathan_sky).cached_tag_list
+ posts(:jonathan_sky).save!
+ assert_equivalent ["Very good", "Nature"], TagList.from(posts(:jonathan_sky).cached_tag_list).names
+ posts(:jonathan_sky).update_attributes!(:tag_list => "None")
+
+ assert_equal 'None', posts(:jonathan_sky).cached_tag_list
+ assert_equal 'None', posts(:jonathan_sky).reload.cached_tag_list
+ end
+end
+
+class ActsAsTaggableOnSteroidsFormTest < Test::Unit::TestCase
+ fixtures :tags, :taggings, :posts, :users, :photos
+
+ include ActionView::Helpers::FormHelper
+
+ def test_tag_list_contents
+ fields_for :post, posts(:jonathan_sky) do |f|
+ assert_match /Very good, Nature/, f.text_field(:tag_list)
+ end
+ end
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/database.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/database.yml
new file mode 100644
index 0000000..47c3736
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/test/database.yml
@@ -0,0 +1,10 @@
+mysql:
+ :adapter: mysql
+ :host: localhost
+ :username: rails
+ :password:
+ :database: rails_plugin_test
+
+sqlite3:
+ :adapter: sqlite3
+ :database: ':memory:'
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb
new file mode 100644
index 0000000..224957f
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb
@@ -0,0 +1,8 @@
+class Photo < ActiveRecord::Base
+ acts_as_taggable
+
+ belongs_to :user
+end
+
+class SpecialPhoto < Photo
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml
new file mode 100644
index 0000000..25a4118
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml
@@ -0,0 +1,24 @@
+jonathan_dog:
+ id: 1
+ user_id: 1
+ title: A small dog
+
+jonathan_questioning_dog:
+ id: 2
+ user_id: 1
+ title: What does this dog want?
+
+jonathan_bad_cat:
+ id: 3
+ user_id: 1
+ title: Bad cat
+
+sam_flower:
+ id: 4
+ user_id: 2
+ title: Flower
+
+sam_sky:
+ id: 5
+ user_id: 2
+ title: Sky
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb
new file mode 100644
index 0000000..bee100a
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb
@@ -0,0 +1,7 @@
+class Post < ActiveRecord::Base
+ acts_as_taggable
+
+ belongs_to :user
+
+ validates_presence_of :text
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml
new file mode 100644
index 0000000..d0cd9ac
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml
@@ -0,0 +1,24 @@
+jonathan_sky:
+ id: 1
+ user_id: 1
+ text: The sky is particularly blue today
+
+jonathan_grass:
+ id: 2
+ user_id: 1
+ text: The grass seems very green
+
+jonathan_rain:
+ id: 3
+ user_id: 1
+ text: Why does the rain fall?
+
+sam_ground:
+ id: 4
+ user_id: 2
+ text: The ground is looking too brown
+
+sam_flowers:
+ id: 5
+ user_id: 2
+ text: Why are the flowers dead?
\ No newline at end of file
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml
new file mode 100644
index 0000000..b6eb440
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml
@@ -0,0 +1,126 @@
+jonathan_sky_good:
+ id: 1
+ tag_id: 1
+ taggable_id: 1
+ taggable_type: Post
+ created_at: 2006-08-01
+
+jonathan_sky_nature:
+ id: 2
+ tag_id: 3
+ taggable_id: 1
+ taggable_type: Post
+ created_at: 2006-08-02
+
+jonathan_grass_nature:
+ id: 3
+ tag_id: 3
+ taggable_id: 2
+ taggable_type: Post
+ created_at: 2006-08-03
+
+jonathan_rain_question:
+ id: 4
+ tag_id: 4
+ taggable_id: 3
+ taggable_type: Post
+ created_at: 2006-08-04
+
+jonathan_rain_nature:
+ id: 5
+ tag_id: 3
+ taggable_id: 3
+ taggable_type: Post
+ created_at: 2006-08-05
+
+sam_ground_nature:
+ id: 6
+ tag_id: 3
+ taggable_id: 4
+ taggable_type: Post
+ created_at: 2006-08-06
+
+sam_ground_bad:
+ id: 7
+ tag_id: 2
+ taggable_id: 4
+ taggable_type: Post
+ created_at: 2006-08-07
+
+sam_flowers_good:
+ id: 8
+ tag_id: 1
+ taggable_id: 5
+ taggable_type: Post
+ created_at: 2006-08-08
+
+sam_flowers_nature:
+ id: 9
+ tag_id: 3
+ taggable_id: 5
+ taggable_type: Post
+ created_at: 2006-08-09
+
+
+jonathan_dog_animal:
+ id: 10
+ tag_id: 5
+ taggable_id: 1
+ taggable_type: Photo
+ created_at: 2006-08-10
+
+jonathan_dog_nature:
+ id: 11
+ tag_id: 3
+ taggable_id: 1
+ taggable_type: Photo
+ created_at: 2006-08-11
+
+jonathan_questioning_dog_animal:
+ id: 12
+ tag_id: 5
+ taggable_id: 2
+ taggable_type: Photo
+ created_at: 2006-08-12
+
+jonathan_questioning_dog_question:
+ id: 13
+ tag_id: 4
+ taggable_id: 2
+ taggable_type: Photo
+ created_at: 2006-08-13
+
+jonathan_bad_cat_bad:
+ id: 14
+ tag_id: 2
+ taggable_id: 3
+ taggable_type: Photo
+ created_at: 2006-08-14
+
+jonathan_bad_cat_animal:
+ id: 15
+ tag_id: 5
+ taggable_id: 3
+ taggable_type: Photo
+ created_at: 2006-08-15
+
+sam_flower_nature:
+ id: 16
+ tag_id: 3
+ taggable_id: 4
+ taggable_type: Photo
+ created_at: 2006-08-16
+
+sam_flower_good:
+ id: 17
+ tag_id: 1
+ taggable_id: 4
+ taggable_type: Photo
+ created_at: 2006-08-17
+
+sam_sky_nature:
+ id: 18
+ tag_id: 3
+ taggable_id: 5
+ taggable_type: Photo
+ created_at: 2006-08-18
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml
new file mode 100644
index 0000000..b8f8367
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml
@@ -0,0 +1,19 @@
+good:
+ id: 1
+ name: Very good
+
+bad:
+ id: 2
+ name: Bad
+
+nature:
+ id: 3
+ name: Nature
+
+question:
+ id: 4
+ name: Question
+
+animal:
+ id: 5
+ name: Crazy animal
\ No newline at end of file
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb
new file mode 100644
index 0000000..c85a292
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb
@@ -0,0 +1,4 @@
+class User < ActiveRecord::Base
+ has_many :posts
+ has_many :photos
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml
new file mode 100644
index 0000000..da94fea
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml
@@ -0,0 +1,7 @@
+jonathan:
+ id: 1
+ name: Jonathan
+
+sam:
+ id: 2
+ name: Sam
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/schema.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/schema.rb
new file mode 100644
index 0000000..3d6f008
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/test/schema.rb
@@ -0,0 +1,27 @@
+ActiveRecord::Schema.define :version => 0 do
+ create_table :tags, :force => true do |t|
+ t.column :name, :string
+ end
+
+ create_table :taggings, :force => true do |t|
+ t.column :tag_id, :integer
+ t.column :taggable_id, :integer
+ t.column :taggable_type, :string
+ t.column :created_at, :datetime
+ end
+
+ create_table :users, :force => true do |t|
+ t.column :name, :string
+ end
+
+ create_table :posts, :force => true do |t|
+ t.column :text, :text
+ t.column :cached_tag_list, :string
+ t.column :user_id, :integer
+ end
+
+ create_table :photos, :force => true do |t|
+ t.column :title, :string
+ t.column :user_id, :integer
+ end
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/tag_list_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_list_test.rb
new file mode 100644
index 0000000..efa84d6
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_list_test.rb
@@ -0,0 +1,98 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+
+class TagListTest < Test::Unit::TestCase
+ def test_blank?
+ assert TagList.new.blank?
+ end
+
+ def test_equality
+ assert_equal TagList.new, TagList.new
+ assert_equal TagList.new("Tag"), TagList.new("Tag")
+
+ assert_not_equal TagList.new, ""
+ assert_not_equal TagList.new, TagList.new("Tag")
+ end
+
+ def test_parse_leaves_string_unchanged
+ tags = '"One ", Two'
+ original = tags.dup
+ TagList.parse(tags)
+ assert_equal tags, original
+ end
+
+ def test_from_single_name
+ assert_equal %w(Fun), TagList.from("Fun").names
+ assert_equal %w(Fun), TagList.from('"Fun"').names
+ end
+
+ def test_from_blank
+ assert_equal [], TagList.from(nil).names
+ assert_equal [], TagList.from("").names
+ end
+
+ def test_from_single_quoted_tag
+ assert_equal ['with, comma'], TagList.from('"with, comma"').names
+ end
+
+ def test_spaces_do_not_delineate
+ assert_equal ['A B', 'C'], TagList.from('A B, C').names
+ end
+
+ def test_from_multiple_tags
+ assert_equivalent %w(Alpha Beta Delta Gamma), TagList.from("Alpha, Beta, Delta, Gamma").names.sort
+ end
+
+ def test_from_multiple_tags_with_quotes
+ assert_equivalent %w(Alpha Beta Delta Gamma), TagList.from('Alpha, "Beta", Gamma , "Delta"').names.sort
+ end
+
+ def test_from_multiple_tags_with_quote_and_commas
+ assert_equivalent ['Alpha, Beta', 'Delta', 'Gamma, something'], TagList.from('"Alpha, Beta", Delta, "Gamma, something"').names
+ end
+
+ def test_from_removes_white_space
+ assert_equivalent %w(Alpha Beta), TagList.from('" Alpha ", "Beta "').names
+ assert_equivalent %w(Alpha Beta), TagList.from(' Alpha, Beta ').names
+ end
+
+ def test_alternative_delimiter
+ TagList.delimiter = " "
+
+ assert_equal %w(One Two), TagList.from("One Two").names
+ assert_equal ['One two', 'three', 'four'], TagList.from('"One two" three four').names
+ ensure
+ TagList.delimiter = ","
+ end
+
+ def test_duplicate_tags_removed
+ assert_equal %w(One), TagList.from("One, One").names
+ end
+
+ def test_to_s_with_commas
+ assert_equal "Question, Crazy Animal", TagList.new(["Question", "Crazy Animal"]).to_s
+ end
+
+ def test_to_s_with_alternative_delimiter
+ TagList.delimiter = " "
+
+ assert_equal '"Crazy Animal" Question', TagList.new(["Crazy Animal", "Question"]).to_s
+ ensure
+ TagList.delimiter = ","
+ end
+
+ def test_add
+ tag_list = TagList.new("One")
+ assert_equal %w(One), tag_list.names
+
+ tag_list.add("Two")
+ assert_equal %w(One Two), tag_list.names
+ end
+
+ def test_remove
+ tag_list = TagList.new("One", "Two")
+ assert_equal %w(One Two), tag_list.names
+
+ tag_list.remove("One")
+ assert_equal %w(Two), tag_list.names
+ end
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb
new file mode 100644
index 0000000..c882a53
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb
@@ -0,0 +1,34 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+
+class TagTest < Test::Unit::TestCase
+ fixtures :tags, :taggings, :users, :photos, :posts
+
+ def test_name_required
+ t = Tag.create
+ assert_match /blank/, t.errors[:name].to_s
+ end
+
+ def test_name_unique
+ t = Tag.create!(:name => "My tag")
+ duplicate = t.clone
+
+ assert !duplicate.save
+ assert_match /taken/, duplicate.errors[:name].to_s
+ end
+
+ def test_taggings
+ assert_equivalent [taggings(:jonathan_sky_good), taggings(:sam_flowers_good), taggings(:sam_flower_good)], tags(:good).taggings
+ assert_equivalent [taggings(:sam_ground_bad), taggings(:jonathan_bad_cat_bad)], tags(:bad).taggings
+ end
+
+ def test_to_s
+ assert_equal tags(:good).name, tags(:good).to_s
+ end
+
+ def test_equality
+ assert_equal tags(:good), tags(:good)
+ assert_equal Tag.find(1), Tag.find(1)
+ assert_equal Tag.new(:name => 'A'), Tag.new(:name => 'A')
+ assert_not_equal Tag.new(:name => 'A'), Tag.new(:name => 'B')
+ end
+end
diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb
new file mode 100644
index 0000000..172b8e2
--- /dev/null
+++ b/vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb
@@ -0,0 +1,13 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+
+class TaggingTest < Test::Unit::TestCase
+ fixtures :tags, :taggings, :posts
+
+ def test_tag
+ assert_equal tags(:good), taggings(:jonathan_sky_good).tag
+ end
+
+ def test_taggable
+ assert_equal posts(:jonathan_sky), taggings(:jonathan_sky_good).taggable
+ end
+end
diff --git a/vendor/plugins/attachment_fu/CHANGELOG b/vendor/plugins/attachment_fu/CHANGELOG
new file mode 100644
index 0000000..3dd22dd
--- /dev/null
+++ b/vendor/plugins/attachment_fu/CHANGELOG
@@ -0,0 +1,19 @@
+* April 2, 2007 *
+
+* don't copy the #full_filename to the default #temp_paths array if it doesn't exist
+* add default ID partitioning for attachments
+* add #binmode call to Tempfile (note: ruby should be doing this!) [Eric Beland]
+* Check for current type of :thumbnails option.
+* allow customization of the S3 configuration file path with the :s3_config_path option.
+* Don't try to remove thumbnails if there aren't any. Closes #3 [ben stiglitz]
+
+* BC * (before changelog)
+
+* add default #temp_paths entry [mattly]
+* add MiniMagick support to attachment_fu [Isacc]
+* update #destroy_file to clear out any empty directories too [carlivar]
+* fix references to S3Backend module [Hunter Hillegas]
+* make #current_data public with db_file and s3 backends [ebryn]
+* oops, actually svn add the files for s3 backend. [Jeffrey Hardy]
+* experimental s3 support, egad, no tests.... [Jeffrey Hardy]
+* doh, fix a few bad references to ActsAsAttachment [sixty4bit]
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/README b/vendor/plugins/attachment_fu/README
new file mode 100644
index 0000000..331fbfb
--- /dev/null
+++ b/vendor/plugins/attachment_fu/README
@@ -0,0 +1,162 @@
+attachment-fu
+=====================
+
+attachment_fu is a plugin by Rick Olson (aka technoweenie ) and is the successor to acts_as_attachment. To get a basic run-through of its capabilities, check out Mike Clark's tutorial .
+
+
+attachment_fu functionality
+===========================
+
+attachment_fu facilitates file uploads in Ruby on Rails. There are a few storage options for the actual file data, but the plugin always at a minimum stores metadata for each file in the database.
+
+There are three storage options for files uploaded through attachment_fu:
+ File system
+ Database file
+ Amazon S3
+
+Each method of storage many options associated with it that will be covered in the following section. Something to note, however, is that the Amazon S3 storage requires you to modify config/amazon_s3.yml and the Database file storage requires an extra table.
+
+
+attachment_fu models
+====================
+
+For all three of these storage options a table of metadata is required. This table will contain information about the file (hence the 'meta') and its location. This table has no restrictions on naming, unlike the extra table required for database storage, which must have a table name of db_files (and by convention a model of DbFile).
+
+In the model there are two methods made available by this plugins: has_attachment and validates_as_attachment.
+
+has_attachment(options = {})
+ This method accepts the options in a hash:
+ :content_type # Allowed content types.
+ # Allows all by default. Use :image to allow all standard image types.
+ :min_size # Minimum size allowed.
+ # 1 byte is the default.
+ :max_size # Maximum size allowed.
+ # 1.megabyte is the default.
+ :size # Range of sizes allowed.
+ # (1..1.megabyte) is the default. This overrides the :min_size and :max_size options.
+ :resize_to # Used by RMagick to resize images.
+ # Pass either an array of width/height, or a geometry string.
+ :thumbnails # Specifies a set of thumbnails to generate.
+ # This accepts a hash of filename suffixes and RMagick resizing options.
+ # This option need only be included if you want thumbnailing.
+ :thumbnail_class # Set which model class to use for thumbnails.
+ # This current attachment class is used by default.
+ :path_prefix # path to store the uploaded files.
+ # Uses public/#{table_name} by default for the filesystem, and just #{table_name} for the S3 backend.
+ # Setting this sets the :storage to :file_system.
+ :storage # Specifies the storage system to use..
+ # Defaults to :db_system. Options are :file_system, :db_file, and :s3.
+ :processor # Sets the image processor to use for resizing of the attached image.
+ # Options include ImageScience, Rmagick, and MiniMagick. Default is whatever is installed.
+
+
+ Examples:
+ has_attachment :max_size => 1.kilobyte
+ has_attachment :size => 1.megabyte..2.megabytes
+ has_attachment :content_type => 'application/pdf'
+ has_attachment :content_type => ['application/pdf', 'application/msword', 'text/plain']
+ has_attachment :content_type => :image, :resize_to => [50,50]
+ has_attachment :content_type => ['application/pdf', :image], :resize_to => 'x50'
+ has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
+ has_attachment :storage => :file_system, :path_prefix => 'public/files'
+ has_attachment :storage => :file_system, :path_prefix => 'public/files',
+ :content_type => :image, :resize_to => [50,50]
+ has_attachment :storage => :file_system, :path_prefix => 'public/files',
+ :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
+ has_attachment :storage => :s3
+
+validates_as_attachment
+ This method prevents files outside of the valid range (:min_size to :max_size, or the :size range) from being saved. It does not however, halt the upload of such files. They will be uploaded into memory regardless of size before validation.
+
+ Example:
+ validates_as_attachment
+
+
+attachment_fu migrations
+========================
+
+Fields for attachment_fu metadata tables...
+ in general:
+ size, :integer # file size in bytes
+ content_type, :string # mime type, ex: application/mp3
+ filename, :string # sanitized filename
+ that reference images:
+ height, :integer # in pixels
+ width, :integer # in pixels
+ that reference images that will be thumbnailed:
+ parent_id, :integer # id of parent image (on the same table, a self-referencing foreign-key).
+ # Only populated if the current object is a thumbnail.
+ thumbnail, :string # the 'type' of thumbnail this attachment record describes.
+ # Only populated if the current object is a thumbnail.
+ # Usage:
+ # [ In Model 'Avatar' ]
+ # has_attachment :content_type => :image,
+ # :storage => :file_system,
+ # :max_size => 500.kilobytes,
+ # :resize_to => '320x200>',
+ # :thumbnails => { :small => '10x10>',
+ # :thumb => '100x100>' }
+ # [ Elsewhere ]
+ # @user.avatar.thumbnails.first.thumbnail #=> 'small'
+ that reference files stored in the database (:db_file):
+ db_file_id, :integer # id of the file in the database (foreign key)
+
+Field for attachment_fu db_files table:
+ data, :binary # binary file data, for use in database file storage
+
+
+attachment_fu views
+===================
+
+There are two main views tasks that will be directly affected by attachment_fu: upload forms and displaying uploaded images.
+
+There are two parts of the upload form that differ from typical usage.
+ 1. Include ':multipart => true' in the html options of the form_for tag.
+ Example:
+ <% form_for(:attachment_metadata, :url => { :action => "create" }, :html => { :multipart => true }) do |form| %>
+
+ 2. Use the file_field helper with :uploaded_data as the field name.
+ Example:
+ <%= form.file_field :uploaded_data %>
+
+Displaying uploaded images is made easy by the public_filename method of the ActiveRecord attachment objects using file system and s3 storage.
+
+public_filename(thumbnail = nil)
+ Returns the public path to the file. If a thumbnail prefix is specified it will return the public file path to the corresponding thumbnail.
+ Examples:
+ attachment_obj.public_filename #=> /attachments/2/file.jpg
+ attachment_obj.public_filename(:thumb) #=> /attachments/2/file_thumb.jpg
+ attachment_obj.public_filename(:small) #=> /attachments/2/file_small.jpg
+
+When serving files from database storage, doing more than simply downloading the file is beyond the scope of this document.
+
+
+attachment_fu controllers
+=========================
+
+There are two considerations to take into account when using attachment_fu in controllers.
+
+The first is when the files have no publicly accessible path and need to be downloaded through an action.
+
+Example:
+ def readme
+ send_file '/path/to/readme.txt', :type => 'plain/text', :disposition => 'inline'
+ end
+
+See the possible values for send_file for reference.
+
+
+The second is when saving the file when submitted from a form.
+Example in view:
+ <%= form.file_field :attachable, :uploaded_data %>
+
+Example in controller:
+ def create
+ @attachable_file = AttachmentMetadataModel.new(params[:attachable])
+ if @attachable_file.save
+ flash[:notice] = 'Attachment was successfully created.'
+ redirect_to attachable_url(@attachable_file)
+ else
+ render :action => :new
+ end
+ end
diff --git a/vendor/plugins/attachment_fu/Rakefile b/vendor/plugins/attachment_fu/Rakefile
new file mode 100644
index 0000000..0851dd4
--- /dev/null
+++ b/vendor/plugins/attachment_fu/Rakefile
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the attachment_fu plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the attachment_fu plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'ActsAsAttachment'
+ rdoc.options << '--line-numbers --inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
diff --git a/vendor/plugins/attachment_fu/amazon_s3.yml.tpl b/vendor/plugins/attachment_fu/amazon_s3.yml.tpl
new file mode 100644
index 0000000..81cb807
--- /dev/null
+++ b/vendor/plugins/attachment_fu/amazon_s3.yml.tpl
@@ -0,0 +1,14 @@
+development:
+ bucket_name: appname_development
+ access_key_id:
+ secret_access_key:
+
+test:
+ bucket_name: appname_test
+ access_key_id:
+ secret_access_key:
+
+production:
+ bucket_name: appname
+ access_key_id:
+ secret_access_key:
diff --git a/vendor/plugins/attachment_fu/init.rb b/vendor/plugins/attachment_fu/init.rb
new file mode 100644
index 0000000..0239e56
--- /dev/null
+++ b/vendor/plugins/attachment_fu/init.rb
@@ -0,0 +1,14 @@
+require 'tempfile'
+
+Tempfile.class_eval do
+ # overwrite so tempfiles use the extension of the basename. important for rmagick and image science
+ def make_tmpname(basename, n)
+ ext = nil
+ sprintf("%s%d-%d%s", basename.to_s.gsub(/\.\w+$/) { |s| ext = s; '' }, $$, n, ext)
+ end
+end
+
+require 'geometry'
+ActiveRecord::Base.send(:extend, Technoweenie::AttachmentFu::ActMethods)
+Technoweenie::AttachmentFu.tempfile_path = ATTACHMENT_FU_TEMPFILE_PATH if Object.const_defined?(:ATTACHMENT_FU_TEMPFILE_PATH)
+FileUtils.mkdir_p Technoweenie::AttachmentFu.tempfile_path
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/install.rb b/vendor/plugins/attachment_fu/install.rb
new file mode 100644
index 0000000..2938164
--- /dev/null
+++ b/vendor/plugins/attachment_fu/install.rb
@@ -0,0 +1,5 @@
+require 'fileutils'
+
+s3_config = File.dirname(__FILE__) + '/../../../config/amazon_s3.yml'
+FileUtils.cp File.dirname(__FILE__) + '/amazon_s3.yml.tpl', s3_config unless File.exist?(s3_config)
+puts IO.read(File.join(File.dirname(__FILE__), 'README'))
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/lib/geometry.rb b/vendor/plugins/attachment_fu/lib/geometry.rb
new file mode 100644
index 0000000..2d6e381
--- /dev/null
+++ b/vendor/plugins/attachment_fu/lib/geometry.rb
@@ -0,0 +1,93 @@
+# This Geometry class was yanked from RMagick. However, it lets ImageMagick handle the actual change_geometry.
+# Use #new_dimensions_for to get new dimensons
+# Used so I can use spiffy RMagick geometry strings with ImageScience
+class Geometry
+ # ! and @ are removed until support for them is added
+ FLAGS = ['', '%', '<', '>']#, '!', '@']
+ RFLAGS = { '%' => :percent,
+ '!' => :aspect,
+ '<' => :>,
+ '>' => :<,
+ '@' => :area }
+
+ attr_accessor :width, :height, :x, :y, :flag
+
+ def initialize(width=nil, height=nil, x=nil, y=nil, flag=nil)
+ # Support floating-point width and height arguments so Geometry
+ # objects can be used to specify Image#density= arguments.
+ raise ArgumentError, "width must be >= 0: #{width}" if width < 0
+ raise ArgumentError, "height must be >= 0: #{height}" if height < 0
+ @width = width.to_f
+ @height = height.to_f
+ @x = x.to_i
+ @y = y.to_i
+ @flag = flag
+ end
+
+ # Construct an object from a geometry string
+ RE = /\A(\d*)(?:x(\d+))?([-+]\d+)?([-+]\d+)?([%!<>@]?)\Z/
+
+ def self.from_s(str)
+ raise(ArgumentError, "no geometry string specified") unless str
+
+ if m = RE.match(str)
+ new(m[1].to_i, m[2].to_i, m[3].to_i, m[4].to_i, RFLAGS[m[5]])
+ else
+ raise ArgumentError, "invalid geometry format"
+ end
+ end
+
+ # Convert object to a geometry string
+ def to_s
+ str = ''
+ str << "%g" % @width if @width > 0
+ str << 'x' if (@width > 0 || @height > 0)
+ str << "%g" % @height if @height > 0
+ str << "%+d%+d" % [@x, @y] if (@x != 0 || @y != 0)
+ str << FLAGS[@flag.to_i]
+ end
+
+ # attempts to get new dimensions for the current geometry string given these old dimensions.
+ # This doesn't implement the aspect flag (!) or the area flag (@). PDI
+ def new_dimensions_for(orig_width, orig_height)
+ new_width = orig_width
+ new_height = orig_height
+
+ case @flag
+ when :percent
+ scale_x = @width.zero? ? 100 : @width
+ scale_y = @height.zero? ? @width : @height
+ new_width = scale_x.to_f * (orig_width.to_f / 100.0)
+ new_height = scale_y.to_f * (orig_height.to_f / 100.0)
+ when :<, :>, nil
+ scale_factor =
+ if new_width.zero? || new_height.zero?
+ 1.0
+ else
+ if @width.nonzero? && @height.nonzero?
+ [@width.to_f / new_width.to_f, @height.to_f / new_height.to_f].min
+ else
+ @width.nonzero? ? (@width.to_f / new_width.to_f) : (@height.to_f / new_height.to_f)
+ end
+ end
+ new_width = scale_factor * new_width.to_f
+ new_height = scale_factor * new_height.to_f
+ new_width = orig_width if @flag && orig_width.send(@flag, new_width)
+ new_height = orig_height if @flag && orig_height.send(@flag, new_height)
+ end
+
+ [new_width, new_height].collect! { |v| v.round }
+ end
+end
+
+class Array
+ # allows you to get new dimensions for the current array of dimensions with a given geometry string
+ #
+ # [50, 64] / '40>' # => [40, 51]
+ def /(geometry)
+ raise ArgumentError, "Only works with a [width, height] pair" if size != 2
+ raise ArgumentError, "Must pass a valid geometry string or object" unless geometry.is_a?(String) || geometry.is_a?(Geometry)
+ geometry = Geometry.from_s(geometry) if geometry.is_a?(String)
+ geometry.new_dimensions_for first, last
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu.rb
new file mode 100644
index 0000000..8894283
--- /dev/null
+++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu.rb
@@ -0,0 +1,405 @@
+module Technoweenie # :nodoc:
+ module AttachmentFu # :nodoc:
+ @@default_processors = %w(ImageScience Rmagick MiniMagick)
+ @@tempfile_path = File.join(RAILS_ROOT, 'tmp', 'attachment_fu')
+ @@content_types = ['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png']
+ mattr_reader :content_types, :tempfile_path, :default_processors
+ mattr_writer :tempfile_path
+
+ class ThumbnailError < StandardError; end
+ class AttachmentError < StandardError; end
+
+ module ActMethods
+ # Options:
+ # * :content_type - Allowed content types. Allows all by default. Use :image to allow all standard image types.
+ # * :min_size - Minimum size allowed. 1 byte is the default.
+ # * :max_size - Maximum size allowed. 1.megabyte is the default.
+ # * :size - Range of sizes allowed. (1..1.megabyte) is the default. This overrides the :min_size and :max_size options.
+ # * :resize_to - Used by RMagick to resize images. Pass either an array of width/height, or a geometry string.
+ # * :thumbnails - Specifies a set of thumbnails to generate. This accepts a hash of filename suffixes and RMagick resizing options.
+ # * :thumbnail_class - Set what class to use for thumbnails. This attachment class is used by default.
+ # * :path_prefix - path to store the uploaded files. Uses public/#{table_name} by default for the filesystem, and just #{table_name}
+ # for the S3 backend. Setting this sets the :storage to :file_system.
+ # * :storage - Use :file_system to specify the attachment data is stored with the file system. Defaults to :db_system.
+ #
+ # Examples:
+ # has_attachment :max_size => 1.kilobyte
+ # has_attachment :size => 1.megabyte..2.megabytes
+ # has_attachment :content_type => 'application/pdf'
+ # has_attachment :content_type => ['application/pdf', 'application/msword', 'text/plain']
+ # has_attachment :content_type => :image, :resize_to => [50,50]
+ # has_attachment :content_type => ['application/pdf', :image], :resize_to => 'x50'
+ # has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
+ # has_attachment :storage => :file_system, :path_prefix => 'public/files'
+ # has_attachment :storage => :file_system, :path_prefix => 'public/files',
+ # :content_type => :image, :resize_to => [50,50]
+ # has_attachment :storage => :file_system, :path_prefix => 'public/files',
+ # :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
+ # has_attachment :storage => :s3
+ def has_attachment(options = {})
+ # this allows you to redefine the acts' options for each subclass, however
+ options[:min_size] ||= 1
+ options[:max_size] ||= 1.megabyte
+ options[:size] ||= (options[:min_size]..options[:max_size])
+ options[:thumbnails] ||= {}
+ options[:thumbnail_class] ||= self
+ options[:s3_access] ||= :public_read
+ options[:content_type] = [options[:content_type]].flatten.collect! { |t| t == :image ? Technoweenie::AttachmentFu.content_types : t }.flatten unless options[:content_type].nil?
+
+ unless options[:thumbnails].is_a?(Hash)
+ raise ArgumentError, ":thumbnails option should be a hash: e.g. :thumbnails => { :foo => '50x50' }"
+ end
+
+ # doing these shenanigans so that #attachment_options is available to processors and backends
+ class_inheritable_accessor :attachment_options
+ self.attachment_options = options
+
+ # only need to define these once on a class
+ unless included_modules.include?(InstanceMethods)
+ attr_accessor :thumbnail_resize_options
+
+ attachment_options[:storage] ||= (attachment_options[:file_system_path] || attachment_options[:path_prefix]) ? :file_system : :db_file
+ attachment_options[:path_prefix] ||= attachment_options[:file_system_path]
+ if attachment_options[:path_prefix].nil?
+ attachment_options[:path_prefix] = attachment_options[:storage] == :s3 ? table_name : File.join("public", table_name)
+ end
+ attachment_options[:path_prefix] = attachment_options[:path_prefix][1..-1] if options[:path_prefix].first == '/'
+
+ with_options :foreign_key => 'parent_id' do |m|
+ m.has_many :thumbnails, :class_name => attachment_options[:thumbnail_class].to_s
+ m.belongs_to :parent, :class_name => base_class.to_s
+ end
+ before_destroy :destroy_thumbnails
+
+ before_validation :set_size_from_temp_path
+ after_save :after_process_attachment
+ after_destroy :destroy_file
+ extend ClassMethods
+ include InstanceMethods
+ include Technoweenie::AttachmentFu::Backends.const_get("#{options[:storage].to_s.classify}Backend")
+ case attachment_options[:processor]
+ when :none
+ when nil
+ processors = Technoweenie::AttachmentFu.default_processors.dup
+ begin
+ include Technoweenie::AttachmentFu::Processors.const_get("#{processors.first}Processor") if processors.any?
+ rescue LoadError, MissingSourceFile
+ processors.shift
+ retry
+ end
+ else
+ begin
+ include Technoweenie::AttachmentFu::Processors.const_get("#{options[:processor].to_s.classify}Processor")
+ rescue LoadError, MissingSourceFile
+ puts "Problems loading #{options[:processor]}Processor: #{$!}"
+ end
+ end
+ after_validation :process_attachment
+ end
+ end
+ end
+
+ module ClassMethods
+ delegate :content_types, :to => Technoweenie::AttachmentFu
+
+ # Performs common validations for attachment models.
+ def validates_as_attachment
+ validates_presence_of :size, :content_type, :filename
+ validate :attachment_attributes_valid?
+ end
+
+ # Returns true or false if the given content type is recognized as an image.
+ def image?(content_type)
+ content_types.include?(content_type)
+ end
+
+ # Callback after an image has been resized.
+ #
+ # class Foo < ActiveRecord::Base
+ # acts_as_attachment
+ # after_resize do |record, img|
+ # record.aspect_ratio = img.columns.to_f / img.rows.to_f
+ # end
+ # end
+ def after_resize(&block)
+ write_inheritable_array(:after_resize, [block])
+ end
+
+ # Callback after an attachment has been saved either to the file system or the DB.
+ # Only called if the file has been changed, not necessarily if the record is updated.
+ #
+ # class Foo < ActiveRecord::Base
+ # acts_as_attachment
+ # after_attachment_saved do |record|
+ # ...
+ # end
+ # end
+ def after_attachment_saved(&block)
+ write_inheritable_array(:after_attachment_saved, [block])
+ end
+
+ # Callback before a thumbnail is saved. Use this to pass any necessary extra attributes that may be required.
+ #
+ # class Foo < ActiveRecord::Base
+ # acts_as_attachment
+ # before_thumbnail_saved do |record, thumbnail|
+ # ...
+ # end
+ # end
+ def before_thumbnail_saved(&block)
+ write_inheritable_array(:before_thumbnail_saved, [block])
+ end
+
+ # Get the thumbnail class, which is the current attachment class by default.
+ # Configure this with the :thumbnail_class option.
+ def thumbnail_class
+ attachment_options[:thumbnail_class] = attachment_options[:thumbnail_class].constantize unless attachment_options[:thumbnail_class].is_a?(Class)
+ attachment_options[:thumbnail_class]
+ end
+
+ # Copies the given file path to a new tempfile, returning the closed tempfile.
+ def copy_to_temp_file(file, temp_base_name)
+ returning Tempfile.new(temp_base_name, Technoweenie::AttachmentFu.tempfile_path) do |tmp|
+ tmp.close
+ FileUtils.cp file, tmp.path
+ end
+ end
+
+ # Writes the given data to a new tempfile, returning the closed tempfile.
+ def write_to_temp_file(data, temp_base_name)
+ returning Tempfile.new(temp_base_name, Technoweenie::AttachmentFu.tempfile_path) do |tmp|
+ tmp.binmode
+ tmp.write data
+ tmp.close
+ end
+ end
+ end
+
+ module InstanceMethods
+ # Checks whether the attachment's content type is an image content type
+ def image?
+ self.class.image?(content_type)
+ end
+
+ # Returns true/false if an attachment is thumbnailable. A thumbnailable attachment has an image content type and the parent_id attribute.
+ def thumbnailable?
+ image? && respond_to?(:parent_id)
+ end
+
+ # Returns the class used to create new thumbnails for this attachment.
+ def thumbnail_class
+ self.class.thumbnail_class
+ end
+
+ # Gets the thumbnail name for a filename. 'foo.jpg' becomes 'foo_thumbnail.jpg'
+ def thumbnail_name_for(thumbnail = nil)
+ return filename if thumbnail.blank?
+ ext = nil
+ basename = filename.gsub /\.\w+$/ do |s|
+ ext = s; ''
+ end
+ "#{basename}_#{thumbnail}#{ext}"
+ end
+
+ # Creates or updates the thumbnail for the current attachment.
+ def create_or_update_thumbnail(temp_file, file_name_suffix, *size)
+ thumbnailable? || raise(ThumbnailError.new("Can't create a thumbnail if the content type is not an image or there is no parent_id column"))
+ returning find_or_initialize_thumbnail(file_name_suffix) do |thumb|
+ thumb.attributes = {
+ :content_type => content_type,
+ :filename => thumbnail_name_for(file_name_suffix),
+ :temp_path => temp_file,
+ :thumbnail_resize_options => size
+ }
+ callback_with_args :before_thumbnail_saved, thumb
+ thumb.save!
+ end
+ end
+
+ # Sets the content type.
+ def content_type=(new_type)
+ write_attribute :content_type, new_type.to_s.strip
+ end
+
+ # Sanitizes a filename.
+ def filename=(new_name)
+ write_attribute :filename, sanitize_filename(new_name)
+ end
+
+ # Returns the width/height in a suitable format for the image_tag helper: (100x100)
+ def image_size
+ [width.to_s, height.to_s] * 'x'
+ end
+
+ # Returns true if the attachment data will be written to the storage system on the next save
+ def save_attachment?
+ File.file?(temp_path.to_s)
+ end
+
+ # nil placeholder in case this field is used in a form.
+ def uploaded_data() nil; end
+
+ # This method handles the uploaded file object. If you set the field name to uploaded_data, you don't need
+ # any special code in your controller.
+ #
+ # <% form_for :attachment, :html => { :multipart => true } do |f| -%>
+ # <%= f.file_field :uploaded_data %>
+ # <%= submit_tag :Save %>
+ # <% end -%>
+ #
+ # @attachment = Attachment.create! params[:attachment]
+ #
+ # TODO: Allow it to work with Merb tempfiles too.
+ def uploaded_data=(file_data)
+ return nil if file_data.nil? || file_data.size == 0
+ self.content_type = file_data.content_type
+ self.filename = file_data.original_filename if respond_to?(:filename)
+ if file_data.is_a?(StringIO)
+ file_data.rewind
+ self.temp_data = file_data.read
+ else
+ self.temp_path = file_data.path
+ end
+ end
+
+ # Gets the latest temp path from the collection of temp paths. While working with an attachment,
+ # multiple Tempfile objects may be created for various processing purposes (resizing, for example).
+ # An array of all the tempfile objects is stored so that the Tempfile instance is held on to until
+ # it's not needed anymore. The collection is cleared after saving the attachment.
+ def temp_path
+ p = temp_paths.first
+ p.respond_to?(:path) ? p.path : p.to_s
+ end
+
+ # Gets an array of the currently used temp paths. Defaults to a copy of #full_filename.
+ def temp_paths
+ @temp_paths ||= (new_record? || !File.exist?(full_filename)) ? [] : [copy_to_temp_file(full_filename)]
+ end
+
+ # Adds a new temp_path to the array. This should take a string or a Tempfile. This class makes no
+ # attempt to remove the files, so Tempfiles should be used. Tempfiles remove themselves when they go out of scope.
+ # You can also use string paths for temporary files, such as those used for uploaded files in a web server.
+ def temp_path=(value)
+ temp_paths.unshift value
+ temp_path
+ end
+
+ # Gets the data from the latest temp file. This will read the file into memory.
+ def temp_data
+ save_attachment? ? File.read(temp_path) : nil
+ end
+
+ # Writes the given data to a Tempfile and adds it to the collection of temp files.
+ def temp_data=(data)
+ self.temp_path = write_to_temp_file data unless data.nil?
+ end
+
+ # Copies the given file to a randomly named Tempfile.
+ def copy_to_temp_file(file)
+ self.class.copy_to_temp_file file, random_tempfile_filename
+ end
+
+ # Writes the given file to a randomly named Tempfile.
+ def write_to_temp_file(data)
+ self.class.write_to_temp_file data, random_tempfile_filename
+ end
+
+ # Stub for creating a temp file from the attachment data. This should be defined in the backend module.
+ def create_temp_file() end
+
+ # Allows you to work with a processed representation (RMagick, ImageScience, etc) of the attachment in a block.
+ #
+ # @attachment.with_image do |img|
+ # self.data = img.thumbnail(100, 100).to_blob
+ # end
+ #
+ def with_image(&block)
+ self.class.with_image(temp_path, &block)
+ end
+
+ protected
+ # Generates a unique filename for a Tempfile.
+ def random_tempfile_filename
+ "#{rand Time.now.to_i}#{filename || 'attachment'}"
+ end
+
+ def sanitize_filename(filename)
+ returning filename.strip do |name|
+ # NOTE: File.basename doesn't work right with Windows paths on Unix
+ # get only the filename, not the whole path
+ name.gsub! /^.*(\\|\/)/, ''
+
+ # Finally, replace all non alphanumeric, underscore or periods with underscore
+ name.gsub! /[^\w\.\-]/, '_'
+ end
+ end
+
+ # before_validation callback.
+ def set_size_from_temp_path
+ self.size = File.size(temp_path) if save_attachment?
+ end
+
+ # validates the size and content_type attributes according to the current model's options
+ def attachment_attributes_valid?
+ [:size, :content_type].each do |attr_name|
+ enum = attachment_options[attr_name]
+ errors.add attr_name, ActiveRecord::Errors.default_error_messages[:inclusion] unless enum.nil? || enum.include?(send(attr_name))
+ end
+ end
+
+ # Initializes a new thumbnail with the given suffix.
+ def find_or_initialize_thumbnail(file_name_suffix)
+ respond_to?(:parent_id) ?
+ thumbnail_class.find_or_initialize_by_thumbnail_and_parent_id(file_name_suffix.to_s, id) :
+ thumbnail_class.find_or_initialize_by_thumbnail(file_name_suffix.to_s)
+ end
+
+ # Stub for a #process_attachment method in a processor
+ def process_attachment
+ @saved_attachment = save_attachment?
+ end
+
+ # Cleans up after processing. Thumbnails are created, the attachment is stored to the backend, and the temp_paths are cleared.
+ def after_process_attachment
+ if @saved_attachment
+ if respond_to?(:process_attachment_with_processing) && thumbnailable? && !attachment_options[:thumbnails].blank? && parent_id.nil?
+ temp_file = temp_path || create_temp_file
+ attachment_options[:thumbnails].each { |suffix, size| create_or_update_thumbnail(temp_file, suffix, *size) }
+ end
+ save_to_storage
+ @temp_paths.clear
+ @saved_attachment = nil
+ callback :after_attachment_saved
+ end
+ end
+
+ # Resizes the given processed img object with either the attachment resize options or the thumbnail resize options.
+ def resize_image_or_thumbnail!(img)
+ if (!respond_to?(:parent_id) || parent_id.nil?) && attachment_options[:resize_to] # parent image
+ resize_image(img, attachment_options[:resize_to])
+ elsif thumbnail_resize_options # thumbnail
+ resize_image(img, thumbnail_resize_options)
+ end
+ end
+
+ # Yanked from ActiveRecord::Callbacks, modified so I can pass args to the callbacks besides self.
+ # Only accept blocks, however
+ def callback_with_args(method, arg = self)
+ notify(method)
+
+ result = nil
+ callbacks_for(method).each do |callback|
+ result = callback.call(self, arg)
+ return false if result == false
+ end
+
+ return result
+ end
+
+ # Removes the thumbnails for the attachment, if it has any
+ def destroy_thumbnails
+ self.thumbnails.each { |thumbnail| thumbnail.destroy } if thumbnailable?
+ end
+ end
+ end
+end
diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/db_file_backend.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/db_file_backend.rb
new file mode 100644
index 0000000..23881e7
--- /dev/null
+++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/db_file_backend.rb
@@ -0,0 +1,39 @@
+module Technoweenie # :nodoc:
+ module AttachmentFu # :nodoc:
+ module Backends
+ # Methods for DB backed attachments
+ module DbFileBackend
+ def self.included(base) #:nodoc:
+ Object.const_set(:DbFile, Class.new(ActiveRecord::Base)) unless Object.const_defined?(:DbFile)
+ base.belongs_to :db_file, :class_name => '::DbFile', :foreign_key => 'db_file_id'
+ end
+
+ # Creates a temp file with the current db data.
+ def create_temp_file
+ write_to_temp_file current_data
+ end
+
+ # Gets the current data from the database
+ def current_data
+ db_file.data
+ end
+
+ protected
+ # Destroys the file. Called in the after_destroy callback
+ def destroy_file
+ db_file.destroy if db_file
+ end
+
+ # Saves the data to the DbFile model
+ def save_to_storage
+ if save_attachment?
+ (db_file || build_db_file).data = temp_data
+ db_file.save!
+ self.class.update_all ['db_file_id = ?', self.db_file_id = db_file.id], ['id = ?', id]
+ end
+ true
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/file_system_backend.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/file_system_backend.rb
new file mode 100644
index 0000000..464b9c7
--- /dev/null
+++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/file_system_backend.rb
@@ -0,0 +1,97 @@
+require 'ftools'
+module Technoweenie # :nodoc:
+ module AttachmentFu # :nodoc:
+ module Backends
+ # Methods for file system backed attachments
+ module FileSystemBackend
+ def self.included(base) #:nodoc:
+ base.before_update :rename_file
+ end
+
+ # Gets the full path to the filename in this format:
+ #
+ # # This assumes a model name like MyModel
+ # # public/#{table_name} is the default filesystem path
+ # RAILS_ROOT/public/my_models/5/blah.jpg
+ #
+ # Overwrite this method in your model to customize the filename.
+ # The optional thumbnail argument will output the thumbnail's filename.
+ def full_filename(thumbnail = nil)
+ file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s
+ File.join(RAILS_ROOT, file_system_path, *partitioned_path(thumbnail_name_for(thumbnail)))
+ end
+
+ # Used as the base path that #public_filename strips off full_filename to create the public path
+ def base_path
+ @base_path ||= File.join(RAILS_ROOT, 'public')
+ end
+
+ # The attachment ID used in the full path of a file
+ def attachment_path_id
+ ((respond_to?(:parent_id) && parent_id) || id).to_i
+ end
+
+ # overrwrite this to do your own app-specific partitioning.
+ # you can thank Jamis Buck for this: http://www.37signals.com/svn/archives2/id_partitioning.php
+ def partitioned_path(*args)
+ ("%08d" % attachment_path_id).scan(/..../) + args
+ end
+
+ # Gets the public path to the file
+ # The optional thumbnail argument will output the thumbnail's filename.
+ def public_filename(thumbnail = nil)
+ full_filename(thumbnail).gsub %r(^#{Regexp.escape(base_path)}), ''
+ end
+
+ def filename=(value)
+ @old_filename = full_filename unless filename.nil? || @old_filename
+ write_attribute :filename, sanitize_filename(value)
+ end
+
+ # Creates a temp file from the currently saved file.
+ def create_temp_file
+ copy_to_temp_file full_filename
+ end
+
+ protected
+ # Destroys the file. Called in the after_destroy callback
+ def destroy_file
+ FileUtils.rm full_filename
+ # remove directory also if it is now empty
+ Dir.rmdir(File.dirname(full_filename)) if (Dir.entries(File.dirname(full_filename))-['.','..']).empty?
+ rescue
+ logger.info "Exception destroying #{full_filename.inspect}: [#{$!.class.name}] #{$1.to_s}"
+ logger.warn $!.backtrace.collect { |b| " > #{b}" }.join("\n")
+ end
+
+ # Renames the given file before saving
+ def rename_file
+ return unless @old_filename && @old_filename != full_filename
+ if save_attachment? && File.exists?(@old_filename)
+ FileUtils.rm @old_filename
+ elsif File.exists?(@old_filename)
+ FileUtils.mv @old_filename, full_filename
+ end
+ @old_filename = nil
+ true
+ end
+
+ # Saves the file to the file system
+ def save_to_storage
+ if save_attachment?
+ # TODO: This overwrites the file if it exists, maybe have an allow_overwrite option?
+ FileUtils.mkdir_p(File.dirname(full_filename))
+ File.cp(temp_path, full_filename)
+ File.chmod(attachment_options[:chmod] || 0644, full_filename)
+ end
+ @old_filename = nil
+ true
+ end
+
+ def current_data
+ File.file?(full_filename) ? File.read(full_filename) : nil
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/s3_backend.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/s3_backend.rb
new file mode 100644
index 0000000..dab1291
--- /dev/null
+++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/s3_backend.rb
@@ -0,0 +1,309 @@
+module Technoweenie # :nodoc:
+ module AttachmentFu # :nodoc:
+ module Backends
+ # = AWS::S3 Storage Backend
+ #
+ # Enables use of {Amazon's Simple Storage Service}[http://aws.amazon.com/s3] as a storage mechanism
+ #
+ # == Requirements
+ #
+ # Requires the {AWS::S3 Library}[http://amazon.rubyforge.org] for S3 by Marcel Molina Jr. installed either
+ # as a gem or a as a Rails plugin.
+ #
+ # == Configuration
+ #
+ # Configuration is done via RAILS_ROOT/config/amazon_s3.yml and is loaded according to the RAILS_ENV.
+ # The minimum connection options that you must specify are a bucket name, your access key id and your secret access key.
+ # If you don't already have your access keys, all you need to sign up for the S3 service is an account at Amazon.
+ # You can sign up for S3 and get access keys by visiting http://aws.amazon.com/s3.
+ #
+ # Example configuration (RAILS_ROOT/config/amazon_s3.yml)
+ #
+ # development:
+ # bucket_name: appname_development
+ # access_key_id:
+ # secret_access_key:
+ #
+ # test:
+ # bucket_name: appname_test
+ # access_key_id:
+ # secret_access_key:
+ #
+ # production:
+ # bucket_name: appname
+ # access_key_id:
+ # secret_access_key:
+ #
+ # You can change the location of the config path by passing a full path to the :s3_config_path option.
+ #
+ # has_attachment :storage => :s3, :s3_config_path => (RAILS_ROOT + '/config/s3.yml')
+ #
+ # === Required configuration parameters
+ #
+ # * :access_key_id - The access key id for your S3 account. Provided by Amazon.
+ # * :secret_access_key - The secret access key for your S3 account. Provided by Amazon.
+ # * :bucket_name - A unique bucket name (think of the bucket_name as being like a database name).
+ #
+ # If any of these required arguments is missing, a MissingAccessKey exception will be raised from AWS::S3.
+ #
+ # == About bucket names
+ #
+ # Bucket names have to be globaly unique across the S3 system. And you can only have up to 100 of them,
+ # so it's a good idea to think of a bucket as being like a database, hence the correspondance in this
+ # implementation to the development, test, and production environments.
+ #
+ # The number of objects you can store in a bucket is, for all intents and purposes, unlimited.
+ #
+ # === Optional configuration parameters
+ #
+ # * :server - The server to make requests to. Defaults to s3.amazonaws.com.
+ # * :port - The port to the requests should be made on. Defaults to 80 or 443 if :use_ssl is set.
+ # * :use_ssl - If set to true, :port will be implicitly set to 443, unless specified otherwise. Defaults to false.
+ #
+ # == Usage
+ #
+ # To specify S3 as the storage mechanism for a model, set the acts_as_attachment :storage option to :s3.
+ #
+ # class Photo < ActiveRecord::Base
+ # has_attachment :storage => :s3
+ # end
+ #
+ # === Customizing the path
+ #
+ # By default, files are prefixed using a pseudo hierarchy in the form of :table_name/:id, which results
+ # in S3 urls that look like: http(s)://:server/:bucket_name/:table_name/:id/:filename with :table_name
+ # representing the customizable portion of the path. You can customize this prefix using the :path_prefix
+ # option:
+ #
+ # class Photo < ActiveRecord::Base
+ # has_attachment :storage => :s3, :path_prefix => 'my/custom/path'
+ # end
+ #
+ # Which would result in URLs like http(s)://:server/:bucket_name/my/custom/path/:id/:filename.
+ #
+ # === Permissions
+ #
+ # By default, files are stored on S3 with public access permissions. You can customize this using
+ # the :s3_access option to has_attachment. Available values are
+ # :private, :public_read_write, and :authenticated_read.
+ #
+ # === Other options
+ #
+ # Of course, all the usual configuration options apply, such as content_type and thumbnails:
+ #
+ # class Photo < ActiveRecord::Base
+ # has_attachment :storage => :s3, :content_type => ['application/pdf', :image], :resize_to => 'x50'
+ # has_attachment :storage => :s3, :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
+ # end
+ #
+ # === Accessing S3 URLs
+ #
+ # You can get an object's URL using the s3_url accessor. For example, assuming that for your postcard app
+ # you had a bucket name like 'postcard_world_development', and an attachment model called Photo:
+ #
+ # @postcard.s3_url # => http(s)://s3.amazonaws.com/postcard_world_development/photos/1/mexico.jpg
+ #
+ # The resulting url is in the form: http(s)://:server/:bucket_name/:table_name/:id/:file.
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
+ #
+ # Additionally, you can get an object's base path relative to the bucket root using
+ # base_path:
+ #
+ # @photo.file_base_path # => photos/1
+ #
+ # And the full path (including the filename) using full_filename:
+ #
+ # @photo.full_filename # => photos/
+ #
+ # Niether base_path or full_filename include the bucket name as part of the path.
+ # You can retrieve the bucket name using the bucket_name method.
+ module S3Backend
+ class RequiredLibraryNotFoundError < StandardError; end
+ class ConfigFileNotFoundError < StandardError; end
+
+ def self.included(base) #:nodoc:
+ mattr_reader :bucket_name, :s3_config
+
+ begin
+ require 'aws/s3'
+ include AWS::S3
+ rescue LoadError
+ raise RequiredLibraryNotFoundError.new('AWS::S3 could not be loaded')
+ end
+
+ begin
+ @@s3_config_path = base.attachment_options[:s3_config_path] || (RAILS_ROOT + '/config/amazon_s3.yml')
+ @@s3_config = YAML.load_file(@@s3_config_path)[ENV['RAILS_ENV']].symbolize_keys
+ #rescue
+ # raise ConfigFileNotFoundError.new('File %s not found' % @@s3_config_path)
+ end
+
+ @@bucket_name = s3_config[:bucket_name]
+
+ Base.establish_connection!(
+ :access_key_id => s3_config[:access_key_id],
+ :secret_access_key => s3_config[:secret_access_key],
+ :server => s3_config[:server],
+ :port => s3_config[:port],
+ :use_ssl => s3_config[:use_ssl]
+ )
+
+ # Bucket.create(@@bucket_name)
+
+ base.before_update :rename_file
+ end
+
+ def self.protocol
+ @protocol ||= s3_config[:use_ssl] ? 'https://' : 'http://'
+ end
+
+ def self.hostname
+ @hostname ||= s3_config[:server] || AWS::S3::DEFAULT_HOST
+ end
+
+ def self.port_string
+ @port_string ||= s3_config[:port] == (s3_config[:use_ssl] ? 443 : 80) ? '' : ":#{s3_config[:port]}"
+ end
+
+ module ClassMethods
+ def s3_protocol
+ Technoweenie::AttachmentFu::Backends::S3Backend.protocol
+ end
+
+ def s3_hostname
+ Technoweenie::AttachmentFu::Backends::S3Backend.hostname
+ end
+
+ def s3_port_string
+ Technoweenie::AttachmentFu::Backends::S3Backend.port_string
+ end
+ end
+
+ # Overwrites the base filename writer in order to store the old filename
+ def filename=(value)
+ @old_filename = filename unless filename.nil? || @old_filename
+ write_attribute :filename, sanitize_filename(value)
+ end
+
+ # The attachment ID used in the full path of a file
+ def attachment_path_id
+ ((respond_to?(:parent_id) && parent_id) || id).to_s
+ end
+
+ # The pseudo hierarchy containing the file relative to the bucket name
+ # Example: :table_name/:id
+ def base_path
+ File.join(attachment_options[:path_prefix], attachment_path_id)
+ end
+
+ # The full path to the file relative to the bucket name
+ # Example: :table_name/:id/:filename
+ def full_filename(thumbnail = nil)
+ File.join(base_path, thumbnail_name_for(thumbnail))
+ end
+
+ # All public objects are accessible via a GET request to the S3 servers. You can generate a
+ # url for an object using the s3_url method.
+ #
+ # @photo.s3_url
+ #
+ # The resulting url is in the form: http(s)://:server/:bucket_name/:table_name/:id/:file where
+ # the :server variable defaults to AWS::S3 URL::DEFAULT_HOST (s3.amazonaws.com) and can be
+ # set using the configuration parameters in RAILS_ROOT/config/amazon_s3.yml.
+ #
+ # The optional thumbnail argument will output the thumbnail's filename (if any).
+ def s3_url(thumbnail = nil)
+ File.join(s3_protocol + s3_hostname + s3_port_string, bucket_name, full_filename(thumbnail))
+ end
+ alias :public_filename :s3_url
+
+ # All private objects are accessible via an authenticated GET request to the S3 servers. You can generate an
+ # authenticated url for an object like this:
+ #
+ # @photo.authenticated_s3_url
+ #
+ # By default authenticated urls expire 5 minutes after they were generated.
+ #
+ # Expiration options can be specified either with an absolute time using the :expires option,
+ # or with a number of seconds relative to now with the :expires_in option:
+ #
+ # # Absolute expiration date (October 13th, 2025)
+ # @photo.authenticated_s3_url(:expires => Time.mktime(2025,10,13).to_i)
+ #
+ # # Expiration in five hours from now
+ # @photo.authenticated_s3_url(:expires_in => 5.hours)
+ #
+ # You can specify whether the url should go over SSL with the :use_ssl option.
+ # By default, the ssl settings for the current connection will be used:
+ #
+ # @photo.authenticated_s3_url(:use_ssl => true)
+ #
+ # Finally, the optional thumbnail argument will output the thumbnail's filename (if any):
+ #
+ # @photo.authenticated_s3_url('thumbnail', :expires_in => 5.hours, :use_ssl => true)
+ def authenticated_s3_url(*args)
+ thumbnail = args.first.is_a?(String) ? args.first : nil
+ options = args.last.is_a?(Hash) ? args.last : {}
+ S3Object.url_for(full_filename(thumbnail), bucket_name, options)
+ end
+
+ def create_temp_file
+ write_to_temp_file current_data
+ end
+
+ def current_data
+ S3Object.value full_filename, bucket_name
+ end
+
+ def s3_protocol
+ Technoweenie::AttachmentFu::Backends::S3Backend.protocol
+ end
+
+ def s3_hostname
+ Technoweenie::AttachmentFu::Backends::S3Backend.hostname
+ end
+
+ def s3_port_string
+ Technoweenie::AttachmentFu::Backends::S3Backend.port_string
+ end
+
+ protected
+ # Called in the after_destroy callback
+ def destroy_file
+ S3Object.delete full_filename, bucket_name
+ end
+
+ def rename_file
+ return unless @old_filename && @old_filename != filename
+
+ old_full_filename = File.join(base_path, @old_filename)
+
+ S3Object.rename(
+ old_full_filename,
+ full_filename,
+ bucket_name,
+ :access => attachment_options[:s3_access]
+ )
+
+ @old_filename = nil
+ true
+ end
+
+ def save_to_storage
+ if save_attachment?
+ S3Object.store(
+ full_filename,
+ temp_data,
+ bucket_name,
+ :content_type => content_type,
+ :access => attachment_options[:s3_access]
+ )
+ end
+
+ @old_filename = nil
+ true
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/image_science_processor.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/image_science_processor.rb
new file mode 100644
index 0000000..37c1415
--- /dev/null
+++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/image_science_processor.rb
@@ -0,0 +1,55 @@
+require 'image_science'
+module Technoweenie # :nodoc:
+ module AttachmentFu # :nodoc:
+ module Processors
+ module ImageScienceProcessor
+ def self.included(base)
+ base.send :extend, ClassMethods
+ base.alias_method_chain :process_attachment, :processing
+ end
+
+ module ClassMethods
+ # Yields a block containing an RMagick Image for the given binary data.
+ def with_image(file, &block)
+ ::ImageScience.with_image file, &block
+ end
+ end
+
+ protected
+ def process_attachment_with_processing
+ return unless process_attachment_without_processing && image?
+ with_image do |img|
+ self.width = img.width if respond_to?(:width)
+ self.height = img.height if respond_to?(:height)
+ resize_image_or_thumbnail! img
+ end
+ end
+
+ # Performs the actual resizing operation for a thumbnail
+ def resize_image(img, size)
+ # create a dummy temp file to write to
+ filename.sub! /gif$/, 'png'
+ self.temp_path = write_to_temp_file(filename)
+ grab_dimensions = lambda do |img|
+ self.width = img.width if respond_to?(:width)
+ self.height = img.height if respond_to?(:height)
+ img.save temp_path
+ callback_with_args :after_resize, img
+ end
+
+ size = size.first if size.is_a?(Array) && size.length == 1
+ if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
+ if size.is_a?(Fixnum)
+ img.thumbnail(size, &grab_dimensions)
+ else
+ img.resize(size[0], size[1], &grab_dimensions)
+ end
+ else
+ new_size = [img.width, img.height] / size.to_s
+ img.resize(new_size[0], new_size[1], &grab_dimensions)
+ end
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb
new file mode 100644
index 0000000..e5a534c
--- /dev/null
+++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb
@@ -0,0 +1,56 @@
+require 'mini_magick'
+module Technoweenie # :nodoc:
+ module AttachmentFu # :nodoc:
+ module Processors
+ module MiniMagickProcessor
+ def self.included(base)
+ base.send :extend, ClassMethods
+ base.alias_method_chain :process_attachment, :processing
+ end
+
+ module ClassMethods
+ # Yields a block containing an MiniMagick Image for the given binary data.
+ def with_image(file, &block)
+ begin
+ binary_data = file.is_a?(MiniMagick::Image) ? file : MiniMagick::Image.from_file(file) unless !Object.const_defined?(:MiniMagick)
+ rescue
+ # Log the failure to load the image.
+ logger.debug("Exception working with image: #{$!}")
+ binary_data = nil
+ end
+ block.call binary_data if block && binary_data
+ ensure
+ !binary_data.nil?
+ end
+ end
+
+ protected
+ def process_attachment_with_processing
+ return unless process_attachment_without_processing
+ with_image do |img|
+ resize_image_or_thumbnail! img
+ self.width = img[:width] if respond_to?(:width)
+ self.height = img[:height] if respond_to?(:height)
+ callback_with_args :after_resize, img
+ end if image?
+ end
+
+ # Performs the actual resizing operation for a thumbnail
+ def resize_image(img, size)
+ size = size.first if size.is_a?(Array) && size.length == 1
+ if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
+ if size.is_a?(Fixnum)
+ size = [size, size]
+ img.resize(size.join('x'))
+ else
+ img.resize(size.join('x') + '!')
+ end
+ else
+ img.resize(size.to_s)
+ end
+ self.temp_path = img
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb
new file mode 100644
index 0000000..7999edb
--- /dev/null
+++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb
@@ -0,0 +1,53 @@
+require 'RMagick'
+module Technoweenie # :nodoc:
+ module AttachmentFu # :nodoc:
+ module Processors
+ module RmagickProcessor
+ def self.included(base)
+ base.send :extend, ClassMethods
+ base.alias_method_chain :process_attachment, :processing
+ end
+
+ module ClassMethods
+ # Yields a block containing an RMagick Image for the given binary data.
+ def with_image(file, &block)
+ begin
+ binary_data = file.is_a?(Magick::Image) ? file : Magick::Image.read(file).first unless !Object.const_defined?(:Magick)
+ rescue
+ # Log the failure to load the image. This should match ::Magick::ImageMagickError
+ # but that would cause acts_as_attachment to require rmagick.
+ logger.debug("Exception working with image: #{$!}")
+ binary_data = nil
+ end
+ block.call binary_data if block && binary_data
+ ensure
+ !binary_data.nil?
+ end
+ end
+
+ protected
+ def process_attachment_with_processing
+ return unless process_attachment_without_processing
+ with_image do |img|
+ resize_image_or_thumbnail! img
+ self.width = img.columns if respond_to?(:width)
+ self.height = img.rows if respond_to?(:height)
+ callback_with_args :after_resize, img
+ end if image?
+ end
+
+ # Performs the actual resizing operation for a thumbnail
+ def resize_image(img, size)
+ size = size.first if size.is_a?(Array) && size.length == 1 && !size.first.is_a?(Fixnum)
+ if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
+ size = [size, size] if size.is_a?(Fixnum)
+ img.thumbnail!(*size)
+ else
+ img.change_geometry(size.to_s) { |cols, rows, image| image.resize!(cols, rows) }
+ end
+ self.temp_path = write_to_temp_file(img.to_blob)
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/test/amazon_s3.yml b/vendor/plugins/attachment_fu/test/amazon_s3.yml
new file mode 100644
index 0000000..0024c8e
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/amazon_s3.yml
@@ -0,0 +1,6 @@
+test:
+ bucket_name: afu
+ access_key_id: YOURACCESSKEY
+ secret_access_key: YOURSECRETACCESSKEY
+ server: 127.0.0.1
+ port: 3002
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/test/backends/db_file_test.rb b/vendor/plugins/attachment_fu/test/backends/db_file_test.rb
new file mode 100644
index 0000000..e95bb49
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/backends/db_file_test.rb
@@ -0,0 +1,16 @@
+require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
+
+class DbFileTest < Test::Unit::TestCase
+ include BaseAttachmentTests
+ attachment_model Attachment
+
+ def test_should_call_after_attachment_saved(klass = Attachment)
+ attachment_model.saves = 0
+ assert_created do
+ upload_file :filename => '/files/rails.png'
+ end
+ assert_equal 1, attachment_model.saves
+ end
+
+ test_against_subclass :test_should_call_after_attachment_saved, Attachment
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/test/backends/file_system_test.rb b/vendor/plugins/attachment_fu/test/backends/file_system_test.rb
new file mode 100644
index 0000000..d3250c1
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/backends/file_system_test.rb
@@ -0,0 +1,80 @@
+require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
+
+class FileSystemTest < Test::Unit::TestCase
+ include BaseAttachmentTests
+ attachment_model FileAttachment
+
+ def test_filesystem_size_for_file_attachment(klass = FileAttachment)
+ attachment_model klass
+ assert_created 1 do
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_equal attachment.size, File.open(attachment.full_filename).stat.size
+ end
+ end
+
+ test_against_subclass :test_filesystem_size_for_file_attachment, FileAttachment
+
+ def test_should_not_overwrite_file_attachment(klass = FileAttachment)
+ attachment_model klass
+ assert_created 2 do
+ real = upload_file :filename => '/files/rails.png'
+ assert_valid real
+ assert !real.new_record?, real.errors.full_messages.join("\n")
+ assert !real.size.zero?
+
+ fake = upload_file :filename => '/files/fake/rails.png'
+ assert_valid fake
+ assert !fake.size.zero?
+
+ assert_not_equal File.open(real.full_filename).stat.size, File.open(fake.full_filename).stat.size
+ end
+ end
+
+ test_against_subclass :test_should_not_overwrite_file_attachment, FileAttachment
+
+ def test_should_store_file_attachment_in_filesystem(klass = FileAttachment)
+ attachment_model klass
+ attachment = nil
+ assert_created do
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_valid attachment
+ assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist"
+ end
+ attachment
+ end
+
+ test_against_subclass :test_should_store_file_attachment_in_filesystem, FileAttachment
+
+ def test_should_delete_old_file_when_updating(klass = FileAttachment)
+ attachment_model klass
+ attachment = upload_file :filename => '/files/rails.png'
+ old_filename = attachment.full_filename
+ assert_not_created do
+ use_temp_file 'files/rails.png' do |file|
+ attachment.filename = 'rails2.png'
+ attachment.temp_path = File.join(fixture_path, file)
+ attachment.save!
+ assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist"
+ assert !File.exists?(old_filename), "#{old_filename} still exists"
+ end
+ end
+ end
+
+ test_against_subclass :test_should_delete_old_file_when_updating, FileAttachment
+
+ def test_should_delete_old_file_when_renaming(klass = FileAttachment)
+ attachment_model klass
+ attachment = upload_file :filename => '/files/rails.png'
+ old_filename = attachment.full_filename
+ assert_not_created do
+ attachment.filename = 'rails2.png'
+ attachment.save
+ assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist"
+ assert !File.exists?(old_filename), "#{old_filename} still exists"
+ assert !attachment.reload.size.zero?
+ assert_equal 'rails2.png', attachment.filename
+ end
+ end
+
+ test_against_subclass :test_should_delete_old_file_when_renaming, FileAttachment
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/test/backends/remote/s3_test.rb b/vendor/plugins/attachment_fu/test/backends/remote/s3_test.rb
new file mode 100644
index 0000000..82520a0
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/backends/remote/s3_test.rb
@@ -0,0 +1,103 @@
+require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'test_helper'))
+require 'net/http'
+
+class S3Test < Test::Unit::TestCase
+ if File.exist?(File.join(File.dirname(__FILE__), '../../amazon_s3.yml'))
+ include BaseAttachmentTests
+ attachment_model S3Attachment
+
+ def test_should_create_correct_bucket_name(klass = S3Attachment)
+ attachment_model klass
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_equal attachment.s3_config[:bucket_name], attachment.bucket_name
+ end
+
+ test_against_subclass :test_should_create_correct_bucket_name, S3Attachment
+
+ def test_should_create_default_path_prefix(klass = S3Attachment)
+ attachment_model klass
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_equal File.join(attachment_model.table_name, attachment.attachment_path_id), attachment.base_path
+ end
+
+ test_against_subclass :test_should_create_default_path_prefix, S3Attachment
+
+ def test_should_create_custom_path_prefix(klass = S3WithPathPrefixAttachment)
+ attachment_model klass
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_equal File.join('some/custom/path/prefix', attachment.attachment_path_id), attachment.base_path
+ end
+
+ test_against_subclass :test_should_create_custom_path_prefix, S3WithPathPrefixAttachment
+
+ def test_should_create_valid_url(klass = S3Attachment)
+ attachment_model klass
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_equal "#{s3_protocol}#{s3_hostname}#{s3_port_string}/#{attachment.bucket_name}/#{attachment.full_filename}", attachment.s3_url
+ end
+
+ test_against_subclass :test_should_create_valid_url, S3Attachment
+
+ def test_should_create_authenticated_url(klass = S3Attachment)
+ attachment_model klass
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_match /^http.+AWSAccessKeyId.+Expires.+Signature.+/, attachment.authenticated_s3_url(:use_ssl => true)
+ end
+
+ test_against_subclass :test_should_create_authenticated_url, S3Attachment
+
+ def test_should_save_attachment(klass = S3Attachment)
+ attachment_model klass
+ assert_created do
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_valid attachment
+ assert attachment.image?
+ assert !attachment.size.zero?
+ assert_kind_of Net::HTTPOK, http_response_for(attachment.s3_url)
+ end
+ end
+
+ test_against_subclass :test_should_save_attachment, S3Attachment
+
+ def test_should_delete_attachment_from_s3_when_attachment_record_destroyed(klass = S3Attachment)
+ attachment_model klass
+ attachment = upload_file :filename => '/files/rails.png'
+
+ urls = [attachment.s3_url] + attachment.thumbnails.collect(&:s3_url)
+
+ urls.each {|url| assert_kind_of Net::HTTPOK, http_response_for(url) }
+ attachment.destroy
+ urls.each do |url|
+ begin
+ http_response_for(url)
+ rescue Net::HTTPForbidden, Net::HTTPNotFound
+ nil
+ end
+ end
+ end
+
+ test_against_subclass :test_should_delete_attachment_from_s3_when_attachment_record_destroyed, S3Attachment
+
+ protected
+ def http_response_for(url)
+ url = URI.parse(url)
+ Net::HTTP.start(url.host, url.port) {|http| http.request_head(url.path) }
+ end
+
+ def s3_protocol
+ Technoweenie::AttachmentFu::Backends::S3Backend.protocol
+ end
+
+ def s3_hostname
+ Technoweenie::AttachmentFu::Backends::S3Backend.hostname
+ end
+
+ def s3_port_string
+ Technoweenie::AttachmentFu::Backends::S3Backend.port_string
+ end
+ else
+ def test_flunk_s3
+ puts "s3 config file not loaded, tests not running"
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/test/base_attachment_tests.rb b/vendor/plugins/attachment_fu/test/base_attachment_tests.rb
new file mode 100644
index 0000000..c9dbbd7
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/base_attachment_tests.rb
@@ -0,0 +1,57 @@
+module BaseAttachmentTests
+ def test_should_create_file_from_uploaded_file
+ assert_created do
+ attachment = upload_file :filename => '/files/foo.txt'
+ assert_valid attachment
+ assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
+ assert attachment.image?
+ assert !attachment.size.zero?
+ #assert_equal 3, attachment.size
+ assert_nil attachment.width
+ assert_nil attachment.height
+ end
+ end
+
+ def test_reassign_attribute_data
+ assert_created 1 do
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_valid attachment
+ assert attachment.size > 0, "no data was set"
+
+ attachment.temp_data = 'wtf'
+ assert attachment.save_attachment?
+ attachment.save!
+
+ assert_equal 'wtf', attachment_model.find(attachment.id).send(:current_data)
+ end
+ end
+
+ def test_no_reassign_attribute_data_on_nil
+ assert_created 1 do
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_valid attachment
+ assert attachment.size > 0, "no data was set"
+
+ attachment.temp_data = nil
+ assert !attachment.save_attachment?
+ end
+ end
+
+ def test_should_overwrite_old_contents_when_updating
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_not_created do # no new db_file records
+ use_temp_file 'files/rails.png' do |file|
+ attachment.filename = 'rails2.png'
+ attachment.temp_path = File.join(fixture_path, file)
+ attachment.save!
+ end
+ end
+ end
+
+ def test_should_save_without_updating_file
+ attachment = upload_file :filename => '/files/foo.txt'
+ assert_valid attachment
+ assert !attachment.save_attachment?
+ assert_nothing_raised { attachment.save! }
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/test/basic_test.rb b/vendor/plugins/attachment_fu/test/basic_test.rb
new file mode 100644
index 0000000..2094eb1
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/basic_test.rb
@@ -0,0 +1,64 @@
+require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
+
+class BasicTest < Test::Unit::TestCase
+ def test_should_set_default_min_size
+ assert_equal 1, Attachment.attachment_options[:min_size]
+ end
+
+ def test_should_set_default_max_size
+ assert_equal 1.megabyte, Attachment.attachment_options[:max_size]
+ end
+
+ def test_should_set_default_size
+ assert_equal (1..1.megabyte), Attachment.attachment_options[:size]
+ end
+
+ def test_should_set_default_thumbnails_option
+ assert_equal Hash.new, Attachment.attachment_options[:thumbnails]
+ end
+
+ def test_should_set_default_thumbnail_class
+ assert_equal Attachment, Attachment.attachment_options[:thumbnail_class]
+ end
+
+ def test_should_normalize_content_types_to_array
+ assert_equal %w(pdf), PdfAttachment.attachment_options[:content_type]
+ assert_equal %w(pdf doc txt), DocAttachment.attachment_options[:content_type]
+ assert_equal ['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png'], ImageAttachment.attachment_options[:content_type]
+ assert_equal ['pdf', 'image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png'], ImageOrPdfAttachment.attachment_options[:content_type]
+ end
+
+ def test_should_sanitize_content_type
+ @attachment = Attachment.new :content_type => ' foo '
+ assert_equal 'foo', @attachment.content_type
+ end
+
+ def test_should_sanitize_filenames
+ @attachment = Attachment.new :filename => 'blah/foo.bar'
+ assert_equal 'foo.bar', @attachment.filename
+
+ @attachment.filename = 'blah\\foo.bar'
+ assert_equal 'foo.bar', @attachment.filename
+
+ @attachment.filename = 'f o!O-.bar'
+ assert_equal 'f_o_O-.bar', @attachment.filename
+ end
+
+ def test_should_convert_thumbnail_name
+ @attachment = FileAttachment.new :filename => 'foo.bar'
+ assert_equal 'foo.bar', @attachment.thumbnail_name_for(nil)
+ assert_equal 'foo.bar', @attachment.thumbnail_name_for('')
+ assert_equal 'foo_blah.bar', @attachment.thumbnail_name_for(:blah)
+ assert_equal 'foo_blah.blah.bar', @attachment.thumbnail_name_for('blah.blah')
+
+ @attachment.filename = 'foo.bar.baz'
+ assert_equal 'foo.bar_blah.baz', @attachment.thumbnail_name_for(:blah)
+ end
+
+ def test_should_require_valid_thumbnails_option
+ klass = Class.new(ActiveRecord::Base)
+ assert_raise ArgumentError do
+ klass.has_attachment :thumbnails => []
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/test/database.yml b/vendor/plugins/attachment_fu/test/database.yml
new file mode 100644
index 0000000..1c6ece7
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/database.yml
@@ -0,0 +1,18 @@
+sqlite:
+ :adapter: sqlite
+ :dbfile: attachment_fu_plugin.sqlite.db
+sqlite3:
+ :adapter: sqlite3
+ :dbfile: attachment_fu_plugin.sqlite3.db
+postgresql:
+ :adapter: postgresql
+ :username: postgres
+ :password: postgres
+ :database: attachment_fu_plugin_test
+ :min_messages: ERROR
+mysql:
+ :adapter: mysql
+ :host: localhost
+ :username: rails
+ :password:
+ :database: attachment_fu_plugin_test
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/test/extra_attachment_test.rb b/vendor/plugins/attachment_fu/test/extra_attachment_test.rb
new file mode 100644
index 0000000..15b1852
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/extra_attachment_test.rb
@@ -0,0 +1,57 @@
+require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
+
+class OrphanAttachmentTest < Test::Unit::TestCase
+ include BaseAttachmentTests
+ attachment_model OrphanAttachment
+
+ def test_should_create_image_from_uploaded_file
+ assert_created do
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_valid attachment
+ assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
+ assert attachment.image?
+ assert !attachment.size.zero?
+ end
+ end
+
+ def test_should_create_file_from_uploaded_file
+ assert_created do
+ attachment = upload_file :filename => '/files/foo.txt'
+ assert_valid attachment
+ assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
+ assert attachment.image?
+ assert !attachment.size.zero?
+ end
+ end
+
+ def test_should_create_image_from_uploaded_file_with_custom_content_type
+ assert_created do
+ attachment = upload_file :content_type => 'foo/bar', :filename => '/files/rails.png'
+ assert_valid attachment
+ assert !attachment.image?
+ assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
+ assert !attachment.size.zero?
+ #assert_equal 1784, attachment.size
+ end
+ end
+
+ def test_should_create_thumbnail
+ attachment = upload_file :filename => '/files/rails.png'
+
+ assert_raise Technoweenie::AttachmentFu::ThumbnailError do
+ attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 50, 50)
+ end
+ end
+
+ def test_should_create_thumbnail_with_geometry_string
+ attachment = upload_file :filename => '/files/rails.png'
+
+ assert_raise Technoweenie::AttachmentFu::ThumbnailError do
+ attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 'x50')
+ end
+ end
+end
+
+class MinimalAttachmentTest < OrphanAttachmentTest
+ attachment_model MinimalAttachment
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/test/fixtures/attachment.rb b/vendor/plugins/attachment_fu/test/fixtures/attachment.rb
new file mode 100644
index 0000000..77d60c3
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/fixtures/attachment.rb
@@ -0,0 +1,127 @@
+class Attachment < ActiveRecord::Base
+ @@saves = 0
+ cattr_accessor :saves
+ has_attachment :processor => :rmagick
+ validates_as_attachment
+ after_attachment_saved do |record|
+ self.saves += 1
+ end
+end
+
+class SmallAttachment < Attachment
+ has_attachment :max_size => 1.kilobyte
+end
+
+class BigAttachment < Attachment
+ has_attachment :size => 1.megabyte..2.megabytes
+end
+
+class PdfAttachment < Attachment
+ has_attachment :content_type => 'pdf'
+end
+
+class DocAttachment < Attachment
+ has_attachment :content_type => %w(pdf doc txt)
+end
+
+class ImageAttachment < Attachment
+ has_attachment :content_type => :image, :resize_to => [50,50]
+end
+
+class ImageOrPdfAttachment < Attachment
+ has_attachment :content_type => ['pdf', :image], :resize_to => 'x50'
+end
+
+class ImageWithThumbsAttachment < Attachment
+ has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }, :resize_to => [55,55]
+ after_resize do |record, img|
+ record.aspect_ratio = img.columns.to_f / img.rows.to_f
+ end
+end
+
+class FileAttachment < ActiveRecord::Base
+ has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', :processor => :rmagick
+ validates_as_attachment
+end
+
+class ImageFileAttachment < FileAttachment
+ has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files',
+ :content_type => :image, :resize_to => [50,50]
+end
+
+class ImageWithThumbsFileAttachment < FileAttachment
+ has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files',
+ :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }, :resize_to => [55,55]
+ after_resize do |record, img|
+ record.aspect_ratio = img.columns.to_f / img.rows.to_f
+ end
+end
+
+class ImageWithThumbsClassFileAttachment < FileAttachment
+ # use file_system_path to test backwards compatibility
+ has_attachment :file_system_path => 'vendor/plugins/attachment_fu/test/files',
+ :thumbnails => { :thumb => [50, 50] }, :resize_to => [55,55],
+ :thumbnail_class => 'ImageThumbnail'
+end
+
+class ImageThumbnail < FileAttachment
+ has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files/thumbnails'
+end
+
+# no parent
+class OrphanAttachment < ActiveRecord::Base
+ has_attachment :processor => :rmagick
+ validates_as_attachment
+end
+
+# no filename, no size, no content_type
+class MinimalAttachment < ActiveRecord::Base
+ has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', :processor => :rmagick
+ validates_as_attachment
+
+ def filename
+ "#{id}.file"
+ end
+end
+
+begin
+ class ImageScienceAttachment < ActiveRecord::Base
+ has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files',
+ :processor => :image_science, :thumbnails => { :thumb => [50, 51], :geometry => '31>' }, :resize_to => 55
+ end
+rescue MissingSourceFile
+ puts $!.message
+ puts "no ImageScience"
+end
+
+begin
+ class MiniMagickAttachment < ActiveRecord::Base
+ has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files',
+ :processor => :mini_magick, :thumbnails => { :thumb => [50, 51], :geometry => '31>' }, :resize_to => 55
+ end
+rescue MissingSourceFile
+ puts $!.message
+ puts "no Mini Magick"
+end
+
+begin
+ class MiniMagickAttachment < ActiveRecord::Base
+ has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files',
+ :processor => :mini_magick, :thumbnails => { :thumb => [50, 51], :geometry => '31>' }, :resize_to => 55
+ end
+rescue MissingSourceFile
+end
+
+begin
+ class S3Attachment < ActiveRecord::Base
+ has_attachment :storage => :s3, :processor => :rmagick, :s3_config_path => File.join(File.dirname(__FILE__), '../amazon_s3.yml')
+ validates_as_attachment
+ end
+
+ class S3WithPathPrefixAttachment < S3Attachment
+ has_attachment :storage => :s3, :path_prefix => 'some/custom/path/prefix', :processor => :rmagick
+ validates_as_attachment
+ end
+rescue Technoweenie::AttachmentFu::Backends::S3Backend::ConfigFileNotFoundError
+ puts "S3 error: #{$!}"
+end
diff --git a/vendor/plugins/attachment_fu/test/fixtures/files/fake/rails.png b/vendor/plugins/attachment_fu/test/fixtures/files/fake/rails.png
new file mode 100644
index 0000000000000000000000000000000000000000..0543c64edae591831b38730c8000e57bd4e251f8
GIT binary patch
literal 4217
zcmV-<5QguGP)=Y7XlbL`H}9+GyYMPd;W5+KB_IGn~#fy4$vo5XQlUyTh74Yp~YlsZqH#tC+w
z#P-vsanf9^?K-jJ*iYT0F|Ci(M-vAbiZBQXgg^*O=wQW4yQ{r-XLfe`efJ%WW@lH|
zUwNNp{LW~O&;0J+J1+a-t-F#T$@GUFt3J7L<9+wKm#?C`WslZ0E@DhhmZkgSqFJ=^
zf}O|Q>=3Y(qN!PLO3$3*i1?y`kpL#;JWr2=Wf|Kj%WvI($L?=Gi8+UVU=s3JjL0=r
z3apb;>=mLt!wFm_l`+*==*8J25h`Of7e5JS79V7+#)H{pMhYct_L@XeHb;}1Ea)Vn
zD94|F6%&vZT*(NmeiuwY0SBInX8_8(^N(w`?I`1gzSn*|@!{KZq~Sy{lSoLR6z(R6
zC6isOX>^C7QXm2$sShbP1IA{zMLf=oKVot?)t{~@Oiy<
z=#vKDe41tJ73HcX6^+2N<)(%`hq7{NwEflVCx1r|%At5-BuKSyB&c-+C$Xs5sLWt+
zgYUOU@bu3aS7)LGq4&>b&2xSYCG*pS0Kwwg%JN8kam^dMuID-aT_8lNnIr>`j
zp?j*<@3JpkYPiNy;lb99e{kj-P3Z}LIz0eE}*?2E)Ve90e#8N8Dw`QCaH_
zjwN9Gn#?pOU~jfpRNIGybiEI}Z0s{4z3)*U=t#t<=P5|Pw|KPRh}
zVJ`Pj-4zcgF?Ovx9U79t5p%iI)w+hSWQJdVqu8Y2gC*~vxBUXFR4Tn<|846Z{7NpX
zjGaCKLy3pexm1ry@{~EM&JGH@j^I+W*j;zuY$L-3CU
zH|`#K{Z};h!VS%pV%^@)kG%Ka>Csq8fx-4iE{$qIe@a?XAnI#-2bP`;^sagM?`tdS
zF25K6ldasM*a?$+)gQs>#-kCT%Ap`Qj$VX#DA3fstmSk9nc?Jt=ijh+gsSb0EnoaP
z48vaE()GkQX0ZKmFrtwWldr21*@sn25pV!d0!emM>`?
z@9w<(%yYdzdNv*JbuL}b3Kmwd6f@381;Flc@O<5^bv3zZGs4sr?
zs@~+aQI^d_12N=4E(
zI>cxs9g7ne1{P6bL^M}T=3qo^#73x(o{eNO{EIWOF*S9V^5TKg11H)oOBxn#Uar42
z1ybi08_S6+AKiEIz>&UJUP|;|8~OG7ieSOD7EcC2yO<1#vOgG^vMrjbXpZA6Z6*mZ
zXq2YW(5WlQp^!~n-~1DKM1_+W
zj+~K0DJJQoiH^6G%jLXg`);qB);tWYmZP4pB=`ldfd)QPpo%~RYW92gY=a|E4IG>>Q0B#s!_2l#~__B$&@^C@?5xU
zfMr=Z8sp3P1)H~;m@*BBVR!(Q<*k;zSyipvUlFSYy(z#hi#xCU=B>{r~%c6d$81GYE+?v79-Z%|Vrp6-`;WbiJt`
zw18#|)jDND{Pg{wk3#5~iYeg`!H#njg$OA>0T6xw!jGPY;?Z+pAAK1x>6NG-eo3v&
z7&WaXrfdjIsqubT;>=lAjwh;G7B4$|SMcm5KDvb=Hq8ijmYqAJ|sEC5QduA3Dn@ZR2
z-C4Ef*6|Cy!4JEVzA#0hic^Ke@ucQrnKG8AX<+fE??je#+N_))=0mp=H_0TkdaEkH
zqIEZ>~@d>@$dp8hiNsM_1o{^ZcXVgn7lT_ON+l(zHf|#4?nL79F&*b#K{t?X}m`
z6mqRkFMjli1r-EsC8&9!BJiE*oTYfl_hH0
zGceKN-Ly?((mF7@24OULEl)dIs-}#6dLXr`Y;9iqoyR}+gYT=Q+-yMVRV}MTk5?6y
zlM9uVm5y5l<(RkH;V?$5*IDl^87PPcHfqbMyR*Xu}#96)+R4yA_c)%La)9O
zSTsh6%PVF8(lVNJZL`_wMhDRS^wXTvF|P(r*tAt$w~8ugUs1vv$e4VNOK4RM$V9J;
zb6H9PX-qht^o<>VU|;)}4hN5&LZvz{tc6P&si;yhmBv6er70~f0@1v{nu*3r0%>!2
z6S7y)%;6_2z?Gb8wH4HYF7R}2cD!nR_?hIwJ}qTRM=
zwJ)BL99Day+rDF?Yn|8FCsGuoMZ|hamM`iBIltaALu~{mB`ITTt*xFH7JSz1cCZcJ
z#dqA@z>{@{oc~(S`DY92EmCsjm+C-9b2i|FI&nHC8lAP+BQ`7%Tnp#BY=P!V85p5V
zWY@FMUOg4V8Id<7VjO-cMWpLv6b?-X1XhxXg!QCMf24R?-AF5fxsO~{@r?vwSP+uB
z5@EBlO|_MBhN_Z7gbn!uRSqJRl)b_fBaTVn@^J+ZHUQD#sBhq_owjvr()|PWx_OH#
zKH5(wfUC_jT1OTjbwtXb4D`MdZyA}e!Xe^*N)IN
zoQ|g+nXkPQnvZ<7Ue^T6rvBsg{`}fZL{VT>=*AM+Ns~_oBZ`GYb|o5P9f~P34mkl1
zVlxPg$_&K`WkSJ(s%z!Jrg<@?C;W-gL6QpHwxN;0WC6#cut~^;o)`-S?Uk-P8Jpkc
zSjV_xD>t!{A*}**;^L{1lWn<308B8f>6X5x8-wRB4ajAtN^i*Qin0-rFO{IQbxXUU
z{*1CpKB8tbDH@;DmF9T=H0fOX_TrBxJnBl7
ztyKSomOPU&rI`?kv=q*D(_Zv5GXTN(#yY#g{a5De6P)4MsAnKHHBWP;g_lmmOnwf+
zi+7%bCziY~%4p>BvEZoTc!gpPi9$R{qCj?F$WRHE#zOlcOqN-wsIqc=`sS4g@A_|!
zl`#U!4NbYwEK=6!#8fj}DHkj>grF8vPS-*4GfB(Ivaovm1UWOeFrCV$Hj)P9@o(dQiaW76m23p$=6V5sAA{q1|#{M}cv`^$&E
zJaD?FDV6cuvT4mXj#yo)Hzb4MbKiX2|Mp3c;qn!q-FxH14{3j;C%pV8f9BVb8QUz>
z8;0=M`p8q^_KW_u4()Z3{Kcw8NVk{|m~sxY6T2jL*&VRlsModUAD+;EEW%$me($^Q
z?Ytd=uWVhm{hz;MNbP3Ff)chxzrMoWVFbR{-%Ip=fjnCl#E&mq|KL|hfI&yl)?w`T
zrSt?VXNE3cKx#fG`kNJJhrsmnh
zDcq;&)4Pdlj1R}J4&^ga?9#tH3%jP)s_H8em`e~@`Gvn+e#c#$P$2T)>CUr1{>Rn(
zAA;_x=Mv>DWstKPV6LBP@A>wVWYIhxrntyRbS+Pg$C#yS)RlBA8=2w%La>(?rzUvx
z+mHPKCGxu4@1dWx^j|>dF8ds
zZO8iFdUM6L4d}e}OPd>S+1#>gFFgVIoAMcZDp-RSmvPPIAN>>wA8$b46^*&(%xG7%
zUw%nUG&D21PqO4sv9>qPJ^PD`ZKvo$&E&GhvTe^ihdWEL@X^10jMfyIPys`m*s_=I
zy>sM$K3Hb6X=Jz(ZmfS~;B=RXOXY6ZYIvt;&xa_uYZ_gaMXR-41n%k$#JSSL=YyRi
zXagNBA14<1!7(KkbF65>&Br-zXs~~@v$tm5@``FwqYu8{j+h55q$32Kvfg9AfALT6
zU&NR^lWY0NciMWU<_bX@mq;3&TuUAFGdOxt)v6}HisK08$TqZgLV$z?uX;<{`{+uo+dNH`WBdGwIl7`)
literal 0
HcmV?d00001
diff --git a/vendor/plugins/attachment_fu/test/fixtures/files/foo.txt b/vendor/plugins/attachment_fu/test/fixtures/files/foo.txt
new file mode 100644
index 0000000..1910281
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/fixtures/files/foo.txt
@@ -0,0 +1 @@
+foo
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/test/fixtures/files/rails.png b/vendor/plugins/attachment_fu/test/fixtures/files/rails.png
new file mode 100644
index 0000000000000000000000000000000000000000..b8441f182e06974083cf08f0acaf0e2fd612bd40
GIT binary patch
literal 1787
zcmVCLdthj)A!BBmWB&y|X`RY;f`BJ<_ju%@N||NoLFD~mQl$aHGjq>;5dG_D{h(5s}0
z6&=HANU$m__3PuddU(lvR_xWj`}Oho@9EyQt-n!E*P(KhM@X_VFV2l&>deNZJT%y8iwA
zoG>u1B`p2=_u9k4v1Mud`1+qvOZoHg#bITJ9U`qBAek?40RR96!AV3xRCwBy*IQ$v
zN(=yC9IhRft9V64L`77pqF_Cx@c;kSNoGK)`?Ps*cP(EtGlYZ{D5cxspMQvjKH)Oh6X(pa|J{
zGy1J$Ej7=Z{uvmMfRRsE;v`p;45B~6*ep#hM^ji
zl$+7qoWq~}ewG=61uFw0He{tJurMU&4Iv?=B^eR(wAHk!miA)O7p_+YR>lbmU3rmn
ze?+ze(+sEd6foB&*l9+?zkr_a-5*v&p*?c}HOGtyHg6r{WFYpQ=#z0Hc7VWLx$>M3|b0|Gn
z+5t#z6*ffSVc6DjpmB2?AAR@@vB!wCK?9Yl;33;Q7^%(401QW|k=R8b!OwtLJPjjm
zO9Ia;qCq)rOq!1Ia*6#A%#xb}yDx1P*pWla>9j$bnMn3CBqe4`TRll_Iy29kmG?4fbKuF=XqU|?3b@B
zA`&a?KIgZ|KJx5eND_c3Em=WZn@xW8hRJ^G&sY^b(FW?WC9W_sb;+lAPdLTdBaKIK;-f}*h4|1aTjw7qX_k~e{TWO7jqcekERN;Jyh%67)q4rKpL*CEYL;|#GY{B@5
zi52XoC?xsoorJKxsliugF#z38MJqrYCWV(t<=G&f;^Me13&AiI9{3jUZ$
zFM`*L(9qc^VMxkz1oaDH!1pcD^IXp>Z0Jb=_qs?Vsrs{mp<^{$N!EC9o+`CO-(o}E
zJ`y{*;9s|wr22-QoJ87y^~;)Q@b%P4UgSSsx>2$o@Vd{%Pk0@4qZ^fhB(vt$c1TG>
z*{Ad;foraENbld`=MCNm4?9kvlgK~&J>ialpJ7nua
zx0oRzwG5;}Qne)Fg(N3kf?JVmB;}y&5(0+~r*aL$0Zof8fe!AtHWH>A^1Y)@G@GsA
zup`R{Qg?{+MaxTq#2n{6w|)c&yaJ7{U4ngAH5v6I)*;@rEBE*ehIPBwKBQU)YKE8F0lR!Sm?sE4Xk-sj&E$|A-9n
dP56HS1^^A-61FoN)nxzx002ovPDHLkV1kw_Sd9Px
literal 0
HcmV?d00001
diff --git a/vendor/plugins/attachment_fu/test/geometry_test.rb b/vendor/plugins/attachment_fu/test/geometry_test.rb
new file mode 100644
index 0000000..ade4f48
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/geometry_test.rb
@@ -0,0 +1,101 @@
+require 'test/unit'
+require File.expand_path(File.join(File.dirname(__FILE__), '../lib/geometry')) unless Object.const_defined?(:Geometry)
+
+class GeometryTest < Test::Unit::TestCase
+ def test_should_resize
+ assert_geometry 50, 64,
+ "50x50" => [39, 50],
+ "60x60" => [47, 60],
+ "100x100" => [78, 100]
+ end
+
+ def test_should_resize_no_width
+ assert_geometry 50, 64,
+ "x50" => [39, 50],
+ "x60" => [47, 60],
+ "x100" => [78, 100]
+ end
+
+ def test_should_resize_no_height
+ assert_geometry 50, 64,
+ "50" => [50, 64],
+ "60" => [60, 77],
+ "100" => [100, 128]
+ end
+
+ def test_should_resize_with_percent
+ assert_geometry 50, 64,
+ "50x50%" => [25, 32],
+ "60x60%" => [30, 38],
+ "120x112%" => [60, 72]
+ end
+
+ def test_should_resize_with_percent_and_no_width
+ assert_geometry 50, 64,
+ "x50%" => [50, 32],
+ "x60%" => [50, 38],
+ "x112%" => [50, 72]
+ end
+
+ def test_should_resize_with_percent_and_no_height
+ assert_geometry 50, 64,
+ "50%" => [25, 32],
+ "60%" => [30, 38],
+ "120%" => [60, 77]
+ end
+
+ def test_should_resize_with_less
+ assert_geometry 50, 64,
+ "50x50<" => [50, 64],
+ "60x60<" => [50, 64],
+ "100x100<" => [78, 100],
+ "100x112<" => [88, 112],
+ "40x70<" => [50, 64]
+ end
+
+ def test_should_resize_with_less_and_no_width
+ assert_geometry 50, 64,
+ "x50<" => [50, 64],
+ "x60<" => [50, 64],
+ "x100<" => [78, 100]
+ end
+
+ def test_should_resize_with_less_and_no_height
+ assert_geometry 50, 64,
+ "50<" => [50, 64],
+ "60<" => [60, 77],
+ "100<" => [100, 128]
+ end
+
+ def test_should_resize_with_greater
+ assert_geometry 50, 64,
+ "50x50>" => [39, 50],
+ "60x60>" => [47, 60],
+ "100x100>" => [50, 64],
+ "100x112>" => [50, 64],
+ "40x70>" => [40, 51]
+ end
+
+ def test_should_resize_with_greater_and_no_width
+ assert_geometry 50, 64,
+ "x40>" => [31, 40],
+ "x60>" => [47, 60],
+ "x100>" => [50, 64]
+ end
+
+ def test_should_resize_with_greater_and_no_height
+ assert_geometry 50, 64,
+ "40>" => [40, 51],
+ "60>" => [50, 64],
+ "100>" => [50, 64]
+ end
+
+ protected
+ def assert_geometry(width, height, values)
+ values.each do |geo, result|
+ # run twice to verify the Geometry string isn't modified after a run
+ geo = Geometry.from_s(geo)
+ 2.times { assert_equal result, [width, height] / geo }
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/test/processors/image_science_test.rb b/vendor/plugins/attachment_fu/test/processors/image_science_test.rb
new file mode 100644
index 0000000..636918d
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/processors/image_science_test.rb
@@ -0,0 +1,31 @@
+require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
+
+class ImageScienceTest < Test::Unit::TestCase
+ attachment_model ImageScienceAttachment
+
+ if Object.const_defined?(:ImageScience)
+ def test_should_resize_image
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_valid attachment
+ assert attachment.image?
+ # test image science thumbnail
+ assert_equal 42, attachment.width
+ assert_equal 55, attachment.height
+
+ thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ }
+ geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ }
+
+ # test exact resize dimensions
+ assert_equal 50, thumb.width
+ assert_equal 51, thumb.height
+
+ # test geometry string
+ assert_equal 31, geo.width
+ assert_equal 41, geo.height
+ end
+ else
+ def test_flunk
+ puts "ImageScience not loaded, tests not running"
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/test/processors/mini_magick_test.rb b/vendor/plugins/attachment_fu/test/processors/mini_magick_test.rb
new file mode 100644
index 0000000..244a4a2
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/processors/mini_magick_test.rb
@@ -0,0 +1,31 @@
+require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
+
+class MiniMagickTest < Test::Unit::TestCase
+ attachment_model MiniMagickAttachment
+
+ if Object.const_defined?(:MiniMagick)
+ def test_should_resize_image
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_valid attachment
+ assert attachment.image?
+ # test MiniMagick thumbnail
+ assert_equal 43, attachment.width
+ assert_equal 55, attachment.height
+
+ thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ }
+ geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ }
+
+ # test exact resize dimensions
+ assert_equal 50, thumb.width
+ assert_equal 51, thumb.height
+
+ # test geometry string
+ assert_equal 31, geo.width
+ assert_equal 40, geo.height
+ end
+ else
+ def test_flunk
+ puts "MiniMagick not loaded, tests not running"
+ end
+ end
+end
diff --git a/vendor/plugins/attachment_fu/test/processors/rmagick_test.rb b/vendor/plugins/attachment_fu/test/processors/rmagick_test.rb
new file mode 100644
index 0000000..af91193
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/processors/rmagick_test.rb
@@ -0,0 +1,240 @@
+require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
+
+class RmagickTest < Test::Unit::TestCase
+ attachment_model Attachment
+
+ if Object.const_defined?(:Magick)
+ def test_should_create_image_from_uploaded_file
+ assert_created do
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_valid attachment
+ assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
+ assert attachment.image?
+ assert !attachment.size.zero?
+ #assert_equal 1784, attachment.size
+ assert_equal 50, attachment.width
+ assert_equal 64, attachment.height
+ assert_equal '50x64', attachment.image_size
+ end
+ end
+
+ def test_should_create_image_from_uploaded_file_with_custom_content_type
+ assert_created do
+ attachment = upload_file :content_type => 'foo/bar', :filename => '/files/rails.png'
+ assert_valid attachment
+ assert !attachment.image?
+ assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
+ assert !attachment.size.zero?
+ #assert_equal 1784, attachment.size
+ assert_nil attachment.width
+ assert_nil attachment.height
+ assert_equal [], attachment.thumbnails
+ end
+ end
+
+ def test_should_create_thumbnail
+ attachment = upload_file :filename => '/files/rails.png'
+
+ assert_created do
+ basename, ext = attachment.filename.split '.'
+ thumbnail = attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 50, 50)
+ assert_valid thumbnail
+ assert !thumbnail.size.zero?
+ #assert_in_delta 4673, thumbnail.size, 2
+ assert_equal 50, thumbnail.width
+ assert_equal 50, thumbnail.height
+ assert_equal [thumbnail.id], attachment.thumbnails.collect(&:id)
+ assert_equal attachment.id, thumbnail.parent_id if thumbnail.respond_to?(:parent_id)
+ assert_equal "#{basename}_thumb.#{ext}", thumbnail.filename
+ end
+ end
+
+ def test_should_create_thumbnail_with_geometry_string
+ attachment = upload_file :filename => '/files/rails.png'
+
+ assert_created do
+ basename, ext = attachment.filename.split '.'
+ thumbnail = attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 'x50')
+ assert_valid thumbnail
+ assert !thumbnail.size.zero?
+ #assert_equal 3915, thumbnail.size
+ assert_equal 39, thumbnail.width
+ assert_equal 50, thumbnail.height
+ assert_equal [thumbnail], attachment.thumbnails
+ assert_equal attachment.id, thumbnail.parent_id if thumbnail.respond_to?(:parent_id)
+ assert_equal "#{basename}_thumb.#{ext}", thumbnail.filename
+ end
+ end
+
+ def test_should_resize_image(klass = ImageAttachment)
+ attachment_model klass
+ assert_equal [50, 50], attachment_model.attachment_options[:resize_to]
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_valid attachment
+ assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
+ assert attachment.image?
+ assert !attachment.size.zero?
+ #assert_in_delta 4673, attachment.size, 2
+ assert_equal 50, attachment.width
+ assert_equal 50, attachment.height
+ end
+
+ test_against_subclass :test_should_resize_image, ImageAttachment
+
+ def test_should_resize_image_with_geometry(klass = ImageOrPdfAttachment)
+ attachment_model klass
+ assert_equal 'x50', attachment_model.attachment_options[:resize_to]
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_valid attachment
+ assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
+ assert attachment.image?
+ assert !attachment.size.zero?
+ #assert_equal 3915, attachment.size
+ assert_equal 39, attachment.width
+ assert_equal 50, attachment.height
+ end
+
+ test_against_subclass :test_should_resize_image_with_geometry, ImageOrPdfAttachment
+
+ def test_should_give_correct_thumbnail_filenames(klass = ImageWithThumbsFileAttachment)
+ attachment_model klass
+ assert_created 3 do
+ attachment = upload_file :filename => '/files/rails.png'
+ thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ }
+ geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ }
+
+ [attachment, thumb, geo].each { |record| assert_valid record }
+
+ assert_match /rails\.png$/, attachment.full_filename
+ assert_match /rails_geometry\.png$/, attachment.full_filename(:geometry)
+ assert_match /rails_thumb\.png$/, attachment.full_filename(:thumb)
+ end
+ end
+
+ test_against_subclass :test_should_give_correct_thumbnail_filenames, ImageWithThumbsFileAttachment
+
+ def test_should_automatically_create_thumbnails(klass = ImageWithThumbsAttachment)
+ attachment_model klass
+ assert_created 3 do
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_valid attachment
+ assert !attachment.size.zero?
+ #assert_equal 1784, attachment.size
+ assert_equal 55, attachment.width
+ assert_equal 55, attachment.height
+ assert_equal 2, attachment.thumbnails.length
+ assert_equal 1.0, attachment.aspect_ratio
+
+ thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ }
+ assert !thumb.new_record?, thumb.errors.full_messages.join("\n")
+ assert !thumb.size.zero?
+ #assert_in_delta 4673, thumb.size, 2
+ assert_equal 50, thumb.width
+ assert_equal 50, thumb.height
+ assert_equal 1.0, thumb.aspect_ratio
+
+ geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ }
+ assert !geo.new_record?, geo.errors.full_messages.join("\n")
+ assert !geo.size.zero?
+ #assert_equal 3915, geo.size
+ assert_equal 50, geo.width
+ assert_equal 50, geo.height
+ assert_equal 1.0, geo.aspect_ratio
+ end
+ end
+
+ test_against_subclass :test_should_automatically_create_thumbnails, ImageWithThumbsAttachment
+
+ # same as above method, but test it on a file model
+ test_against_class :test_should_automatically_create_thumbnails, ImageWithThumbsFileAttachment
+ test_against_subclass :test_should_automatically_create_thumbnails_on_class, ImageWithThumbsFileAttachment
+
+ def test_should_use_thumbnail_subclass(klass = ImageWithThumbsClassFileAttachment)
+ attachment_model klass
+ attachment = nil
+ assert_difference ImageThumbnail, :count do
+ attachment = upload_file :filename => '/files/rails.png'
+ assert_valid attachment
+ end
+ assert_kind_of ImageThumbnail, attachment.thumbnails.first
+ assert_equal attachment.id, attachment.thumbnails.first.parent.id
+ assert_kind_of FileAttachment, attachment.thumbnails.first.parent
+ assert_equal 'rails_thumb.png', attachment.thumbnails.first.filename
+ assert_equal attachment.thumbnails.first.full_filename, attachment.full_filename(attachment.thumbnails.first.thumbnail),
+ "#full_filename does not use thumbnail class' path."
+ end
+
+ test_against_subclass :test_should_use_thumbnail_subclass, ImageWithThumbsClassFileAttachment
+
+ def test_should_remove_old_thumbnail_files_when_updating(klass = ImageWithThumbsFileAttachment)
+ attachment_model klass
+ attachment = nil
+ assert_created 3 do
+ attachment = upload_file :filename => '/files/rails.png'
+ end
+
+ old_filenames = [attachment.full_filename] + attachment.thumbnails.collect(&:full_filename)
+
+ assert_not_created do
+ use_temp_file "files/rails.png" do |file|
+ attachment.filename = 'rails2.png'
+ attachment.temp_path = File.join(fixture_path, file)
+ attachment.save
+ new_filenames = [attachment.reload.full_filename] + attachment.thumbnails.collect { |t| t.reload.full_filename }
+ new_filenames.each { |f| assert File.exists?(f), "#{f} does not exist" }
+ old_filenames.each { |f| assert !File.exists?(f), "#{f} still exists" }
+ end
+ end
+ end
+
+ test_against_subclass :test_should_remove_old_thumbnail_files_when_updating, ImageWithThumbsFileAttachment
+
+ def test_should_delete_file_when_in_file_system_when_attachment_record_destroyed(klass = ImageWithThumbsFileAttachment)
+ attachment_model klass
+ attachment = upload_file :filename => '/files/rails.png'
+ filenames = [attachment.full_filename] + attachment.thumbnails.collect(&:full_filename)
+ filenames.each { |f| assert File.exists?(f), "#{f} never existed to delete on destroy" }
+ attachment.destroy
+ filenames.each { |f| assert !File.exists?(f), "#{f} still exists" }
+ end
+
+ test_against_subclass :test_should_delete_file_when_in_file_system_when_attachment_record_destroyed, ImageWithThumbsFileAttachment
+
+ def test_should_overwrite_old_thumbnail_records_when_updating(klass = ImageWithThumbsAttachment)
+ attachment_model klass
+ attachment = nil
+ assert_created 3 do
+ attachment = upload_file :filename => '/files/rails.png'
+ end
+ assert_not_created do # no new db_file records
+ use_temp_file "files/rails.png" do |file|
+ attachment.filename = 'rails2.png'
+ attachment.temp_path = File.join(fixture_path, file)
+ attachment.save!
+ end
+ end
+ end
+
+ test_against_subclass :test_should_overwrite_old_thumbnail_records_when_updating, ImageWithThumbsAttachment
+
+ def test_should_overwrite_old_thumbnail_records_when_renaming(klass = ImageWithThumbsAttachment)
+ attachment_model klass
+ attachment = nil
+ assert_created 3 do
+ attachment = upload_file :class => klass, :filename => '/files/rails.png'
+ end
+ assert_not_created do # no new db_file records
+ attachment.filename = 'rails2.png'
+ attachment.save
+ assert !attachment.reload.size.zero?
+ assert_equal 'rails2.png', attachment.filename
+ end
+ end
+
+ test_against_subclass :test_should_overwrite_old_thumbnail_records_when_renaming, ImageWithThumbsAttachment
+ else
+ def test_flunk
+ puts "RMagick not installed, no tests running"
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/test/schema.rb b/vendor/plugins/attachment_fu/test/schema.rb
new file mode 100644
index 0000000..b2e284d
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/schema.rb
@@ -0,0 +1,86 @@
+ActiveRecord::Schema.define(:version => 0) do
+ create_table :attachments, :force => true do |t|
+ t.column :db_file_id, :integer
+ t.column :parent_id, :integer
+ t.column :thumbnail, :string
+ t.column :filename, :string, :limit => 255
+ t.column :content_type, :string, :limit => 255
+ t.column :size, :integer
+ t.column :width, :integer
+ t.column :height, :integer
+ t.column :aspect_ratio, :float
+ end
+
+ create_table :file_attachments, :force => true do |t|
+ t.column :parent_id, :integer
+ t.column :thumbnail, :string
+ t.column :filename, :string, :limit => 255
+ t.column :content_type, :string, :limit => 255
+ t.column :size, :integer
+ t.column :width, :integer
+ t.column :height, :integer
+ t.column :type, :string
+ t.column :aspect_ratio, :float
+ end
+
+ create_table :image_science_attachments, :force => true do |t|
+ t.column :parent_id, :integer
+ t.column :thumbnail, :string
+ t.column :filename, :string, :limit => 255
+ t.column :content_type, :string, :limit => 255
+ t.column :size, :integer
+ t.column :width, :integer
+ t.column :height, :integer
+ t.column :type, :string
+ end
+
+ create_table :mini_magick_attachments, :force => true do |t|
+ t.column :parent_id, :integer
+ t.column :thumbnail, :string
+ t.column :filename, :string, :limit => 255
+ t.column :content_type, :string, :limit => 255
+ t.column :size, :integer
+ t.column :width, :integer
+ t.column :height, :integer
+ t.column :type, :string
+ end
+
+ create_table :mini_magick_attachments, :force => true do |t|
+ t.column :parent_id, :integer
+ t.column :thumbnail, :string
+ t.column :filename, :string, :limit => 255
+ t.column :content_type, :string, :limit => 255
+ t.column :size, :integer
+ t.column :width, :integer
+ t.column :height, :integer
+ t.column :type, :string
+ end
+
+ create_table :orphan_attachments, :force => true do |t|
+ t.column :db_file_id, :integer
+ t.column :filename, :string, :limit => 255
+ t.column :content_type, :string, :limit => 255
+ t.column :size, :integer
+ end
+
+ create_table :minimal_attachments, :force => true do |t|
+ t.column :size, :integer
+ t.column :content_type, :string, :limit => 255
+ end
+
+ create_table :db_files, :force => true do |t|
+ t.column :data, :binary
+ end
+
+ create_table :s3_attachments, :force => true do |t|
+ t.column :parent_id, :integer
+ t.column :thumbnail, :string
+ t.column :filename, :string, :limit => 255
+ t.column :content_type, :string, :limit => 255
+ t.column :size, :integer
+ t.column :width, :integer
+ t.column :height, :integer
+ t.column :type, :string
+ t.column :aspect_ratio, :float
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/test/test_helper.rb b/vendor/plugins/attachment_fu/test/test_helper.rb
new file mode 100644
index 0000000..66a0b72
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/test_helper.rb
@@ -0,0 +1,142 @@
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+
+ENV['RAILS_ENV'] = 'test'
+
+require 'test/unit'
+require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
+require 'breakpoint'
+require 'active_record/fixtures'
+require 'action_controller/test_process'
+
+config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
+ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
+
+db_adapter = ENV['DB']
+
+# no db passed, try one of these fine config-free DBs before bombing.
+db_adapter ||=
+ begin
+ require 'rubygems'
+ require 'sqlite'
+ 'sqlite'
+ rescue MissingSourceFile
+ begin
+ require 'sqlite3'
+ 'sqlite3'
+ rescue MissingSourceFile
+ end
+ end
+
+if db_adapter.nil?
+ raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3."
+end
+
+ActiveRecord::Base.establish_connection(config[db_adapter])
+
+load(File.dirname(__FILE__) + "/schema.rb")
+
+Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures"
+$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
+
+class Test::Unit::TestCase #:nodoc:
+ include ActionController::TestProcess
+ def create_fixtures(*table_names)
+ if block_given?
+ Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
+ else
+ Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
+ end
+ end
+
+ def setup
+ Attachment.saves = 0
+ DbFile.transaction { [Attachment, FileAttachment, OrphanAttachment, MinimalAttachment, DbFile].each { |klass| klass.delete_all } }
+ attachment_model self.class.attachment_model
+ end
+
+ def teardown
+ FileUtils.rm_rf File.join(File.dirname(__FILE__), 'files')
+ end
+
+ self.use_transactional_fixtures = true
+ self.use_instantiated_fixtures = false
+
+ def self.attachment_model(klass = nil)
+ @attachment_model = klass if klass
+ @attachment_model
+ end
+
+ def self.test_against_class(test_method, klass, subclass = false)
+ define_method("#{test_method}_on_#{:sub if subclass}class") do
+ klass = Class.new(klass) if subclass
+ attachment_model klass
+ send test_method, klass
+ end
+ end
+
+ def self.test_against_subclass(test_method, klass)
+ test_against_class test_method, klass, true
+ end
+
+ protected
+ def upload_file(options = {})
+ use_temp_file options[:filename] do |file|
+ att = attachment_model.create :uploaded_data => fixture_file_upload(file, options[:content_type] || 'image/png')
+ att.reload unless att.new_record?
+ return att
+ end
+ end
+
+ def use_temp_file(fixture_filename)
+ temp_path = File.join('/tmp', File.basename(fixture_filename))
+ FileUtils.mkdir_p File.join(fixture_path, 'tmp')
+ FileUtils.cp File.join(fixture_path, fixture_filename), File.join(fixture_path, temp_path)
+ yield temp_path
+ ensure
+ FileUtils.rm_rf File.join(fixture_path, 'tmp')
+ end
+
+ def assert_created(num = 1)
+ assert_difference attachment_model.base_class, :count, num do
+ if attachment_model.included_modules.include? DbFile
+ assert_difference DbFile, :count, num do
+ yield
+ end
+ else
+ yield
+ end
+ end
+ end
+
+ def assert_not_created
+ assert_created(0) { yield }
+ end
+
+ def should_reject_by_size_with(klass)
+ attachment_model klass
+ assert_not_created do
+ attachment = upload_file :filename => '/files/rails.png'
+ assert attachment.new_record?
+ assert attachment.errors.on(:size)
+ assert_nil attachment.db_file if attachment.respond_to?(:db_file)
+ end
+ end
+
+ def assert_difference(object, method = nil, difference = 1)
+ initial_value = object.send(method)
+ yield
+ assert_equal initial_value + difference, object.send(method)
+ end
+
+ def assert_no_difference(object, method, &block)
+ assert_difference object, method, 0, &block
+ end
+
+ def attachment_model(klass = nil)
+ @attachment_model = klass if klass
+ @attachment_model
+ end
+end
+
+require File.join(File.dirname(__FILE__), 'fixtures/attachment')
+require File.join(File.dirname(__FILE__), 'base_attachment_tests')
\ No newline at end of file
diff --git a/vendor/plugins/attachment_fu/test/validation_test.rb b/vendor/plugins/attachment_fu/test/validation_test.rb
new file mode 100644
index 0000000..a14cf99
--- /dev/null
+++ b/vendor/plugins/attachment_fu/test/validation_test.rb
@@ -0,0 +1,55 @@
+require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
+
+class ValidationTest < Test::Unit::TestCase
+ def test_should_invalidate_big_files
+ @attachment = SmallAttachment.new
+ assert !@attachment.valid?
+ assert @attachment.errors.on(:size)
+
+ @attachment.size = 2000
+ assert !@attachment.valid?
+ assert @attachment.errors.on(:size), @attachment.errors.full_messages.to_sentence
+
+ @attachment.size = 1000
+ assert !@attachment.valid?
+ assert_nil @attachment.errors.on(:size)
+ end
+
+ def test_should_invalidate_small_files
+ @attachment = BigAttachment.new
+ assert !@attachment.valid?
+ assert @attachment.errors.on(:size)
+
+ @attachment.size = 2000
+ assert !@attachment.valid?
+ assert @attachment.errors.on(:size), @attachment.errors.full_messages.to_sentence
+
+ @attachment.size = 1.megabyte
+ assert !@attachment.valid?
+ assert_nil @attachment.errors.on(:size)
+ end
+
+ def test_should_validate_content_type
+ @attachment = PdfAttachment.new
+ assert !@attachment.valid?
+ assert @attachment.errors.on(:content_type)
+
+ @attachment.content_type = 'foo'
+ assert !@attachment.valid?
+ assert @attachment.errors.on(:content_type)
+
+ @attachment.content_type = 'pdf'
+ assert !@attachment.valid?
+ assert_nil @attachment.errors.on(:content_type)
+ end
+
+ def test_should_require_filename
+ @attachment = Attachment.new
+ assert !@attachment.valid?
+ assert @attachment.errors.on(:filename)
+
+ @attachment.filename = 'foo'
+ assert !@attachment.valid?
+ assert_nil @attachment.errors.on(:filename)
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/CHANGELOG b/vendor/plugins/liquid/CHANGELOG
new file mode 100644
index 0000000..b1bb18a
--- /dev/null
+++ b/vendor/plugins/liquid/CHANGELOG
@@ -0,0 +1,38 @@
+Changelog
+
+Implemented .to_liquid for all objects which can be passed to liquid like Strings Arrays Hashes Numerics and Booleans.
+To export new objects to liquid just implement .to_liquid on them and return objects which themselves have .to_liquid methods.
+
+Added more tags to standard library
+
+Added include tag ( like partials in rails )
+
+[...] Gazillion of detail improvements
+
+Added strainers as filter hosts for better security [Tobias Luetke]
+
+Fixed that rails integration would call filter with the wrong "self" [Michael Geary]
+
+Fixed bad error reporting when a filter called a method which doesn't exist. Liquid told you that it couldn't find the
+filter which was obviously misleading [Tobias Luetke]
+
+Removed count helper from standard lib. use size [Tobias Luetke]
+
+Fixed bug with string filter parameters failing to tolerate commas in strings. [Paul Hammond]
+
+Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ArgumentSeparator: , by default [Paul Hammond]
+ {{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }}
+
+
+Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke]
+
+ class ProductDrop < Liquid::Drop
+ def top_sales
+ Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
+ end
+ end
+ t = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {% endfor %} ' )
+ t.render('product' => ProductDrop.new )
+
+
+Added filter parameters support. Example: {{ date | format_date: "%Y" }} [Paul Hammond]
diff --git a/vendor/plugins/liquid/MIT-LICENSE b/vendor/plugins/liquid/MIT-LICENSE
new file mode 100644
index 0000000..441ca02
--- /dev/null
+++ b/vendor/plugins/liquid/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2005, 2006 Tobias Luetke
+
+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.
\ No newline at end of file
diff --git a/vendor/plugins/liquid/Manifest.txt b/vendor/plugins/liquid/Manifest.txt
new file mode 100644
index 0000000..2941b97
--- /dev/null
+++ b/vendor/plugins/liquid/Manifest.txt
@@ -0,0 +1,60 @@
+CHANGELOG
+MIT-LICENSE
+Manifest.txt
+README
+Rakefile
+example/server/example_servlet.rb
+example/server/liquid_servlet.rb
+example/server/server.rb
+example/server/templates/index.liquid
+example/server/templates/products.liquid
+init.rb
+lib/extras/liquid_view.rb
+lib/liquid.rb
+lib/liquid/block.rb
+lib/liquid/condition.rb
+lib/liquid/context.rb
+lib/liquid/document.rb
+lib/liquid/drop.rb
+lib/liquid/errors.rb
+lib/liquid/extensions.rb
+lib/liquid/file_system.rb
+lib/liquid/htmltags.rb
+lib/liquid/standardfilters.rb
+lib/liquid/strainer.rb
+lib/liquid/tag.rb
+lib/liquid/tags/assign.rb
+lib/liquid/tags/capture.rb
+lib/liquid/tags/case.rb
+lib/liquid/tags/comment.rb
+lib/liquid/tags/cycle.rb
+lib/liquid/tags/for.rb
+lib/liquid/tags/if.rb
+lib/liquid/tags/ifchanged.rb
+lib/liquid/tags/include.rb
+lib/liquid/tags/unless.rb
+lib/liquid/template.rb
+lib/liquid/variable.rb
+test/block_test.rb
+test/context_test.rb
+test/drop_test.rb
+test/error_handling_test.rb
+test/extra/breakpoint.rb
+test/extra/caller.rb
+test/file_system_test.rb
+test/filter_test.rb
+test/helper.rb
+test/html_tag_test.rb
+test/if_else_test.rb
+test/include_tag_test.rb
+test/output_test.rb
+test/parsing_quirks_test.rb
+test/regexp_test.rb
+test/security_test.rb
+test/standard_filter_test.rb
+test/standard_tag_test.rb
+test/statements_test.rb
+test/strainer_test.rb
+test/template_test.rb
+test/unless_else_test.rb
+test/variable_test.rb
diff --git a/vendor/plugins/liquid/README b/vendor/plugins/liquid/README
new file mode 100644
index 0000000..1d019af
--- /dev/null
+++ b/vendor/plugins/liquid/README
@@ -0,0 +1,38 @@
+= Liquid template engine
+
+Liquid is a template engine which I wrote for very specific requirements
+
+* It has to have beautiful and simple markup.
+ Template engines which don't produce good looking markup are no fun to use.
+* It needs to be non evaling and secure. Liquid templates are made so that users can edit them. You don't want to run code on your server which your users wrote.
+* It has to be stateless. Compile and render steps have to be seperate so that the expensive parsing and compiling can be done once and later on you can
+ just render it passing in a hash with local variables and objects.
+
+== Why should i use Liquid
+
+* You want to allow your users to edit the appearance of your application but don't want them to run insecure code on your server.
+* You want to render templates directly from the database
+* You like smarty style template engines
+* You need a template engine which does HTML just as well as Emails
+* You don't like the markup of your current one
+
+== What does it look like?
+
+
+ {% for product in products %}
+ -
+
{{product.name}}
+ Only {{product.price | price }}
+
+ {{product.description | prettyprint | paragraph }}
+
+ {% endfor %}
+
+
+== Howto use Liquid
+
+Liquid supports a very simple API based around the Liquid::Template class.
+For standard use you can just pass it the content of a file and call render with a parameters hash.
+
+ @template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template
+ @template.render( 'name' => 'tobi' ) # => "hi tobi"
\ No newline at end of file
diff --git a/vendor/plugins/liquid/Rakefile b/vendor/plugins/liquid/Rakefile
new file mode 100755
index 0000000..1c6df69
--- /dev/null
+++ b/vendor/plugins/liquid/Rakefile
@@ -0,0 +1,24 @@
+#!/usr/bin/env ruby
+require 'rubygems'
+require 'rake'
+require 'hoe'
+
+PKG_VERSION = "1.7.0"
+PKG_NAME = "liquid"
+PKG_DESC = "A secure non evaling end user template engine with aesthetic markup."
+
+Rake::TestTask.new(:test) do |t|
+ t.libs << "lib"
+ t.libs << "test"
+ t.pattern = 'test/*_test.rb'
+ t.verbose = false
+end
+
+Hoe.new(PKG_NAME, PKG_VERSION) do |p|
+ p.rubyforge_name = PKG_NAME
+ p.summary = PKG_DESC
+ p.description = nil
+ p.author = "Tobias Luetke"
+ p.email = "tobi@leetsoft.com"
+ p.url = "http://home.leetsoft.com/liquid"
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/example/server/example_servlet.rb b/vendor/plugins/liquid/example/server/example_servlet.rb
new file mode 100644
index 0000000..18e528e
--- /dev/null
+++ b/vendor/plugins/liquid/example/server/example_servlet.rb
@@ -0,0 +1,37 @@
+module ProductsFilter
+ def price(integer)
+ sprintf("$%.2d USD", integer / 100.0)
+ end
+
+ def prettyprint(text)
+ text.gsub( /\*(.*)\*/, '\1' )
+ end
+
+ def count(array)
+ array.size
+ end
+
+ def paragraph(p)
+ "#{p}
"
+ end
+end
+
+class Servlet < LiquidServlet
+
+ def index
+ { 'date' => Time.now }
+ end
+
+ def products
+ { 'products' => products_list, 'section' => 'Snowboards', 'cool_products' => true}
+ end
+
+ private
+
+ def products_list
+ [{'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
+ {'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling'},
+ {'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity'}]
+ end
+
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/example/server/liquid_servlet.rb b/vendor/plugins/liquid/example/server/liquid_servlet.rb
new file mode 100644
index 0000000..8f24f00
--- /dev/null
+++ b/vendor/plugins/liquid/example/server/liquid_servlet.rb
@@ -0,0 +1,28 @@
+class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
+
+ def do_GET(req, res)
+ handle(:get, req, res)
+ end
+
+ def do_POST(req, res)
+ handle(:post, req, res)
+ end
+
+ private
+
+ def handle(type, req, res)
+ @request, @response = req, res
+
+ @request.path_info =~ /(\w+)$/
+ @action = $1 || 'index'
+ @assigns = send(@action) if respond_to?(@action)
+
+ @response['Content-Type'] = "text/html"
+ @response.status = 200
+ @response.body = Liquid::Template.parse(read_template).render(@assigns, :filters => [ProductsFilter])
+ end
+
+ def read_template(filename = @action)
+ File.read( File.dirname(__FILE__) + "/templates/#{filename}.liquid" )
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/example/server/server.rb b/vendor/plugins/liquid/example/server/server.rb
new file mode 100644
index 0000000..6d71c72
--- /dev/null
+++ b/vendor/plugins/liquid/example/server/server.rb
@@ -0,0 +1,12 @@
+require 'webrick'
+require 'rexml/document'
+
+require File.dirname(__FILE__) + '/../../lib/liquid'
+require File.dirname(__FILE__) + '/liquid_servlet'
+require File.dirname(__FILE__) + '/example_servlet'
+
+# Setup webrick
+server = WEBrick::HTTPServer.new( :Port => ARGV[1] || 3000 )
+server.mount('/', Servlet)
+trap("INT"){ server.shutdown }
+server.start
\ No newline at end of file
diff --git a/vendor/plugins/liquid/example/server/templates/index.liquid b/vendor/plugins/liquid/example/server/templates/index.liquid
new file mode 100644
index 0000000..79a52b4
--- /dev/null
+++ b/vendor/plugins/liquid/example/server/templates/index.liquid
@@ -0,0 +1,6 @@
+Hello world!
+
+It is {{date}}
+
+
+Check out the Products screen
\ No newline at end of file
diff --git a/vendor/plugins/liquid/example/server/templates/products.liquid b/vendor/plugins/liquid/example/server/templates/products.liquid
new file mode 100644
index 0000000..05af4f7
--- /dev/null
+++ b/vendor/plugins/liquid/example/server/templates/products.liquid
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+ products
+
+
+
+
+
+
+
+
+
+
+ There are currently {{products | count}} products in the {{section}} catalog
+
+ {% if cool_products %}
+ Cool products :)
+ {% else %}
+ Uncool products :(
+ {% endif %}
+
+
+
+ {% for product in products %}
+ -
+
{{product.name}}
+ Only {{product.price | price }}
+
+ {{product.description | prettyprint | paragraph }}
+
+ {{ 'it rocks!' | paragraph }}
+
+
+ {% endfor %}
+
+
+
+
+
diff --git a/vendor/plugins/liquid/init.rb b/vendor/plugins/liquid/init.rb
new file mode 100644
index 0000000..4d80f13
--- /dev/null
+++ b/vendor/plugins/liquid/init.rb
@@ -0,0 +1,6 @@
+require 'liquid'
+require 'extras/liquid_view'
+
+ActionView::Base::register_template_handler :liquid, LiquidView
+
+
diff --git a/vendor/plugins/liquid/lib/extras/liquid_view.rb b/vendor/plugins/liquid/lib/extras/liquid_view.rb
new file mode 100644
index 0000000..f26d72a
--- /dev/null
+++ b/vendor/plugins/liquid/lib/extras/liquid_view.rb
@@ -0,0 +1,27 @@
+# LiquidView is a action view extension class. You can register it with rails
+# and use liquid as an template system for .liquid files
+#
+# Example
+#
+# ActionView::Base::register_template_handler :liquid, LiquidView
+class LiquidView
+
+ def initialize(action_view)
+ @action_view = action_view
+ end
+
+
+ def render(template, local_assigns)
+ @action_view.controller.headers["Content-Type"] ||= 'text/html; charset=utf-8'
+ assigns = @action_view.assigns.dup
+
+ if content_for_layout = @action_view.instance_variable_get("@content_for_layout")
+ assigns['content_for_layout'] = content_for_layout
+ end
+ assigns.merge!(local_assigns)
+
+ liquid = Liquid::Template.parse(template)
+ liquid.render(assigns, :filters => [@action_view.controller.master_helper_module], :registers => {:action_view => @action_view, :controller => @action_view.controller})
+ end
+
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid.rb b/vendor/plugins/liquid/lib/liquid.rb
new file mode 100644
index 0000000..54dc51d
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid.rb
@@ -0,0 +1,66 @@
+# Copyright (c) 2005 Tobias Luetke
+#
+# 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.
+
+$LOAD_PATH.unshift(File.dirname(__FILE__))
+
+module Liquid
+ FilterSperator = /\|/
+ ArgumentSeparator = ','
+ FilterArgumentSeparator = ':'
+ VariableAttributeSeparator = '.'
+ TagStart = /\{\%/
+ TagEnd = /\%\}/
+ VariableSignature = /\(?[\w\-\.\[\]]\)?/
+ VariableSegment = /[\w\-]\??/
+ VariableStart = /\{\{/
+ VariableEnd = /\}\}/
+ QuotedFragment = /"[^"]+"|'[^']+'|[^\s,|]+/
+ TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/
+ TemplateParser = /(#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableEnd})/
+ VariableParser = /\[[^\]]+\]|#{VariableSegment}+/
+end
+
+require 'liquid/drop'
+require 'liquid/extensions'
+require 'liquid/errors'
+require 'liquid/strainer'
+require 'liquid/context'
+require 'liquid/tag'
+require 'liquid/block'
+require 'liquid/document'
+require 'liquid/variable'
+require 'liquid/file_system'
+require 'liquid/template'
+require 'liquid/htmltags'
+require 'liquid/standardfilters'
+require 'liquid/condition'
+
+# Load all the tags of the standard library
+#
+Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
+
+
+
+
+
+
+
+
diff --git a/vendor/plugins/liquid/lib/liquid/block.rb b/vendor/plugins/liquid/lib/liquid/block.rb
new file mode 100644
index 0000000..13cc818
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/block.rb
@@ -0,0 +1,102 @@
+module Liquid
+
+ class Block < Tag
+
+ def parse(tokens)
+ @nodelist ||= []
+ @nodelist.clear
+
+ while token = tokens.shift
+
+ case token
+ when /^#{TagStart}/
+ if token =~ /^#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/
+
+ # if we found the proper block delimitor just end parsing here and let the outer block
+ # proceed
+ if block_delimiter == $1
+ end_tag
+ return
+ end
+
+ # fetch the tag from registered blocks
+ if tag = Template.tags[$1]
+ @nodelist << tag.new($1, $2, tokens)
+ else
+ # this tag is not registered with the system
+ # pass it to the current block for special handling or error reporting
+ unknown_tag($1, $2, tokens)
+ end
+ else
+ raise SyntaxError, "Tag '#{token}' was not properly terminated with regexp: #{TagEnd.inspect} "
+ end
+ when /^#{VariableStart}/
+ @nodelist << create_variable(token)
+ when ''
+ # pass
+ else
+ @nodelist << token
+ end
+ end
+
+ # Make sure that its ok to end parsing in the current block.
+ # Effectively this method will throw and exception unless the current block is
+ # of type Document
+ assert_missing_delimitation!
+ end
+
+ def end_tag
+ end
+
+ def unknown_tag(tag, params, tokens)
+ case tag
+ when 'else'
+ raise SyntaxError, "#{block_name} tag does not expect else tag"
+ when 'end'
+ raise SyntaxError, "'end' is not a valid delimiter for #{block_name} tags. use #{block_delimiter}"
+ else
+ raise SyntaxError, "Unknown tag '#{tag}'"
+ end
+ end
+
+ def block_delimiter
+ "end#{block_name}"
+ end
+
+ def block_name
+ @tag_name
+ end
+
+ def create_variable(token)
+ token.scan(/^#{VariableStart}(.*)#{VariableEnd}$/) do |content|
+ return Variable.new(content.first)
+ end
+ raise SyntaxError.new("Variable '#{token}' was not properly terminated with regexp: #{VariableEnd.inspect} ")
+ end
+
+ def render(context)
+ render_all(@nodelist, context)
+ end
+
+ protected
+
+ def assert_missing_delimitation!
+ raise SyntaxError.new("#{block_name} tag was never closed")
+ end
+
+ def render_all(list, context)
+ list.collect do |token|
+ begin
+ if token.respond_to?(:render)
+ token.render(context)
+ else
+ token.to_s
+ end
+ rescue Exception => e
+ context.handle_error(e)
+ end
+
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/condition.rb b/vendor/plugins/liquid/lib/liquid/condition.rb
new file mode 100644
index 0000000..4746aca
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/condition.rb
@@ -0,0 +1,99 @@
+module Liquid
+ # Container for liquid nodes which conveniently wraps decision making logic
+ #
+ # Example:
+ #
+ # c = Condition.new('1', '==', '1')
+ # c.evaluate #=> true
+ #
+ class Condition #:nodoc:
+ @@operators = {
+ '==' => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
+ '!=' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
+ '<>' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
+ '<' => :<,
+ '>' => :>,
+ '>=' => :>=,
+ '<=' => :<=,
+ 'contains' => lambda { |cond, left, right| left.include?(right) },
+ }
+
+ def self.operators
+ @@operators
+ end
+
+ attr_reader :attachment
+ attr_accessor :left, :operator, :right
+
+ def initialize(left = nil, operator = nil, right = nil)
+ @left, @operator, @right = left, operator, right
+ end
+
+ def evaluate(context = Context.new)
+ interpret_condition(left, right, operator, context)
+ end
+
+ def attach(attachment)
+ @attachment = attachment
+ end
+
+ def else?
+ false
+ end
+
+ private
+
+ def equal_variables(left, right)
+ if left.is_a?(Symbol)
+ if right.respond_to?(left)
+ return right.send(left.to_s)
+ else
+ return nil
+ end
+ end
+
+ if right.is_a?(Symbol)
+ if left.respond_to?(right)
+ return left.send(right.to_s)
+ else
+ return nil
+ end
+ end
+
+ left == right
+ end
+
+ def interpret_condition(left, right, op, context)
+
+ # If the operator is empty this means that the decision statement is just
+ # a single variable. We can just poll this variable from the context and
+ # return this as the result.
+ return context[left] if op == nil
+
+ left, right = context[left], context[right]
+
+
+ operation = self.class.operators[op] || raise(ArgumentError.new("Error in tag '#{name}' - Unknown operator #{op}"))
+
+ if operation.respond_to?(:call)
+ operation.call(self, left, right)
+ elsif left.respond_to?(operation) and right.respond_to?(operation)
+ left.send(operation, right)
+ else
+ nil
+ end
+ end
+ end
+
+ class ElseCondition < Condition
+
+ def else?
+ true
+ end
+
+ def evaluate(context)
+ true
+ end
+ end
+
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/context.rb b/vendor/plugins/liquid/lib/liquid/context.rb
new file mode 100644
index 0000000..2a42f12
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/context.rb
@@ -0,0 +1,243 @@
+module Liquid
+
+ class ContextError < StandardError
+ end
+
+ # Context keeps the variable stack and resolves variables, as well as keywords
+ #
+ # context['variable'] = 'testing'
+ # context['variable'] #=> 'testing'
+ # context['true'] #=> true
+ # context['10.2232'] #=> 10.2232
+ #
+ # context.stack do
+ # context['bob'] = 'bobsen'
+ # end
+ #
+ # context['bob'] #=> nil class Context
+ class Context
+ attr_reader :scopes
+ attr_reader :errors, :registers
+
+ def initialize(assigns = {}, registers = {}, rethrow_errors = false)
+ @scopes = [(assigns || {})]
+ @registers = registers
+ @errors = []
+ @rethrow_errors = rethrow_errors
+ end
+
+ def strainer
+ @strainer ||= Strainer.create(self)
+ end
+
+ # adds filters to this context.
+ # this does not register the filters with the main Template object. see Template.register_filter
+ # for that
+ def add_filters(filters)
+ filters = [filters].flatten.compact
+
+ filters.each do |f|
+ raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
+ strainer.extend(f)
+ end
+ end
+
+ def handle_error(e)
+ errors.push(e)
+ raise if @rethrow_errors
+
+ case e
+ when SyntaxError then "Liquid syntax error: #{e.message}"
+ else "Liquid error: #{e.message}"
+ end
+ end
+
+
+ def invoke(method, *args)
+ if strainer.respond_to?(method)
+ strainer.__send__(method, *args)
+ else
+ args.first
+ end
+ end
+
+ # push new local scope on the stack. use Context#stack instead
+ def push
+ @scopes.unshift({})
+ end
+
+ # merge a hash of variables in the current local scope
+ def merge(new_scopes)
+ @scopes[0].merge!(new_scopes)
+ end
+
+ # pop from the stack. use Context#stack instead
+ def pop
+ raise ContextError if @scopes.size == 1
+ @scopes.shift
+ end
+
+ # pushes a new local scope on the stack, pops it at the end of the block
+ #
+ # Example:
+ #
+ # context.stack do
+ # context['var'] = 'hi'
+ # end
+ # context['var] #=> nil
+ #
+ def stack(&block)
+ result = nil
+ push
+ begin
+ result = yield
+ ensure
+ pop
+ end
+ result
+ end
+
+ # Only allow String, Numeric, Hash, Array, Proc, Boolean or Liquid::Drop
+ def []=(key, value)
+ @scopes[0][key] = value
+ end
+
+ def [](key)
+ resolve(key)
+ end
+
+ def has_key?(key)
+ resolve(key) != nil
+ end
+
+ private
+
+ # Look up variable, either resolve directly after considering the name. We can directly handle
+ # Strings, digits, floats and booleans (true,false). If no match is made we lookup the variable in the current scope and
+ # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
+ # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
+ #
+ # Example:
+ #
+ # products == empty #=> products.empty?
+ #
+ def resolve(key)
+ case key
+ when nil, 'nil', 'null', ''
+ nil
+ when 'true'
+ true
+ when 'false'
+ false
+ when 'empty'
+ :empty?
+ # Single quoted strings
+ when /^'(.*)'$/
+ $1.to_s
+ # Double quoted strings
+ when /^"(.*)"$/
+ $1.to_s
+ # Integer and floats
+ when /^(\d+)$/
+ $1.to_i
+ # Ranges
+ when /^\((\S+)\.\.(\S+)\)$/
+ (resolve($1).to_i..resolve($2).to_i)
+ # Floats
+ when /^(\d[\d\.]+)$/
+ $1.to_f
+ else
+ variable(key)
+ end
+ end
+
+ # fetches an object starting at the local scope and then moving up
+ # the hierachy
+ def find_variable(key)
+ @scopes.each do |scope|
+ if scope.has_key?(key)
+ variable = scope[key]
+ variable = scope[key] = variable.call(self) if variable.is_a?(Proc)
+ variable = variable.to_liquid
+ variable.context = self if variable.respond_to?(:context=)
+ return variable
+ end
+ end
+ nil
+ end
+
+ # resolves namespaced queries gracefully.
+ #
+ # Example
+ #
+ # @context['hash'] = {"name" => 'tobi'}
+ # assert_equal 'tobi', @context['hash.name']
+ # assert_equal 'tobi', @context['hash[name]']
+ #
+ def variable(markup)
+ parts = markup.scan(VariableParser)
+ square_bracketed = /^\[(.*)\]$/
+
+ first_part = parts.shift
+ if first_part =~ square_bracketed
+ first_part = resolve($1)
+ end
+
+ if object = find_variable(first_part)
+
+ parts.each do |part|
+
+ # If object is a hash we look for the presence of the key and if its available
+ # we return it
+
+ if part =~ square_bracketed
+ part = resolve($1)
+
+ object[pos] = object[part].call(self) if object[part].is_a?(Proc) and object.respond_to?(:[]=)
+ object = object[part].to_liquid
+
+ else
+
+ # Hash
+ if object.respond_to?(:has_key?) and object.has_key?(part)
+
+ # if its a proc we will replace the entry in the hash table with the proc
+ res = object[part]
+ res = object[part] = res.call(self) if res.is_a?(Proc) and object.respond_to?(:[]=)
+ object = res.to_liquid
+
+ # Array
+ elsif object.respond_to?(:fetch) and part =~ /^\d+$/
+ pos = part.to_i
+
+ object[pos] = object[pos].call(self) if object[pos].is_a?(Proc) and object.respond_to?(:[]=)
+ object = object[pos].to_liquid
+
+ # Some special cases. If no key with the same name was found we interpret following calls
+ # as commands and call them on the current object
+ elsif object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
+
+ object = object.send(part.intern).to_liquid
+
+ # No key was present with the desired value and it wasn't one of the directly supported
+ # keywords either. The only thing we got left is to return nil
+ else
+ return nil
+ end
+ end
+
+ # If we are dealing with a drop here we have to
+ object.context = self if object.respond_to?(:context=)
+ end
+ end
+
+ object
+ end
+
+ private
+
+ def execute_proc(proc)
+ proc.call(self)
+ end
+ end
+end
diff --git a/vendor/plugins/liquid/lib/liquid/document.rb b/vendor/plugins/liquid/lib/liquid/document.rb
new file mode 100644
index 0000000..abffbde
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/document.rb
@@ -0,0 +1,17 @@
+module Liquid
+ class Document < Block
+ # we don't need markup to open this block
+ def initialize(tokens)
+ parse(tokens)
+ end
+
+ # There isn't a real delimter
+ def block_delimiter
+ []
+ end
+
+ # Document blocks don't need to be terminated since they are not actually opened
+ def assert_missing_delimitation!
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/drop.rb b/vendor/plugins/liquid/lib/liquid/drop.rb
new file mode 100644
index 0000000..7a2719e
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/drop.rb
@@ -0,0 +1,48 @@
+module Liquid
+
+ # A drop in liquid is a class which allows you to to export DOM like things to liquid
+ # Methods of drops are callable.
+ # The main use for liquid drops is the implement lazy loaded objects.
+ # If you would like to make data available to the web designers which you don't want loaded unless needed then
+ # a drop is a great way to do that
+ #
+ # Example:
+ #
+ # class ProductDrop < Liquid::Drop
+ # def top_sales
+ # Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
+ # end
+ # end
+ #
+ # tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
+ # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
+ #
+ # Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
+ # catch all
+ class Drop
+ attr_writer :context
+
+ # Catch all for the method
+ def before_method(method)
+ nil
+ end
+
+ # called by liquid to invoke a drop
+ def invoke_drop(method)
+ result = before_method(method)
+ result ||= send(method.to_sym) if self.class.public_instance_methods.include?(method.to_s)
+ result
+ end
+
+ def has_key?(name)
+ true
+ end
+
+ def to_liquid
+ self
+ end
+
+ alias :[] :invoke_drop
+ end
+
+end
diff --git a/vendor/plugins/liquid/lib/liquid/errors.rb b/vendor/plugins/liquid/lib/liquid/errors.rb
new file mode 100644
index 0000000..4e493b8
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/errors.rb
@@ -0,0 +1,7 @@
+module Liquid
+ class FilterNotFound < StandardError
+ end
+
+ class FileSystemError < StandardError
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/extensions.rb b/vendor/plugins/liquid/lib/liquid/extensions.rb
new file mode 100644
index 0000000..2752ba3
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/extensions.rb
@@ -0,0 +1,56 @@
+require 'time'
+require 'date'
+
+class String # :nodoc:
+ def to_liquid
+ self
+ end
+end
+
+class Array # :nodoc:
+ def to_liquid
+ self
+ end
+end
+
+class Hash # :nodoc:
+ def to_liquid
+ self
+ end
+end
+
+class Numeric # :nodoc:
+ def to_liquid
+ self
+ end
+end
+
+class Time # :nodoc:
+ def to_liquid
+ self
+ end
+end
+
+class DateTime < Date # :nodoc:
+ def to_liquid
+ self
+ end
+end
+
+class Date # :nodoc:
+ def to_liquid
+ self
+ end
+end
+
+def true.to_liquid # :nodoc:
+ self
+end
+
+def false.to_liquid # :nodoc:
+ self
+end
+
+def nil.to_liquid # :nodoc:
+ self
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/file_system.rb b/vendor/plugins/liquid/lib/liquid/file_system.rb
new file mode 100644
index 0000000..8c6b76d
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/file_system.rb
@@ -0,0 +1,62 @@
+module Liquid
+ # A Liquid file system is way to let your templates retrieve other templates for use with the include tag.
+ #
+ # You can implement subclasses that retrieve templates from the database, from the file system using a different
+ # path structure, you can provide them as hard-coded inline strings, or any manner that you see fit.
+ #
+ # You can add additional instance variables, arguments, or methods as needed.
+ #
+ # Example:
+ #
+ # Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
+ # liquid = Liquid::Template.parse(template)
+ #
+ # This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
+ class BlankFileSystem
+ # Called by Liquid to retrieve a template file
+ def read_template_file(template_path)
+ raise FileSystemError, "This liquid context does not allow includes."
+ end
+ end
+
+ # This implements an abstract file system which retrieves template files named in a manner similar to Rails partials,
+ # ie. with the template name prefixed with an underscore. The extension ".liquid" is also added.
+ #
+ # For security reasons, template paths are only allowed to contain letters, numbers, and underscore.
+ #
+ # Example:
+ #
+ # file_system = Liquid::LocalFileSystem.new("/some/path")
+ #
+ # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
+ # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
+ #
+ class LocalFileSystem
+ attr_accessor :root
+
+ def initialize(root)
+ @root = root
+ end
+
+ def read_template_file(template_path)
+ full_path = full_path(template_path)
+ raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path)
+
+ File.read(full_path)
+ end
+
+ def full_path(template_path)
+ raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/
+
+ full_path = if template_path.include?('/')
+ File.join(root, File.dirname(template_path), "_#{File.basename(template_path)}.liquid")
+ else
+ File.join(root, "_#{template_path}.liquid")
+ end
+
+ raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(root)}/
+
+ full_path
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/htmltags.rb b/vendor/plugins/liquid/lib/liquid/htmltags.rb
new file mode 100644
index 0000000..2fb8e4e
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/htmltags.rb
@@ -0,0 +1,64 @@
+module Liquid
+ class TableRow < Block
+ Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/
+
+ def initialize(tag_name, markup, tokens)
+ if markup =~ Syntax
+ @variable_name = $1
+ @collection_name = $2
+ @attributes = {}
+ markup.scan(TagAttributes) do |key, value|
+ @attributes[key] = value
+ end
+ else
+ raise SyntaxError.new("Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3")
+ end
+
+ super
+ end
+
+ def render(context)
+ collection = context[@collection_name] or return ''
+
+ if @attributes['limit'] or @attributes['offset']
+ limit = context[@attributes['limit']] || -1
+ offset = context[@attributes['offset']] || 0
+ collection = collection[offset.to_i..(limit.to_i + offset.to_i - 1)]
+ end
+
+ length = collection.length
+
+ cols = context[@attributes['cols']].to_i
+
+ row = 1
+ col = 0
+
+ result = ["\n"]
+ context.stack do
+
+ collection.each_with_index do |item, index|
+ context[@variable_name] = item
+ context['tablerowloop'] = {
+ 'length' => length,
+ 'index' => index + 1,
+ 'index0' => index,
+ 'rindex' => length - index,
+ 'rindex0' => length - index -1,
+ 'first' => (index == 0),
+ 'last' => (index == length - 1) }
+
+ result << [""] + render_all(@nodelist, context) + [' | ']
+
+ if col == cols and not (index == length - 1)
+ col = 0
+ result << ["
\n"]
+ end
+
+ end
+ end
+ result + ["
\n"]
+ end
+ end
+
+ Template.register_tag('tablerow', TableRow)
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/standardfilters.rb b/vendor/plugins/liquid/lib/liquid/standardfilters.rb
new file mode 100644
index 0000000..720f9be
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/standardfilters.rb
@@ -0,0 +1,133 @@
+require 'cgi'
+
+module Liquid
+
+ module StandardFilters
+
+ # Return the size of an array or of an string
+ def size(input)
+
+ input.respond_to?(:size) ? input.size : 0
+ end
+
+ # convert a input string to DOWNCASE
+ def downcase(input)
+ input.to_s.downcase
+ end
+
+ # convert a input string to UPCASE
+ def upcase(input)
+ input.to_s.upcase
+ end
+
+ # capitalize words in the input centence
+ def capitalize(input)
+ input.to_s.capitalize
+ end
+
+ def escape(input)
+ CGI.escapeHTML(input) rescue input
+ end
+
+ alias_method :h, :escape
+
+ # Truncate a string down to x characters
+ def truncate(input, length = 50, truncate_string = "...")
+ if input.nil? then return end
+ l = length.to_i - truncate_string.length
+ l = 0 if l < 0
+ input.length > length.to_i ? input[0...l] + truncate_string : input
+ end
+
+ def truncatewords(input, words = 15, truncate_string = "...")
+ if input.nil? then return end
+ wordlist = input.to_s.split
+ l = words.to_i - 1
+ l = 0 if l < 0
+ wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input
+ end
+
+ def strip_html(input)
+ input.to_s.gsub(/<.*?>/, '')
+ end
+
+ # Join elements of the array with certain character between them
+ def join(input, glue = ' ')
+ [input].flatten.join(glue)
+ end
+
+ # Sort elements of the array
+ def sort(input)
+ [input].flatten.sort
+ end
+
+ # Reformat a date
+ #
+ # %a - The abbreviated weekday name (``Sun'')
+ # %A - The full weekday name (``Sunday'')
+ # %b - The abbreviated month name (``Jan'')
+ # %B - The full month name (``January'')
+ # %c - The preferred local date and time representation
+ # %d - Day of the month (01..31)
+ # %H - Hour of the day, 24-hour clock (00..23)
+ # %I - Hour of the day, 12-hour clock (01..12)
+ # %j - Day of the year (001..366)
+ # %m - Month of the year (01..12)
+ # %M - Minute of the hour (00..59)
+ # %p - Meridian indicator (``AM'' or ``PM'')
+ # %S - Second of the minute (00..60)
+ # %U - Week number of the current year,
+ # starting with the first Sunday as the first
+ # day of the first week (00..53)
+ # %W - Week number of the current year,
+ # starting with the first Monday as the first
+ # day of the first week (00..53)
+ # %w - Day of the week (Sunday is 0, 0..6)
+ # %x - Preferred representation for the date alone, no time
+ # %X - Preferred representation for the time alone, no date
+ # %y - Year without a century (00..99)
+ # %Y - Year with century
+ # %Z - Time zone name
+ # %% - Literal ``%'' character
+ def date(input, format)
+
+ if format.to_s.empty?
+ return input.to_s
+ end
+
+ date = case input
+ when String
+ Time.parse(input)
+ when Date, Time, DateTime
+ input
+ else
+ return input
+ end
+
+ date.strftime(format.to_s)
+ rescue => e
+ input
+ end
+
+ # Get the first element of the passed in array
+ #
+ # Example:
+ # {{ product.images | first | to_img }}
+ #
+ def first(array)
+ array.first if array.respond_to?(:first)
+ end
+
+ # Get the last element of the passed in array
+ #
+ # Example:
+ # {{ product.images | last | to_img }}
+ #
+ def last(array)
+ array.last if array.respond_to?(:last)
+ end
+
+ end
+
+ Template.register_filter(StandardFilters)
+end
diff --git a/vendor/plugins/liquid/lib/liquid/strainer.rb b/vendor/plugins/liquid/lib/liquid/strainer.rb
new file mode 100644
index 0000000..285f2f7
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/strainer.rb
@@ -0,0 +1,43 @@
+module Liquid
+
+ # Strainer is the parent class for the filters system.
+ # New filters are mixed into the strainer class which is then instanciated for each liquid template render run.
+ #
+ # One of the strainer's responsibilities is to keep malicious method calls out
+ class Strainer #:nodoc:
+
+ @@required_methods = ["__send__", "__id__", "respond_to?", "extend", "methods", "class"]
+
+ @@filters = []
+
+ def initialize(context)
+ @context = context
+ end
+
+ def self.global_filter(filter)
+ raise StandardError, "Passed filter is not a module" unless filter.is_a?(Module)
+ @@filters << filter
+ end
+
+ def self.create(context)
+ strainer = Strainer.new(context)
+ @@filters.each { |m| strainer.extend(m) }
+ strainer
+ end
+
+ def respond_to?(method)
+ method_name = method.to_s
+ return false if method_name =~ /^__/
+ return false if @@required_methods.include?(method_name)
+ super
+ end
+
+ # remove all standard methods from the bucket so circumvent security
+ # problems
+ instance_methods.each do |m|
+ unless @@required_methods.include?(m)
+ undef_method m
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/tag.rb b/vendor/plugins/liquid/lib/liquid/tag.rb
new file mode 100644
index 0000000..e0bf35d
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/tag.rb
@@ -0,0 +1,26 @@
+module Liquid
+
+ class Tag
+ attr_accessor :nodelist
+
+ def initialize(tag_name, markup, tokens)
+ @tag_name = tag_name
+ @markup = markup
+ parse(tokens)
+ end
+
+ def parse(tokens)
+ end
+
+ def name
+ self.class.name.downcase
+ end
+
+ def render(context)
+ ''
+ end
+ end
+
+
+end
+
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/tags/assign.rb b/vendor/plugins/liquid/lib/liquid/tags/assign.rb
new file mode 100644
index 0000000..a68cf30
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/tags/assign.rb
@@ -0,0 +1,33 @@
+module Liquid
+
+ # Assign sets a variable in your template.
+ #
+ # {% assign foo = 'monkey' %}
+ #
+ # You can then use the variable later in the page.
+ #
+ # {{ monkey }}
+ #
+ class Assign < Tag
+ Syntax = /(#{VariableSignature}+)\s*=\s*(#{QuotedFragment}+)/
+
+ def initialize(tag_name, markup, tokens)
+ if markup =~ Syntax
+ @to = $1
+ @from = $2
+ else
+ raise SyntaxError.new("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]")
+ end
+
+ super
+ end
+
+ def render(context)
+ context.scopes.last[@to.to_s] = context[@from]
+ ''
+ end
+
+ end
+
+ Template.register_tag('assign', Assign)
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/tags/capture.rb b/vendor/plugins/liquid/lib/liquid/tags/capture.rb
new file mode 100644
index 0000000..f4f6f3c
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/tags/capture.rb
@@ -0,0 +1,35 @@
+module Liquid
+
+ # Capture stores the result of a block into a variable without rendering it inplace.
+ #
+ # {% capture heading %}
+ # Monkeys!
+ # {% endcapture %}
+ # ...
+ # {{ monkeys }}
+ #
+ # Capture is useful for saving content for use later in your template, such as
+ # in a sidebar or footer.
+ #
+ class Capture < Block
+ Syntax = /(\w+)/
+
+ def initialize(tag_name, markup, tokens)
+ if markup =~ Syntax
+ @to = $1
+ else
+ raise SyntaxError.new("Syntax Error in 'capture' - Valid syntax: capture [var]")
+ end
+
+ super
+ end
+
+ def render(context)
+ output = super
+ context[@to] = output.to_s
+ ''
+ end
+ end
+
+ Template.register_tag('capture', Capture)
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/tags/case.rb b/vendor/plugins/liquid/lib/liquid/tags/case.rb
new file mode 100644
index 0000000..0733c51
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/tags/case.rb
@@ -0,0 +1,83 @@
+module Liquid
+ class Case < Block
+ Syntax = /(#{QuotedFragment})/
+ WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/
+
+ def initialize(tag_name, markup, tokens)
+ @blocks = []
+
+ if markup =~ Syntax
+ @left = $1
+ else
+ raise SyntaxError.new("Syntax Error in tag 'case' - Valid syntax: case [condition]")
+ end
+
+ super
+ end
+
+ def unknown_tag(tag, markup, tokens)
+ @nodelist = []
+ case tag
+ when 'when'
+ record_when_condition(markup)
+ when 'else'
+ record_else_condition(markup)
+ else
+ super
+ end
+ end
+
+ def render(context)
+ context.stack do
+ execute_else_block = true
+
+ @blocks.inject([]) do |output, block|
+
+ if block.else?
+
+ return render_all(block.attachment, context) if execute_else_block
+
+ elsif block.evaluate(context)
+
+ execute_else_block = false
+ output += render_all(block.attachment, context)
+ end
+
+ output
+ end
+ end
+ end
+
+ private
+
+ def record_when_condition(markup)
+ while markup
+ # Create a new nodelist and assign it to the new block
+ if not markup =~ WhenSyntax
+ raise SyntaxError.new("Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %} ")
+ end
+
+ markup = $2
+
+ block = Condition.new(@left, '==', $1)
+ block.attach(@nodelist)
+ @blocks.push(block)
+ end
+ end
+
+ def record_else_condition(markup)
+
+ if not markup.strip.empty?
+ raise SyntaxError.new("Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) ")
+ end
+
+ block = ElseCondition.new
+ block.attach(@nodelist)
+ @blocks << block
+ end
+
+
+ end
+
+ Template.register_tag('case', Case)
+end
diff --git a/vendor/plugins/liquid/lib/liquid/tags/comment.rb b/vendor/plugins/liquid/lib/liquid/tags/comment.rb
new file mode 100644
index 0000000..8ce7e0e
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/tags/comment.rb
@@ -0,0 +1,9 @@
+module Liquid
+ class Comment < Block
+ def render(context)
+ ''
+ end
+ end
+
+ Template.register_tag('comment', Comment)
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/tags/cycle.rb b/vendor/plugins/liquid/lib/liquid/tags/cycle.rb
new file mode 100644
index 0000000..a34f29a
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/tags/cycle.rb
@@ -0,0 +1,60 @@
+module Liquid
+
+ # Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
+ #
+ # {% for item in items %}
+ # {{ item }}
+ # {% end %}
+ #
+ # Item one
+ # Item two
+ # Item three
+ # Item four
+ # Item five
+ #
+ class Cycle < Tag
+ SimpleSyntax = /#{QuotedFragment}/
+ NamedSyntax = /(#{QuotedFragment})\s*\:\s*(.*)/
+
+ def initialize(tag_name, markup, tokens)
+ case markup
+ when NamedSyntax
+ @variables = variables_from_string($2)
+ @name = $1
+ when SimpleSyntax
+ @variables = variables_from_string(markup)
+ @name = "'#{@variables.to_s}'"
+ else
+ raise SyntaxError.new("Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]")
+ end
+
+ super
+ end
+
+ def render(context)
+ context.registers[:cycle] ||= Hash.new(0)
+
+ context.stack do
+ key = context[@name]
+ iteration = context.registers[:cycle][key]
+ result = context[@variables[iteration]]
+ iteration += 1
+ iteration = 0 if iteration >= @variables.size
+ context.registers[:cycle][key] = iteration
+ result
+ end
+ end
+
+ private
+
+ def variables_from_string(markup)
+ markup.split(',').collect do |var|
+ var =~ /\s*(#{QuotedFragment})\s*/
+ $1 ? $1 : nil
+ end.compact
+ end
+
+ end
+
+ Template.register_tag('cycle', Cycle)
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/tags/for.rb b/vendor/plugins/liquid/lib/liquid/tags/for.rb
new file mode 100644
index 0000000..80c135f
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/tags/for.rb
@@ -0,0 +1,118 @@
+module Liquid
+
+ # "For" iterates over an array or collection.
+ # Several useful variables are available to you within the loop.
+ #
+ # == Basic usage:
+ # {% for item in collection %}
+ # {{ forloop.index }}: {{ item.name }}
+ # {% endfor %}
+ #
+ # == Advanced usage:
+ # {% for item in collection %}
+ #
+ # Item {{ forloop.index }}: {{ item.name }}
+ #
+ # {% endfor %}
+ #
+ # You can also define a limit and offset much like SQL. Remember
+ # that offset starts at 0 for the first item.
+ #
+ # {% for item in collection limit:5 offset:10 %}
+ # {{ item.name }}
+ # {% end %}
+ #
+ # == Available variables:
+ #
+ # forloop.name:: 'item-collection'
+ # forloop.length:: Length of the loop
+ # forloop.index:: The current item's position in the collection;
+ # forloop.index starts at 1.
+ # This is helpful for non-programmers who start believe
+ # the first item in an array is 1, not 0.
+ # forloop.index0:: The current item's position in the collection
+ # where the first item is 0
+ # forloop.rindex:: Number of items remaining in the loop
+ # (length - index) where 1 is the last item.
+ # forloop.rindex0:: Number of items remaining in the loop
+ # where 0 is the last item.
+ # forloop.first:: Returns true if the item is the first item.
+ # forloop.last:: Returns true if the item is the last item.
+ #
+ class For < Block
+ Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/
+
+ def initialize(tag_name, markup, tokens)
+ if markup =~ Syntax
+ @variable_name = $1
+ @collection_name = $2
+ @name = "#{$1}-#{$2}"
+ @attributes = {}
+ markup.scan(TagAttributes) do |key, value|
+ @attributes[key] = value
+ end
+ else
+ raise SyntaxError.new("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]")
+ end
+
+ super
+ end
+
+ def render(context)
+ context.registers[:for] ||= Hash.new(0)
+
+ collection = context[@collection_name]
+ collection = collection.to_a if collection.is_a?(Range)
+
+ return '' if collection.nil? or collection.empty?
+
+ range = (0..collection.length)
+
+ if @attributes['limit'] or @attributes['offset']
+ offset = 0
+ if @attributes['offset'] == 'continue'
+ offset = context.registers[:for][@name]
+ else
+ offset = context[@attributes['offset']] || 0
+ end
+ limit = context[@attributes['limit']]
+
+ range_end = limit ? offset + limit : collection.length
+ range = (offset..range_end-1)
+
+ # Save the range end in the registers so that future calls to
+ # offset:continue have something to pick up
+ context.registers[:for][@name] = range_end
+ end
+
+ result = []
+ segment = collection[range]
+ return '' if segment.nil?
+
+ context.stack do
+ length = segment.length
+
+ segment.each_with_index do |item, index|
+ context[@variable_name] = item
+ context['forloop'] = {
+ 'name' => @name,
+ 'length' => length,
+ 'index' => index + 1,
+ 'index0' => index,
+ 'rindex' => length - index,
+ 'rindex0' => length - index -1,
+ 'first' => (index == 0),
+ 'last' => (index == length - 1) }
+
+ result << render_all(@nodelist, context)
+ end
+ end
+
+ # Store position of last element we rendered. This allows us to do
+
+ result
+ end
+ end
+
+ Template.register_tag('for', For)
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/tags/if.rb b/vendor/plugins/liquid/lib/liquid/tags/if.rb
new file mode 100644
index 0000000..9e7c8bc
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/tags/if.rb
@@ -0,0 +1,66 @@
+module Liquid
+
+ # If is the conditional block
+ #
+ # {% if user.admin %}
+ # Admin user!
+ # {% else %}
+ # Not admin user
+ # {% endif %}
+ #
+ # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
+ #
+ # Note you can't use "and" within the If block. You should wrap complex logic in
+ # the Drop class or in a helper method.
+ #
+ class If < Block
+ Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/
+
+ def initialize(tag_name, markup, tokens)
+
+ @blocks = []
+
+ push_block('if', markup)
+
+ super
+ end
+
+ def unknown_tag(tag, markup, tokens)
+ if ['elsif', 'else'].include?(tag)
+ push_block(tag, markup)
+ else
+ super
+ end
+ end
+
+ def render(context)
+ context.stack do
+ @blocks.each do |block|
+ if block.evaluate(context)
+ return render_all(block.attachment, context)
+ end
+ end
+ ''
+ end
+ end
+
+ private
+
+ def push_block(tag, markup)
+
+ block = if tag == 'else'
+ ElseCondition.new
+ elsif markup =~ Syntax
+ Condition.new($1, $2, $3)
+ else
+ raise SyntaxError.new("Syntax Error in tag '#{tag}' - Valid syntax: #{tag} [condition]")
+ end
+
+ @blocks.push(block)
+ @nodelist = block.attach(Array.new)
+ end
+
+ end
+
+ Template.register_tag('if', If)
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/tags/ifchanged.rb b/vendor/plugins/liquid/lib/liquid/tags/ifchanged.rb
new file mode 100644
index 0000000..a4406c6
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/tags/ifchanged.rb
@@ -0,0 +1,20 @@
+module Liquid
+ class Ifchanged < Block
+
+ def render(context)
+ context.stack do
+
+ output = render_all(@nodelist, context)
+
+ if output != context.registers[:ifchanged]
+ context.registers[:ifchanged] = output
+ output
+ else
+ ''
+ end
+ end
+ end
+ end
+
+ Template.register_tag('ifchanged', Ifchanged)
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/tags/include.rb b/vendor/plugins/liquid/lib/liquid/tags/include.rb
new file mode 100644
index 0000000..2f9439f
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/tags/include.rb
@@ -0,0 +1,55 @@
+module Liquid
+ class Include < Tag
+ Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/
+
+ def initialize(tag_name, markup, tokens)
+ if markup =~ Syntax
+
+ @template_name = $1
+ @variable_name = $3
+ @attributes = {}
+
+ markup.scan(TagAttributes) do |key, value|
+ @attributes[key] = value
+ end
+
+ else
+ raise SyntaxError.new("Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]")
+ end
+
+ super
+ end
+
+ def parse(tokens)
+ end
+
+ def render(context)
+ source = Liquid::Template.file_system.read_template_file(context[@template_name])
+ partial = Liquid::Template.parse(source)
+
+ variable = context[@variable_name || @template_name[1..-2]]
+
+ context.stack do
+ @attributes.each do |key, value|
+ context[key] = context[value]
+ end
+
+ if variable.is_a?(Array)
+
+ variable.collect do |variable|
+ context[@template_name[1..-2]] = variable
+ partial.render(context)
+ end
+
+ else
+
+ context[@template_name[1..-2]] = variable
+ partial.render(context)
+
+ end
+ end
+ end
+ end
+
+ Template.register_tag('include', Include)
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/tags/unless.rb b/vendor/plugins/liquid/lib/liquid/tags/unless.rb
new file mode 100644
index 0000000..74a76ab
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/tags/unless.rb
@@ -0,0 +1,33 @@
+require File.dirname(__FILE__) + '/if'
+
+module Liquid
+
+ # Unless is a conditional just like 'if' but works on the inverse logic.
+ #
+ # {% unless x < 0 %} x is greater than zero {% end %}
+ #
+ class Unless < If
+ def render(context)
+ context.stack do
+
+ # First condition is interpreted backwards ( if not )
+ block = @blocks.first
+ unless block.evaluate(context)
+ return render_all(block.attachment, context)
+ end
+
+ # After the first condition unless works just like if
+ @blocks[1..-1].each do |block|
+ if block.evaluate(context)
+ return render_all(block.attachment, context)
+ end
+ end
+
+ ''
+ end
+ end
+ end
+
+
+ Template.register_tag('unless', Unless)
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/lib/liquid/template.rb b/vendor/plugins/liquid/lib/liquid/template.rb
new file mode 100644
index 0000000..ad7bcd9
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/template.rb
@@ -0,0 +1,145 @@
+module Liquid
+
+ # Templates are central to liquid.
+ # Interpretating templates is a two step process. First you compile the
+ # source code you got. During compile time some extensive error checking is performed.
+ # your code should expect to get some SyntaxErrors.
+ #
+ # After you have a compiled template you can then render it.
+ # You can use a compiled template over and over again and keep it cached.
+ #
+ # Example:
+ #
+ # template = Liquid::Template.parse(source)
+ # template.render('user_name' => 'bob')
+ #
+ class Template
+ attr_accessor :root
+ @@file_system = BlankFileSystem.new
+
+ class <Template object from liquid source code
+ def parse(source)
+ template = Template.new
+ template.parse(source)
+ template
+ end
+ end
+
+ # creates a new Template from an array of tokens. Use Template.parse instead
+ def initialize
+ end
+
+ # Parse source code.
+ # Returns self for easy chaining
+ def parse(source)
+ @root = Document.new(tokenize(source))
+ self
+ end
+
+ def registers
+ @registers ||= {}
+ end
+
+ def assigns
+ @assigns ||= {}
+ end
+
+ def errors
+ @errors ||= []
+ end
+
+ # Render takes a hash with local variables.
+ #
+ # if you use the same filters over and over again consider registering them globally
+ # with Template.register_filter
+ #
+ # Following options can be passed:
+ #
+ # * filters : array with local filters
+ # * registers : hash with register variables. Those can be accessed from
+ # filters and tags and might be useful to integrate liquid more with its host application
+ #
+ def render(*args)
+ return '' if @root.nil?
+
+ context = case args.first
+ when Liquid::Context
+ args.shift
+ when Hash
+ self.assigns.merge!(args.shift)
+ Context.new(assigns, registers, @rethrow_errors)
+ when nil
+ Context.new(assigns, registers, @rethrow_errors)
+ else
+ raise ArgumentError, "Expect Hash or Liquid::Context as parameter"
+ end
+
+ case args.last
+ when Hash
+ options = args.pop
+
+ if options[:registers].is_a?(Hash)
+ self.registers.merge!(options[:registers])
+ end
+
+ if options[:filters]
+ context.add_filters(options[:filters])
+ end
+ when Module
+ context.add_filters(args.pop)
+ when Array
+ context.add_filters(args.pop)
+ end
+
+
+ # render the nodelist.
+ # for performance reasons we get a array back here. to_s will make a string out of it
+ begin
+ @root.render(context).to_s
+ ensure
+ @errors = context.errors
+ end
+ end
+
+ def render!(*args)
+ @rethrow_errors = true; render(*args)
+ end
+
+ private
+
+ # Uses the Liquid::TemplateParser regexp to tokenize the passed source
+ def tokenize(source)
+ return [] if source.to_s.empty?
+ tokens = source.split(TemplateParser)
+
+ # removes the rogue empty element at the beginning of the array
+ tokens.shift if tokens[0] and tokens[0].empty?
+
+ tokens
+ end
+
+ end
+end
diff --git a/vendor/plugins/liquid/lib/liquid/variable.rb b/vendor/plugins/liquid/lib/liquid/variable.rb
new file mode 100644
index 0000000..16d7f99
--- /dev/null
+++ b/vendor/plugins/liquid/lib/liquid/variable.rb
@@ -0,0 +1,52 @@
+module Liquid
+
+ # Holds variables. Variables are only loaded "just in time"
+ # and are not evaluated as part of the render stage
+ #
+ # {{ monkey }}
+ # {{ user.name }}
+ #
+ # Variables can be combined with filters:
+ #
+ # {{ user | link }}
+ #
+ class Variable
+ attr_accessor :filters, :name
+
+ def initialize(markup)
+ @markup = markup
+ @name = nil
+ @filters = []
+ if match = markup.match(/\s*(#{QuotedFragment})/)
+ @name = match[1]
+ if markup.match(/#{FilterSperator}\s*(.*)/)
+ filters = Regexp.last_match(1).split(/#{FilterSperator}/)
+
+ filters.each do |f|
+ if matches = f.match(/\s*(\w+)/)
+ filtername = matches[1]
+ filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*(#{QuotedFragment})/).flatten
+ @filters << [filtername.to_sym, filterargs]
+ end
+ end
+ end
+ end
+ end
+
+ def render(context)
+ return '' if @name.nil?
+ output = context[@name]
+ @filters.inject(output) do |output, filter|
+ filterargs = filter[1].to_a.collect do |a|
+ context[a]
+ end
+ begin
+ output = context.invoke(filter[0], output, *filterargs)
+ rescue FilterNotFound
+ raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found."
+ end
+ end
+ output
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/test/block_test.rb b/vendor/plugins/liquid/test/block_test.rb
new file mode 100644
index 0000000..270938e
--- /dev/null
+++ b/vendor/plugins/liquid/test/block_test.rb
@@ -0,0 +1,58 @@
+require File.dirname(__FILE__) + '/helper'
+
+class VariableTest < Test::Unit::TestCase
+ include Liquid
+
+ def test_blankspace
+ template = Liquid::Template.parse(" ")
+ assert_equal [" "], template.root.nodelist
+ end
+
+ def test_variable_beginning
+ template = Liquid::Template.parse("{{funk}} ")
+ assert_equal 2, template.root.nodelist.size
+ assert_equal Variable, template.root.nodelist[0].class
+ assert_equal String, template.root.nodelist[1].class
+ end
+
+ def test_variable_end
+ template = Liquid::Template.parse(" {{funk}}")
+ assert_equal 2, template.root.nodelist.size
+ assert_equal String, template.root.nodelist[0].class
+ assert_equal Variable, template.root.nodelist[1].class
+ end
+
+ def test_variable_middle
+ template = Liquid::Template.parse(" {{funk}} ")
+ assert_equal 3, template.root.nodelist.size
+ assert_equal String, template.root.nodelist[0].class
+ assert_equal Variable, template.root.nodelist[1].class
+ assert_equal String, template.root.nodelist[2].class
+ end
+
+ def test_variable_many_embedded_fragments
+ template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ")
+ assert_equal 7, template.root.nodelist.size
+ assert_equal [String, Variable, String, Variable, String, Variable, String], block_types(template.root.nodelist)
+ end
+
+ def test_with_block
+ template = Liquid::Template.parse(" {% comment %} {% endcomment %} ")
+ assert_equal [String, Comment, String], block_types(template.root.nodelist)
+ assert_equal 3, template.root.nodelist.size
+ end
+
+ def test_with_custom_tag
+ Liquid::Template.register_tag("testtag", Block)
+
+ assert_nothing_thrown do
+ template = Liquid::Template.parse( "{% testtag %} {% endtesttag %}")
+ end
+ end
+
+ private
+
+ def block_types(nodelist)
+ nodelist.collect { |node| node.class }
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/test/condition_test.rb b/vendor/plugins/liquid/test/condition_test.rb
new file mode 100644
index 0000000..3d2627b
--- /dev/null
+++ b/vendor/plugins/liquid/test/condition_test.rb
@@ -0,0 +1,75 @@
+require File.dirname(__FILE__) + '/helper'
+
+class ConditionTest < Test::Unit::TestCase
+ include Liquid
+
+ def test_default_operators_evalute_true
+ assert_evalutes_true '1', '==', '1'
+ assert_evalutes_true '1', '!=', '2'
+ assert_evalutes_true '1', '<>', '2'
+ assert_evalutes_true '1', '<', '2'
+ assert_evalutes_true '2', '>', '1'
+ assert_evalutes_true '1', '>=', '1'
+ assert_evalutes_true '2', '>=', '1'
+ assert_evalutes_true '1', '<=', '2'
+ assert_evalutes_true '1', '<=', '1'
+ end
+
+ def test_default_operators_evalute_false
+ assert_evalutes_false '1', '==', '2'
+ assert_evalutes_false '1', '!=', '1'
+ assert_evalutes_false '1', '<>', '1'
+ assert_evalutes_false '1', '<', '0'
+ assert_evalutes_false '2', '>', '4'
+ assert_evalutes_false '1', '>=', '3'
+ assert_evalutes_false '2', '>=', '4'
+ assert_evalutes_false '1', '<=', '0'
+ assert_evalutes_false '1', '<=', '0'
+ end
+
+ def test_contains_works_on_strings
+ assert_evalutes_true "'bob'", 'contains', "'o'"
+ assert_evalutes_true "'bob'", 'contains', "'b'"
+ assert_evalutes_true "'bob'", 'contains', "'bo'"
+ assert_evalutes_true "'bob'", 'contains', "'ob'"
+ assert_evalutes_true "'bob'", 'contains', "'bob'"
+
+ assert_evalutes_false "'bob'", 'contains', "'bob2'"
+ assert_evalutes_false "'bob'", 'contains', "'a'"
+ assert_evalutes_false "'bob'", 'contains', "'---'"
+ end
+
+ def test_contains_works_on_arrays
+ @context = Liquid::Context.new
+ @context['array'] = [1,2,3,4,5]
+
+ assert_evalutes_false "array", 'contains', '0'
+ assert_evalutes_true "array", 'contains', '1'
+ assert_evalutes_true "array", 'contains', '2'
+ assert_evalutes_true "array", 'contains', '3'
+ assert_evalutes_true "array", 'contains', '4'
+ assert_evalutes_true "array", 'contains', '5'
+ assert_evalutes_false "array", 'contains', '6'
+
+ assert_evalutes_false "array", 'contains', '"1"'
+
+ end
+
+ def test_should_allow_custom_proc_operator
+ Condition.operators['starts_with'] = Proc.new { |cond, left, right| left =~ %r{^#{right}}}
+
+ assert_evalutes_true "'bob'", 'starts_with', "'b'"
+ assert_evalutes_false "'bob'", 'starts_with', "'o'"
+ ensure
+ Condition.operators.delete 'starts_with'
+ end
+
+ private
+ def assert_evalutes_true(left, op, right)
+ assert Condition.new(left, op, right).evaluate(@context || Liquid::Context.new), "Evaluated false: #{left} #{op} #{right}"
+ end
+
+ def assert_evalutes_false(left, op, right)
+ assert !Condition.new(left, op, right).evaluate(@context || Liquid::Context.new), "Evaluated true: #{left} #{op} #{right}"
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/test/context_test.rb b/vendor/plugins/liquid/test/context_test.rb
new file mode 100644
index 0000000..9819c0a
--- /dev/null
+++ b/vendor/plugins/liquid/test/context_test.rb
@@ -0,0 +1,411 @@
+require File.dirname(__FILE__) + '/helper'
+class HundredCentes
+ def to_liquid
+ 100
+ end
+end
+
+class CentsDrop < Liquid::Drop
+ def amount
+ HundredCentes.new
+ end
+
+ def non_zero?
+ true
+ end
+end
+
+class ContextSensitiveDrop < Liquid::Drop
+ def test
+ @context['test']
+ end
+end
+
+class Category < Liquid::Drop
+ attr_accessor :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def to_liquid
+ CategoryDrop.new(self)
+ end
+end
+
+class CategoryDrop
+ attr_accessor :category, :context
+ def initialize(category)
+ @category = category
+ end
+end
+
+
+class ContextTest < Test::Unit::TestCase
+ include Liquid
+
+ def setup
+ @template = Liquid::Template.new
+ @context = Liquid::Context.new(@template.assigns, @template.registers)
+ end
+
+ def test_variables
+ @context['string'] = 'string'
+ assert_equal 'string', @context['string']
+
+ @context['num'] = 5
+ assert_equal 5, @context['num']
+
+ @context['time'] = Time.parse('2006-06-06 12:00:00')
+ assert_equal Time.parse('2006-06-06 12:00:00'), @context['time']
+
+ @context['date'] = Date.today
+ assert_equal Date.today, @context['date']
+
+ now = DateTime.now
+ @context['datetime'] = now
+ assert_equal now, @context['datetime']
+
+ @context['bool'] = true
+ assert_equal true, @context['bool']
+
+ @context['bool'] = false
+ assert_equal false, @context['bool']
+
+ @context['nil'] = nil
+ assert_equal nil, @context['nil']
+ assert_equal nil, @context['nil']
+ end
+
+ def test_variables_not_existing
+ assert_equal nil, @context['does_not_exist']
+ end
+
+ def test_scoping
+ assert_nothing_raised do
+ @context.push
+ @context.pop
+ end
+
+ assert_raise(Liquid::ContextError) do
+ @context.pop
+ end
+
+ assert_raise(Liquid::ContextError) do
+ @context.push
+ @context.pop
+ @context.pop
+ end
+ end
+
+ def test_length_query
+
+ @context['numbers'] = [1,2,3,4]
+
+ assert_equal 4, @context['numbers.size']
+
+ @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4}
+
+ assert_equal 4, @context['numbers.size']
+
+ @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4, 'size' => 1000}
+
+ assert_equal 1000, @context['numbers.size']
+
+ end
+
+ def test_add_filter
+
+ filter = Module.new do
+ def hi(output)
+ output + ' hi!'
+ end
+ end
+
+ context = Context.new(@template)
+ context.add_filters(filter)
+ assert_equal 'hi? hi!', context.invoke(:hi, 'hi?')
+
+ context = Context.new(@template)
+ assert_equal 'hi?', context.invoke(:hi, 'hi?')
+
+ context.add_filters(filter)
+ assert_equal 'hi? hi!', context.invoke(:hi, 'hi?')
+
+ end
+
+ def test_override_global_filter
+ global = Module.new do
+ def notice(output)
+ "Global #{output}"
+ end
+ end
+
+ local = Module.new do
+ def notice(output)
+ "Local #{output}"
+ end
+ end
+
+ Template.register_filter(global)
+ assert_equal 'Global test', Template.parse("{{'test' | notice }}").render
+ assert_equal 'Local test', Template.parse("{{'test' | notice }}").render({}, :filters => [local])
+ end
+
+ def test_only_intended_filters_make_it_there
+
+ filter = Module.new do
+ def hi(output)
+ output + ' hi!'
+ end
+ end
+
+ context = Context.new(@template)
+ methods = context.strainer.methods
+ context.add_filters(filter)
+ assert_equal (methods + ['hi']).sort, context.strainer.methods.sort
+ end
+
+ def test_add_item_in_outer_scope
+ @context['test'] = 'test'
+ @context.push
+ assert_equal 'test', @context['test']
+ @context.pop
+ assert_equal 'test', @context['test']
+ end
+
+ def test_add_item_in_inner_scope
+ @context.push
+ @context['test'] = 'test'
+ assert_equal 'test', @context['test']
+ @context.pop
+ assert_equal nil, @context['test']
+ end
+
+ def test_hierachical_data
+ @context['hash'] = {"name" => 'tobi'}
+ assert_equal 'tobi', @context['hash.name']
+ end
+
+ def test_keywords
+ assert_equal true, @context['true']
+ assert_equal false, @context['false']
+ end
+
+ def test_digits
+ assert_equal 100, @context['100']
+ assert_equal 100.00, @context['100.00']
+ end
+
+ def test_strings
+ assert_equal "hello!", @context['"hello!"']
+ assert_equal "hello!", @context["'hello!'"]
+ end
+
+ def test_merge
+ @context.merge({ "test" => "test" })
+ assert_equal 'test', @context['test']
+ @context.merge({ "test" => "newvalue", "foo" => "bar" })
+ assert_equal 'newvalue', @context['test']
+ assert_equal 'bar', @context['foo']
+ end
+
+ def test_array_notation
+ @context['test'] = [1,2,3,4,5]
+
+ assert_equal 1, @context['test[0]']
+ assert_equal 2, @context['test[1]']
+ assert_equal 3, @context['test[2]']
+ assert_equal 4, @context['test[3]']
+ assert_equal 5, @context['test[4]']
+ end
+
+ def test_recoursive_array_notation
+ @context['test'] = {'test' => [1,2,3,4,5]}
+
+ assert_equal 1, @context['test.test[0]']
+
+ @context['test'] = [{'test' => 'worked'}]
+
+ assert_equal 'worked', @context['test[0].test']
+ end
+
+ def test_hash_to_array_transition
+ @context['colors'] = {
+ 'Blue' => ['003366','336699', '6699CC', '99CCFF'],
+ 'Green' => ['003300','336633', '669966', '99CC99'],
+ 'Yellow' => ['CC9900','FFCC00', 'FFFF99', 'FFFFCC'],
+ 'Red' => ['660000','993333', 'CC6666', 'FF9999']
+ }
+
+ assert_equal '003366', @context['colors.Blue[0]']
+ assert_equal 'FF9999', @context['colors.Red[3]']
+ end
+
+ def test_try_first
+ @context['test'] = [1,2,3,4,5]
+
+ assert_equal 1, @context['test.first']
+ assert_equal 5, @context['test.last']
+
+ @context['test'] = {'test' => [1,2,3,4,5]}
+
+ assert_equal 1, @context['test.test.first']
+ assert_equal 5, @context['test.test.last']
+
+ @context['test'] = [1]
+ assert_equal 1, @context['test.first']
+ assert_equal 1, @context['test.last']
+ end
+
+ def test_access_hashes_with_hash_notation
+
+ @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
+ @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]}
+
+
+ assert_equal 5, @context['products["count"]']
+ assert_equal 'deepsnow', @context['products["tags"][0]']
+ assert_equal 'deepsnow', @context['products["tags"].first']
+ assert_equal 'draft151cm', @context['product["variants"][0]["title"]']
+ assert_equal 'element151cm', @context['product["variants"][1]["title"]']
+ assert_equal 'draft151cm', @context['product["variants"][0]["title"]']
+ assert_equal 'element151cm', @context['product["variants"].last["title"]']
+ end
+
+ def test_access_variable_with_hash_notation
+ @context['foo'] = 'baz'
+ @context['bar'] = 'foo'
+
+ assert_equal 'baz', @context['["foo"]']
+ assert_equal 'baz', @context['[bar]']
+ end
+
+ def test_access_hashes_with_hash_access_variables
+
+ @context['var'] = 'tags'
+ @context['nested'] = {'var' => 'tags'}
+ @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
+
+ assert_equal 'deepsnow', @context['products[var].first']
+ assert_equal 'freestyle', @context['products[nested.var].last']
+ end
+
+
+ def test_first_can_appear_in_middle_of_callchain
+
+ @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]}
+
+ assert_equal 'draft151cm', @context['product.variants[0].title']
+ assert_equal 'element151cm', @context['product.variants[1].title']
+ assert_equal 'draft151cm', @context['product.variants.first.title']
+ assert_equal 'element151cm', @context['product.variants.last.title']
+
+ end
+
+ def test_cents
+ @context.merge( "cents" => HundredCentes.new )
+ assert_equal 100, @context['cents']
+ end
+
+ def test_nested_cents
+ @context.merge( "cents" => { 'amount' => HundredCentes.new} )
+ assert_equal 100, @context['cents.amount']
+
+ @context.merge( "cents" => { 'cents' => { 'amount' => HundredCentes.new} } )
+ assert_equal 100, @context['cents.cents.amount']
+ end
+
+ def test_cents_through_drop
+ @context.merge( "cents" => CentsDrop.new )
+ assert_equal 100, @context['cents.amount']
+ end
+
+ def test_nested_cents_through_drop
+ @context.merge( "vars" => {"cents" => CentsDrop.new} )
+ assert_equal 100, @context['vars.cents.amount']
+ end
+
+ def test_drop_methods_with_question_marks
+ @context.merge( "cents" => CentsDrop.new )
+ assert @context['cents.non_zero?']
+ end
+
+ def test_context_from_within_drop
+ @context.merge( "test" => '123', "vars" => ContextSensitiveDrop.new )
+ assert_equal '123', @context['vars.test']
+ end
+
+ def test_nested_context_from_within_drop
+ @context.merge( "test" => '123', "vars" => {"local" => ContextSensitiveDrop.new } )
+ assert_equal '123', @context['vars.local.test']
+ end
+
+ def test_ranges
+ @context.merge( "test" => '5' )
+ assert_equal (1..5), @context['(1..5)']
+ assert_equal (1..5), @context['(1..test)']
+ assert_equal (5..5), @context['(test..test)']
+ end
+
+ def test_cents_through_drop_nestedly
+ @context.merge( "cents" => {"cents" => CentsDrop.new} )
+ assert_equal 100, @context['cents.cents.amount']
+
+ @context.merge( "cents" => { "cents" => {"cents" => CentsDrop.new}} )
+ assert_equal 100, @context['cents.cents.cents.amount']
+ end
+
+ def test_proc_as_variable
+ @context['dynamic'] = Proc.new { 'Hello' }
+
+ assert_equal 'Hello', @context['dynamic']
+ end
+
+ def test_lambda_as_variable
+ @context['dynamic'] = lambda { 'Hello' }
+
+ assert_equal 'Hello', @context['dynamic']
+ end
+
+ def test_nested_lambda_as_variable
+ @context['dynamic'] = { "lambda" => lambda { 'Hello' } }
+
+ assert_equal 'Hello', @context['dynamic.lambda']
+ end
+
+ def test_lambda_is_called_once
+ @context['callcount'] = lambda { @global ||= 0; @global += 1; @global.to_s }
+
+ assert_equal '1', @context['callcount']
+ assert_equal '1', @context['callcount']
+ assert_equal '1', @context['callcount']
+
+ @global = nil
+ end
+
+ def test_nested_lambda_is_called_once
+ @context['callcount'] = { "lambda" => lambda { @global ||= 0; @global += 1; @global.to_s } }
+
+ assert_equal '1', @context['callcount.lambda']
+ assert_equal '1', @context['callcount.lambda']
+ assert_equal '1', @context['callcount.lambda']
+
+ @global = nil
+ end
+
+ def test_access_to_context_from_proc
+ @context.registers[:magic] = 345392
+
+ @context['magic'] = lambda { @context.registers[:magic] }
+
+ assert_equal 345392, @context['magic']
+ end
+
+ def test_to_liquid_and_context_at_first_level
+ @context['category'] = Category.new("foobar")
+ assert_kind_of CategoryDrop, @context['category']
+ assert_equal @context, @context['category'].context
+ end
+
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/test/drop_test.rb b/vendor/plugins/liquid/test/drop_test.rb
new file mode 100644
index 0000000..8a6921e
--- /dev/null
+++ b/vendor/plugins/liquid/test/drop_test.rb
@@ -0,0 +1,141 @@
+
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/helper'
+
+class ContextDrop < Liquid::Drop
+ def scopes
+ @context.scopes.size
+ end
+
+ def scopes_as_array
+ (1..@context.scopes.size).to_a
+ end
+
+ def loop_pos
+ @context['forloop.index']
+ end
+
+ def break
+ Breakpoint.breakpoint
+ end
+
+ def before_method(method)
+ return @context[method]
+ end
+end
+
+
+class ProductDrop < Liquid::Drop
+
+ class TextDrop < Liquid::Drop
+ def array
+ ['text1', 'text2']
+ end
+
+ def text
+ 'text1'
+ end
+ end
+
+ class CatchallDrop < Liquid::Drop
+ def before_method(method)
+ return 'method: ' << method
+ end
+ end
+
+ def texts
+ TextDrop.new
+ end
+
+ def catchall
+ CatchallDrop.new
+ end
+
+ def context
+ ContextDrop.new
+ end
+
+ protected
+ def callmenot
+ "protected"
+ end
+end
+
+
+class DropsTest < Test::Unit::TestCase
+ include Liquid
+
+ def test_product_drop
+
+ assert_nothing_raised do
+ tpl = Liquid::Template.parse( ' ' )
+ tpl.render('product' => ProductDrop.new)
+ end
+ end
+
+ def test_text_drop
+ output = Liquid::Template.parse( ' {{ product.texts.text }} ' ).render('product' => ProductDrop.new)
+ assert_equal ' text1 ', output
+
+ end
+
+ def test_text_drop
+ output = Liquid::Template.parse( ' {{ product.catchall.unknown }} ' ).render('product' => ProductDrop.new)
+ assert_equal ' method: unknown ', output
+
+ end
+
+ def test_text_array_drop
+ output = Liquid::Template.parse( '{% for text in product.texts.array %} {{text}} {% endfor %}' ).render('product' => ProductDrop.new)
+ assert_equal ' text1 text2 ', output
+ end
+
+ def test_context_drop
+ output = Liquid::Template.parse( ' {{ context.bar }} ' ).render('context' => ContextDrop.new, 'bar' => "carrot")
+ assert_equal ' carrot ', output
+ end
+
+ def test_nested_context_drop
+ output = Liquid::Template.parse( ' {{ product.context.foo }} ' ).render('product' => ProductDrop.new, 'foo' => "monkey")
+ assert_equal ' monkey ', output
+ end
+
+ def test_protected
+ output = Liquid::Template.parse( ' {{ product.callmenot }} ' ).render('product' => ProductDrop.new)
+ assert_equal ' ', output
+ end
+
+ def test_scope
+ assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render('context' => ContextDrop.new)
+ assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1])
+ assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1])
+ end
+
+ def test_scope_though_proc
+ assert_equal '1', Liquid::Template.parse( '{{ s }}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] })
+ assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ s }}{%endfor%}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1])
+ assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1])
+ end
+
+ def test_scope_with_assigns
+ assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render('context' => ContextDrop.new)
+ assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1])
+ assert_equal 'test', Liquid::Template.parse( '{% assign header_gif = "test"%}{{header_gif}}' ).render('context' => ContextDrop.new)
+ assert_equal 'test', Liquid::Template.parse( "{% assign header_gif = 'test'%}{{header_gif}}" ).render('context' => ContextDrop.new)
+ end
+
+ def test_scope_from_tags
+ assert_equal '1', Liquid::Template.parse( '{% for i in context.scopes_as_array %}{{i}}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1])
+ assert_equal '12', Liquid::Template.parse( '{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1])
+ assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1])
+ end
+
+ def test_access_context_from_drop
+ assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{{ context.loop_pos }}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1,2,3])
+ end
+
+
+
+end
+
+
diff --git a/vendor/plugins/liquid/test/error_handling_test.rb b/vendor/plugins/liquid/test/error_handling_test.rb
new file mode 100644
index 0000000..3fbb27f
--- /dev/null
+++ b/vendor/plugins/liquid/test/error_handling_test.rb
@@ -0,0 +1,65 @@
+
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/helper'
+
+class ErrorDrop < Liquid::Drop
+ def standard_error
+ raise StandardError, 'standard error'
+ end
+
+ def argument_error
+ raise ArgumentError, 'argument error'
+ end
+
+ def syntax_error
+ raise SyntaxError, 'syntax error'
+ end
+
+end
+
+
+class ErrorHandlingTest < Test::Unit::TestCase
+ include Liquid
+
+ def test_standard_error
+ assert_nothing_raised do
+ template = Liquid::Template.parse( ' {{ errors.standard_error }} ' )
+ assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
+
+ assert_equal 1, template.errors.size
+ assert_equal StandardError, template.errors.first.class
+ end
+ end
+
+ def test_syntax
+
+ assert_nothing_raised do
+
+ template = Liquid::Template.parse( ' {{ errors.syntax_error }} ' )
+ assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)
+
+ assert_equal 1, template.errors.size
+ assert_equal SyntaxError, template.errors.first.class
+
+ end
+
+ end
+
+ def test_argument
+
+ assert_nothing_raised do
+
+ template = Liquid::Template.parse( ' {{ errors.argument_error }} ' )
+ assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)
+
+ assert_equal 1, template.errors.size
+ assert_equal ArgumentError, template.errors.first.class
+
+ end
+
+ end
+
+
+end
+
+
diff --git a/vendor/plugins/liquid/test/extra/breakpoint.rb b/vendor/plugins/liquid/test/extra/breakpoint.rb
new file mode 100755
index 0000000..c118e22
--- /dev/null
+++ b/vendor/plugins/liquid/test/extra/breakpoint.rb
@@ -0,0 +1,547 @@
+# The Breakpoint library provides the convenience of
+# being able to inspect and modify state, diagnose
+# bugs all via IRB by simply setting breakpoints in
+# your applications by the call of a method.
+#
+# This library was written and is supported by me,
+# Florian Gross. I can be reached at flgr@ccan.de
+# and enjoy getting feedback about my libraries.
+#
+# The whole library (including breakpoint_client.rb
+# and binding_of_caller.rb) is licensed under the
+# same license that Ruby uses. (Which is currently
+# either the GNU General Public License or a custom
+# one that allows for commercial usage.) If you for
+# some good reason need to use this under another
+# license please contact me.
+
+require 'irb'
+require 'caller'
+require 'drb'
+require 'drb/acl'
+require 'thread'
+
+module Breakpoint
+ id = %q$Id: breakpoint.rb 52 2005-02-26 19:43:19Z flgr $
+ current_version = id.split(" ")[2]
+ unless defined?(Version)
+ # The Version of ruby-breakpoint you are using as String of the
+ # 1.2.3 form where the digits stand for release, major and minor
+ # version respectively.
+ Version = "0.5.0"
+ end
+
+ extend self
+
+ # This will pop up an interactive ruby session at a
+ # pre-defined break point in a Ruby application. In
+ # this session you can examine the environment of
+ # the break point.
+ #
+ # You can get a list of variables in the context using
+ # local_variables via +local_variables+. You can then
+ # examine their values by typing their names.
+ #
+ # You can have a look at the call stack via +caller+.
+ #
+ # The source code around the location where the breakpoint
+ # was executed can be examined via +source_lines+. Its
+ # argument specifies how much lines of context to display.
+ # The default amount of context is 5 lines. Note that
+ # the call to +source_lines+ can raise an exception when
+ # it isn't able to read in the source code.
+ #
+ # breakpoints can also return a value. They will execute
+ # a supplied block for getting a default return value.
+ # A custom value can be returned from the session by doing
+ # +throw(:debug_return, value)+.
+ #
+ # You can also give names to break points which will be
+ # used in the message that is displayed upon execution
+ # of them.
+ #
+ # Here's a sample of how breakpoints should be placed:
+ #
+ # class Person
+ # def initialize(name, age)
+ # @name, @age = name, age
+ # breakpoint("Person#initialize")
+ # end
+ #
+ # attr_reader :age
+ # def name
+ # breakpoint("Person#name") { @name }
+ # end
+ # end
+ #
+ # person = Person.new("Random Person", 23)
+ # puts "Name: #{person.name}"
+ #
+ # And here is a sample debug session:
+ #
+ # Executing break point "Person#initialize" at file.rb:4 in `initialize'
+ # irb(#):001:0> local_variables
+ # => ["name", "age", "_", "__"]
+ # irb(#):002:0> [name, age]
+ # => ["Random Person", 23]
+ # irb(#):003:0> [@name, @age]
+ # => ["Random Person", 23]
+ # irb(#):004:0> self
+ # => #
+ # irb(#):005:0> @age += 1; self
+ # => #
+ # irb(#):006:0> exit
+ # Executing break point "Person#name" at file.rb:9 in `name'
+ # irb(#):001:0> throw(:debug_return, "Overriden name")
+ # Name: Overriden name
+ #
+ # Breakpoint sessions will automatically have a few
+ # convenience methods available. See Breakpoint::CommandBundle
+ # for a list of them.
+ #
+ # Breakpoints can also be used remotely over sockets.
+ # This is implemented by running part of the IRB session
+ # in the application and part of it in a special client.
+ # You have to call Breakpoint.activate_drb to enable
+ # support for remote breakpoints and then run
+ # breakpoint_client.rb which is distributed with this
+ # library. See the documentation of Breakpoint.activate_drb
+ # for details.
+ def breakpoint(id = nil, context = nil, &block)
+ callstack = caller
+ callstack.slice!(0, 3) if callstack.first["breakpoint"]
+ file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures
+
+ message = "Executing break point " + (id ? "#{id.inspect} " : "") +
+ "at #{file}:#{line}" + (method ? " in `#{method}'" : "")
+
+ if context then
+ return handle_breakpoint(context, message, file, line, &block)
+ end
+
+ Binding.of_caller do |binding_context|
+ handle_breakpoint(binding_context, message, file, line, &block)
+ end
+ end
+
+ # These commands are automatically available in all breakpoint shells.
+ module CommandBundle
+ # Proxy to a Breakpoint client. Lets you directly execute code
+ # in the context of the client.
+ class Client
+ def initialize(eval_handler) # :nodoc:
+ eval_handler.untaint
+ @eval_handler = eval_handler
+ end
+
+ instance_methods.each do |method|
+ next if method[/^__.+__$/]
+ undef_method method
+ end
+
+ # Executes the specified code at the client.
+ def eval(code)
+ @eval_handler.call(code)
+ end
+
+ # Will execute the specified statement at the client.
+ def method_missing(method, *args, &block)
+ if args.empty? and not block
+ result = eval "#{method}"
+ else
+ # This is a bit ugly. The alternative would be using an
+ # eval context instead of an eval handler for executing
+ # the code at the client. The problem with that approach
+ # is that we would have to handle special expressions
+ # like "self", "nil" or constants ourself which is hard.
+ remote = eval %{
+ result = lambda { |block, *args| #{method}(*args, &block) }
+ def result.call_with_block(*args, &block)
+ call(block, *args)
+ end
+ result
+ }
+ remote.call_with_block(*args, &block)
+ end
+
+ return result
+ end
+ end
+
+ # Returns the source code surrounding the location where the
+ # breakpoint was issued.
+ def source_lines(context = 5, return_line_numbers = false)
+ lines = File.readlines(@__bp_file).map { |line| line.chomp }
+
+ break_line = @__bp_line
+ start_line = [break_line - context, 1].max
+ end_line = break_line + context
+
+ result = lines[(start_line - 1) .. (end_line - 1)]
+
+ if return_line_numbers then
+ return [start_line, break_line, result]
+ else
+ return result
+ end
+ end
+
+ # Lets an object that will forward method calls to the breakpoint
+ # client. This is useful for outputting longer things at the client
+ # and so on. You can for example do these things:
+ #
+ # client.puts "Hello" # outputs "Hello" at client console
+ # # outputs "Hello" into the file temp.txt at the client
+ # client.File.open("temp.txt", "w") { |f| f.puts "Hello" }
+ def client()
+ if Breakpoint.use_drb? then
+ sleep(0.5) until Breakpoint.drb_service.eval_handler
+ Client.new(Breakpoint.drb_service.eval_handler)
+ else
+ Client.new(lambda { |code| eval(code, TOPLEVEL_BINDING) })
+ end
+ end
+ end
+
+ def handle_breakpoint(context, message, file = "", line = "", &block) # :nodoc:
+ catch(:debug_return) do |value|
+ eval(%{
+ @__bp_file = #{file.inspect}
+ @__bp_line = #{line}
+ extend Breakpoint::CommandBundle
+ extend DRbUndumped if self
+ }, context) rescue nil
+
+ if not use_drb? then
+ puts message
+ IRB.start(nil, IRB::WorkSpace.new(context))
+ else
+ @drb_service.add_breakpoint(context, message)
+ end
+
+ block.call if block
+ end
+ end
+
+ # These exceptions will be raised on failed asserts
+ # if Breakpoint.asserts_cause_exceptions is set to
+ # true.
+ class FailedAssertError < RuntimeError
+ end
+
+ # This asserts that the block evaluates to true.
+ # If it doesn't evaluate to true a breakpoint will
+ # automatically be created at that execution point.
+ #
+ # You can disable assert checking in production
+ # code by setting Breakpoint.optimize_asserts to
+ # true. (It will still be enabled when Ruby is run
+ # via the -d argument.)
+ #
+ # Example:
+ # person_name = "Foobar"
+ # assert { not person_name.nil? }
+ #
+ # Note: If you want to use this method from an
+ # unit test, you will have to call it by its full
+ # name, Breakpoint.assert.
+ def assert(context = nil, &condition)
+ return if Breakpoint.optimize_asserts and not $DEBUG
+ return if yield
+
+ callstack = caller
+ callstack.slice!(0, 3) if callstack.first["assert"]
+ file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures
+
+ message = "Assert failed at #{file}:#{line}#{" in `#{method}'" if method}."
+
+ if Breakpoint.asserts_cause_exceptions and not $DEBUG then
+ raise(Breakpoint::FailedAssertError, message)
+ end
+
+ message += " Executing implicit breakpoint."
+
+ if context then
+ return handle_breakpoint(context, message, file, line)
+ end
+
+ Binding.of_caller do |context|
+ handle_breakpoint(context, message, file, line)
+ end
+ end
+
+ # Whether asserts should be ignored if not in debug mode.
+ # Debug mode can be enabled by running ruby with the -d
+ # switch or by setting $DEBUG to true.
+ attr_accessor :optimize_asserts
+ self.optimize_asserts = false
+
+ # Whether an Exception should be raised on failed asserts
+ # in non-$DEBUG code or not. By default this is disabled.
+ attr_accessor :asserts_cause_exceptions
+ self.asserts_cause_exceptions = false
+ @use_drb = false
+
+ attr_reader :drb_service # :nodoc:
+
+ class DRbService # :nodoc:
+ include DRbUndumped
+
+ def initialize
+ @handler = @eval_handler = @collision_handler = nil
+
+ IRB.instance_eval { @CONF[:RC] = true }
+ IRB.run_config
+ end
+
+ def collision
+ sleep(0.5) until @collision_handler
+
+ @collision_handler.untaint
+
+ @collision_handler.call
+ end
+
+ def ping() end
+
+ def add_breakpoint(context, message)
+ workspace = IRB::WorkSpace.new(context)
+ workspace.extend(DRbUndumped)
+
+ sleep(0.5) until @handler
+
+ @handler.untaint
+ @handler.call(workspace, message)
+ rescue Errno::ECONNREFUSED, DRb::DRbConnError
+ raise if Breakpoint.use_drb?
+ end
+
+ attr_accessor :handler, :eval_handler, :collision_handler
+ end
+
+ # Will run Breakpoint in DRb mode. This will spawn a server
+ # that can be attached to via the breakpoint-client command
+ # whenever a breakpoint is executed. This is useful when you
+ # are debugging CGI applications or other applications where
+ # you can't access debug sessions via the standard input and
+ # output of your application.
+ #
+ # You can specify an URI where the DRb server will run at.
+ # This way you can specify the port the server runs on. The
+ # default URI is druby://localhost:42531.
+ #
+ # Please note that breakpoints will be skipped silently in
+ # case the DRb server can not spawned. (This can happen if
+ # the port is already used by another instance of your
+ # application on CGI or another application.)
+ #
+ # Also note that by default this will only allow access
+ # from localhost. You can however specify a list of
+ # allowed hosts or nil (to allow access from everywhere).
+ # But that will still not protect you from somebody
+ # reading the data as it goes through the net.
+ #
+ # A good approach for getting security and remote access
+ # is setting up an SSH tunnel between the DRb service
+ # and the client. This is usually done like this:
+ #
+ # $ ssh -L20000:127.0.0.1:20000 -R10000:127.0.0.1:10000 example.com
+ # (This will connect port 20000 at the client side to port
+ # 20000 at the server side, and port 10000 at the server
+ # side to port 10000 at the client side.)
+ #
+ # After that do this on the server side: (the code being debugged)
+ # Breakpoint.activate_drb("druby://127.0.0.1:20000", "localhost")
+ #
+ # And at the client side:
+ # ruby breakpoint_client.rb -c druby://127.0.0.1:10000 -s druby://127.0.0.1:20000
+ #
+ # Running through such a SSH proxy will also let you use
+ # breakpoint.rb in case you are behind a firewall.
+ #
+ # Detailed information about running DRb through firewalls is
+ # available at http://www.rubygarden.org/ruby?DrbTutorial
+ #
+ # == Security considerations
+ # Usually you will be fine when using the default druby:// URI and the default
+ # access control list. However, if you are sitting on a machine where there are
+ # local users that you likely can not trust (this is the case for example on
+ # most web hosts which have multiple users sitting on the same physical machine)
+ # you will be better off by doing client/server communication through a unix
+ # socket. This can be accomplished by calling with a drbunix:/ style URI, e.g.
+ # Breakpoint.activate_drb('drbunix:/tmp/breakpoint_server')
. This
+ # will only work on Unix based platforms.
+ def activate_drb(uri = nil, allowed_hosts = ['localhost', '127.0.0.1', '::1'],
+ ignore_collisions = false)
+
+ return false if @use_drb
+
+ uri ||= 'druby://localhost:42531'
+
+ if allowed_hosts then
+ acl = ["deny", "all"]
+
+ Array(allowed_hosts).each do |host|
+ acl += ["allow", host]
+ end
+
+ DRb.install_acl(ACL.new(acl))
+ end
+
+ @use_drb = true
+ @drb_service = DRbService.new
+ did_collision = false
+ begin
+ @service = DRb.start_service(uri, @drb_service)
+ rescue Errno::EADDRINUSE
+ if ignore_collisions then
+ nil
+ else
+ # The port is already occupied by another
+ # Breakpoint service. We will try to tell
+ # the old service that we want its port.
+ # It will then forward that request to the
+ # user and retry.
+ unless did_collision then
+ DRbObject.new(nil, uri).collision
+ did_collision = true
+ end
+ sleep(10)
+ retry
+ end
+ end
+
+ return true
+ end
+
+ # Deactivates a running Breakpoint service.
+ def deactivate_drb
+ Thread.exclusive do
+ @service.stop_service unless @service.nil?
+ @service = nil
+ @use_drb = false
+ @drb_service = nil
+ end
+ end
+
+ # Returns true when Breakpoints are used over DRb.
+ # Breakpoint.activate_drb causes this to be true.
+ def use_drb?
+ @use_drb == true
+ end
+end
+
+module IRB # :nodoc:
+ class << self; remove_method :start; end
+ def self.start(ap_path = nil, main_context = nil, workspace = nil)
+ $0 = File::basename(ap_path, ".rb") if ap_path
+
+ # suppress some warnings about redefined constants
+ old_verbose, $VERBOSE = $VERBOSE, nil
+ IRB.setup(ap_path)
+ $VERBOSE = old_verbose
+
+ if @CONF[:SCRIPT] then
+ irb = Irb.new(main_context, @CONF[:SCRIPT])
+ else
+ irb = Irb.new(main_context)
+ end
+
+ if workspace then
+ irb.context.workspace = workspace
+ end
+
+ @CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC]
+ @CONF[:MAIN_CONTEXT] = irb.context
+
+ old_sigint = trap("SIGINT") do
+ begin
+ irb.signal_handle
+ rescue RubyLex::TerminateLineInput
+ # ignored
+ end
+ end
+
+ catch(:IRB_EXIT) do
+ irb.eval_input
+ end
+ ensure
+ trap("SIGINT", old_sigint)
+ end
+
+ class << self
+ alias :old_CurrentContext :CurrentContext
+ remove_method :CurrentContext
+ remove_method :parse_opts
+ end
+
+ def IRB.CurrentContext
+ if old_CurrentContext.nil? and Breakpoint.use_drb? then
+ result = Object.new
+ def result.last_value; end
+ return result
+ else
+ old_CurrentContext
+ end
+ end
+ def IRB.parse_opts() end
+
+ class Context # :nodoc:
+ alias :old_evaluate :evaluate
+ def evaluate(line, line_no)
+ if line.chomp == "exit" then
+ exit
+ else
+ old_evaluate(line, line_no)
+ end
+ end
+ end
+
+ class WorkSpace # :nodoc:
+ alias :old_evaluate :evaluate
+
+ def evaluate(*args)
+ if Breakpoint.use_drb? then
+ result = old_evaluate(*args)
+ if args[0] != :no_proxy and
+ not [true, false, nil].include?(result)
+ then
+ result.extend(DRbUndumped) rescue nil
+ end
+ return result
+ else
+ old_evaluate(*args)
+ end
+ end
+ end
+
+ module InputCompletor # :nodoc:
+ def self.eval(code, context, *more)
+ # Big hack, this assumes that InputCompletor
+ # will only call eval() when it wants code
+ # to be executed in the IRB context.
+ IRB.conf[:MAIN_CONTEXT].workspace.evaluate(:no_proxy, code, *more)
+ end
+ end
+end
+
+module DRb # :nodoc:
+ class DRbObject # :nodoc:
+ undef :inspect if method_defined?(:inspect)
+ undef :clone if method_defined?(:clone)
+ end
+end
+
+# See Breakpoint.breakpoint
+def breakpoint(id = nil, &block)
+ Binding.of_caller do |context|
+ Breakpoint.breakpoint(id, context, &block)
+ end
+end
+
+# See Breakpoint.assert
+def assert(&block)
+ Binding.of_caller do |context|
+ Breakpoint.assert(context, &block)
+ end
+end
diff --git a/vendor/plugins/liquid/test/extra/caller.rb b/vendor/plugins/liquid/test/extra/caller.rb
new file mode 100755
index 0000000..14c96eb
--- /dev/null
+++ b/vendor/plugins/liquid/test/extra/caller.rb
@@ -0,0 +1,80 @@
+class Continuation # :nodoc:
+ def self.create(*args, &block) # :nodoc:
+ cc = nil; result = callcc {|c| cc = c; block.call(cc) if block and args.empty?}
+ result ||= args
+ return *[cc, *result]
+ end
+end
+
+class Binding; end # for RDoc
+# This method returns the binding of the method that called your
+# method. It will raise an Exception when you're not inside a method.
+#
+# It's used like this:
+# def inc_counter(amount = 1)
+# Binding.of_caller do |binding|
+# # Create a lambda that will increase the variable 'counter'
+# # in the caller of this method when called.
+# inc = eval("lambda { |arg| counter += arg }", binding)
+# # We can refer to amount from inside this block safely.
+# inc.call(amount)
+# end
+# # No other statements can go here. Put them inside the block.
+# end
+# counter = 0
+# 2.times { inc_counter }
+# counter # => 2
+#
+# Binding.of_caller must be the last statement in the method.
+# This means that you will have to put everything you want to
+# do after the call to Binding.of_caller into the block of it.
+# This should be no problem however, because Ruby has closures.
+# If you don't do this an Exception will be raised. Because of
+# the way that Binding.of_caller is implemented it has to be
+# done this way.
+def Binding.of_caller(&block)
+ old_critical = Thread.critical
+ Thread.critical = true
+ count = 0
+ cc, result, error, extra_data = Continuation.create(nil, nil)
+ error.call if error
+
+ tracer = lambda do |*args|
+ type, context, extra_data = args[0], args[4], args
+ if type == "return"
+ count += 1
+ # First this method and then calling one will return --
+ # the trace event of the second event gets the context
+ # of the method which called the method that called this
+ # method.
+ if count == 2
+ # It would be nice if we could restore the trace_func
+ # that was set before we swapped in our own one, but
+ # this is impossible without overloading set_trace_func
+ # in current Ruby.
+ set_trace_func(nil)
+ cc.call(eval("binding", context), nil, extra_data)
+ end
+ elsif type == "line" then
+ nil
+ elsif type == "c-return" and extra_data[3] == :set_trace_func then
+ nil
+ else
+ set_trace_func(nil)
+ error_msg = "Binding.of_caller used in non-method context or " +
+ "trailing statements of method using it aren't in the block."
+ cc.call(nil, lambda { raise(ArgumentError, error_msg) }, nil)
+ end
+ end
+
+ unless result
+ set_trace_func(tracer)
+ return nil
+ else
+ Thread.critical = old_critical
+ case block.arity
+ when 1 then yield(result)
+ else yield(result, extra_data)
+ end
+ end
+end
diff --git a/vendor/plugins/liquid/test/file_system_test.rb b/vendor/plugins/liquid/test/file_system_test.rb
new file mode 100644
index 0000000..d3ab948
--- /dev/null
+++ b/vendor/plugins/liquid/test/file_system_test.rb
@@ -0,0 +1,30 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/helper'
+
+class FileSystemTest < Test::Unit::TestCase
+ include Liquid
+
+ def test_default
+ assert_raise(FileSystemError) do
+ BlankFileSystem.new.read_template_file("dummy")
+ end
+ end
+
+ def test_local
+ file_system = Liquid::LocalFileSystem.new("/some/path")
+ assert_equal "/some/path/_mypartial.liquid" , file_system.full_path("mypartial")
+ assert_equal "/some/path/dir/_mypartial.liquid", file_system.full_path("dir/mypartial")
+
+ assert_raise(FileSystemError) do
+ file_system.full_path("../dir/mypartial")
+ end
+
+ assert_raise(FileSystemError) do
+ file_system.full_path("/dir/../../dir/mypartial")
+ end
+
+ assert_raise(FileSystemError) do
+ file_system.full_path("/etc/passwd")
+ end
+ end
+end
\ No newline at end of file
diff --git a/vendor/plugins/liquid/test/filter_test.rb b/vendor/plugins/liquid/test/filter_test.rb
new file mode 100644
index 0000000..6cc5221
--- /dev/null
+++ b/vendor/plugins/liquid/test/filter_test.rb
@@ -0,0 +1,98 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/helper'
+
+
+module MoneyFilter
+ def money(input)
+ sprintf(' %d$ ', input)
+ end
+
+ def money_with_underscore(input)
+ sprintf(' %d$ ', input)
+ end
+end
+
+module CanadianMoneyFilter
+ def money(input)
+ sprintf(' %d$ CAD ', input)
+ end
+end
+
+
+class FiltersTest < Test::Unit::TestCase
+ include Liquid
+
+ def setup
+ @context = Context.new
+ end
+
+ def test_local_filter
+ @context['var'] = 1000
+ @context.add_filters(MoneyFilter)
+ assert_equal ' 1000$ ', Variable.new("var | money").render(@context)
+ end
+
+ def test_underscore_in_filter_name
+ @context['var'] = 1000
+ @context.add_filters(MoneyFilter)
+ assert_equal ' 1000$ ', Variable.new("var | money_with_underscore").render(@context)
+ end
+
+ def test_second_filter_overwrites_first
+ @context['var'] = 1000
+ @context.add_filters(MoneyFilter)
+ @context.add_filters(CanadianMoneyFilter)
+ assert_equal ' 1000$ CAD ', Variable.new("var | money").render(@context)
+ end
+
+ def test_size
+ @context['var'] = 'abcd'
+ @context.add_filters(MoneyFilter)
+ assert_equal 4, Variable.new("var | size").render(@context)
+ end
+
+ def test_join
+ @context['var'] = [1,2,3,4]
+ assert_equal "1 2 3 4", Variable.new("var | join").render(@context)
+ end
+
+ def test_sort
+ @context['value'] = 3
+ @context['numbers'] = [2,1,4,3]
+ @context['words'] = ['expected', 'as', 'alphabetic']
+ @context['arrays'] = [['flattened'], ['are']]
+ assert_equal [1,2,3,4], Variable.new("numbers | sort").render(@context)
+ assert_equal ['alphabetic', 'as', 'expected'],
+ Variable.new("words | sort").render(@context)
+ assert_equal [3], Variable.new("value | sort").render(@context)
+ assert_equal ['are', 'flattened'], Variable.new("arrays | sort").render(@context)
+ end
+
+ def test_strip_html
+ @context['var'] = "bla blub
<%= link_to pluralize(entry.comments_count, 'comment'), + entry_path(:user_id => entry.user, :id => entry) -%>
+