From 22a6661b514257568bf0fbfac84ab2ec90addb1c Mon Sep 17 00:00:00 2001 From: Paul Mackay Date: Thu, 5 May 2016 14:21:16 +0100 Subject: [PATCH 001/110] Improved layout and image for 404 and 500 error pages --- public/404.html | 43 +++++++++++++++++++++++------------------- public/500.html | 49 ++++++++++++++++++++++++------------------------ public/500.jpg | Bin 40506 -> 37388 bytes 3 files changed, 48 insertions(+), 44 deletions(-) diff --git a/public/404.html b/public/404.html index b650949066..85150ae6c6 100644 --- a/public/404.html +++ b/public/404.html @@ -3,35 +3,40 @@ The page you were looking for doesn't exist (404) - +
- -

It seems the page you're looking for is in a grump. -

Return home

-

+ +

Oops! Page not found.

+ Please try again +

This might be a temporary problem. Please click the back button to return to the previous screen or go back to Home and try again.

+ Contact support +

If the problem persists or is urgent, please tell us about it. Find our contact details from the global Open Food Network Local page.

+

It really helps us if you can give as much detail as possible about what the missing page is about.

diff --git a/public/500.html b/public/500.html index 5c7fa05f07..29bf60dda5 100644 --- a/public/500.html +++ b/public/500.html @@ -3,24 +3,26 @@ We're sorry, but something went wrong (500) @@ -29,17 +31,14 @@
-

We're sorry, but something went wrong. -
Try refreshing the page, or -

Return home

-
Want to let us know what went wrong? Email us at: -

- - hello [at] openfoodnetwork.org -

-

+

We're sorry, but something went wrong.

+ Please try again +

This might be a temporary problem. Please click the back button to return to the previous screen or go back to Home and try again.

+ We're on it +

If you have seen this problem before, we probably already know about it and are working on a fix. We record all the errors that come up.

+ Contact support +

If the problem persists or is urgent, please tell us about it. Find our contact details from the global Open Food Network Local page.

+

It really helps us if you can give as much detail as possible about what you were doing when this error occurred.

diff --git a/public/500.jpg b/public/500.jpg index 2cfabee4233803e3c852d31c989e070874aa3ef0..f1c75efe6c6b3edbda43804019d0894ae17df261 100644 GIT binary patch literal 37388 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 literal 40506 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⁡Ch?AsS-TWpRTGi zjhBNU)L3^~SU^LJzZg4G0Y$Wvt$F%4-qglbk@bOROOE&hay2k-i|O!@Gm!f;zF$J; zS(R@3bNps43XCT|-whOi`=g!t=r?0i;yEha)Z~g>ain11bM#s;oGIvLYDO zZxZVAu_Vq&+P5_m6u><2y8pJ#<-ijt+GW`s#|-Ylp-?i^I!1M`2dZkxQOyoD!9tQZ zbMX|L#K?OQIGMWR_+o$FbSWXl7QM20&zNLVrN(qu-832+963S!mtU zT~+u!JI2Q}P8$AG%=d~kELB-;XEJ@wO!a7^3-7Yj%?e0bL>z;bd7t<$3GyA2(6fL0?+7i!QfjijQ5meqO3oc;l- zXW|jdSSOT#1Ot+YSHQ3vD&7Lf$-HNn@6S&UnnAHa>d)heoj8QTfe_gs4i*ZG(Qr$gq~a$-6nQQ+QDjgKz`3|VyvQ}31_rGP^5HVliMaU7aBCUP6l%s+spq!s-(qKmEccQSJZ zi>Nzura(D+L3Z_>Ce;!M?YR5a#)78Bl8!xjL_=5{@TU>LZPDSWq>Ks}efu`@(kd1# zT{P{dC!BUjYLZOyo7JFq>~ds!Od)C4QvX(1*oG4lk2RMXlp8>Boa8Ims0vTY=iN!f z$3s8O#Dy?EhEZ*f@pszAkx_~_y24UWt=IW_M0gfe7Q&Jw9OVzG_!(h9V5Jr4^ur$d zsMQEsVoZ9&pD;FnuYfpG#q=FcN!YV_%ju`u6}TZG9rlfs#W93_ID$ph007U5l+A^) z1VECSzUXA3ZbUCbV2MI2t0zpF)Mbdtt<>qS1JuMc3X!fK#yysAS0QYXbyn$JzYB8M zfL=PfTN$tF7wEU4%{MC|lFlbkCIErDT-%l|ds!?oHa-SGMk@W o{meI4f;@d?xq z@(7UNAX6OfjrT}*jClD4*^L1!^jR#~>Ic7^(llm(wSEJ&2;+w{h+H6)ey=o7G<&z{ znweUUj%~n7bSCh4v+?=^Tfay?bkPQcO8v<%$|7eXJcKRg@^_4MM6tsRz&5R0OsGtnA?o>eOhPl~f-eV2 z84!)2(>?M4jgqsN*9ihNKaQ|8o701O5rV#)@LmJG3S?%Y3o*p>Fp7G6xGFso!lN{J zc&i+`uAb6otbX%Vr%wpTMx;6N#WbZj0G+TTIw8MsG_Jt~cC(i|sjZ5)56+Asnq**M zAcj?qA5qOsdy9fL5zayupR5j#AWOXfy~alCo+ubgRf0&O>figd>=;^O^VS^Al{G!+ zh>p5YH6l#*cvz3Ch$Fd*JeNkhz|WZ~kXF+a@BnmKyY!cWmjMv4m7i$f0OHjF5<;GymAF6RB#iLd*b9p~8U z!tCuMX(9vfwC1Lr;wt*9W$hAwuDyk2D852h!9}9FP4RqSiv=gYUm+|*l<$eeLsGsa z=AMuC&S;jkn!%L}lUHK3dTY^%kG2*C;N>qF*JpMX?;Ii`d9qzp;p!ER#`$6{VS{=x zRMtcOQ9|Nj|EH<*j7vg)-#;KC;E1K+3;`9l;>cA2qN3u~a@D~-a<|Mng@}eq3b;)R zx0ZWlW`!e@w9+zjl$lyuwoi4eb2{I@uh;L%|KaEP=YDWs*ZsaOB21rHlLMlsDvp4` z!aX;!=B$fMcn!Gse}D?3xSkg0lJ(iDsLp9ik`w-J^})|(faYL~p?9K97CV+A+VMfn z<=&$d7<8&i(ltGWQ#Rtbi4uL=lZ#&FS+yXpP_wXo$FOkrYizicatifd)>=9rl#3In z9#jd_ei7N-j~wZ?GdU2gL^P)P6cM5)tZt3FDbOf}Xzh5~fmUr*%f)onx|gpwsX|@B z6Sc&Tp>rTl;=2qjSVw1N%}&L%hmKW%(v-0`Kn#{CM%oSKTZ7gEI(2k{Lm%1TXrNYw z-D1nK6RO7GdRBvwKs2Lge8LIVgMql;KlYfwR`c~E5zLo8v#WB9$tWRsGwI4G4~zvW zLgi(rFWj!z#ya_!EDyQl5TQ70#f58&`xjK%j>k(Ik3ZRw`6h?x^)BH*Tc;WK=hdMp zk3-wPY)ZTW@ z@blJpWJcyUB8sn4kh`N!VE|FJ>U$E>-6TSXf&*f@8lHw0*HGDwp@Xq1$hPFfRYu`K0$%ZlmYA7z(Ve5_Fa8*%!p>U&(CB+ zB!XJvo`{rr&*~q;Eg~nsXl}RW$)SlzAkdJzXZ}p=h<2WDB+zs*6{Jj46{u0|FWXk> zx<9o-bqkVMKN~Q>FboYrJ^QSB9ci3XB!10^^LGBcu%p&8+41Yaccu&i1z=cJ`GMUE z_zFx}ZRErYj|x3Uf^%q)X?U*%8R>z^`CI}T_!w(E+)5ESRV$~XRhGz9J-Ys{{4loOE*$__p=_-tprU2n(wwnSl>&SRigI{wqof4ok{oCyM?k!%#Z z99G}@8v+K>5%`N4?QVi~G$|8cGhoYJWTP06?uuB0Sa_s#9P@eYiZM&=Y7)OE6tHhG@BXYSJaZHH2 z&x4mXx1=D0VG+T0FKoZ$F5{IQmkH$D>C=O^7!}`-b^qyXXQJ2uL<453BV^l=B8@)k zrdH)c)hx1_2t|coLaS*hQ2l`JHjCSyT}234G6Ul?`}fuHlxr)Hbs_`YWAtRQ*pf@6 z5#zK?yZ>EJl9i!62KPwbFMF9^bSs`BlZ51+A5KW^PD(dZ;Kxig3lentD|EU!!T+)n z<}N<<(;u$!vwG7hzwgx@ca(YX?;>{Lr5qW`W_@>)dgtf#SMkCC@Z-9eo^#3pmjf zN>_k~QO$U9^GW}tQK;FRr*z31y;R`+oSmzk?l!ijbbW-(7?_ zT%`l3e9T~bb6VH`iFhpR6tnON^niOs>OgWLBud}#k!Dm7ch)_)=FgNjOj>~Uq!^Ai zyq(h0{n_0Rrm3Vc=ImL?E#WoDV`jG_d|HoFSZN-6;Da|xkLZPuvUG35Cp!^Yg3los zsePW*OxG8P(9NcoE$pD_aGnkc#5+vtR|NE|aG;ncA62YvO;pC;=*r{}S! z!!{R|A$^8GMs}p;9?<8r<*NF)e*1{R*s%&I6A?4fbo;>jPyf;v$RI_4HCxM^Rc|@X z?O`=sc_~Yg&GJ>aCDpqo0jE&dVcn=zSjB-0XM=__AHy2$(N0C{WUL{*oQHfb8RL2DSWscO*4M z=MwHOx9=M3l4#D8b>saPSGs72!AuzxAbm@(HNOsJREAz&nwW#9s?@)hzuQo`NW_ z-vz00{l1fC(Ai(Fyl**)a7IFxM|R3+Fg{Eh;E_lUp9sV-ddx!7rf!#S`sPE$?JHeB z?C#D_6ScJ_-kC1RXsR3$gY-`99XGGTO6JWI`45p6Z6MC2@9638RbMe0!2Y<_JmGc{iroof#A1>o4kbYne|> zlTc+D7=w*-w1O_hT)>f-^6!frNO7DmKsWrkPo}JZNJ?o|Ta6jF@eAm0iGy|+-a4uP z(^+9JhUk4LhGkA5N_5zZa_?V%X1V&yFIna~Z1YczK*4NpORtW+@sR{jcA7Y3?8@y} zbu)l`u7&AGzO2PT9$O|Ewrdh(G{Ib+(2V;beT<;t)aVRP2fKk$kn=;>bmV-++qDY* zz$Q_-7_gz1>*^lqR`2-iI&&Xgk1@UQ(`m2pBLXZ!?eubVypnb87h~yLoK-^wl}2D# z7rOXl^Nk>VCacq*Oylj{Un#9J66-{kdYFECcP}NA;;5~scHzwz)kvZdXB8G+CViX3 zK?tMimPKzLy62eFsgMFjWqX=xMu4`RQ%GT%PvNGs!z=I73$CC$-=`uRad|rOk0oAf z#%GJt*4-VQx`M!V060V6+RwF|sjtd>q%Vxg+`oM_K$cR~TyTe)g0z)mHstPjfddcE z<%txpJ3LmXQom6oh%ZwAHZbWRmV;t!-H(miAQ^}vz)R4czvWRRjjF5C#PGcw_Mr;I zg>0i+U&eDYC7l$~8C=1aBtru3@knuP3{PqTi~zx?PtunabfL87$`sKhpZ^+^>n6%jLte z@Vr|$G~|F0j?tM~{Xk~Mz&HDAW={y24qa+;r} zjrWz95!t!9qS*0^{$M$}I%F&TKhFG<^xy)W-t|O3Pv_I6zRHa^R*F(wce`|fwr-PZ6Gx8Cu5O?(?Abvii z3`}IIMpB_Sj{K9XLDP7SlLfLh@6Zy|j}FQoW_y)Yv9om>ejvt{0=};3IJ3}%sec5m zj)yGsf_SG+ZKtZKNJ*d$weWLI8g;I2F>yqR%jgD>$G{&3mec*7T|?xh1XD!#zMr48 zG%kuG6Y8JYWP4{Gv+kVsyeFpwGoQl8LoVfg5tci_g*50`)s&4DRRzW>!76&~vMTD0 ziD(82EOrsTA9v|5t{|wnSCSLvabY(X z3T3qe!D@_XM*@sylH0dHGx!UEa-3Nr7>8om$=j7~T+>G=XP)@y(3;^~ubziC0` z0pEi<;v}=(8dPCTf6wOG6?V(u&ivpfQ6l5T zswE-sM)$4#_YN%&9=`qrO9CjR8l6=LO(?-z?;O`(8+|qsKBv(FoO7~_s?B5QzdpT8 zY@Uu z81ZQ@8dR8$xf^QlD;1FB;KvuHSbMHD$IW=Io-sRIH9pEG&DYQVU?hOda@W7?`*rae zqKWl5aQwf6E}o>#;l`PI+l=MPFXHeScf%wZq|#D;DRqCEu81B}gKq;8A6c=-pw0}x zOhBO`rW72h=%N(2#U{ka35*!dG0y`n+48!hvg8cKB=SIQ?IEn;ZRjTuUyh}Mup~DO z97IeR@WQM$Jr=Txci&&^y1!KTk1hoW&OLP}W5^D1v(W=8V1#XG%_Oi}8wiZ*6(prr zC;k$ikp3r5&~I`rv1l`5|CL6X?W~WRbJyh;cfOE+Bz?YZA_dIuQb+`*cRt^fvP?iZ zPsle=`Gc)kI;+6-P5?4Sr2pb_=Ika9f~-SYP$8)P`#DO!jJ(#ZW+~C_gU1m@M(bD9 zQ^oxn4yqETeEB+un-4&3(NvnhAD5+u{>GaZ_QYs1WNxgeA;s_W#R-IslyL9i3ZAo10h5Y`Z zwbIQ6_ay$c{CiIaavAGDV2!%Dy02sUZ%{N;wQ}?h3=XYW^qjsif+q4PZMAUd(-1y< ziUS#?Gss!3xUdAMwwWg7S?02Rn*+$7Qm-HNW5uHf;bq!-@W4l2*98)MG+1_nPxd9j znMZ_}^G8EOU#WZB;_qp1Nt}_idh9D3K3z!x|4NC|uixQ+Rn8*^M(w!OJEYd4pVVz<>qOsZnRHv-7;-T);Ise3*Ub38oqQJ zp-@Zt;@$%a2RpAe_;n&u<*L{p}0&EOe{2Y8{3|wL-0D8 zCybN5RIS9p;$aj139gnhO)eCQqQ>+Nmy9TK&Z>7&eAhBvKW>D8vH;asSC!YJi^u;UOEvVP)HRQReXfpGQda1IaROxU;RkAE zDNO$RX&xUmL#;2BLU<7m@o{ccYXBXSq;!g{De~;%y2t?5vg0`}grq9MN_1 z+I);NosX&o;l+}PTP_Wh9@pD8)k!K4Jzfd6*ny(~HjpPmy zSQyH)a^~BDvkIU)H^#o)2}H%Z>5mDT%F{4)jP|Vk;PBfz^6!!;joUq!D0~ZVicjCb zUx!3ICm59MeWdFA5_9l!Rhn4iS)NcW_qN_yNTvt-@@Z4(<6h*QU@gy-4;?v4C-qGt zwB0PBMIh4OmSIY+>2_~E+Dey*f?-zP9Kos8Qf%5x!@E4qUlOh*YL3jL>5Mz>MV-TS z5!v5+05hqpyl>JuGe)_L-;(1~td8lmS5D;@K4&+V??RKfVBDfVVJ>9I(H>W_3wn4Q z`d}c=oA0UZm9Y>X^NxfFul+WYD}`3tXvfz7sy@o! z8=mpLec_`D%g3jne)8P*SlPu4^KOjCgL3(Zt3NbD_Tvp4aVCSAGNwsH23K#m7i^S-G^Fwq-X5+E=Rw$?AG~dcQpA$!K)``Fsv;3LMn3+F1V*}coK{_^(I?i`>g?;q_KYZpPYuG+3wTdtGH z2yJ)^&q z8yR`jK3v+b&=G4aeULyW-^r-1`1s(s?Nykbq8`xXR`x-4XbSp~9&D9 zd{^cVoqbh%8L9M`vOJYK3C@l#(3UI2Hq@xS@<|UWJCR@=-|{9JSZWf@Zwl0k)3bJs z#;cFg*H_V+7+}i5ESER$53$kW{g=H6O|OME z5z3Fprbx3)l~<$6w{U_gVb!~`QcdS^k{~bq3`uV^lURBvj+kQZo$>rN+HeGYYM%N%#|(Wz*q?R3nPN;^0GX71UT`%4SN%7tKNRb4>j;d7=!-$L)-ERug)yRVd)Nve^^<}EUoa+NNIzFKl zrgQd{yM|sl8>FL%o_4fIizlDPkFpJtw2RcP&iPdwMa>a)oqqlU4xLsg@>$K8`CGZ3 ziUNG*tesuM5p&s~`s)X*Qv;V>Ju1-&yt3+ zgcY_+V!VEGiy7p{Ip*LuQkW?%QH zyb4f>rSpxknxgMy;sOS6k;q*y5Y{}PMQY>2NnDle@I4g8jp-V<_fEP+9cp|da(ZmP isXq2_d8y%(%R7N}|HpQM{*Uea|6u7I(sSpv@c#fD>I9_# From 6dd989484ab40af3434a142b87ae9a6fe6ff88bf Mon Sep 17 00:00:00 2001 From: Paul Mackay Date: Tue, 10 May 2016 05:32:40 +0100 Subject: [PATCH 002/110] Switch to using the Spree GA tracker code --- .../spree/layouts/admin/add_analytics.html.haml.deface | 2 +- app/views/layouts/darkswarm.html.haml | 2 +- app/views/shared/_analytics.html.haml | 9 --------- 3 files changed, 2 insertions(+), 11 deletions(-) delete mode 100644 app/views/shared/_analytics.html.haml diff --git a/app/overrides/spree/layouts/admin/add_analytics.html.haml.deface b/app/overrides/spree/layouts/admin/add_analytics.html.haml.deface index 548439b60f..ced2d1b7b6 100644 --- a/app/overrides/spree/layouts/admin/add_analytics.html.haml.deface +++ b/app/overrides/spree/layouts/admin/add_analytics.html.haml.deface @@ -1,3 +1,3 @@ / insert_bottom "[data-hook='admin_footer_scripts']" -= render 'shared/analytics' += render 'spree/shared/google_analytics' diff --git a/app/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml index c758e4836e..d3f0a78087 100644 --- a/app/views/layouts/darkswarm.html.haml +++ b/app/views/layouts/darkswarm.html.haml @@ -43,4 +43,4 @@ #footer %loading - = render 'shared/analytics' + = render 'spree/shared/google_analytics' diff --git a/app/views/shared/_analytics.html.haml b/app/views/shared/_analytics.html.haml deleted file mode 100644 index 16ad08ff5f..0000000000 --- a/app/views/shared/_analytics.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- if Rails.env.production? - :javascript - (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ - (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), - m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) - })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); - - ga('create', 'UA-62912229-1', 'auto'); - ga('send', 'pageview'); From 041d9814a7337d9fe448a9e1c21fa39446ffdd3d Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 18 May 2016 15:54:10 +1000 Subject: [PATCH 003/110] Remove redundant CSS and jpg --- public/404.html | 3 +-- public/404.jpg | Bin 36526 -> 0 bytes public/500.html | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 public/404.jpg diff --git a/public/404.html b/public/404.html index 85150ae6c6..4e42e63b73 100644 --- a/public/404.html +++ b/public/404.html @@ -11,7 +11,6 @@ } div.dialog { width: 600px; - margin: 2em auto 0 auto; margin: auto; } @media only screen @@ -28,7 +27,7 @@ - +

Oops! Page not found.

diff --git a/public/404.jpg b/public/404.jpg deleted file mode 100644 index 836b4e01886e5fad7ca0a8520de9e78e9a858334..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36526 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 diff --git a/public/500.html b/public/500.html index 29bf60dda5..b3dd25194a 100644 --- a/public/500.html +++ b/public/500.html @@ -11,7 +11,6 @@ } div.dialog { width: 600px; - margin: 2em auto 0 auto; margin: auto; } @media only screen From aa04da48f3b8953b47447ced053488517e9ba3b6 Mon Sep 17 00:00:00 2001 From: elf Pavlik Date: Sun, 22 May 2016 18:40:42 -0500 Subject: [PATCH 004/110] fixed i18n on admin edit enterprise --- .../side_menu_controller.js.coffee | 30 +++++----- app/views/admin/enterprises/_form.html.haml | 60 +++++++++---------- app/views/admin/shared/_side_menu.html.haml | 2 +- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee index a0105fefa4..913ff59d3e 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -5,21 +5,21 @@ angular.module("admin.enterprises") $scope.select = SideMenu.select $scope.menu.setItems [ - { name: t('primary_details'), icon_class: "icon-home" } - { name: t('users'), icon_class: "icon-user" } - { name: t('address'), icon_class: "icon-map-marker" } - { name: t('contact'), icon_class: "icon-phone" } - { name: t('social'), icon_class: "icon-twitter" } - { name: t('about'), icon_class: "icon-pencil" } - { name: t('business_details'), icon_class: "icon-briefcase" } - { name: t('images'), icon_class: "icon-picture" } - { name: t('properties'), icon_class: "icon-tags", show: "showProperties()" } - { name: t('shipping_methods'), icon_class: "icon-truck", show: "showShippingMethods()" } - { name: t('payment_methods'), icon_class: "icon-money", show: "showPaymentMethods()" } - { name: t('enterprise_fees'), icon_class: "icon-tasks", show: "showEnterpriseFees()" } - { name: t('inventory_settings'), icon_class: "icon-list-ol", show: "enterpriseIsShop()" } - { name: t('tag_rules'), icon_class: "icon-random", show: "enterpriseIsShop()" } - { name: t('shop_preferences'), icon_class: "icon-shopping-cart", show: "enterpriseIsShop()" } + { name: 'primary_details', label: t('primary_details'), icon_class: "icon-home" } + { name: 'users', label: t('users'), icon_class: "icon-user" } + { name: 'address', label: t('address'), icon_class: "icon-map-marker" } + { name: 'contact', label: t('contact'), icon_class: "icon-phone" } + { name: 'social', label: t('social'), icon_class: "icon-twitter" } + { name: 'about', label: t('about'), icon_class: "icon-pencil" } + { name: 'business_details', label: t('business_details'), icon_class: "icon-briefcase" } + { name: 'images', label: t('images'), icon_class: "icon-picture" } + { name: 'properties', label: t('properties'), icon_class: "icon-tags", show: "showProperties()" } + { name: 'shipping_methods', label: t('shipping_methods'), icon_class: "icon-truck", show: "showShippingMethods()" } + { name: 'payment_methods', label: t('payment_methods'), icon_class: "icon-money", show: "showPaymentMethods()" } + { name: 'enterprise_fees', label: t('enterprise_fees'), icon_class: "icon-tasks", show: "showEnterpriseFees()" } + { name: 'inventory_settings', label: t('inventory_settings'), icon_class: "icon-list-ol", show: "enterpriseIsShop()" } + { name: 'tag_rules', label: t('tag_rules'), icon_class: "icon-random", show: "enterpriseIsShop()" } + { name: 'shop_preferences', label: t('shop_preferences'), icon_class: "icon-shopping-cart", show: "enterpriseIsShop()" } ] $scope.select(0) diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index e363078733..ee3dfa2183 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -1,60 +1,60 @@ -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Primary Details'" } } - %legend Primary Details +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='primary_details'" } } + %legend {{menu.selected.label}} = render 'admin/enterprises/form/primary_details', f: f -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Users'" } } - %legend Users +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='users'" } } + %legend {{menu.selected.label}} = render 'admin/enterprises/form/users', f: f = f.fields_for :address do |af| - %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Address'" } } - %legend Address + %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='address'" } } + %legend {{menu.selected.label}} = render 'admin/enterprises/form/address', af: af -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Contact'" } } - %legend Contact +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='contact'" } } + %legend {{menu.selected.label}} = render 'admin/enterprises/form/contact', f: f -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Social'" } } - %legend Social +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='social'" } } + %legend {{menu.selected.label}} = render 'admin/enterprises/form/social', f: f -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Business Details'" } } - %legend Business Details +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='business_details'" } } + %legend {{menu.selected.label}} = render 'admin/enterprises/form/business_details', f: f -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='About'" } } - %legend About Us +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='about'" } } + %legend {{menu.selected.label}} = render 'admin/enterprises/form/about_us', f: f -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Images'" } } - %legend Images +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='images'" } } + %legend {{menu.selected.label}} = render 'admin/enterprises/form/images', f: f -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Properties'" } } - %legend Properties +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='properties'" } } + %legend {{menu.selected.label}} = render 'admin/enterprises/form/properties', f: f -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Shipping Methods'" } } - %legend Shipping Methods +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='shipping_methods'" } } + %legend {{menu.selected.label}} = render 'admin/enterprises/form/shipping_methods', f: f -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Payment Methods'" } } - %legend Payment Methods +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='payment_methods'" } } + %legend {{menu.selected.label}} = render 'admin/enterprises/form/payment_methods', f: f -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Enterprise Fees'" } } - %legend Enterprise Fees +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='enterprise_fees'" } } + %legend {{menu.selected.label}} = render 'admin/enterprises/form/enterprise_fees', f: f -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Inventory Settings'" } } - %legend Inventory Settings +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='inventory_settings'" } } + %legend {{menu.selected.label}} = render 'admin/enterprises/form/inventory_settings', f: f -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Shop Preferences'" } } - %legend Shop Preferences +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='shop_preferences'" } } + %legend {{menu.selected.label}} = render 'admin/enterprises/form/shop_preferences', f: f -%fieldset.alpha.no-border-bottom{ ng: { if: "menu.selected.name=='Tag Rules'" } } - %legend Tag Rules +%fieldset.alpha.no-border-bottom{ ng: { if: "menu.selected.name=='tag_rules'" } } + %legend {{menu.selected.label}} = render 'admin/enterprises/form/tag_rules', f: f diff --git a/app/views/admin/shared/_side_menu.html.haml b/app/views/admin/shared/_side_menu.html.haml index cf1d3d42f7..267a341991 100644 --- a/app/views/admin/shared/_side_menu.html.haml +++ b/app/views/admin/shared/_side_menu.html.haml @@ -5,4 +5,4 @@ show: '!showItem || showItem(item)', class: '{ selected: item.selected }' } } %i{ class: "{{item.icon_class}}" } - %span {{ item.name }} + %span {{ item.label }} From 4e4175ee4c130ee6d43a4cd9b7dfa6f8f471a920 Mon Sep 17 00:00:00 2001 From: elf Pavlik Date: Sun, 22 May 2016 19:03:34 -0500 Subject: [PATCH 005/110] fixed i18n on admin edit groups --- .../controllers/side_menu_controller.js.coffee | 12 ++++++------ .../admin/enterprise_groups/_form_about.html.haml | 5 ++--- .../admin/enterprise_groups/_form_address.html.haml | 5 ++--- .../admin/enterprise_groups/_form_images.html.haml | 5 ++--- .../_form_primary_details.html.haml | 5 ++--- .../admin/enterprise_groups/_form_users.html.haml | 5 ++--- .../admin/enterprise_groups/_form_web.html.haml | 5 ++--- config/locales/en.yml | 3 --- 8 files changed, 18 insertions(+), 27 deletions(-) diff --git a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee index 45ca04bfe7..f69c5365d5 100644 --- a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee @@ -4,12 +4,12 @@ angular.module("admin.enterprise_groups") $scope.select = SideMenu.select $scope.menu.setItems [ - { name: 'Primary Details', icon_class: "icon-user" } - { name: (t('users')), icon_class: "icon-user" } - { name: (t('about')), icon_class: "icon-pencil" } - { name: (t('images')), icon_class: "icon-picture" } - { name: (t('contact')), icon_class: "icon-phone" } - { name: (t('web')), icon_class: "icon-globe" } + { name: 'primary_details', label: t('primary_details'), icon_class: "icon-user" } + { name: 'users', label: t('users'), icon_class: "icon-user" } + { name: 'about', label: t('about'), icon_class: "icon-pencil" } + { name: 'images', label: t('images'), icon_class: "icon-picture" } + { name: 'contact', label: t('admin_entreprise_groups_contact'), icon_class: "icon-phone" } + { name: 'web', label: t('admin_entreprise_groups_web'), icon_class: "icon-globe" } ] $scope.select(0) diff --git a/app/views/admin/enterprise_groups/_form_about.html.haml b/app/views/admin/enterprise_groups/_form_about.html.haml index 1e97697a4e..e7932bc272 100644 --- a/app/views/admin/enterprise_groups/_form_about.html.haml +++ b/app/views/admin/enterprise_groups/_form_about.html.haml @@ -1,6 +1,5 @@ -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='About'" } } - %legend - = t 'admin_entreprise_groups_about' +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='about'" } } + %legend {{menu.selected.label}} = f.field_container :long_description do %text-angular{'id' => 'enterprise_group_long_description', 'name' => 'enterprise_group[long_description]', 'class' => 'text-angular', 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]"} diff --git a/app/views/admin/enterprise_groups/_form_address.html.haml b/app/views/admin/enterprise_groups/_form_address.html.haml index fa7b1e7e75..2b8add5392 100644 --- a/app/views/admin/enterprise_groups/_form_address.html.haml +++ b/app/views/admin/enterprise_groups/_form_address.html.haml @@ -1,7 +1,6 @@ = f.fields_for :address do |af| - %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Contact'" } } - %legend - = t 'admin_entreprise_groups_contact' + %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='contact'" } } + %legend {{menu.selected.label}} .row .alpha.three.columns = af.label :phone diff --git a/app/views/admin/enterprise_groups/_form_images.html.haml b/app/views/admin/enterprise_groups/_form_images.html.haml index 24bfad3bf0..7add7c914a 100644 --- a/app/views/admin/enterprise_groups/_form_images.html.haml +++ b/app/views/admin/enterprise_groups/_form_images.html.haml @@ -1,6 +1,5 @@ -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Images'" } } - %legend - = t 'admin_entreprise_groups_images' +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='images'" } } + %legend {{menu.selected.label}} .row .alpha.three.columns = f.label :logo, 'ofn-with-tip' => t('admin_entreprise_groups_data_powertip_logo') diff --git a/app/views/admin/enterprise_groups/_form_primary_details.html.haml b/app/views/admin/enterprise_groups/_form_primary_details.html.haml index 56771c458c..f003963b4e 100644 --- a/app/views/admin/enterprise_groups/_form_primary_details.html.haml +++ b/app/views/admin/enterprise_groups/_form_primary_details.html.haml @@ -1,6 +1,5 @@ -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Primary Details'" } } - %legend - = t "admin_entreprise_groups_primary_details" +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='primary_details'" } } + %legend {{menu.selected.label}} = f.field_container :name do = f.label :name %br/ diff --git a/app/views/admin/enterprise_groups/_form_users.html.haml b/app/views/admin/enterprise_groups/_form_users.html.haml index 6bd6eaa4a3..22ac7921ca 100644 --- a/app/views/admin/enterprise_groups/_form_users.html.haml +++ b/app/views/admin/enterprise_groups/_form_users.html.haml @@ -1,6 +1,5 @@ -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Users'" } } - %legend - = t(:users) +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='users'" } } + %legend {{menu.selected.label}} .row .three.columns.alpha =f.label :owner_id, t(:admin_entreprise_groups_owner) diff --git a/app/views/admin/enterprise_groups/_form_web.html.haml b/app/views/admin/enterprise_groups/_form_web.html.haml index 5d982f1bbb..e0de2f7159 100644 --- a/app/views/admin/enterprise_groups/_form_web.html.haml +++ b/app/views/admin/enterprise_groups/_form_web.html.haml @@ -1,6 +1,5 @@ -%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Web'" } } - %legend - = t 'admin_entreprise_groups_web' +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='web'" } } + %legend {{menu.selected.label}} .row .alpha.three.columns = f.label :website diff --git a/config/locales/en.yml b/config/locales/en.yml index e895522903..308e83c076 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -838,12 +838,9 @@ Please follow the instructions there to make your enterprise visible on the Open admin_entreprise_groups_owner: "Owner" admin_entreprise_groups_on_front_page: "On front page ?" admin_entreprise_groups_entreprise: "Enterprises" - admin_entreprise_groups_primary_details: "Primary Details" admin_entreprise_groups_data_powertip: "The primary user responsible for this group." admin_entreprise_groups_data_powertip_logo: "This is the logo for the group" admin_entreprise_groups_data_powertip_promo_image: "This image is displayed at the top of the Group profile" - admin_entreprise_groups_about: "About" - admin_entreprise_groups_images: "Images" admin_entreprise_groups_contact: "Contact" admin_entreprise_groups_contact_phone_placeholder: "eg. 98 7654 3210" admin_entreprise_groups_contact_address1_placeholder: "eg. 123 High Street" From 02be66116370f170c1e126d3fb8ef441758deb10 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 17 Apr 2016 14:02:28 +1000 Subject: [PATCH 006/110] Bumping angularjs version to 1.3.15 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- app/assets/javascripts/darkswarm/all.js.coffee | 2 +- .../javascripts/shared/mm-foundation-tpls-0.2.2.min.js | 9 --------- .../javascripts/shared/mm-foundation-tpls-0.8.0.min.js | 10 ++++++++++ spec/javascripts/application_spec.js | 2 +- 6 files changed, 15 insertions(+), 14 deletions(-) delete mode 100644 app/assets/javascripts/shared/mm-foundation-tpls-0.2.2.min.js create mode 100644 app/assets/javascripts/shared/mm-foundation-tpls-0.8.0.min.js diff --git a/Gemfile b/Gemfile index d581c8d10b..1a635a80f9 100644 --- a/Gemfile +++ b/Gemfile @@ -28,7 +28,7 @@ gem 'comfortable_mexican_sofa' gem 'simple_form', :github => 'RohanM/simple_form' gem 'unicorn' -gem 'angularjs-rails', '1.2.13' +gem 'angularjs-rails', '1.3.15' gem 'bugsnag' gem 'newrelic_rpm' gem 'haml' diff --git a/Gemfile.lock b/Gemfile.lock index 0e31157d96..ed44002f2a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -153,7 +153,7 @@ GEM sprockets (~> 2) tilt angularjs-file-upload-rails (1.1.0) - angularjs-rails (1.2.13) + angularjs-rails (1.3.15) ansi (1.4.2) arel (3.0.3) atomic (1.1.99) @@ -650,7 +650,7 @@ DEPENDENCIES andand angular-rails-templates (~> 0.2.0) angularjs-file-upload-rails (~> 1.1.0) - angularjs-rails (= 1.2.13) + angularjs-rails (= 1.3.15) atomic awesome_print aws-sdk diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index f4303f8037..f5429da413 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -11,7 +11,7 @@ #= require lodash.underscore.js #= require angular-scroll.min.js #= require angular-google-maps.min.js -#= require ../shared/mm-foundation-tpls-0.2.2.min.js +#= require ../shared/mm-foundation-tpls-0.8.0.min.js #= require ../shared/bindonce.min.js #= require ../shared/ng-infinite-scroll.min.js #= require ../shared/angular-local-storage.js diff --git a/app/assets/javascripts/shared/mm-foundation-tpls-0.2.2.min.js b/app/assets/javascripts/shared/mm-foundation-tpls-0.2.2.min.js deleted file mode 100644 index be82613518..0000000000 --- a/app/assets/javascripts/shared/mm-foundation-tpls-0.2.2.min.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * angular-mm-foundation - * http://madmimi.github.io/angular-foundation/ - - * Version: 0.2.2 - 2014-06-25 - * License: MIT - */ -angular.module("mm.foundation",["mm.foundation.tpls","mm.foundation.accordion","mm.foundation.alert","mm.foundation.bindHtml","mm.foundation.buttons","mm.foundation.position","mm.foundation.dropdownToggle","mm.foundation.transition","mm.foundation.modal","mm.foundation.offcanvas","mm.foundation.pagination","mm.foundation.tooltip","mm.foundation.popover","mm.foundation.progressbar","mm.foundation.rating","mm.foundation.tabs","mm.foundation.tour","mm.foundation.typeahead"]),angular.module("mm.foundation.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/tour/tour.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("mm.foundation.accordion",[]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(this.groups.indexOf(a),1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",["$parse",function(a){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(b,c,d,e){var f,g;e.addGroup(b),b.isOpen=!1,d.isOpen&&(f=a(d.isOpen),g=f.assign,b.$parent.$watch(f,function(a){b.isOpen=!!a})),b.$watch("isOpen",function(a){a&&e.closeOthers(b),g&&g(b.$parent,a)})}}}]).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",compile:function(a,b,c){return function(a,b,d,e){e.setHeading(c(a,function(){}))}}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.accordionTransclude]},function(a){a&&(b.html(""),b.append(a))})}}}),angular.module("mm.foundation.alert",[]).controller("AlertController",["$scope","$attrs",function(a,b){a.closeable="close"in b}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"=",close:"&"}}}),angular.module("mm.foundation.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(a,b,c){b.addClass("ng-binding").data("$binding",c.bindHtmlUnsafe),a.$watch(c.bindHtmlUnsafe,function(a){b.html(a||"")})}}),angular.module("mm.foundation.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(a){this.activeClass=a.activeClass,this.toggleEvent=a.toggleEvent}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){var e=d[0],f=d[1];f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.btnRadio)))},b.bind(e.toggleEvent,function(){b.hasClass(e.activeClass)||a.$apply(function(){f.$setViewValue(a.$eval(c.btnRadio)),f.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){var d=a.$eval(b);return angular.isDefined(d)?d:c}var h=d[0],i=d[1];i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.bind(h.toggleEvent,function(){a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("mm.foundation.position",[]).factory("$position",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].body.scrollTop||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].body.scrollLeft||a[0].documentElement.scrollLeft)}}}}]),angular.module("mm.foundation.dropdownToggle",["mm.foundation.position"]).directive("dropdownToggle",["$document","$location","$position",function(a,b,c){var d=null,e=angular.noop;return{restrict:"CA",scope:{dropdownToggle:"@"},link:function(b,f){var g=angular.element(a[0].querySelector(b.dropdownToggle));b.$watch("$location.path",function(){e()}),f.bind("click",function(h){g=angular.element(a[0].querySelector(b.dropdownToggle));var i=f===d;if(h.preventDefault(),h.stopPropagation(),d&&e(),!i&&!f.hasClass("disabled")&&!f.prop("disabled")){g.css("display","block");var j=c.offset(f),k=c.offset(angular.element(g[0].offsetParent));g.css({left:j.left-k.left+"px",top:j.top-k.top+j.height+"px"}),d=f,e=function(){a.unbind("click",e),g.css("display","none"),e=angular.noop,d=null},a.bind("click",e)}}),g&&g.css("display","none")}}}]),angular.module("mm.foundation.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,b,c){function d(a){for(var b in a)if(void 0!==f.style[b])return a[b]}var e=function(d,f,g){g=g||{};var h=a.defer(),i=e[g.animation?"animationEndEventName":"transitionEndEventName"],j=function(){c.$apply(function(){d.unbind(i,j),h.resolve(d)})};return i&&d.bind(i,j),b(function(){angular.isString(f)?d.addClass(f):angular.isFunction(f)?f(d):angular.isObject(f)&&d.css(f),i||h.resolve(d)}),h.promise.cancel=function(){i&&d.unbind(i,j),h.reject("Transition cancelled")},h.promise},f=document.createElement("trans"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"},h={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};return e.transitionEndEventName=d(g),e.animationEndEventName=d(h),e}]),angular.module("mm.foundation.modal",["mm.foundation.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c0)}function i(){if(k&&-1==g()){var a=l;j(k,l,150,function(){a.$destroy(),a=null}),k=void 0,l=void 0}}function j(c,d,e,f){function g(){g.done||(g.done=!0,c.remove(),f&&f())}d.animate=!1;var h=a.transitionEndEventName;if(h){var i=b(g,e);c.bind(h,function(){b.cancel(i),g(),d.$apply()})}else b(g,0)}var k,l,m="modal-open",n=f.createNew(),o={};return e.$watch(g,function(a){l&&(l.index=a)}),c.bind("keydown",function(a){var b;27===a.which&&(b=n.top(),b&&b.value.keyboard&&e.$apply(function(){o.dismiss(b.key)}))}),o.open=function(a,b){n.add(a,{deferred:b.deferred,modalScope:b.scope,backdrop:b.backdrop,keyboard:b.keyboard});var f=c.find("body").eq(0),h=g();h>=0&&!k&&(l=e.$new(!0),l.index=h,k=d("
")(l),f.append(k));var i=angular.element("
");i.attr("window-class",b.windowClass),i.attr("index",n.length()-1),i.attr("animate","animate"),i.html(b.content);var j=d(i)(b.scope);n.top().value.modalDomEl=j,f.append(j),f.addClass(m)},o.close=function(a,b){var c=n.get(a).value;c&&(c.deferred.resolve(b),h(a))},o.dismiss=function(a,b){var c=n.get(a).value;c&&(c.deferred.reject(b),h(a))},o.dismissAll=function(a){for(var b=this.getTop();b;)this.dismiss(b.key,a),b=this.getTop()},o.getTop=function(){return n.top()},o}]).provider("$modal",function(){var a={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?d.when(a.template):e.get(a.templateUrl,{cache:f}).then(function(a){return a.data})}function j(a){var c=[];return angular.forEach(a,function(a){(angular.isFunction(a)||angular.isArray(a))&&c.push(d.when(b.invoke(a)))}),c}var k={};return k.open=function(b){var e=d.defer(),f=d.defer(),k={result:e.promise,opened:f.promise,close:function(a){h.close(k,a)},dismiss:function(a){h.dismiss(k,a)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var l=d.all([i(b)].concat(j(b.resolve)));return l.then(function(a){var d=(b.scope||c).$new();d.$close=k.close,d.$dismiss=k.dismiss;var f,i={},j=1;b.controller&&(i.$scope=d,i.$modalInstance=k,angular.forEach(b.resolve,function(b,c){i[c]=a[j++]}),f=g(b.controller,i)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,windowClass:b.windowClass})},function(a){e.reject(a)}),l.then(function(){f.resolve(!0)},function(){f.reject(!1)}),k},k}]};return a}),angular.module("mm.foundation.offcanvas",[]).directive("offCanvasWrap",["$window",function(a){return{scope:{},restrict:"C",link:function(b,c){var d=angular.element(a),e=b.sidebar=c;b.hide=function(){e.removeClass("move-left"),e.removeClass("move-right")},d.bind("resize.body",b.hide),b.$on("$destroy",function(){d.unbind("resize.body",b.hide)})},controller:["$scope",function(a){this.leftToggle=function(){a.sidebar.toggleClass("move-right")},this.rightToggle=function(){a.sidebar.toggleClass("move-left")},this.hide=function(){a.hide()}}]}}]).directive("leftOffCanvasToggle",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.leftToggle()})}}}]).directive("rightOffCanvasToggle",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.rightToggle()})}}}]).directive("exitOffCanvas",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.hide()})}}}]).directive("offCanvasList",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.hide()})}}}]),angular.module("mm.foundation.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse","$interpolate",function(a,b,c,d){var e=this,f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(d){b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){e.itemsPerPage=parseInt(b,10),a.totalPages=e.calculateTotalPages()}):this.itemsPerPage=d},this.noPrevious=function(){return 1===this.page},this.noNext=function(){return this.page===a.totalPages},this.isActive=function(a){return this.page===a},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.getAttributeValue=function(b,c,e){return angular.isDefined(b)?e?d(b)(a.$parent):a.$parent.$eval(b):c},this.render=function(){this.page=parseInt(a.page,10)||1,this.page>0&&this.page<=a.totalPages&&(a.pages=this.getPages(this.page,a.totalPages))},a.selectPage=function(b){!e.isActive(b)&&b>0&&b<=a.totalPages&&(a.page=b,a.onSelectPage({page:b}))},a.$watch("page",function(){e.render()}),a.$watch("totalItems",function(){a.totalPages=e.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),e.page>b?a.selectPage(b):e.render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(a,b){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(c,d,e,f){function g(a,b,c,d){return{number:a,text:b,active:c,disabled:d}}var h,i=f.getAttributeValue(e.boundaryLinks,b.boundaryLinks),j=f.getAttributeValue(e.directionLinks,b.directionLinks),k=f.getAttributeValue(e.firstText,b.firstText,!0),l=f.getAttributeValue(e.previousText,b.previousText,!0),m=f.getAttributeValue(e.nextText,b.nextText,!0),n=f.getAttributeValue(e.lastText,b.lastText,!0),o=f.getAttributeValue(e.rotate,b.rotate);f.init(b.itemsPerPage),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){h=parseInt(a,10),f.render()}),f.getPages=function(a,b){var c=[],d=1,e=b,p=angular.isDefined(h)&&b>h;p&&(o?(d=Math.max(a-Math.floor(h/2),1),e=d+h-1,e>b&&(e=b,d=e-h+1)):(d=(Math.ceil(a/h)-1)*h+1,e=Math.min(d+h-1,b)));for(var q=d;e>=q;q++){var r=g(q,q,f.isActive(q),!1);c.push(r)}if(p&&!o){if(d>1){var s=g(d-1,"...",!1,!1);c.unshift(s)}if(b>e){var t=g(e+1,"...",!1,!1);c.push(t)}}if(j){var u=g(a-1,l,!1,f.noPrevious());c.unshift(u);var v=g(a+1,m,!1,f.noNext());c.push(v)}if(i){var w=g(1,k,!1,f.noPrevious());c.unshift(w);var x=g(b,n,!1,f.noNext());c.push(x)}return c}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(a){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(b,c,d,e){function f(a,b,c,d,e){return{number:a,text:b,disabled:c,previous:i&&d,next:i&&e}}var g=e.getAttributeValue(d.previousText,a.previousText,!0),h=e.getAttributeValue(d.nextText,a.nextText,!0),i=e.getAttributeValue(d.align,a.align);e.init(a.itemsPerPage),e.getPages=function(a){return[f(a-1,g,e.noPrevious(),!0,!1),f(a+1,h,e.noNext(),!1,!0)]}}}}]),angular.module("mm.foundation.tooltip",["mm.foundation.position","mm.foundation.bindHtml"]).provider("$tooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$parse","$document","$position","$interpolate",function(e,f,g,h,i,j,k){return function(e,l,m){function n(a){var b=a||o.trigger||m,d=c[b]||b;return{show:b,hide:d}}var o=angular.extend({},b,d),p=a(e),q=k.startSymbol(),r=k.endSymbol(),s="
';return{restrict:"EA",scope:!0,compile:function(){var a=f(s);return function(b,c,d){function f(){b.tt_isOpen?m():k()}function k(){(!z||b.$eval(d[l+"Enable"]))&&(b.tt_popupDelay?(v=g(p,b.tt_popupDelay,!1),v.then(function(a){a()})):p()())}function m(){b.$apply(function(){q()})}function p(){return b.tt_content?(r(),u&&g.cancel(u),t.css({top:0,left:0,display:"block"}),w?i.find("body").append(t):c.after(t),A(),b.tt_isOpen=!0,b.$digest(),A):angular.noop}function q(){b.tt_isOpen=!1,g.cancel(v),b.tt_animation?u=g(s,500):s()}function r(){t&&s(),t=a(b,function(){}),b.$digest()}function s(){t&&(t.remove(),t=null)}var t,u,v,w=angular.isDefined(o.appendToBody)?o.appendToBody:!1,x=n(void 0),y=!1,z=angular.isDefined(d[l+"Enable"]),A=function(){var a,d,e,f;switch(a=w?j.offset(c):j.position(c),d=t.prop("offsetWidth"),e=t.prop("offsetHeight"),b.tt_placement){case"right":f={top:a.top+a.height/2-e/2,left:a.left+a.width+10};break;case"bottom":f={top:a.top+a.height+10,left:a.left};break;case"left":f={top:a.top+a.height/2-e/2,left:a.left-d-10};break;default:f={top:a.top-e-10,left:a.left}}f.top+="px",f.left+="px",t.css(f)};b.tt_isOpen=!1,d.$observe(e,function(a){b.tt_content=a,!a&&b.tt_isOpen&&q()}),d.$observe(l+"Title",function(a){b.tt_title=a}),d.$observe(l+"Placement",function(a){b.tt_placement=angular.isDefined(a)?a:o.placement}),d.$observe(l+"PopupDelay",function(a){var c=parseInt(a,10);b.tt_popupDelay=isNaN(c)?o.popupDelay:c});var B=function(){y&&(c.unbind(x.show,k),c.unbind(x.hide,m))},C=function(){};d.$observe(l+"Trigger",function(a){B(),C(),x=n(a),angular.isFunction(x.show)?C=b.$watch(function(){return x.show(b,c,d)},function(a){return g(a?p:q)}):x.show===x.hide?c.bind(x.show,f):(c.bind(x.show,k),c.bind(x.hide,m)),y=!0});var D=b.$eval(d[l+"Animation"]);b.tt_animation=angular.isDefined(D)?!!D:o.animation,d.$observe(l+"AppendToBody",function(a){w=angular.isDefined(a)?h(a)(b):w}),w&&b.$on("$locationChangeSuccess",function(){b.tt_isOpen&&q()}),b.$on("$destroy",function(){g.cancel(u),g.cancel(v),B(),C(),s()})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(a){return a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("mm.foundation.popover",["mm.foundation.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("mm.foundation.progressbar",["mm.foundation.transition"]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig","$transition",function(a,b,c,d){var e=this,f=[],g=angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max,h=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.addBar=function(a,b){var c=0,d=a.$parent.$index;angular.isDefined(d)&&f[d]&&(c=f[d].value),f.push(a),this.update(b,a.value,c),a.$watch("value",function(a,c){a!==c&&e.update(b,a,c)}),a.$on("$destroy",function(){e.removeBar(a)})},this.update=function(a,b,c){var e=this.getPercentage(b);h?(a.css("width",this.getPercentage(c)+"%"),d(a,{width:e+"%"})):a.css({transition:"none",width:e+"%"})},this.removeBar=function(a){f.splice(f.indexOf(a),1)},this.getPercentage=function(a){return Math.round(100*a/g)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},template:'
'}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("mm.foundation.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","$parse","ratingConfig",function(a,b,c,d){this.maxRange=angular.isDefined(b.max)?a.$parent.$eval(b.max):d.max,this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):d.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):d.stateOff,this.createRateObjects=function(a){for(var b={stateOn:this.stateOn,stateOff:this.stateOff},c=0,d=a.length;d>c;c++)a[c]=angular.extend({index:c},b,a[c]);return a},a.range=this.createRateObjects(angular.isDefined(b.ratingStates)?angular.copy(a.$parent.$eval(b.ratingStates)):new Array(this.maxRange)),a.rate=function(b){a.value===b||a.readonly||(a.value=b)},a.enter=function(b){a.readonly||(a.val=b),a.onHover({value:b})},a.reset=function(){a.val=angular.copy(a.value),a.onLeave()},a.$watch("value",function(b){a.val=b}),a.readonly=!1,b.readonly&&a.$parent.$watch(c(b.readonly),function(b){a.readonly=!!b})}]).directive("rating",function(){return{restrict:"EA",scope:{value:"=",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0}}),angular.module("mm.foundation.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];b.select=function(a){a.selectExpression(a.$parent)},b.addTab=function(a){c.push(a)},b.removeTab=function(a){var d=c.indexOf(a);if(a.active&&c.length>1){var e=d==c.length-1?d-1:d+1;b.select(c[e])}c.splice(d,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1,a.type=angular.isDefined(c.type)?a.$parent.$eval(c.type):"tabs"}}}).directive("tab",["$parse",function(a){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(b,c,d){return function(b,c,e,f){var g,h;e.select&&(b.selectExpression=a(e.select)),e.active?(g=a(e.active),h=g.assign,b.$parent.$watch(g,function(a,c){a!==c&&(b.active=!!a)}),b.active=g(b.$parent)):h=g=angular.noop,b.disabled=!1,e.disabled&&b.$parent.$watch(a(e.disabled),function(a){b.disabled=!!a}),b.select=function(){b.disabled||b.selectExpression(b.$parent)},f.addTab(b),b.$on("$destroy",function(){f.removeTab(b)}),b.$transcludeFn=d}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("mm.foundation.tour",["mm.foundation.position","mm.foundation.tooltip"]).service("$tour",["$window",function(a){function b(){return parseInt(a.localStorage.getItem("mm.tour.step"),10)}function c(b){d=b,a.localStorage.setItem("mm.tour.step",b)}var d=b(),e={};this.add=function(a,b){e[a]=b},this.has=function(a){return!!e[a]},this.isActive=function(){return d>0},this.current=function(a){return a?void c(d):d},this.start=function(){c(1)},this.next=function(){c(d+1)},this.end=function(){c(0)}}]).directive("stepTextPopup",["$tour",function(a){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tour/tour.html",link:function(b,c){b.isLastStep=function(){return!a.has(a.current()+1)},b.endTour=function(){c.remove(),a.end()},b.nextStep=function(){c.remove(),a.next()}}}}]).directive("stepText",["$position","$tooltip","$tour","$window",function(a,b,c,d){function e(a){var b=a[0].getBoundingClientRect();return b.top>=0&&b.left>=0&&b.bottom<=d.innerHeight-80&&b.right<=d.innerWidth}function f(b,f,g){var h=parseInt(g.stepIndex,10);if(c.isActive()&&h&&(c.add(h,g),h===c.current())){if(!e(f)){var i=a.offset(f);d.scrollTo(0,i.top-d.innerHeight/2)}return!0}return!1}return b("stepText","step",f)}]),angular.module("mm.foundation.typeahead",["mm.foundation.position","mm.foundation.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error("Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_' but got '"+c+"'.");return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(a,b,c,d,e,f,g){var h=[9,13,27,38,40];return{require:"ngModel",link:function(i,j,k,l){var m,n=i.$eval(k.typeaheadMinLength)||1,o=i.$eval(k.typeaheadWaitMs)||0,p=i.$eval(k.typeaheadEditable)!==!1,q=b(k.typeaheadLoading).assign||angular.noop,r=b(k.typeaheadOnSelect),s=k.typeaheadInputFormatter?b(k.typeaheadInputFormatter):void 0,t=k.typeaheadAppendToBody?b(k.typeaheadAppendToBody):!1,u=b(k.ngModel).assign,v=g.parse(k.typeahead),w=angular.element("
");w.attr({matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(k.typeaheadTemplateUrl)&&w.attr("template-url",k.typeaheadTemplateUrl);var x=i.$new();i.$on("$destroy",function(){x.$destroy()});var y=function(){x.matches=[],x.activeIdx=-1},z=function(a){var b={$viewValue:a};q(i,!0),c.when(v.source(i,b)).then(function(c){if(a===l.$viewValue&&m){if(c.length>0){x.activeIdx=0,x.matches.length=0;for(var d=0;d=n?o>0?(A&&d.cancel(A),A=d(function(){z(a)},o)):z(a):(q(i,!1),y()),p?a:a?void l.$setValidity("editable",!1):(l.$setValidity("editable",!0),a)}),l.$formatters.push(function(a){var b,c,d={};return s?(d.$model=a,s(i,d)):(d[v.itemName]=a,b=v.viewMapper(i,d),d[v.itemName]=void 0,c=v.viewMapper(i,d),b!==c?b:a)}),x.select=function(a){var b,c,d={};d[v.itemName]=c=x.matches[a].model,b=v.modelMapper(i,d),u(i,b),l.$setValidity("editable",!0),r(i,{$item:c,$model:b,$label:v.viewMapper(i,d)}),y(),j[0].focus()},j.bind("keydown",function(a){0!==x.matches.length&&-1!==h.indexOf(a.which)&&(a.preventDefault(),40===a.which?(x.activeIdx=(x.activeIdx+1)%x.matches.length,x.$digest()):38===a.which?(x.activeIdx=(x.activeIdx?x.activeIdx:x.matches.length)-1,x.$digest()):13===a.which||9===a.which?x.$apply(function(){x.select(x.activeIdx)}):27===a.which&&(a.stopPropagation(),y(),x.$digest()))}),j.bind("blur",function(){m=!1});var B=function(a){j[0]!==a.target&&(y(),x.$digest())};e.bind("click",B),i.$on("$destroy",function(){e.unbind("click",B)});var C=a(w)(x);t?e.find("body").append(C):j.after(C)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(a,b,c,d){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(e,f,g){var h=d(g.templateUrl)(e.$parent)||"template/typeahead/typeahead-match.html";a.get(h,{cache:b}).success(function(a){f.replaceWith(c(a.trim())(e))})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?b.replace(new RegExp(a(c),"gi"),"$&"):b}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion-group.html",'
\n {{heading}}\n
\n
\n')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion.html",'
\n')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("template/alert/alert.html","
\n \n ×\n
\n")}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("template/modal/backdrop.html",'
\n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(a){a.put("template/modal/window.html",'
\n
\n
\n')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pager.html",'\n')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pagination.html",'\n')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-unsafe-popup.html",'\n \n \n\n') -}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-popup.html",'\n \n \n\n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover.html",'
\n \n
\n

\n

\n
\n
\n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/bar.html",'\n')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progress.html",'
\n')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progressbar.html",'
\n \n
\n')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("template/rating/rating.html",'\n \n\n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tab.html",'
\n {{heading}}\n
\n')}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'
\n
\n
\n
\n
\n
\n
\n
\n')}]),angular.module("template/tour/tour.html",[]).run(["$templateCache",function(a){a.put("template/tour/tour.html",'
\n \n
\n

\n

\n Next\n End\n ×\n
\n
\n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html","
    \n"+'
  • \n
    \n
  • \n
\n')}]); diff --git a/app/assets/javascripts/shared/mm-foundation-tpls-0.8.0.min.js b/app/assets/javascripts/shared/mm-foundation-tpls-0.8.0.min.js new file mode 100644 index 0000000000..8f4e630b9f --- /dev/null +++ b/app/assets/javascripts/shared/mm-foundation-tpls-0.8.0.min.js @@ -0,0 +1,10 @@ +/* + * angular-mm-foundation + * http://pineconellc.github.io/angular-foundation/ + + * Version: 0.8.0 - 2015-10-13 + * License: MIT + * (c) Pinecone, LLC + */ +angular.module("mm.foundation",["mm.foundation.tpls","mm.foundation.accordion","mm.foundation.alert","mm.foundation.bindHtml","mm.foundation.buttons","mm.foundation.position","mm.foundation.mediaQueries","mm.foundation.dropdownToggle","mm.foundation.interchange","mm.foundation.transition","mm.foundation.modal","mm.foundation.offcanvas","mm.foundation.pagination","mm.foundation.tooltip","mm.foundation.popover","mm.foundation.progressbar","mm.foundation.rating","mm.foundation.tabs","mm.foundation.topbar","mm.foundation.tour","mm.foundation.typeahead"]),angular.module("mm.foundation.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/topbar/has-dropdown.html","template/topbar/toggle-top-bar.html","template/topbar/top-bar-dropdown.html","template/topbar/top-bar-section.html","template/topbar/top-bar.html","template/tour/tour.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("mm.foundation.accordion",[]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(b,1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",["$parse",function(a){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(b,c,d,e){var f,g;e.addGroup(b),b.isOpen=!1,d.isOpen&&(f=a(d.isOpen),g=f.assign,b.$parent.$watch(f,function(a){b.isOpen=!!a})),b.$watch("isOpen",function(a){a&&e.closeOthers(b),g&&g(b.$parent,a)})}}}]).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",compile:function(a,b,c){return function(a,b,d,e){e.setHeading(c(a,function(){}))}}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.accordionTransclude]},function(a){a&&(b.html(""),b.append(a))})}}}),angular.module("mm.foundation.alert",[]).controller("AlertController",["$scope","$attrs",function(a,b){a.closeable="close"in b&&"undefined"!=typeof b.close}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"=",close:"&"}}}),angular.module("mm.foundation.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(a,b,c){b.addClass("ng-binding").data("$binding",c.bindHtmlUnsafe),a.$watch(c.bindHtmlUnsafe,function(a){b.html(a||"")})}}),angular.module("mm.foundation.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(a){this.activeClass=a.activeClass,this.toggleEvent=a.toggleEvent}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){var e=d[0],f=d[1];f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.btnRadio)))},b.bind(e.toggleEvent,function(){b.hasClass(e.activeClass)||a.$apply(function(){f.$setViewValue(a.$eval(c.btnRadio)),f.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){var d=a.$eval(b);return angular.isDefined(d)?d:c}var h=d[0],i=d[1];i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.bind(h.toggleEvent,function(){a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("mm.foundation.position",[]).factory("$position",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].body.scrollTop||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].body.scrollLeft||a[0].documentElement.scrollLeft)}}}}]),angular.module("mm.foundation.mediaQueries",[]).factory("matchMedia",["$document","$window",function(a,b){return b.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='­',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a[0])}]).factory("mediaQueries",["$document","matchMedia",function(a,b){var c=angular.element(a[0].querySelector("head"));c.append(''),c.append(''),c.append(''),c.append('');var d=/^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g,e={topbar:getComputedStyle(c[0].querySelector("meta.foundation-mq-topbar")).fontFamily.replace(d,""),small:getComputedStyle(c[0].querySelector("meta.foundation-mq-small")).fontFamily.replace(d,""),medium:getComputedStyle(c[0].querySelector("meta.foundation-mq-medium")).fontFamily.replace(d,""),large:getComputedStyle(c[0].querySelector("meta.foundation-mq-large")).fontFamily.replace(d,"")};return{topbarBreakpoint:function(){return!b(e.topbar).matches},small:function(){return b(e.small).matches},medium:function(){return b(e.medium).matches},large:function(){return b(e.large).matches}}}]),angular.module("mm.foundation.dropdownToggle",["mm.foundation.position","mm.foundation.mediaQueries"]).controller("DropdownToggleController",["$scope","$attrs","mediaQueries",function(a,b,c){this.small=function(){return c.small()&&!c.medium()}}]).directive("dropdownToggle",["$document","$window","$location","$position",function(a,b,c,d){var e=null,f=angular.noop;return{restrict:"CA",controller:"DropdownToggleController",link:function(c,g,h,i){var j=g.parent(),k=angular.element(a[0].querySelector(h.dropdownToggle)),l=function(){return j.hasClass("has-dropdown")},m=function(c){k=angular.element(a[0].querySelector(h.dropdownToggle));var m=g===e;if(c.preventDefault(),c.stopPropagation(),e&&f(),!m&&!g.hasClass("disabled")&&!g.prop("disabled")){k.css("display","block"),k.addClass("f-open-dropdown");var n=d.offset(g),o=d.offset(angular.element(k[0].offsetParent)),p=k.prop("offsetWidth"),q={top:n.top-o.top+n.height+"px"};if(i.small())q.left=Math.max((o.width-p)/2,8)+"px",q.position="absolute",q.width="95%",q["max-width"]="none";else{var r=Math.round(n.left-o.left),s=b.innerWidth-p-8;r>s&&(r=s,k.removeClass("left").addClass("right")),q.left=r+"px",q.position=null,q["max-width"]=null}k.css(q),g.addClass("expanded"),l()&&j.addClass("hover"),e=g,f=function(){a.off("click",f),k.css("display","none"),k.removeClass("f-open-dropdown"),g.removeClass("expanded"),f=angular.noop,e=null,j.hasClass("hover")&&j.removeClass("hover")},a.on("click",f)}};k&&k.css("display","none"),c.$watch("$location.path",function(){f()}),g.on("click",m),g.on("$destroy",function(){g.off("click",m)})}}}]),angular.module("mm.foundation.interchange",["mm.foundation.mediaQueries"]).factory("interchangeQueries",["$document",function(a){for(var b,c,d={"default":"only screen",landscape:"only screen and (orientation: landscape)",portrait:"only screen and (orientation: portrait)",retina:"only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2),only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx)"},e="foundation-mq-",f=["small","medium","large","xlarge","xxlarge"],g=angular.element(a[0].querySelector("head")),h=0;h'),b=getComputedStyle(g[0].querySelector("meta."+e+f[h])),c=b.fontFamily.replace(/^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g,""),d[f[h]]=c;return d}]).factory("interchangeQueriesManager",["interchangeQueries",function(a){return{add:function(b,c){return b&&c&&angular.isString(b)&&angular.isString(c)&&!a[b]?(a[b]=c,!0):!1}}}]).factory("interchangeTools",["$window","matchMedia","interchangeQueries",function(a,b,c){var d=function(a){for(var b,c=a.split(/\[(.*?)\]/),d=c.length,e=/^(.+)\,\ \((.+)\)$/,f={};d--;)c[d].replace(/[\W\d]+/,"").length>4&&(b=e.exec(c[d]),b&&3===b.length&&(f[b[2]]=b[1]));return f},e=function(a){var d,e,f;for(d in a)if(e=c[d]||d,f=b(e),f.matches)return a[d]};return{parseAttribute:d,findCurrentMediaFile:e}}]).directive("interchange",["$window","$rootScope","interchangeTools",function(a,b,c){var d=/[A-Za-z0-9-_]+\.(jpg|jpeg|png|gif|bmp|tiff)\ *,/i;return{restrict:"A",scope:!0,priority:450,compile:function(e,f){return"DIV"!==e[0].nodeName||d.test(f.interchange)||e.html(''),{pre:function(){},post:function(d,e,f){var g,h;switch(h=e&&e[0]&&e[0].nodeName,d.fileMap=c.parseAttribute(f.interchange),h){case"DIV":g=c.findCurrentMediaFile(d.fileMap),d.type=/[A-Za-z0-9-_]+\.(jpg|jpeg|png|gif|bmp|tiff)$/i.test(g)?"background":"include";break;case"IMG":d.type="image";break;default:return}var i=function(a){var f=c.findCurrentMediaFile(d.fileMap);if(!d.currentFile||d.currentFile!==f){switch(d.currentFile=f,d.type){case"image":e.attr("src",d.currentFile);break;case"background":e.css("background-image","url("+d.currentFile+")")}b.$emit("replace",e,d),a&&d.$apply()}};i(),a.addEventListener("resize",i),d.$on("$destroy",function(){a.removeEventListener("resize",i)})}}}}}]),angular.module("mm.foundation.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,b,c){function d(a){for(var b in a)if(void 0!==f.style[b])return a[b]}var e=function(d,f,g){g=g||{};var h=a.defer(),i=e[g.animation?"animationEndEventName":"transitionEndEventName"],j=function(){c.$apply(function(){d.unbind(i,j),h.resolve(d)})};return i&&d.bind(i,j),b(function(){angular.isString(f)?d.addClass(f):angular.isFunction(f)?f(d):angular.isObject(f)&&d.css(f),i||h.resolve(d)}),h.promise.cancel=function(){i&&d.unbind(i,j),h.reject("Transition cancelled")},h.promise},f=document.createElement("trans"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"},h={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};return e.transitionEndEventName=d(g),e.animationEndEventName=d(h),e}]),angular.module("mm.foundation.modal",["mm.foundation.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c0?c[0].querySelectorAll("[autofocus]")[0].focus():c[0].focus()})}}}]).factory("$modalStack",["$window","$transition","$timeout","$document","$compile","$rootScope","$$stackedMap",function(a,b,c,d,e,f,g){function h(){for(var a=-1,b=q.keys(),c=0;c0),j()})}function j(){if(m&&-1==h()){var a=n;k(m,n,150,function(){a.$destroy(),a=null}),m=void 0,n=void 0}}function k(a,d,e,f){function g(){g.done||(g.done=!0,a.remove(),f&&f())}d.animate=!1;var h=b.transitionEndEventName;if(h){var i=c(g,e);a.bind(h,function(){c.cancel(i),g(),d.$apply()})}else c(g,0)}function l(b,c){angular.isUndefined(c)&&(c=0);var d=a.pageYOffset||0;return c+d}var m,n,o,p="modal-open",q=g.createNew(),r={};return f.$watch(h,function(a){n&&(n.index=a)}),d.bind("keydown",function(a){var b;27===a.which&&(b=q.top(),b&&b.value.keyboard&&f.$apply(function(){r.dismiss(b.key)}))}),r.open=function(b,c){b.options={deferred:c.deferred,modalScope:c.scope,backdrop:c.backdrop,keyboard:c.keyboard,parent:c.parent},q.add(b,b.options);var g=d.find(c.parent).eq(0),i=h();i>=0&&!m&&(n=f.$new(!0),n.index=i,m=e("
")(n),g.append(m));var j=angular.element('
');g.append(j[0]),o=parseInt(a.getComputedStyle(j[0]).top)||0;var k=l(j,o);j.remove();var r=angular.element('
').attr({"window-class":c.windowClass,index:q.length()-1,animate:"animate"});r.html(c.content);var s=e(r)(c.scope);q.top().value.modalDomEl=s,g.append(s),g.addClass(p)},r.reposition=function(a){var b=q.get(a).value;if(b){var c=b.modalDomEl,d=l(c,o);c.css("top",d+"px")}},r.close=function(a,b){var c=q.get(a);c&&(c.value.deferred.resolve(b),i(a))},r.dismiss=function(a,b){var c=q.get(a);c&&(c.value.deferred.reject(b),i(a))},r.dismissAll=function(a){for(var b=this.getTop();b;)this.dismiss(b.key,a),b=this.getTop()},r.getTop=function(){return q.top()},r}]).provider("$modal",function(){var a={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?d.when(a.template):e.get(a.templateUrl,{cache:f}).then(function(a){return a.data})}function j(a){var c=[];return angular.forEach(a,function(a){(angular.isFunction(a)||angular.isArray(a))&&c.push(d.when(b.invoke(a)))}),c}var k={};return k.open=function(b){var e=d.defer(),f=d.defer(),k={result:e.promise,opened:f.promise,close:function(a){h.close(k,a)},dismiss:function(a){h.dismiss(k,a)},reposition:function(){h.reposition(k)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var l=d.all([i(b)].concat(j(b.resolve)));return l.then(function(a){var d=(b.scope||c).$new();d.$close=k.close,d.$dismiss=k.dismiss;var f,i={},j=1;b.controller&&(i.$scope=d,i.$modalInstance=k,angular.forEach(b.resolve,function(b,c){i[c]=a[j++]}),f=g(b.controller,i),b.controllerAs&&(d[b.controllerAs]=f)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,windowClass:b.windowClass,parent:b.parent||"body"})},function(a){e.reject(a)}),l.then(function(){f.resolve(!0)},function(){f.reject(!1)}),k},k}]};return a}),angular.module("mm.foundation.offcanvas",[]).directive("offCanvasWrap",["$window",function(a){return{scope:{},restrict:"C",link:function(b,c){var d=angular.element(a),e=b.sidebar=c;b.hide=function(){e.removeClass("move-left"),e.removeClass("move-right")},d.bind("resize.body",b.hide),b.$on("$destroy",function(){d.unbind("resize.body",b.hide)})},controller:["$scope",function(a){this.leftToggle=function(){a.sidebar.toggleClass("move-right")},this.rightToggle=function(){a.sidebar.toggleClass("move-left")},this.hide=function(){a.hide()}}]}}]).directive("leftOffCanvasToggle",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.leftToggle()})}}}]).directive("rightOffCanvasToggle",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.rightToggle()})}}}]).directive("exitOffCanvas",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.hide()})}}}]).directive("offCanvasList",[function(){return{require:"^offCanvasWrap",restrict:"C",link:function(a,b,c,d){b.on("click",function(){d.hide()})}}}]),angular.module("mm.foundation.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse","$interpolate",function(a,b,c,d){var e=this,f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(d){b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){e.itemsPerPage=parseInt(b,10),a.totalPages=e.calculateTotalPages()}):this.itemsPerPage=d},this.noPrevious=function(){return 1===this.page},this.noNext=function(){return this.page===a.totalPages},this.isActive=function(a){return this.page===a},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.getAttributeValue=function(b,c,e){return angular.isDefined(b)?e?d(b)(a.$parent):a.$parent.$eval(b):c},this.render=function(){this.page=parseInt(a.page,10)||1,this.page>0&&this.page<=a.totalPages&&(a.pages=this.getPages(this.page,a.totalPages))},a.selectPage=function(b){!e.isActive(b)&&b>0&&b<=a.totalPages&&(a.page=b,a.onSelectPage({page:b}))},a.$watch("page",function(){e.render()}),a.$watch("totalItems",function(){a.totalPages=e.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),e.page>b?a.selectPage(b):e.render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(a,b){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(c,d,e,f){function g(a,b,c,d){return{number:a,text:b,active:c,disabled:d}}var h,i=f.getAttributeValue(e.boundaryLinks,b.boundaryLinks),j=f.getAttributeValue(e.directionLinks,b.directionLinks),k=f.getAttributeValue(e.firstText,b.firstText,!0),l=f.getAttributeValue(e.previousText,b.previousText,!0),m=f.getAttributeValue(e.nextText,b.nextText,!0),n=f.getAttributeValue(e.lastText,b.lastText,!0),o=f.getAttributeValue(e.rotate,b.rotate);f.init(b.itemsPerPage),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){h=parseInt(a,10),f.render()}),f.getPages=function(a,b){var c=[],d=1,e=b,p=angular.isDefined(h)&&b>h;p&&(o?(d=Math.max(a-Math.floor(h/2),1),e=d+h-1,e>b&&(e=b,d=e-h+1)):(d=(Math.ceil(a/h)-1)*h+1,e=Math.min(d+h-1,b)));for(var q=d;e>=q;q++){var r=g(q,q,f.isActive(q),!1);c.push(r)}if(p&&!o){if(d>1){var s=g(d-1,"...",!1,!1);c.unshift(s)}if(b>e){var t=g(e+1,"...",!1,!1);c.push(t)}}if(j){var u=g(a-1,l,!1,f.noPrevious());c.unshift(u);var v=g(a+1,m,!1,f.noNext());c.push(v)}if(i){var w=g(1,k,!1,f.noPrevious());c.unshift(w);var x=g(b,n,!1,f.noNext());c.push(x)}return c}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(a){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(b,c,d,e){function f(a,b,c,d,e){return{number:a,text:b,disabled:c,previous:i&&d,next:i&&e}}var g=e.getAttributeValue(d.previousText,a.previousText,!0),h=e.getAttributeValue(d.nextText,a.nextText,!0),i=e.getAttributeValue(d.align,a.align);e.init(a.itemsPerPage),e.getPages=function(a){return[f(a-1,g,e.noPrevious(),!0,!1),f(a+1,h,e.noNext(),!1,!0)]}}}}]),angular.module("mm.foundation.tooltip",["mm.foundation.position","mm.foundation.bindHtml"]).provider("$tooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$parse","$document","$position","$interpolate",function(e,f,g,h,i,j,k){return function(e,l,m){function n(a){var b=a||o.trigger||m,d=c[b]||b;return{show:b,hide:d}}var o=angular.extend({},b,d),p=a(e),q=k.startSymbol(),r=k.endSymbol(),s="
';return{restrict:"EA",scope:!0,compile:function(){var a=f(s);return function(b,c,d){function f(){b.tt_isOpen?m():k()}function k(){(!z||b.$eval(d[l+"Enable"]))&&(b.tt_popupDelay?(v=g(p,b.tt_popupDelay,!1),v.then(function(a){a()})):p()())}function m(){b.$apply(function(){q()})}function p(){return b.tt_content?(r(),u&&g.cancel(u),t.css({top:0,left:0,display:"block"}),w?i.find("body").append(t):c.after(t),A(),b.tt_isOpen=!0,b.$digest(),A):angular.noop}function q(){b.tt_isOpen=!1,g.cancel(v),b.tt_animation?u=g(s,500):s()}function r(){t&&s(),t=a(b,function(){}),b.$digest()}function s(){t&&(t.remove(),t=null)}var t,u,v,w=angular.isDefined(o.appendToBody)?o.appendToBody:!1,x=n(void 0),y=!1,z=angular.isDefined(d[l+"Enable"]),A=function(){var a,d,e,f;switch(a=w?j.offset(c):j.position(c),d=t.prop("offsetWidth"),e=t.prop("offsetHeight"),b.tt_placement){case"right":f={top:a.top+a.height/2-e/2,left:a.left+a.width+10};break;case"bottom":f={top:a.top+a.height+10,left:a.left};break;case"left":f={top:a.top+a.height/2-e/2,left:a.left-d-10};break;default:f={top:a.top-e-10,left:a.left}}f.top+="px",f.left+="px",t.css(f)};b.tt_isOpen=!1,d.$observe(e,function(a){b.tt_content=a,!a&&b.tt_isOpen&&q()}),d.$observe(l+"Title",function(a){b.tt_title=a}),d[l+"Placement"]=d[l+"Placement"]||null,d.$observe(l+"Placement",function(a){b.tt_placement=angular.isDefined(a)&&a?a:o.placement}),d[l+"PopupDelay"]=d[l+"PopupDelay"]||null,d.$observe(l+"PopupDelay",function(a){var c=parseInt(a,10);b.tt_popupDelay=isNaN(c)?o.popupDelay:c});var B=function(){y&&(angular.isFunction(x.show)?C():(c.unbind(x.show,k),c.unbind(x.hide,m)))},C=function(){};d[l+"Trigger"]=d[l+"Trigger"]||null,d.$observe(l+"Trigger",function(a){B(),C(),x=n(a),angular.isFunction(x.show)?C=b.$watch(function(){return x.show(b,c,d)},function(a){return g(a?p:q)}):x.show===x.hide?c.bind(x.show,f):(c.bind(x.show,k),c.bind(x.hide,m)),y=!0});var D=b.$eval(d[l+"Animation"]);b.tt_animation=angular.isDefined(D)?!!D:o.animation,d.$observe(l+"AppendToBody",function(a){w=angular.isDefined(a)?h(a)(b):w}),w&&b.$on("$locationChangeSuccess",function(){b.tt_isOpen&&q()}),b.$on("$destroy",function(){g.cancel(u),g.cancel(v),B(),C(),s()})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(a){return a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("mm.foundation.popover",["mm.foundation.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("mm.foundation.progressbar",["mm.foundation.transition"]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig","$transition",function(a,b,c,d){var e=this,f=[],g=angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max,h=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.addBar=function(a,b){var c=0,d=a.$parent.$index;angular.isDefined(d)&&f[d]&&(c=f[d].value),f.push(a),this.update(b,a.value,c),a.$watch("value",function(a,c){a!==c&&e.update(b,a,c)}),a.$on("$destroy",function(){e.removeBar(a)})},this.update=function(a,b,c){var e=this.getPercentage(b);h?(a.css("width",this.getPercentage(c)+"%"),d(a,{width:e+"%"})):a.css({transition:"none",width:e+"%"})},this.removeBar=function(a){f.splice(f.indexOf(a),1)},this.getPercentage=function(a){return Math.round(100*a/g)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},template:'
'}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("mm.foundation.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","$parse","ratingConfig",function(a,b,c,d){this.maxRange=angular.isDefined(b.max)?a.$parent.$eval(b.max):d.max,this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):d.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):d.stateOff,this.createRateObjects=function(a){for(var b={stateOn:this.stateOn,stateOff:this.stateOff},c=0,d=a.length;d>c;c++)a[c]=angular.extend({index:c},b,a[c]);return a},a.range=this.createRateObjects(angular.isDefined(b.ratingStates)?angular.copy(a.$parent.$eval(b.ratingStates)):new Array(this.maxRange)),a.rate=function(b){a.value===b||a.readonly||(a.value=b)},a.enter=function(b){a.readonly||(a.val=b),a.onHover({value:b})},a.reset=function(){a.val=angular.copy(a.value),a.onLeave()},a.$watch("value",function(b){a.val=b}),a.readonly=!1,b.readonly&&a.$parent.$watch(c(b.readonly),function(b){a.readonly=!!b})}]).directive("rating",function(){return{restrict:"EA",scope:{value:"=",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0}}),angular.module("mm.foundation.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];angular.isUndefined(a.openOnLoad)&&(a.openOnLoad=!0),b.select=function(a){angular.forEach(c,function(a){a.active=!1}),a.active=!0},b.addTab=function(d){c.push(d),a.openOnLoad&&(1===c.length||d.active)&&b.select(d)},b.removeTab=function(a){var d=c.indexOf(a);if(a.active&&c.length>1){var e=d==c.length-1?d-1:d+1;b.select(c[e])}c.splice(d,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{openOnLoad:"=?"},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1,a.type=angular.isDefined(c.type)?a.$parent.$eval(c.type):"tabs"}}}).directive("tab",["$parse",function(a){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(b,c,d){return function(b,c,e,f){var g,h;e.active?(g=a(e.active),h=g.assign,b.$parent.$watch(g,function(a,c){a!==c&&(b.active=!!a)}),b.active=g(b.$parent)):h=g=angular.noop,b.$watch("active",function(a){angular.isFunction(h)&&(h(b.$parent,a),a?(f.select(b),b.onSelect()):b.onDeselect())}),b.disabled=!1,e.disabled&&b.$parent.$watch(a(e.disabled),function(a){b.disabled=!!a}),b.select=function(){b.disabled||(b.active=!0)},f.addTab(b),b.$on("$destroy",function(){f.removeTab(b)}),b.$transcludeFn=d}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("mm.foundation.topbar",["mm.foundation.mediaQueries"]).factory("closest",[function(){return function(a,b){for(var c=function(a,b){for(var c=(a.parentNode||a.document).querySelectorAll(b),d=-1;c[++d]&&c[d]!=a;);return!!c[d]},d=a[0];d;){if(c(d,b))return angular.element(d);d=d.parentElement}return!1}}]).directive("topBar",["$timeout","$compile","$window","$document","mediaQueries",function(a,b,c,d,e){return{scope:{stickyClass:"@",backText:"@",stickyOn:"=",customBackText:"=",isHover:"=",mobileShowParentLink:"=",scrolltop:"="},restrict:"EA",replace:!0,templateUrl:"template/topbar/top-bar.html",transclude:!0,controller:["$window","$scope","closest",function(b,c,f){c.settings={},c.settings.stickyClass=c.stickyClass||"sticky",c.settings.backText=c.backText||"Back",c.settings.stickyOn=c.stickyOn||"all",c.settings.customBackText=void 0===c.customBackText?!0:c.customBackText,c.settings.isHover=void 0===c.isHover?!0:c.isHover,c.settings.mobileShowParentLink=void 0===c.mobileShowParentLink?!0:c.mobileShowParentLink,c.settings.scrolltop=void 0===c.scrolltop?!0:c.scrolltop,this.settings=c.settings,c.index=0;var g=function(a){var b=a.offsetHeight,c=a.currentStyle||getComputedStyle(a);return b+=parseInt(c.marginTop,10)+parseInt(c.marginBottom,10)},h=[];this.addSection=function(a){h.push(a)},this.removeSection=function(a){var b=h.indexOf(a);b>-1&&h.splice(b,1)};var i=/rtl/i.test(d.find("html").attr("dir"))?"right":"left";c.$watch("index",function(a){for(var b=0;bb&&!g.hasClass("fixed")?(g.addClass("fixed"),h.css("padding-top",a.originalHeight+"px")):c.pageYOffset<=b&&g.hasClass("fixed")&&(g.removeClass("fixed"),h.css("padding-top",""))}},l=function(){var b=e.topbarBreakpoint();if(i!==b){i=e.topbarBreakpoint(),f.removeClass("expanded"),f.parent().removeClass("expanded"),a.height="";var c=angular.element(f[0].querySelectorAll("section"));angular.forEach(c,function(a){angular.element(a.querySelectorAll("li.moved")).removeClass("moved")}),a.$apply()}},m=function(){k(),a.$apply()};if(a.toggle=function(b){if(!e.topbarBreakpoint())return!1;var d=void 0===b?!f.hasClass("expanded"):b;d?f.addClass("expanded"):f.removeClass("expanded"),a.settings.scrolltop?!d&&f.hasClass("fixed")?(f.parent().addClass("fixed"),f.removeClass("fixed"),h.css("padding-top",a.originalHeight+"px")):d&&f.parent().hasClass("fixed")&&(f.parent().removeClass("fixed"),f.addClass("fixed"),h.css("padding-top",""),c.scrollTo(0,0)):(j()&&f.parent().addClass("fixed"),f.parent().hasClass("fixed")&&(d?(f.addClass("fixed"),f.parent().addClass("expanded"),h.css("padding-top",a.originalHeight+"px")):(f.removeClass("fixed"),f.parent().removeClass("expanded"),k())))},g.hasClass("fixed")||j()){a.stickyTopbar=!0,a.height=g[0].offsetHeight;var n=g[0].getBoundingClientRect().top+c.pageYOffset}else a.height=f[0].offsetHeight;a.originalHeight=a.height,a.$watch("height",function(a){a?f.css("height",a+"px"):f.css("height","")}),angular.element(c).bind("resize",l),angular.element(c).bind("scroll",m),a.$on("$destroy",function(){angular.element(c).unbind("scroll",l),angular.element(c).unbind("resize",m)}),g.hasClass("fixed")&&h.css("padding-top",a.originalHeight+"px")}}}]).directive("toggleTopBar",["closest",function(a){return{scope:{},require:"^topBar",restrict:"A",replace:!0,templateUrl:"template/topbar/toggle-top-bar.html",transclude:!0,link:function(b,c,d,e){c.bind("click",function(b){var c=a(angular.element(b.currentTarget),"li");c.hasClass("back")||c.hasClass("has-dropdown")||e.toggle()}),b.$on("$destroy",function(){c.unbind("click")})}}}]).directive("topBarSection",["$compile","closest",function(a,b){return{scope:{},require:"^topBar",restrict:"EA",replace:!0,templateUrl:"template/topbar/top-bar-section.html",transclude:!0,link:function(a,c,d,e){var f=c;a.reset=function(){angular.element(f[0].querySelectorAll("li.moved")).removeClass("moved")},a.move=function(a,b){f.css("left"===a?{left:-100*b+"%"}:{right:-100*b+"%"})},e.addSection(a),a.$on("$destroy",function(){e.removeSection(a)});var g=f[0].querySelectorAll("li>a");angular.forEach(g,function(c){var d=angular.element(c),f=b(d,"li");f.hasClass("has-dropdown")||f.hasClass("back")||f.hasClass("title")||(d.bind("click",function(){e.toggle(!1)}),a.$on("$destroy",function(){d.bind("click")}))})}}}]).directive("hasDropdown",["mediaQueries",function(a){return{scope:{},require:"^topBar",restrict:"A",templateUrl:"template/topbar/has-dropdown.html",replace:!0,transclude:!0,link:function(b,c,d,e){b.triggerLink=c.children("a")[0];var f=angular.element(b.triggerLink);f.bind("click",function(a){e.forward(a)}),b.$on("$destroy",function(){f.unbind("click")}),c.bind("mouseenter",function(){e.settings.isHover&&!a.topbarBreakpoint()&&c.addClass("not-click")}),c.bind("click",function(){e.settings.isHover||a.topbarBreakpoint()||c.toggleClass("not-click")}),c.bind("mouseleave",function(){c.removeClass("not-click")}),b.$on("$destroy",function(){c.unbind("click"),c.unbind("mouseenter"),c.unbind("mouseleave")})},controller:["$window","$scope",function(a,b){this.triggerLink=b.triggerLink}]}}]).directive("topBarDropdown",["$compile",function(a){return{scope:{},require:["^topBar","^hasDropdown"],restrict:"A",replace:!0,templateUrl:"template/topbar/top-bar-dropdown.html",transclude:!0,link:function(b,c,d,e){var f,g=e[0],h=e[1],i=angular.element(h.triggerLink),j=i.attr("href");b.linkText=i.text(),b.back=function(a){g.back(a)},b.backText=g.settings.customBackText?g.settings.backText:"« "+i.html(),f=angular.element(g.settings.mobileShowParentLink&&j&&j.length>1?'
  • {{backText}}
  • {{linkText}}
  • ':'
  • {{backText}}
  • '),a(f)(b),c.prepend(f)}}}]),angular.module("mm.foundation.tour",["mm.foundation.position","mm.foundation.tooltip"]).service("$tour",["$window",function(a){function b(){try{return parseInt(a.localStorage.getItem("mm.tour.step"),10)}catch(b){if("SecurityError"!==b.name)throw b}}function c(){try{a.localStorage.setItem("mm.tour.step",e)}catch(b){if("SecurityError"!==b.name)throw b}}function d(a){e=a,c()}var e=b(),f={};this.add=function(a,b){f[a]=b},this.has=function(a){return!!f[a]},this.isActive=function(){return e>0},this.current=function(a){return a?void d(e):e},this.start=function(){d(1)},this.next=function(){d(e+1)},this.end=function(){d(0)}}]).directive("stepTextPopup",["$tour",function(a){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tour/tour.html",link:function(b,c){b.isLastStep=function(){return!a.has(a.current()+1)},b.endTour=function(){c.remove(),a.end()},b.nextStep=function(){c.remove(),a.next()},b.$on("$locationChangeSuccess",b.endTour)}}}]).directive("stepText",["$position","$tooltip","$tour","$window",function(a,b,c,d){function e(a){var b=a[0].getBoundingClientRect();return b.top>=0&&b.left>=0&&b.bottom<=d.innerHeight-80&&b.right<=d.innerWidth}function f(b,f,g){var h=parseInt(g.stepIndex,10);if(c.isActive()&&h&&(c.add(h,g),h===c.current())){if(!e(f)){var i=a.offset(f);d.scrollTo(0,i.top-d.innerHeight/2)}return!0}return!1}return b("stepText","step",f)}]),angular.module("mm.foundation.typeahead",["mm.foundation.position","mm.foundation.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error("Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_' but got '"+c+"'.");return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(a,b,c,d,e,f,g){var h=[9,13,27,38,40];return{require:"ngModel",link:function(i,j,k,l){var m,n=i.$eval(k.typeaheadMinLength)||1,o=i.$eval(k.typeaheadWaitMs)||0,p=i.$eval(k.typeaheadEditable)!==!1,q=b(k.typeaheadLoading).assign||angular.noop,r=b(k.typeaheadOnSelect),s=k.typeaheadInputFormatter?b(k.typeaheadInputFormatter):void 0,t=k.typeaheadAppendToBody?b(k.typeaheadAppendToBody):!1,u=b(k.ngModel).assign,v=g.parse(k.typeahead),w=angular.element("
    ");w.attr({matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(k.typeaheadTemplateUrl)&&w.attr("template-url",k.typeaheadTemplateUrl);var x=i.$new();i.$on("$destroy",function(){x.$destroy()});var y=function(){x.matches=[],x.activeIdx=-1},z=function(a){var b={$viewValue:a};q(i,!0),c.when(v.source(i,b)).then(function(c){if(a===l.$viewValue&&m)if(c.length>0){x.activeIdx=0,x.matches.length=0;for(var d=0;d=n?o>0?(A&&d.cancel(A),A=d(function(){z(a)},o)):z(a):(q(i,!1),y()),p?a:a?void l.$setValidity("editable",!1):(l.$setValidity("editable",!0),a)}),l.$formatters.push(function(a){var b,c,d={};return s?(d.$model=a,s(i,d)):(d[v.itemName]=a,b=v.viewMapper(i,d),d[v.itemName]=void 0,c=v.viewMapper(i,d),b!==c?b:a)}),x.select=function(a){var b,c,d={};d[v.itemName]=c=x.matches[a].model,b=v.modelMapper(i,d),u(i,b),l.$setValidity("editable",!0),r(i,{$item:c,$model:b,$label:v.viewMapper(i,d)}),y(),j[0].focus()},j.bind("keydown",function(a){0!==x.matches.length&&-1!==h.indexOf(a.which)&&(a.preventDefault(),40===a.which?(x.activeIdx=(x.activeIdx+1)%x.matches.length,x.$digest()):38===a.which?(x.activeIdx=(x.activeIdx?x.activeIdx:x.matches.length)-1,x.$digest()):13===a.which||9===a.which?x.$apply(function(){x.select(x.activeIdx)}):27===a.which&&(a.stopPropagation(),y(),x.$digest()))}),j.bind("blur",function(){m=!1}),j.bind("focus",function(){m=!0});var B=function(a){j[0]!==a.target&&(y(),x.$digest())};e.bind("click",B),i.$on("$destroy",function(){e.unbind("click",B)});var C=a(w)(x);t?e.find("body").append(C):j.after(C)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(a,b,c,d){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(e,f,g){var h=d(g.templateUrl)(e.$parent)||"template/typeahead/typeahead-match.html";a.get(h,{cache:b}).success(function(a){f.replaceWith(c(a.trim())(e))})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?b.replace(new RegExp(a(c),"gi"),"$&"):b}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion-group.html",'
    \n {{heading}}\n
    \n
    \n')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion.html",'
    \n')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("template/alert/alert.html","
    \n \n ×\n
    \n")}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("template/modal/backdrop.html",'
    \n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(a){a.put("template/modal/window.html",'
    \n
    \n
    \n')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pager.html",'\n')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pagination.html",'\n')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-unsafe-popup.html",'\n \n \n\n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-popup.html",'\n \n \n\n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover.html",'
    \n \n
    \n

    \n

    \n
    \n
    \n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/bar.html",'\n')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progress.html",'
    \n')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progressbar.html",'
    \n \n
    \n')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("template/rating/rating.html",'\n \n\n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tab.html",'
    \n {{heading}}\n
    \n')}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n')}]),angular.module("template/topbar/has-dropdown.html",[]).run(["$templateCache",function(a){a.put("template/topbar/has-dropdown.html",'
  • ')}]),angular.module("template/topbar/toggle-top-bar.html",[]).run(["$templateCache",function(a){a.put("template/topbar/toggle-top-bar.html",'')}]),angular.module("template/topbar/top-bar-dropdown.html",[]).run(["$templateCache",function(a){a.put("template/topbar/top-bar-dropdown.html",'')}]),angular.module("template/topbar/top-bar-section.html",[]).run(["$templateCache",function(a){a.put("template/topbar/top-bar-section.html",'
    ')}]),angular.module("template/topbar/top-bar.html",[]).run(["$templateCache",function(a){a.put("template/topbar/top-bar.html",'')}]),angular.module("template/tour/tour.html",[]).run(["$templateCache",function(a){a.put("template/tour/tour.html",'
    \n \n
    \n

    \n

    \n Next\n End\n ×\n
    \n
    \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html","
      \n"+'
    • \n
      \n
    • \n
    \n')}]); diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js index 07202027b4..94f56a20c5 100644 --- a/spec/javascripts/application_spec.js +++ b/spec/javascripts/application_spec.js @@ -8,7 +8,7 @@ //= require lodash.underscore.js //= require angular-flash.min.js //= require shared/ng-tags-input.min.js -//= require shared/mm-foundation-tpls-0.2.2.min.js +//= require shared/mm-foundation-tpls-0.8.0.min.js //= require textAngular.min.js //= require textAngular-sanitize.min.js //= require moment From fe739f6a8daa1d6ff1656aa041b67682ebb9f005 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 17 Apr 2016 14:09:37 +1000 Subject: [PATCH 007/110] angular-foundation tabs directive on shop pages is closed by default --- .../forgot_controller.js.coffee | 1 + .../authentication/login_controller.js.coffee | 1 + .../signup_controller.js.coffee | 2 ++ .../authentication_controller.js.coffee | 2 +- .../services/authentication_service.js.coffee | 2 +- .../darkswarm/services/navigation.js.coffee | 6 +++--- .../javascripts/templates/forgot.html.haml | 12 +++++------ .../javascripts/templates/login.html.haml | 20 +++++++++---------- .../javascripts/templates/signup.html.haml | 2 +- app/views/shopping_shared/_tabs.html.haml | 3 +-- 10 files changed, 27 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/authentication/forgot_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication/forgot_controller.js.coffee index 85920de958..85d2a699a3 100644 --- a/app/assets/javascripts/darkswarm/controllers/authentication/forgot_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/authentication/forgot_controller.js.coffee @@ -1,5 +1,6 @@ Darkswarm.controller "ForgotCtrl", ($scope, $http, $location, AuthenticationService) -> $scope.path = "/forgot" + $scope.active = $scope.isActive($scope.path) $scope.sent = false $scope.submit = -> diff --git a/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee index f91e137ca7..07bef90f84 100644 --- a/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee @@ -1,5 +1,6 @@ Darkswarm.controller "LoginCtrl", ($scope, $http, $window, AuthenticationService, Redirections, Loading) -> $scope.path = "/login" + $scope.active = $scope.isActive($scope.path) $scope.submit = -> Loading.message = t 'logging_in' diff --git a/app/assets/javascripts/darkswarm/controllers/authentication/signup_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication/signup_controller.js.coffee index 123079b6b5..dfa61f82b2 100644 --- a/app/assets/javascripts/darkswarm/controllers/authentication/signup_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/authentication/signup_controller.js.coffee @@ -1,5 +1,7 @@ Darkswarm.controller "SignupCtrl", ($scope, $http, $window, $location, Redirections, AuthenticationService) -> $scope.path = "/signup" + $scope.active = $scope.isActive($scope.path) + $scope.errors = email: null password: null diff --git a/app/assets/javascripts/darkswarm/controllers/authentication_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication_controller.js.coffee index 1a22f34b0c..7d65b853c7 100644 --- a/app/assets/javascripts/darkswarm/controllers/authentication_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/authentication_controller.js.coffee @@ -3,5 +3,5 @@ Darkswarm.controller "AuthenticationCtrl", ($scope, AuthenticationService, Spree $scope.toggle = AuthenticationService.toggle $scope.spree_user = SpreeUser.spree_user - $scope.active = AuthenticationService.active + $scope.isActive = AuthenticationService.isActive $scope.select = AuthenticationService.select diff --git a/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee b/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee index 10f75675f8..478f5f28e1 100644 --- a/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee +++ b/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee @@ -22,7 +22,7 @@ Darkswarm.factory "AuthenticationService", (Navigation, $modal, $location, Redir @selectedPath = path Navigation.navigate @selectedPath - active: Navigation.active + isActive: Navigation.isActive close: -> if location.pathname in ["/", "/checkout"] diff --git a/app/assets/javascripts/darkswarm/services/navigation.js.coffee b/app/assets/javascripts/darkswarm/services/navigation.js.coffee index fd59d0f348..e2b04f3ad1 100644 --- a/app/assets/javascripts/darkswarm/services/navigation.js.coffee +++ b/app/assets/javascripts/darkswarm/services/navigation.js.coffee @@ -1,9 +1,9 @@ Darkswarm.factory 'Navigation', ($location, $window) -> new class Navigation - path: null + path: null - active: (path)-> - $location.path() == path + isActive: (path)-> + $location.path() == path navigate: (path)=> @path = path diff --git a/app/assets/javascripts/templates/forgot.html.haml b/app/assets/javascripts/templates/forgot.html.haml index c1990d778a..f85b91ab0a 100644 --- a/app/assets/javascripts/templates/forgot.html.haml +++ b/app/assets/javascripts/templates/forgot.html.haml @@ -1,6 +1,6 @@ %tab#forgot{"ng-controller" => "ForgotCtrl", heading: "{{'forgot_password' | t}}", - active: "active(path)", + active: "active", select: "select(path)"} %form{"ng-submit" => "submit()"} @@ -13,18 +13,18 @@ %div{"ng-show" => "!sent"} .alert-box.alert{"ng-show" => "errors != null"} {{ errors }} - + .row .large-12.columns %label{for: "email"} {{'signup_email' | t}} - %input.title.input-text{name: "email", + %input.title.input-text{name: "email", type: "email", id: "email", tabindex: 1, "ng-model" => "spree_user.email"} .row .large-12.columns - %input.button.primary{name: "commit", - tabindex: "3", - type: "submit", + %input.button.primary{name: "commit", + tabindex: "3", + type: "submit", value: "{{'reset_password' | t}}"} diff --git a/app/assets/javascripts/templates/login.html.haml b/app/assets/javascripts/templates/login.html.haml index 44d2e08ced..3d2f9a3e06 100644 --- a/app/assets/javascripts/templates/login.html.haml +++ b/app/assets/javascripts/templates/login.html.haml @@ -1,6 +1,6 @@ -%tab#login-content{"ng-controller" => "LoginCtrl", +%tab#login-content{"ng-controller" => "LoginCtrl", heading: "{{'label_login' | t}}", - active: "active(path)", + active: "active", select: "select(path)"} %form{"ng-submit" => "submit()"} .row @@ -10,7 +10,7 @@ .row .large-12.columns %label{for: "email"} {{'email' | t}} - %input.title.input-text{name: "email", + %input.title.input-text{name: "email", type: "email", id: "email", tabindex: 1, @@ -18,7 +18,7 @@ .row .large-12.columns %label{for: "password"} {{'password' | t}} - %input.title.input-text{name: "password", + %input.title.input-text{name: "password", type: "password", id: "password", autocomplete: "off", @@ -26,15 +26,15 @@ "ng-model" => "spree_user.password"} .row .large-12.columns - %input{name: "remember_me", - type: "checkbox", - id: "remember_me", + %input{name: "remember_me", + type: "checkbox", + id: "remember_me", value: "1", "ng-model" => "spree_user.remember_me"} %label{for: "remember_me"} {{'remember_me' | t}} .row .large-12.columns - %input.button.primary{name: "commit", - tabindex: "3", - type: "submit", + %input.button.primary{name: "commit", + tabindex: "3", + type: "submit", value: "{{'label_login' | t}}"} diff --git a/app/assets/javascripts/templates/signup.html.haml b/app/assets/javascripts/templates/signup.html.haml index 2e780aabb5..c71d9453bd 100644 --- a/app/assets/javascripts/templates/signup.html.haml +++ b/app/assets/javascripts/templates/signup.html.haml @@ -1,6 +1,6 @@ %tab#sign-up-content{"ng-controller" => "SignupCtrl", heading: "{{'label_signup' | t}}", - active: "active(path)", + active: 'active', select: "select(path)"} %form{"ng-submit" => "submit()"} .row diff --git a/app/views/shopping_shared/_tabs.html.haml b/app/views/shopping_shared/_tabs.html.haml index 8d0d9e021d..0d3cd79939 100644 --- a/app/views/shopping_shared/_tabs.html.haml +++ b/app/views/shopping_shared/_tabs.html.haml @@ -1,6 +1,6 @@ #tabs{"ng-controller" => "TabsCtrl", "ng-cloak" => true} .row - %tabset + %tabset{ 'open-on-load' => 'false' } -# Build all tabs. - for name, heading_cols in { about: [t(:shopping_tabs_about, distributor: current_distributor.name), 6], producers: [t(:label_producers),2], @@ -10,7 +10,6 @@ - heading, cols = heading_cols %tab.columns{heading: heading, id: "tab_#{name}", - active: "active(\'#{name}\')", select: "toggle(\'#{name}\')", class: "small-12 medium-#{cols}" } = render "shopping_shared/#{name}" From 5de9eed48ada02c7994753f8e7d95c30c08c87dd Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 17 Apr 2016 15:17:42 +1000 Subject: [PATCH 008/110] Fixing broken instagram link --- app/views/shopping_shared/_contact.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shopping_shared/_contact.html.haml b/app/views/shopping_shared/_contact.html.haml index 100d64d958..405c958768 100644 --- a/app/views/shopping_shared/_contact.html.haml +++ b/app/views/shopping_shared/_contact.html.haml @@ -57,6 +57,6 @@ - unless current_distributor.instagram.blank? %span - %a{href: "http://instagram.com.#{current_distributor.instagram}", target: "_blank" } + %a{href: "http://instagram.com/#{current_distributor.instagram}", target: "_blank" } %i.ofn-i_043-instagram / = current_distributor.instagram From e5ca494db83e067f6f6863619561d5e7e8206bbd Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 17 Apr 2016 19:28:04 +1000 Subject: [PATCH 009/110] Replacing bindonce with native Angularjs syntax in Darkswarm Involved changing bo-text -> ng-bind, bo-href-i -> ng-href, bo-src-i -> ng-src and ng-html -> ng-bind-html --- .../javascripts/darkswarm/all.js.coffee | 1 - .../javascripts/darkswarm/darkswarm.js.coffee | 1 - .../templates/filter_selector.html.haml | 4 +- .../templates/partials/contact.html.haml | 14 ++-- .../partials/enterprise_details.html.haml | 6 +- .../partials/enterprise_header.html.haml | 20 +++--- .../templates/partials/follow.html.haml | 29 ++++---- .../templates/partials/hub_details.html.haml | 18 ++--- .../partials/producer_details.html.haml | 20 +++--- .../shop_variant_no_group_buy.html.haml | 2 +- .../shop_variant_with_group_buy.html.haml | 2 +- .../templates/price_breakdown.html.haml | 33 +++++---- .../templates/product_modal.html.haml | 12 ++-- .../templates/registration/details.html.haml | 10 +-- .../templates/registration/type.html.haml | 4 +- .../templates/shop_variant.html.haml | 4 +- app/views/groups/_contact.html.haml | 12 ++-- app/views/groups/index.html.haml | 8 +-- app/views/groups/show.html.haml | 4 +- app/views/home/_fat.html.haml | 26 +++---- app/views/home/_skinny.html.haml | 40 +++++------ app/views/producers/_fat.html.haml | 70 +++++++++---------- app/views/producers/_skinny.html.haml | 18 ++--- app/views/producers/index.html.haml | 2 +- app/views/shop/products/_form.html.haml | 7 +- app/views/shop/products/_summary.html.haml | 6 +- app/views/shopping_shared/_about.html.haml | 6 +- app/views/spree/users/_fat.html.haml | 34 ++++----- app/views/spree/users/_skinny.html.haml | 4 +- app/views/spree/users/show.html.haml | 4 +- 30 files changed, 206 insertions(+), 215 deletions(-) diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index f5429da413..600044f09c 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -12,7 +12,6 @@ #= require angular-scroll.min.js #= require angular-google-maps.min.js #= require ../shared/mm-foundation-tpls-0.8.0.min.js -#= require ../shared/bindonce.min.js #= require ../shared/ng-infinite-scroll.min.js #= require ../shared/angular-local-storage.js #= require ../shared/angular-slideables.js diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index a062ead058..ce5a73d69e 100644 --- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee +++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee @@ -1,7 +1,6 @@ window.Darkswarm = angular.module("Darkswarm", ["ngResource", 'mm.foundation', 'angularLocalStorage', - 'pasvaz.bindonce', 'infinite-scroll', 'angular-flash.service', 'templates', diff --git a/app/assets/javascripts/templates/filter_selector.html.haml b/app/assets/javascripts/templates/filter_selector.html.haml index a53c4f44db..cd4135ebee 100644 --- a/app/assets/javascripts/templates/filter_selector.html.haml +++ b/app/assets/javascripts/templates/filter_selector.html.haml @@ -1,4 +1,4 @@ -%ul{ bindonce: true } +%ul %active-selector{ ng: { repeat: "selector in allSelectors", show: "ifDefined(selector.fits, true)" } } %render-svg{path: "{{selector.object.icon}}", ng: { if: "selector.object.icon"} } - %span{"bo-text" => "selector.object.name"} + %span{"ng-bind" => "::selector.object.name"} diff --git a/app/assets/javascripts/templates/partials/contact.html.haml b/app/assets/javascripts/templates/partials/contact.html.haml index 12aa5ff061..d173bcf366 100644 --- a/app/assets/javascripts/templates/partials/contact.html.haml +++ b/app/assets/javascripts/templates/partials/contact.html.haml @@ -1,11 +1,11 @@ -%div.contact-container{bindonce: true} - %div.modal-centered{"bo-if" => "enterprise.email_address || enterprise.website || enterprise.phone"} +%div.contact-container + %div.modal-centered{"ng-if" => "::enterprise.email_address || enterprise.website || enterprise.phone"} %p.modal-header {{'contact' | t}} - %p{"bo-if" => "enterprise.phone", "bo-text" => "enterprise.phone"} + %p{"ng-if" => "::enterprise.phone", "ng-bind" => "::enterprise.phone"} - %p.word-wrap{"ng-if" => "enterprise.email_address"} - %a{"bo-href" => "enterprise.email_address | stripUrl", target: "_blank", mailto: true} - %span.email{"bo-bind" => "enterprise.email_address | stripUrl"} + %p.word-wrap{"ng-if" => "::enterprise.email_address"} + %a{"ng-href" => "{{::enterprise.email_address | stripUrl}}", target: "_blank", mailto: true} + %span.email{"ng-bind" => "::enterprise.email_address | stripUrl"} %p.word-wrap{"ng-if" => "enterprise.website"} - %a{"bo-href-i" => "http://{{enterprise.website | stripUrl}}", target: "_blank", "bo-bind" => "enterprise.website | stripUrl"} + %a{"ng-href" => "http://{{::enterprise.website | stripUrl}}", target: "_blank", "ng-bind" => "::enterprise.website | stripUrl"} diff --git a/app/assets/javascripts/templates/partials/enterprise_details.html.haml b/app/assets/javascripts/templates/partials/enterprise_details.html.haml index fc0a638c8d..547e233958 100644 --- a/app/assets/javascripts/templates/partials/enterprise_details.html.haml +++ b/app/assets/javascripts/templates/partials/enterprise_details.html.haml @@ -1,4 +1,4 @@ -.row{bindonce: true} +.row .small-12.large-8.columns / TODO: Rob add logic for taxons and properties too: / %div{"ng-if" => "enterprise.long_description.length > 0 || enterprise.logo"} @@ -22,8 +22,8 @@ -# / TODO: Rob - need popover, use will's directive or this? http://pineconellc.github.io/angular-foundation/ -# .about-container.pad-top - %img.enterprise-logo{"bo-src" => "enterprise.logo", "bo-if" => "enterprise.logo"} - %p.text-small{"ng-bind-html" => "enterprise.long_description"} + %img.enterprise-logo{"ng-src" => "::enterprise.logo", "ng-if" => "::enterprise.logo"} + %p.text-small{"ng-bind-html" => "::enterprise.long_description"} .small-12.large-4.columns %ng-include{src: "'partials/contact.html'"} %ng-include{src: "'partials/follow.html'"} diff --git a/app/assets/javascripts/templates/partials/enterprise_header.html.haml b/app/assets/javascripts/templates/partials/enterprise_header.html.haml index 44175a57fe..39b967be06 100644 --- a/app/assets/javascripts/templates/partials/enterprise_header.html.haml +++ b/app/assets/javascripts/templates/partials/enterprise_header.html.haml @@ -1,13 +1,13 @@ -.highlight{bindonce: true, "ng-class" => "{'is_distributor' : enterprise.is_distributor}"} +.highlight{"ng-class" => "::{'is_distributor' : enterprise.is_distributor}"} .highlight-top.row .small-12.medium-7.large-8.columns - %h3{"ng-if" => "enterprise.is_distributor"} - %a{"bo-href" => "enterprise.path", "ofn-change-hub" => "enterprise"} - %i{"ng-class" => "enterprise.icon_font"} - %span{"bo-text" => "enterprise.name"} - %h3{"ng-if" => "!enterprise.is_distributor", "ng-class" => "{'is_producer' : enterprise.is_primary_producer}"} - %i{"ng-class" => "enterprise.icon_font"} - %span{"bo-text" => "enterprise.name"} + %h3{"ng-if" => "::enterprise.is_distributor"} + %a{"ng-href" => "{{::enterprise.path}}", "ofn-change-hub" => "enterprise"} + %i{"ng-class" => "::enterprise.icon_font"} + %span{"ng-bind" => "::enterprise.name"} + %h3{"ng-if" => "::!enterprise.is_distributor", "ng-class" => "::{'is_producer' : enterprise.is_primary_producer}"} + %i{"ng-class" => "::enterprise.icon_font"} + %span{"ng-bind" => "::enterprise.name"} .small-12.medium-5.large-4.columns.text-right.small-only-text-left - %p{"bo-bind" => "[enterprise.address.city, enterprise.address.state_name] | printArray"} - %img.hero-img{"bo-src" => "enterprise.promo_image"} + %p{"ng-bind" => "::[enterprise.address.city, enterprise.address.state_name] | printArray"} + %img.hero-img{"ng-src" => "::enterprise.promo_image"} diff --git a/app/assets/javascripts/templates/partials/follow.html.haml b/app/assets/javascripts/templates/partials/follow.html.haml index c04567a357..a47054d1f3 100644 --- a/app/assets/javascripts/templates/partials/follow.html.haml +++ b/app/assets/javascripts/templates/partials/follow.html.haml @@ -1,19 +1,18 @@ -%div.modal-centered{bindonce: true, "bo-if" => "enterprise.twitter || enterprise.facebook || enterprise.linkedin || enterprise.instagram"} +%div.modal-centered{ "ng-if" => "::enterprise.twitter || enterprise.facebook || enterprise.linkedin || enterprise.instagram"} %p.modal-header {{'follow' | t}} .follow-icons - %span{"bo-if" => "enterprise.twitter"} - %a{"bo-href-i" => "http://twitter.com/{{enterprise.twitter}}", target: "_blank"} + %span{"ng-if" => "::enterprise.twitter"} + %a{"ng-href" => "http://twitter.com/{{::enterprise.twitter}}", target: "_blank"} %i.ofn-i_041-twitter - - %span{"bo-if" => "enterprise.facebook"} - %a{"bo-href-i" => "http://{{enterprise.facebook | stripUrl}}", target: "_blank"} - %i.ofn-i_044-facebook - - %span{"bo-if" => "enterprise.linkedin"} - %a{"bo-href-i" => "http://{{enterprise.linkedin | stripUrl}}", target: "_blank"} - %i.ofn-i_042-linkedin - - %span{"bo-if" => "enterprise.instagram"} - %a{"bo-href-i" => "http://instagram.com/{{enterprise.instagram}}", target: "_blank"} - %i.ofn-i_043-instagram + %span{"ng-if" => "::enterprise.facebook"} + %a{"ng-href" => "http://{{::enterprise.facebook | stripUrl}}", target: "_blank"} + %i.ofn-i_044-facebook + + %span{"ng-if" => "::enterprise.linkedin"} + %a{"ng-href" => "http://{{::enterprise.linkedin | stripUrl}}", target: "_blank"} + %i.ofn-i_042-linkedin + + %span{"ng-if" => "::enterprise.instagram"} + %a{"ng-href" => "http://instagram.com/{{::enterprise.instagram}}", target: "_blank"} + %i.ofn-i_043-instagram diff --git a/app/assets/javascripts/templates/partials/hub_details.html.haml b/app/assets/javascripts/templates/partials/hub_details.html.haml index fa75f72253..e3a0234df1 100644 --- a/app/assets/javascripts/templates/partials/hub_details.html.haml +++ b/app/assets/javascripts/templates/partials/hub_details.html.haml @@ -1,24 +1,24 @@ -.row.pad-top{bindonce: true, ng: { if: 'enterprise.is_distributor' } } +.row.pad-top{ng: { if: 'enterprise.is_distributor' } } .cta-container.small-12.columns .row .small-4.columns %label{"active-table-hub-link" => "enterprise", change: "{{'change_shop' | t}}", shop: "{{'shop_at' | t}}"} .small-8.columns.right - %label.right{"bo-if" => "enterprise.pickup || enterprise.delivery"} + %label.right{"ng-if" => "::enterprise.pickup || enterprise.delivery"} {{'hubs_delivery_options' | t}}: - %span{"bo-if" => "enterprise.pickup"} + %span{"ng-if" => "::enterprise.pickup"} %i.ofn-i_038-takeaway {{'hubs_pickup' | t}} - %span{"bo-if" => "enterprise.delivery"} + %span{"ng-if" => "::enterprise.delivery"} %i.ofn-i_039-delivery {{'hubs_delivery' | t}} .row .columns.small-12 - %a.cta-hub{"bo-href" => "enterprise.path", + %a.cta-hub{"ng-href" => "{{::enterprise.path}}", "ng-class" => "{primary: enterprise.active, secondary: !enterprise.active}", "ofn-change-hub" => "enterprise"} - %i.ofn-i_033-open-sign{"bo-if" => "enterprise.active"} - %i.ofn-i_032-closed-sign{"bo-if" => "!enterprise.active"} - .hub-name{"bo-text" => "enterprise.name"} - .button-address{"bo-bind" => "[enterprise.address.city, enterprise.address.state_name] | printArray"} + %i.ofn-i_033-open-sign{"ng-if" => "::enterprise.active"} + %i.ofn-i_032-closed-sign{"ng-if" => "::!enterprise.active"} + .hub-name{"ng-bind" => "::enterprise.name"} + .button-address{"ng-bind" => "::[enterprise.address.city, enterprise.address.state_name] | printArray"} / %i.ofn-i_007-caret-right diff --git a/app/assets/javascripts/templates/partials/producer_details.html.haml b/app/assets/javascripts/templates/partials/producer_details.html.haml index 599b977dfc..cb7ee70482 100644 --- a/app/assets/javascripts/templates/partials/producer_details.html.haml +++ b/app/assets/javascripts/templates/partials/producer_details.html.haml @@ -1,20 +1,20 @@ -# Show places to buy products from this producer, when there are any -# Do not show this for producer shops selling only their own produce, -# Since a shopping link will already have been displayed in hub_details.html.haml -.row.active_table_row.pad-top{bindonce: true, "ng-if" => "enterprise.is_primary_producer && enterprise.hubs.length > 0 && !(enterprise.hubs.length == 1 && enterprise.hubs[0] == enterprise)"} +.row.active_table_row.pad-top{ "ng-if" => "enterprise.is_primary_producer && enterprise.hubs.length > 0 && !(enterprise.hubs.length == 1 && enterprise.hubs[0] == enterprise)"} .columns.small-12 .row .columns.small-12.fat - %div{"bo-if" => "enterprise.name"} - %label{"bo-html" => "'shop_for_products_html' | t:{enterprise: enterprise.name}"} - %div.show-for-medium-up{"bo-if" => "!enterprise.name"} + %div{"ng-if" => "::enterprise.name"} + %label{"ng-html" => "::'shop_for_products_html' | t:{enterprise: enterprise.name}"} + %div.show-for-medium-up{"ng-if" => "::!enterprise.name"}   .row.cta-container .columns.small-12 %a.cta-hub{"ng-repeat" => "hub in enterprise.hubs | filter:{id: '!'+enterprise.id} | orderBy:'-active'", - "bo-href" => "hub.path", "ofn-empties-cart" => "hub", - "bo-class" => "{primary: hub.active, secondary: !hub.active}"} - %i.ofn-i_033-open-sign{"bo-if" => "hub.active"} - %i.ofn-i_032-closed-sign{"bo-if" => "!hub.active"} - .hub-name{"bo-text" => "hub.name"} - .button-address{"bo-bind" => "[hub.address.city, hub.address.state_name] | printArray"} + "ng-href" => "{{::hub.path}}", "ofn-empties-cart" => "hub", + "ng-class" => "::{primary: hub.active, secondary: !hub.active}"} + %i.ofn-i_033-open-sign{"ng-if" => "::hub.active"} + %i.ofn-i_032-closed-sign{"ng-if" => "::!hub.active"} + .hub-name{"ng-bind" => "::hub.name"} + .button-address{"ng-bind" => "::[hub.address.city, hub.address.state_name] | printArray"} diff --git a/app/assets/javascripts/templates/partials/shop_variant_no_group_buy.html.haml b/app/assets/javascripts/templates/partials/shop_variant_no_group_buy.html.haml index 1153d93b0f..3a52ba9d07 100644 --- a/app/assets/javascripts/templates/partials/shop_variant_no_group_buy.html.haml +++ b/app/assets/javascripts/templates/partials/shop_variant_no_group_buy.html.haml @@ -1,4 +1,4 @@ -.small-5.medium-3.large-3.columns.text-right{"bo-if" => "!variant.product.group_buy"} +.small-5.medium-3.large-3.columns.text-right{"ng-if" => "::!variant.product.group_buy"} %input{type: :number, integer: true, diff --git a/app/assets/javascripts/templates/partials/shop_variant_with_group_buy.html.haml b/app/assets/javascripts/templates/partials/shop_variant_with_group_buy.html.haml index 6601f0c019..d4c88092b6 100644 --- a/app/assets/javascripts/templates/partials/shop_variant_with_group_buy.html.haml +++ b/app/assets/javascripts/templates/partials/shop_variant_with_group_buy.html.haml @@ -1,4 +1,4 @@ -.small-5.medium-3.large-3.columns.text-right{"bo-if" => "variant.product.group_buy"} +.small-5.medium-3.large-3.columns.text-right{"ng-if" => "::variant.product.group_buy"} %span.bulk-input-container %span.bulk-input %input.bulk.first{type: :number, diff --git a/app/assets/javascripts/templates/price_breakdown.html.haml b/app/assets/javascripts/templates/price_breakdown.html.haml index 993c20bc9b..fa884cd4f0 100644 --- a/app/assets/javascripts/templates/price_breakdown.html.haml +++ b/app/assets/javascripts/templates/price_breakdown.html.haml @@ -1,38 +1,37 @@ -.joyride-tip-guide.price_breakdown{bindonce: true, "ng-class" => "{ in: tt_isOpen, fade: tt_animation }"} +.joyride-tip-guide.price_breakdown{"ng-class" => "{ in: tt_isOpen, fade: tt_animation }"} %span.joyride-nub.right .joyride-content-wrapper .collapsed{"ng-show" => "!expanded"} %price-percentage{percentage: 'variant.basePricePercentage'} - %a{"ng-click" => "expanded = !expanded"} - %span{"bo-text" => "'price_breakdown' | t"} + %a{"ng-click" => "expanded = !expanded"} + %span{"ng-bind" => "::'price_breakdown' | t"} %i.ofn-i_005-caret-down .expanded{"ng-show" => "expanded"} %ul %li.cost .right {{ variant.price | localizeCurrency }} - %span{"bo-text" => "'item_cost' | t"} - %li.admin-fee{"bo-if" => "variant.fees.admin"} + %span{"ng-bind" => "::'item_cost' | t"} + %li.admin-fee{"ng-if" => "::variant.fees.admin"} .right {{ variant.fees.admin | localizeCurrency }} - %span{"bo-text" => "'admin_fee' | t"} - %li.sales-fee{"bo-if" => "variant.fees.sales"} + %span{"ng-bind" => "::'admin_fee' | t"} + %li.sales-fee{"ng-if" => "::variant.fees.sales"} .right {{ variant.fees.sales | localizeCurrency }} - %span{"bo-text" => "'sales_fee' | t"} - %li.packing-fee{"bo-if" => "variant.fees.packing"} + %span{"ng-bind" => "::'sales_fee' | t"} + %li.packing-fee{"ng-if" => "::variant.fees.packing"} .right {{ variant.fees.packing | localizeCurrency }} - %span{"bo-text" => "'packing_fee' | t"} - %li.transport-fee{"bo-if" => "variant.fees.transport"} + %span{"ng-bind" => "::'packing_fee' | t"} + %li.transport-fee{"ng-if" => "::variant.fees.transport"} .right {{ variant.fees.transport | localizeCurrency }} - %span{"bo-text" => "'transport_fee' | t"} - %li.fundraising-fee{"bo-if" => "variant.fees.fundraising"} + %span{"ng-bind" => "::'transport_fee' | t"} + %li.fundraising-fee{"ng-if" => "::variant.fees.fundraising"} .right {{ variant.fees.fundraising | localizeCurrency }} - %span{"bo-text" => "'fundraising_fee' | t"} + %span{"ng-bind" => "::'fundraising_fee' | t"} %li.total %strong .right = {{ variant.price_with_fees | localizeCurrency }}   - %a{"ng-click" => "expanded = !expanded"} - %span{"bo-text" => "'price_graph' | t"} + %a{"ng-click" => "expanded = !expanded"} + %span{"ng-bind" => "::'price_graph' | t"} %i.ofn-i_006-caret-up - diff --git a/app/assets/javascripts/templates/product_modal.html.haml b/app/assets/javascripts/templates/product_modal.html.haml index e13f6df60d..f4839f6512 100644 --- a/app/assets/javascripts/templates/product_modal.html.haml +++ b/app/assets/javascripts/templates/product_modal.html.haml @@ -1,10 +1,10 @@ -.row{bindonce: true} +.row .columns.small-12.large-6.product-header - %h3{"bo-text" => "product.name"} + %h3{"ng-bind" => "::product.name"} %span %em {{'products_from' | t}} - %span{"bo-text" => "enterprise.name"} + %span{"ng-bind" => "::enterprise.name"} %br @@ -16,11 +16,11 @@ %div{"ng-if" => "product.description"} %hr - %p.text-small{"bo-text" => "product.description"} + %p.text-small{"ng-bind" => "::product.description"} %hr .columns.small-12.large-6 - %img.product-img{"bo-src" => "product.largeImage", "bo-if" => "product.largeImage"} - %img.product-img.placeholder{"bo-src" => "'/assets/noimage/large.png'", "bo-if" => "!product.largeImage"} + %img.product-img{"ng-src" => "{{::product.largeImage}}", "ng-if" => "::product.largeImage"} + %img.product-img.placeholder{ src: "/assets/noimage/large.png", "ng-if" => "::!product.largeImage"} %ng-include{src: "'partials/close.html'"} diff --git a/app/assets/javascripts/templates/registration/details.html.haml b/app/assets/javascripts/templates/registration/details.html.haml index f03a48ec59..6bd3fc02ba 100644 --- a/app/assets/javascripts/templates/registration/details.html.haml +++ b/app/assets/javascripts/templates/registration/details.html.haml @@ -1,19 +1,19 @@ -.container#registration-details{bindonce: true} +.container#registration-details %ng-include{ src: "'registration/steps.html'" } .row .small-12.columns %header %h2 {{'registration_detail_headline' | t}} - %h5{ bo: { if: "enterprise.type != 'own'" } } {{'registration_detail_enterprise' | t}} - %h5{ bo: { if: "enterprise.type == 'own'" } } {{'registration_detail_producer' | t}} + %h5{ ng: { if: "::enterprise.type != 'own'" } } {{'registration_detail_enterprise' | t}} + %h5{ ng: { if: "::enterprise.type == 'own'" } } {{'registration_detail_producer' | t}} %form{ name: 'details', novalidate: true, ng: { controller: "RegistrationFormCtrl", submit: "selectIfValid('contact',details)" } } .row .small-12.medium-9.large-12.columns.end .field - %label{ for: 'enterprise_name', bo: { if: "enterprise.type != 'own'" } } {{'registration_detail_name_enterprise' | t}} - %label{ for: 'enterprise_name', bo: { if: "enterprise.type == 'own'" } } {{'registration_detail_name_producer' | t}} + %label{ for: 'enterprise_name', ng: { if: "::enterprise.type != 'own'" } } {{'registration_detail_name_enterprise' | t}} + %label{ for: 'enterprise_name', ng: { if: "::enterprise.type == 'own'" } } {{'registration_detail_name_producer' | t}} %input.chunky{ id: 'enterprise_name', name: 'name', placeholder: "{{'registration_detail_name_placeholder' | t}}", required: true, ng: { model: 'enterprise.name' } } %span.error{ ng: { show: "details.name.$error.required && submitted" } } {{'registration_detail_name_error' | t}} diff --git a/app/assets/javascripts/templates/registration/type.html.haml b/app/assets/javascripts/templates/registration/type.html.haml index c63b40239e..85a81398d7 100644 --- a/app/assets/javascripts/templates/registration/type.html.haml +++ b/app/assets/javascripts/templates/registration/type.html.haml @@ -1,4 +1,4 @@ -.container#registration-type{bindonce: true} +.container#registration-type %ng-include{ src: "'registration/steps.html'" } @@ -10,7 +10,7 @@ {{'registration_type_question' | t}} %form{ name: 'type', novalidate: true, ng: { controller: "RegistrationFormCtrl", submit: "create(type)" } } - .row#enterprise-types{ 'data-equalizer' => true, bo: { if: "enterprise.type != 'own'" } } + .row#enterprise-types{ 'data-equalizer' => true, ng: { if: "::enterprise.type != 'own'" } } .small-12.columns.field .row .small-12.medium-6.large-6.columns{ 'data-equalizer-watch' => true } diff --git a/app/assets/javascripts/templates/shop_variant.html.haml b/app/assets/javascripts/templates/shop_variant.html.haml index 2c9eb932d5..1d06ffbb39 100644 --- a/app/assets/javascripts/templates/shop_variant.html.haml +++ b/app/assets/javascripts/templates/shop_variant.html.haml @@ -2,16 +2,14 @@ .small-12.medium-4.large-4.columns.variant-name .table-cell .inline {{ variant.name_to_display }} - .bulk-buy.inline{"bo-if" => "variant.product.group_buy"} + .bulk-buy.inline{"ng-if" => "::variant.product.group_buy"} %i.ofn-i_056-bulk>< %em>< \ {{'bulk' | t}} - %ng-include{src: "'partials/shop_variant_no_group_buy.html'"} %ng-include{src: "'partials/shop_variant_with_group_buy.html'"} - .small-3.medium-1.large-1.columns.variant-unit .table-cell %em {{ variant.unit_to_display }} diff --git a/app/views/groups/_contact.html.haml b/app/views/groups/_contact.html.haml index 320da464bd..5c1e3aef5e 100644 --- a/app/views/groups/_contact.html.haml +++ b/app/views/groups/_contact.html.haml @@ -1,4 +1,4 @@ -%div.contact-container{bindonce: true} +%div.contact-container - if @group.email.present? || @group.website.present? || @group.phone.present? %div.modal-centered %p.modal-header @@ -16,12 +16,12 @@ =link_to_service "http://", @group.website do = t :groups_contact_website -%div{bindonce: true} +%div - if @group.facebook.present? || @group.twitter.present? || @group.linkedin.present? || @group.instagram.present? %div.modal-centered.pad-top %p.modal-header = t :groups_contact_web - .follow-icons{bindonce: true} + .follow-icons =link_to_service "http://twitter.com/", @group.twitter do %i.ofn-i_041-twitter =link_to_service "https://www.facebook.com/", @group.facebook do @@ -30,8 +30,8 @@ %i.ofn-i_042-linkedin =link_to_service "http://instagram.com/", @group.instagram do %i.ofn-i_043-instagram - -%div{bindonce: true} + +%div - if @group.address1.present? || @group.city.present? %div.modal-centered.pad-top %p.modal-header @@ -46,5 +46,3 @@ = @group.city = @group.state = @group.zipcode - - diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml index afb19e1b91..ad93d4f366 100644 --- a/app/views/groups/index.html.haml +++ b/app/views/groups/index.html.haml @@ -25,14 +25,14 @@ .group.animate-repeat{"ng-repeat" => "group in groups = (Groups.groups | groups:query | orderBy:order)", name: "group{{group.id}}", id: "group{{group.id}}"} - .row.pad-top{bindonce: true} + .row.pad-top .small-12.medium-6.columns .groups-header - %a{"bo-href-i" => "/groups/{{group.permalink}}"} + %a{"ng-href" => "/groups/{{::group.permalink}}"} %i.ofn-i_035-groups - %span.group-name{"bo-text" => "group.name"} + %span.group-name{"ng-bind" => "::group.name"} .small-3.medium-2.columns - %p{"bo-text" => "group.state"} + %p{"ng-bind" => "::group.state"} .small-9.medium-4.columns.groups-icons %p %link-to-service.ofn-i_050-mail-circle{service: '""', ref: 'group.email.split("").reverse().join("")', mailto: true} diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index b5222e5f86..8d5385f1f4 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -62,7 +62,7 @@ = render partial: "shared/components/enterprise_search" = render partial: "producers/filters" - .row{bindonce: true} + .row .small-12.columns .active_table %producer.active_table_node.row.animate-repeat{id: "{{producer.path}}", @@ -89,7 +89,7 @@ = render partial: "shared/components/enterprise_search" = render partial: "hub_filters" - .row{bindonce: true} + .row .small-12.columns .active_table %hub.active_table_node.row.animate-repeat{id: "{{hub.hash}}", diff --git a/app/views/home/_fat.html.haml b/app/views/home/_fat.html.haml index b96b13885e..4e90771cc1 100644 --- a/app/views/home/_fat.html.haml +++ b/app/views/home/_fat.html.haml @@ -1,45 +1,45 @@ -.row.active_table_row{"ng-show" => "open()", "ng-click" => "toggle($event)", "ng-class" => "{'open' : !ofn-i_032-closed-sign()}", bindonce: true} +.row.active_table_row{"ng-show" => "open()", "ng-click" => "toggle($event)", "ng-class" => "{'open' : !ofn-i_032-closed-sign()}"} .columns.small-12.medium-6.large-5.fat - %div{"bo-if" => "hub.taxons"} + %div{"ng-if" => "::hub.taxons"} %label = t :hubs_buy .trans-sentence %span.fat-taxons{"ng-repeat" => "taxon in hub.taxons"} %render-svg{path: "{{taxon.icon}}"} - %span{"bo-text" => "taxon.name"} - %div.show-for-medium-up{"bo-if" => "hub.taxons.length==0"} + %span{"ng-bind" => "::taxon.name"} + %div.show-for-medium-up{"ng-if" => "::hub.taxons.length==0"}   .columns.small-12.medium-3.large-2.fat - %div{"bo-if" => "hub.pickup || hub.delivery"} + %div{"ng-if" => "::(hub.pickup || hub.delivery)"} %label = t :hubs_delivery_options %ul.small-block-grid-2.medium-block-grid-1.large-block-grid-1 - %li.pickup{"bo-if" => "hub.pickup"} + %li.pickup{"ng-if" => "::hub.pickup"} %i.ofn-i_038-takeaway = t :hubs_pickup - %li.delivery{"bo-if" => "hub.delivery"} + %li.delivery{"ng-if" => "::hub.delivery"} %i.ofn-i_039-delivery = t :hubs_delivery .columns.small-12.medium-3.large-5.fat - %div{"bo-if" => "hub.producers"} + %div{"ng-if" => "::hub.producers"} %label = t :hubs_producers %ul.small-block-grid-2.medium-block-grid-1.large-block-grid-2{"ng-class" => "{'show-more-producers' : toggleMoreProducers}", "class" => "producers-list"} %li{"ng-repeat" => "enterprise in hub.producers | limitTo:7"} %enterprise-modal %i.ofn-i_036-producers - %span{"bo-text" => "enterprise.name"} - %li{"data-is-link" => "true", "class" => "more-producers-link", "bo-show" => "hub.producers.length>7"} + %span{"ng-bind" => "::enterprise.name"} + %li{"data-is-link" => "true", "class" => "more-producers-link", "ng-show" => "::hub.producers.length>7"} %a{"ng-click" => "toggleMoreProducers=!toggleMoreProducers"} .more + - %span{"bo-text" => "hub.producers.length-7"} + %span{"ng-bind" => "::hub.producers.length-7"} = t :label_more .less = t :label_less %li{"ng-repeat" => "enterprise in hub.producers.slice(7,hub.producers.length)", "class" => "additional-producer"} %enterprise-modal %i.ofn-i_036-producers - %span{"bo-text" => "enterprise.name"} - %div.show-for-medium-up{"bo-if" => "hub.producers.length==0"} + %span{"ng-bind" => "::enterprise.name"} + %div.show-for-medium-up{"ng-if" => "::hub.producers.length==0"}   diff --git a/app/views/home/_skinny.html.haml b/app/views/home/_skinny.html.haml index 85e200fb94..fbc008a5c6 100644 --- a/app/views/home/_skinny.html.haml +++ b/app/views/home/_skinny.html.haml @@ -1,46 +1,46 @@ -.row.active_table_row{"ng-if" => "hub.is_distributor", "ng-click" => "toggle($event)", "ng-class" => "{'closed' : !open(), 'is_distributor' : producer.is_distributor}", bindonce: true} +.row.active_table_row{"ng-if" => "hub.is_distributor", "ng-click" => "toggle($event)", "ng-class" => "{'closed' : !open(), 'is_distributor' : producer.is_distributor}"} .columns.small-12.medium-5.large-5.skinny-head - %a.hub{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub", "data-is-link" => "true"} - %i{bo: {class: "hub.icon_font"}} - %span.margin-top.hub-name-listing{"bo-bind" => "hub.name | truncate:40"} + %a.hub{"ng-href" => "{{::hub.path}}", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub", "data-is-link" => "true"} + %i{ng: {class: "::hub.icon_font"}} + %span.margin-top.hub-name-listing{"ng-bind" => "::hub.name | truncate:40"} .columns.small-4.medium-2.large-2 - %span.margin-top{"bo-text" => "hub.address.city"} + %span.margin-top{"ng-bind" => "::hub.address.city"} .columns.small-2.medium-1.large-1 - %span.margin-top{"bo-bind" => "hub.address.state_name | uppercase"} + %span.margin-top{"ng-bind" => "::hub.address.state_name | uppercase"} %span.margin-top{"ng-if" => "hub.distance != null && hub.distance > 0"} ({{ hub.distance / 1000 | number:0 }} km) - .columns.small-4.medium-3.large-3.text-right{"bo-if" => "hub.active"} - %a.hub.open_closed{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"} + .columns.small-4.medium-3.large-3.text-right{"ng-if" => "::hub.active"} + %a.hub.open_closed{"ng-href" => "{{::hub.path}}", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"} %i.ofn-i_033-open-sign - %span.margin-top{ bo: { if: "current()" } } + %span.margin-top{ ng: { if: "::current()" } } %em= t :hubs_shopping_here - %span.margin-top{ bo: { if: "!current()" } } - %span{"bo-bind" => "hub.orders_close_at | sensible_timeframe"} + %span.margin-top{ ng: { if: "::!current()" } } + %span{"ng-bind" => "::hub.orders_close_at | sensible_timeframe"} - .columns.small-4.medium-3.large-3.text-right{"bo-if" => "!hub.active"} - %a.hub.open_closed{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"} + .columns.small-4.medium-3.large-3.text-right{"ng-if" => "::!hub.active"} + %a.hub.open_closed{"ng-href" => "{{::hub.path}}", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"} %i.ofn-i_032-closed-sign - %span.margin-top{ bo: { if: "current()" } } + %span.margin-top{ ng: { if: "::current()" } } %em= t :hubs_shopping_here - %span.margin-top{ bo: { if: "!current()" } } + %span.margin-top{ ng: { if: "::!current()" } } = t :hubs_orders_closed .columns.small-2.medium-1.large-1.text-right %span.margin-top %i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"} -.row.active_table_row{"ng-if" => "!hub.is_distributor", "ng-class" => "closed", bindonce: true} +.row.active_table_row{"ng-if" => "!hub.is_distributor", "ng-class" => "closed"} .columns.small-12.medium-6.large-5.skinny-head %a.hub{"ng-click" => "openModal(hub)", "ng-class" => "{primary: hub.active, secondary: !hub.active}"} %i{ng: {class: "hub.icon_font"}} - %span.margin-top.hub-name-listing{"bo-bind" => "hub.name | truncate:40"} + %span.margin-top.hub-name-listing{"ng-bind" => "::hub.name | truncate:40"} .columns.small-4.medium-2.large-2 - %span.margin-top{"bo-text" => "hub.address.city"} + %span.margin-top{"ng-bind" => "::hub.address.city"} .columns.small-2.medium-1.large-1 - %span.margin-top{"bo-bind" => "hub.address.state_name | uppercase"} + %span.margin-top{"ng-bind" => "::hub.address.state_name | uppercase"} .columns.small-6.medium-3.large-4.text-right - %span.margin-top{ bo: { if: "!current()" } } + %span.margin-top{ ng: { if: "::!current()" } } %em= t :hubs_profile_only diff --git a/app/views/producers/_fat.html.haml b/app/views/producers/_fat.html.haml index 9faa239c02..6b665b4f34 100644 --- a/app/views/producers/_fat.html.haml +++ b/app/views/producers/_fat.html.haml @@ -2,78 +2,78 @@ .columns.small-12.medium-7.large-7.fat / Will add in long description available once clean up HTML formatting producer.long_description - %div{"bo-if" => "producer.description"} + %div{"ng-if" => "::producer.description"} %label = t :producers_about - %img.right.show-for-medium-up{"bo-src" => "producer.logo" } - %p.text-small{ "bo-text" => "producer.description"} - %div.show-for-medium-up{"bo-if" => "producer.description.length==0"} + %img.right.show-for-medium-up{"ng-src" => "{{::producer.logo}}" } + %p.text-small{ "ng-bind" => "::producer.description"} + %div.show-for-medium-up{"ng-if" => "::producer.description.length==0"} %label   .columns.small-12.medium-5.large-5.fat - %div{"bo-if" => "producer.supplied_taxons"} + %div{"ng-if" => "::producer.supplied_taxons"} %label = t :producers_buy %p.trans-sentence %span.fat-taxons{"ng-repeat" => "taxon in producer.supplied_taxons"} %render-svg{path: "{{taxon.icon}}"} - %span{"bo-text" => "taxon.name"} + %span{"ng-bind" => "::taxon.name"} %div.show-for-medium-up{"ng-if" => "producer.supplied_taxons.length==0"}   - %div{"bo-if" => "producer.email_address || producer.website || producer.phone"} + %div{"ng-if" => "::producer.email_address || producer.website || producer.phone"} %label = t :producers_contact - %p.word-wrap{"bo-if" => "producer.phone"} + %p.word-wrap{"ng-if" => "::producer.phone"} = t :producers_contact_phone - %span{"bo-text" => "producer.phone"} + %span{"ng-bind" => "::producer.phone"} - %p.word-wrap{"bo-if" => "producer.email_address"} - %a{"bo-href" => "producer.email_address | stripUrl", target: "_blank", mailto: true} - %span.email{"bo-bind" => "producer.email_address | stripUrl"} + %p.word-wrap{"ng-if" => "::producer.email_address"} + %a{"ng-href" => "{{::producer.email_address | stripUrl}}", target: "_blank", mailto: true} + %span.email{"ng-bind" => "::producer.email_address | stripUrl"} - %p.word-wrap{"bo-if" => "producer.website"} - %a{"bo-href-i" => "http://{{producer.website | stripUrl}}", target: "_blank" } - %span{"bo-bind" => "producer.website | stripUrl"} + %p.word-wrap{"ng-if" => "::producer.website"} + %a{"ng-href" => "http://{{::producer.website | stripUrl}}", target: "_blank" } + %span{"ng-bind" => "::producer.website | stripUrl"} - %div{"bo-if" => "producer.twitter || producer.facebook || producer.linkedin || producer.instagram"} + %div{"ng-if" => "::producer.twitter || producer.facebook || producer.linkedin || producer.instagram"} %label = t :producers_social - .follow-icons{bindonce: true} - %span{"bo-if" => "producer.twitter"} - %a{"bo-href-i" => "http://twitter.com/{{producer.twitter}}", target: "_blank"} + .follow-icons + %span{"ng-if" => "::producer.twitter"} + %a{"ng-href" => "http://twitter.com/{{::producer.twitter}}", target: "_blank"} %i.ofn-i_041-twitter - %span{"bo-if" => "producer.facebook"} - %a{"bo-href-i" => "http://{{producer.facebook | stripUrl}}", target: "_blank"} + %span{"ng-if" => "::producer.facebook"} + %a{"ng-href" => "http://{{::producer.facebook | stripUrl}}", target: "_blank"} %i.ofn-i_044-facebook - %span{"bo-if" => "producer.linkedin"} - %a{"bo-href-i" => "http://{{producer.linkedin | stripUrl}}", target: "_blank"} + %span{"ng-if" => "::producer.linkedin"} + %a{"ng-href" => "http://{{::producer.linkedin | stripUrl}}", target: "_blank"} %i.ofn-i_042-linkedin - %span{"bo-if" => "producer.instagram"} - %a{"bo-href-i" => "http://instagram.com/{{producer.instagram}}", target: "_blank"} + %span{"ng-if" => "::producer.instagram"} + %a{"ng-href" => "http://instagram.com/{{::producer.instagram}}", target: "_blank"} %i.ofn-i_043-instagram -.row.active_table_row.pad-top{"ng-if" => "open()", "bo-if" => "producer.hubs"} +.row.active_table_row.pad-top{"ng-if" => "open() && producer.hubs"} .columns.small-12 .row .columns.small-12.fat - %div{"bo-if" => "producer.name"} + %div{"ng-if" => "::producer.name"} %label - = t :producers_buy_at_html, {enterprise: ''.html_safe} - %div.show-for-medium-up{"bo-if" => "!producer.name"} + = t :producers_buy_at_html, {enterprise: ''.html_safe} + %div.show-for-medium-up{"ng-if" => "::!producer.name"}   .row.cta-container .columns.small-12 %a.cta-hub{"ng-repeat" => "hub in producer.hubs | visible | orderBy:'-active'", - "bo-href" => "hub.path", "ofn-change-hub" => "hub", - "bo-class" => "{primary: hub.active, secondary: !hub.active}"} - %i.ofn-i_033-open-sign{"bo-if" => "hub.active"} - %i.ofn-i_032-closed-sign{"bo-if" => "!hub.active"} - .hub-name{"bo-text" => "hub.name"} - .button-address{"bo-bind" => "[hub.address.city, hub.address.state_name] | printArray"} + "ng-href" => "{{::hub.path}}", "ofn-change-hub" => "hub", + "ng-class" => "::{primary: hub.active, secondary: !hub.active}"} + %i.ofn-i_033-open-sign{"ng-if" => "::hub.active"} + %i.ofn-i_032-closed-sign{"ng-if" => "::!hub.active"} + .hub-name{"ng-bind" => "::hub.name"} + .button-address{"ng-bind" => "::[hub.address.city, hub.address.state_name] | printArray"} diff --git a/app/views/producers/_skinny.html.haml b/app/views/producers/_skinny.html.haml index cf066be05a..311de9daf5 100644 --- a/app/views/producers/_skinny.html.haml +++ b/app/views/producers/_skinny.html.haml @@ -1,20 +1,20 @@ .row.active_table_row{"ng-click" => "toggle($event)", "ng-class" => "{'closed' : !open(), 'is_distributor' : producer.is_distributor}"} .columns.small-12.medium-4.large-4.skinny-head - %span{"bo-if" => "producer.is_distributor" } - %a.is_distributor{"bo-href" => "producer.path" } - %i{bo: {class: "producer.producer_icon_font"}} + %span{"ng-if" => "::producer.is_distributor" } + %a.is_distributor{"ng-href" => "{{::producer.path}}" } + %i{ng: {class: "::producer.producer_icon_font"}} %span.margin-top - %strong{"bo-text" => "producer.name"} - %span.producer-name{"bo-if" => "!producer.is_distributor" } - %i{bo: {class: "producer.producer_icon_font"}} + %strong{"ng-bind" => "::producer.name"} + %span.producer-name{"ng-if" => "::!producer.is_distributor" } + %i{ng: {class: "::producer.producer_icon_font"}} %span.margin-top - %strong{"bo-text" => "producer.name"} + %strong{"ng-bind" => "::producer.name"} .columns.small-6.medium-3.large-3 - %span.margin-top{"bo-text" => "producer.address.city"} + %span.margin-top{"ng-bind" => "::producer.address.city"} .columns.small-4.medium-3.large-4 - %span.margin-top{"bo-bind" => "producer.address.state_name | uppercase"} + %span.margin-top{"ng-bind" => "::producer.address.state_name | uppercase"} .columns.small-2.medium-2.large-1.text-right %span.margin-top %i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"} diff --git a/app/views/producers/index.html.haml b/app/views/producers/index.html.haml index e842568cb3..8b7fd5695e 100644 --- a/app/views/producers/index.html.haml +++ b/app/views/producers/index.html.haml @@ -12,7 +12,7 @@ = render partial: "shared/components/enterprise_search" = render partial: "producers/filters" - .row{bindonce: true} + .row .small-12.columns .active_table %producer.active_table_node.row.animate-repeat{id: "{{producer.path}}", diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index f2be498451..1decc0a43c 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -29,11 +29,10 @@ .small-12.medium-6.large-6.large-offset-1.columns = render partial: "shop/products/filters" - %div.pad-top{bindonce: true} - %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", - "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons | properties: activeProperties) track by product.id ", "id" => "product-{{ product.id }}"} + %div.pad-top + %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons | properties: activeProperties) track by product.id ", "id" => "product-{{ product.id }}"} = render "shop/products/summary" - %shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants", "id" => "variant-{{ product.master.id }}"} + %shop-variant{variant: 'product.master', "ng-if" => "::!product.hasVariants", "id" => "variant-{{ product.master.id }}"} %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id", "id" => "variant-{{ variant.id }}", "ng-class" => "{'out-of-stock': !variant.on_demand && variant.count_on_hand == 0}"} %product{"ng-show" => "Products.loading"} diff --git a/app/views/shop/products/_summary.html.haml b/app/views/shop/products/_summary.html.haml index 8671b00cbf..fcf2aa6d7b 100644 --- a/app/views/shop/products/_summary.html.haml +++ b/app/views/shop/products/_summary.html.haml @@ -1,13 +1,13 @@ .product-thumb %a{"ng-click" => "triggerProductModal()"} %i.ofn-i_057-expand - %img{"bo-src" => "product.primaryImageOrMissing", "ng-click" => "triggerProductModal()"} + %img{"ng-src" => "::product.primaryImageOrMissing", "ng-click" => "triggerProductModal()"} .row.summary .small-10.medium-10.large-11.columns.summary-header %h3 %a{"ng-click" => "triggerProductModal()"} - %span{"bo-text" => "product.name"} + %span{"ng-bind" => "::product.name"} %i.ofn-i_057-expand %small %em @@ -15,7 +15,7 @@ %span %enterprise-modal %i.ofn-i_036-producers - %span{"bo-bind" => "enterprise.name"} + %span{"ng-bind" => "::enterprise.name"} .small-2.medium-2.large-1.columns.text-center .taxon-flag %render-svg{path: "{{product.primary_taxon.icon}}"} diff --git a/app/views/shopping_shared/_about.html.haml b/app/views/shopping_shared/_about.html.haml index ffe66c6c82..eac2fe5658 100644 --- a/app/views/shopping_shared/_about.html.haml +++ b/app/views/shopping_shared/_about.html.haml @@ -1,8 +1,8 @@ -.content#about{"ng-controller" => "AboutUsCtrl", bindonce: true} +.content#about{"ng-controller" => "AboutUsCtrl"} .panel .row .small-12.large-8.columns - %img.hero-img-small{"bo-src" => "CurrentHub.hub.promo_image", "bo-if" => "CurrentHub.hub.promo_image"} - %p{"bo-html" => "CurrentHub.hub.long_description"} + %img.hero-img-small{"ng-src" => "{{::CurrentHub.hub.promo_image}}", "ng-if" => "::CurrentHub.hub.promo_image"} + %p{"ng-bind-html" => "::CurrentHub.hub.long_description"} .small-12.large-4.columns   diff --git a/app/views/spree/users/_fat.html.haml b/app/views/spree/users/_fat.html.haml index 5c87077b4c..4ce051e5b8 100644 --- a/app/views/spree/users/_fat.html.haml +++ b/app/views/spree/users/_fat.html.haml @@ -12,20 +12,20 @@ %tbody.transaction-group{"ng-repeat" => "order in distributor.distributed_orders", "ng-class-odd"=>"'odd'", "ng-class-even"=>"'even'"} %tr.order-row %td.order1 - %a{"bo-href" => "order.path", "bo-text" => "('order' | t )+ ' ' + order.number"} - %td.order2{"bo-text" => "order.completed_at"} - %td.order3.show-for-large-up{"bo-text" => "'spree.payment_states.' + order.payment_state | t | capitalize"} - %td.order4.show-for-large-up{"bo-text" => "'spree.shipment_states.' + order.shipment_state | t | capitalize"} - %td.order5.text-right{"ng-class" => "{'credit' : order.total < 0, 'debit' : order.total > 0, 'paid' : order.total == 0}","bo-text" => "order.total | localizeCurrency"} - %td.order6.text-right.show-for-large-up{"ng-class" => "{'credit' : order.outstanding_balance < 0, 'debit' : order.outstanding_balance > 0, 'paid' : order.outstanding_balance == 0}", "bo-text" => "order.outstanding_balance | localizeCurrency"} - %td.order7.text-right{"ng-class" => "{'credit' : order.running_balance < 0, 'debit' : order.running_balance > 0, 'paid' : order.running_balance == 0}", "bo-text" => "order.running_balance | localizeCurrency"} - %tr.payment-row{"ng-repeat" => "payment in order.payments", "ng-class" => "{'invalid': payment.state != 'completed'}"} - %td.order1{"bo-text" => "payment.payment_method"} - %td.order2{"bo-text" => "payment.updated_at"} - %td.order3.show-for-large-up - %i{"ng-class" => "{'ofn-i_012-warning': payment.state == 'invalid' || payment.state == 'void' || payment.state == 'failed'}"} - %span{"bo-text" => "'spree.payment_states.' + payment.state | t | capitalize"} - %td.order4.show-for-large-up - %td.order5.text-right{"ng-class" => "{'credit' : payment.amount > 0, 'debit' : payment.amount < 0, 'paid' : payment.amount == 0}","bo-text" => "payment.amount | localizeCurrency"} - %td.order6.show-for-large-up - %td.order7 + %a{"ng-href" => "{{::order.path}}", "ng-bind" => "::('order' | t )+ ' ' + order.number"} + %td.order2{"ng-bind" => "::order.completed_at"} + %td.order3.show-for-large-up{"ng-bind" => "::'spree.payment_states.' + order.payment_state | t | capitalize"} + %td.order4.show-for-large-up{"ng-bind" => "::'spree.shipment_states.' + order.shipment_state | t | capitalize"} + %td.order5.text-right{"ng-class" => "{'credit' : order.total < 0, 'debit' : order.total > 0, 'paid' : order.total == 0}","ng-bind" => "::order.total | localizeCurrency"} + %td.order6.text-right.show-for-large-up{"ng-class" => "{'credit' : order.outstanding_balance < 0, 'debit' : order.outstanding_balance > 0, 'paid' : order.outstanding_balance == 0}", "ng-bind" => "::order.outstanding_balance | localizeCurrency"} + %td.order7.text-right{"ng-class" => "{'credit' : order.running_balance < 0, 'debit' : order.running_balance > 0, 'paid' : order.running_balance == 0}", "ng-bind" => "::order.running_balance | localizeCurrency"} + %tr.payment-row{"ng-repeat" => "payment in order.payments", "ng-class" => "{'invalid': payment.state != 'completed'}"} + %td.order1{"ng-bind" => "::payment.payment_method"} + %td.order2{"ng-bind" => "::payment.updated_at"} + %td.order3.show-for-large-up + %i{"ng-class" => "{'ofn-i_012-warning': payment.state == 'invalid' || payment.state == 'void' || payment.state == 'failed'}"} + %span{"ng-bind" => "::'spree.payment_states.' + payment.state | t | capitalize"} + %td.order4.show-for-large-up + %td.order5.text-right{"ng-class" => "{'credit' : payment.amount > 0, 'debit' : payment.amount < 0, 'paid' : payment.amount == 0}","ng-bind" => "::payment.amount | localizeCurrency"} + %td.order6.show-for-large-up + %td.order7 diff --git a/app/views/spree/users/_skinny.html.haml b/app/views/spree/users/_skinny.html.haml index 14c04f024a..4894824296 100644 --- a/app/views/spree/users/_skinny.html.haml +++ b/app/views/spree/users/_skinny.html.haml @@ -4,9 +4,9 @@ %img.account-logo{"logo-fallback" => true, "ng-src" => "{{distributor.logo}}"} .columns.small-10.medium-5 %span.margin-top - %strong{"bo-text" => "distributor.name"} + %strong{"ng-bind" => "::distributor.name"} .columns.small-8.small-offset-2.medium-3.text-right - %span.margin-top.distributor-balance{"bo-text" => "distributor.balance | formatBalance", "ng-class" => "{'credit' : distributor.balance < 0, 'debit' : distributor.balance > 0, 'paid' : distributor.balance == 0}" } + %span.margin-top.distributor-balance{"ng-bind" => "::distributor.balance | formatBalance", "ng-class" => "{'credit' : distributor.balance < 0, 'debit' : distributor.balance > 0, 'paid' : distributor.balance == 0}" } .columns.small-2.medium-2.text-right %span.margin-top %i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"} diff --git a/app/views/spree/users/show.html.haml b/app/views/spree/users/show.html.haml index 83171daf0d..0b4800f79e 100644 --- a/app/views/spree/users/show.html.haml +++ b/app/views/spree/users/show.html.haml @@ -9,7 +9,7 @@ (#{link_to t(:edit), spree.edit_account_path}) %h3= t(:my_orders) .orders{"ng-controller" => "OrdersCtrl", "ng-cloak" => true} - .row{bindonce: true} + .row .small-12.columns .active_table %distributor.active_table_node.row.animate-repeat{"ng-if" => "Orders.orders_by_distributor.length > 0", "ng-repeat" => "(key, distributor) in Orders.orders_by_distributor", @@ -19,6 +19,6 @@ .small-12.columns = render partial: "spree/users/skinny" = render partial: "spree/users/fat" - .message{"ng-if" => "Orders.orders_by_distributor.length == 0", "bo-text" => "'you_have_no_orders_yet' | t"} + .message{"ng-if" => "Orders.orders_by_distributor.length == 0", "ng-bind" => "::'you_have_no_orders_yet' | t"} = render partial: "shared/footer" From 1d837c32eea480ea7c817201c3404eaa6856eb2e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 17 Apr 2016 20:02:19 +1000 Subject: [PATCH 010/110] Replacing bindonce with native Angularjs syntax in admin --- app/assets/javascripts/admin/all.js | 1 - .../admin/enterprises/enterprises.js.coffee | 2 +- .../variant_overrides/variant_overrides.js.coffee | 2 +- .../templates/admin/panels/enterprise_status.html.haml | 4 ++-- .../admin/enterprises/_enterprise_user_index.html.haml | 8 ++++---- app/views/admin/enterprises/form/_tag_rules.html.haml | 6 +++--- .../admin/variant_overrides/_hidden_products.html.haml | 10 +++++----- .../admin/variant_overrides/_new_products.html.haml | 10 +++++----- app/views/admin/variant_overrides/_products.html.haml | 2 +- .../variant_overrides/_products_product.html.haml | 4 ++-- .../variant_overrides/_products_variants.html.haml | 4 ++-- 11 files changed, 26 insertions(+), 27 deletions(-) diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 9f99dc1dcd..73b51a20be 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -45,7 +45,6 @@ //= require ./variant_overrides/variant_overrides //= require textAngular.min.js //= require textAngular-sanitize.min.js -//= require ../shared/bindonce.min.js //= require darkswarm/i18n.js //= require darkswarm/i18n.translate.js diff --git a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee index 2074a1ea05..da122761b8 100644 --- a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee +++ b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee @@ -1 +1 @@ -angular.module("admin.enterprises", [ "admin.paymentMethods", "admin.utils", "admin.shippingMethods", "admin.users", "textAngular", "admin.side_menu", "admin.taxons", 'admin.indexUtils', 'admin.tagRules', 'admin.dropdown', 'pasvaz.bindonce', 'ngSanitize'] ) \ No newline at end of file +angular.module("admin.enterprises", [ "admin.paymentMethods", "admin.utils", "admin.shippingMethods", "admin.users", "textAngular", "admin.side_menu", "admin.taxons", 'admin.indexUtils', 'admin.tagRules', 'admin.dropdown', 'ngSanitize'] ) \ No newline at end of file diff --git a/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee b/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee index c302c0463e..2af7ba1c16 100644 --- a/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee @@ -1 +1 @@ -angular.module("admin.variantOverrides", ["pasvaz.bindonce", "admin.indexUtils", "admin.utils", "admin.dropdown", "admin.inventoryItems"]) +angular.module("admin.variantOverrides", ["admin.indexUtils", "admin.utils", "admin.dropdown", "admin.inventoryItems"]) diff --git a/app/assets/javascripts/templates/admin/panels/enterprise_status.html.haml b/app/assets/javascripts/templates/admin/panels/enterprise_status.html.haml index 39392a497d..518122774d 100644 --- a/app/assets/javascripts/templates/admin/panels/enterprise_status.html.haml +++ b/app/assets/javascripts/templates/admin/panels/enterprise_status.html.haml @@ -17,13 +17,13 @@ %td.severity %i.icon-warning-sign.issue %td.description - %span{ bo: { bind: "issue.description" } } + %span{ ng: { bind: "::issue.description" } } %td.resolve %div{ ng: { bind: { html: "issue.link" } } } %tr{ ng: { repeat: "warning in warnings"} } %td.severity %i.icon-warning-sign.warning %td.description - %span{ bo: { bind: "warning.description" } } + %span{ ng: { bind: "::warning.description" } } %td.resolve %div{ ng: { bind: { html: "warning.link" } } } diff --git a/app/views/admin/enterprises/_enterprise_user_index.html.haml b/app/views/admin/enterprises/_enterprise_user_index.html.haml index 1fb35e595c..e53541fde2 100644 --- a/app/views/admin/enterprises/_enterprise_user_index.html.haml +++ b/app/views/admin/enterprises/_enterprise_user_index.html.haml @@ -15,7 +15,7 @@ %h1#no_results No enterprises found. - .row{ ng: { show: "loaded && filteredEnterprises.length > 0" }, bindonce: true } + .row{ ng: { show: "loaded && filteredEnterprises.length > 0" } } %table.index#enterprises %col.name{ width: "28%", ng: { show: 'columns.name.visible' } } %col.producer{ width: "18%", ng: { show: 'columns.producer.visible' }} @@ -33,15 +33,15 @@ %tbody{ :id => "e_{{enterprise.id}}", ng: { repeat: "enterprise in filteredEnterprises = ( allEnterprises | filter:{ name: quickSearch } )", controller: 'EnterpriseIndexRowCtrl' } } %tr.enterprise.panel-toggle-row{ object: "enterprise", ng: { class: { even: "'even'", odd: "'odd'"} } } %td.name{ ng: { show: 'columns.name.visible' } } - %span{ bo: { bind: "enterprise.name" } } + %span{ ng: { bind: "::enterprise.name" } } %td.producer.panel-toggle.text-center{ ng: { show: 'columns.producer.visible', class: "{error: producerError}" }, name: "producer" } %h5{ ng: { bind: "producer" } } %td.package.panel-toggle.text-center{ ng: { show: 'columns.package.visible', class: "{error: packageError}" }, name: "package" } %h5{ ng: { bind: "package" } } %td.status.panel-toggle.text-center{ ng: { show: 'columns.status.visible' }, name: "status" } - %i.icon-status{ bo: { class: "status" } } + %i.icon-status{ ng: { class: "::status()" } } %td.manage{ ng: { show: 'columns.manage.visible' } } - %a.button.fullwidth{ bo: { href: 'enterprise.edit_path' } } + %a.button.fullwidth{ ng: { href: '{{::enterprise.edit_path}}' } } Manage %i.icon-arrow-right diff --git a/app/views/admin/enterprises/form/_tag_rules.html.haml b/app/views/admin/enterprises/form/_tag_rules.html.haml index 1a50e4f353..4ed9aeb5c1 100644 --- a/app/views/admin/enterprises/form/_tag_rules.html.haml +++ b/app/views/admin/enterprises/form/_tag_rules.html.haml @@ -3,7 +3,7 @@ .eleven.columns.alpha.omega .no_tags{ ng: { show: "tagGroups.length == 0" } } No tags apply to this enterprise yet - .customer_tag{ ng: { repeat: "tagGroup in tagGroups" }, bindonce: true } + .customer_tag{ ng: { repeat: "tagGroup in tagGroups" } } .header %table %colgroup @@ -23,8 +23,8 @@ %table %tr.tag_rule{ id: "tr_{{rule.id}}", ng: { repeat: "rule in tagGroup.rules" } } %td - %discount-order{ bo: { if: "rule.type == 'TagRule::DiscountOrder'" } } - %filter-shipping-methods{ bo: { if: "rule.type == 'TagRule::FilterShippingMethods'" } } + %discount-order{ ng: { if: "::rule.type == 'TagRule::DiscountOrder'" } } + %filter-shipping-methods{ ng: { if: "::rule.type == 'TagRule::FilterShippingMethods'" } } %td.actions %a{ ng: { click: "deleteTagRule(tagGroup, rule)" }, :class => "delete-tag-rule icon-trash no-text" } .add_rule.text-center diff --git a/app/views/admin/variant_overrides/_hidden_products.html.haml b/app/views/admin/variant_overrides/_hidden_products.html.haml index 902e24a232..9f0d4f2d94 100644 --- a/app/views/admin/variant_overrides/_hidden_products.html.haml +++ b/app/views/admin/variant_overrides/_hidden_products.html.haml @@ -10,13 +10,13 @@ %th.product=t('admin.product') %th.variant=t('(admin.variant') %th.add=t('admin.inventory.add') - %tbody{ bindonce: true, ng: { repeat: 'product in filteredProducts | limitTo:productLimit' } } + %tbody{ ng: { repeat: 'product in filteredProducts | limitTo:productLimit' } } %tr{ id: "v_{{variant.id}}", ng: { repeat: 'variant in product.variants | inventoryVariants:hub_id:views' } } - %td.producer{ bo: { bind: 'producersByID[product.producer_id].name'} } - %td.product{ bo: { bind: 'product.name'} } + %td.producer{ ng: { bind: '::producersByID[product.producer_id].name'} } + %td.product{ ng: { bind: '::product.name'} } %td.variant - %span{ bo: { bind: 'variant.display_name || ""'} } - .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } + %span{ ng: { bind: '::variant.display_name || ""'} } + .variant-override-unit{ ng: { bind: '::variant.unit_to_display'} } %td.add %button.fullwidth.icon-plus{ ng: { click: "setVisibility(hub_id,variant.id,true)" } } = t('admin.inventory.add') diff --git a/app/views/admin/variant_overrides/_new_products.html.haml b/app/views/admin/variant_overrides/_new_products.html.haml index 86ca180b8f..414fba224d 100644 --- a/app/views/admin/variant_overrides/_new_products.html.haml +++ b/app/views/admin/variant_overrides/_new_products.html.haml @@ -11,13 +11,13 @@ %th.variant=t('(admin.variant') %th.add=t('admin.inventory.add') %th.hide=t('admin.inventory.hide') - %tbody{ bindonce: true, ng: { repeat: 'product in filteredProducts | limitTo:productLimit' } } + %tbody{ ng: { repeat: 'product in filteredProducts | limitTo:productLimit' } } %tr{ id: "v_{{variant.id}}", ng: { repeat: 'variant in product.variants | inventoryVariants:hub_id:views' } } - %td.producer{ bo: { bind: 'producersByID[product.producer_id].name'} } - %td.product{ bo: { bind: 'product.name'} } + %td.producer{ ng: { bind: '::producersByID[product.producer_id].name'} } + %td.product{ ng: { bind: '::product.name'} } %td.variant - %span{ bo: { bind: 'variant.display_name || ""'} } - .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } + %span{ ng: { bind: '::variant.display_name || ""'} } + .variant-override-unit{ ng: { bind: '::variant.unit_to_display'} } %td.add %button.fullwidth.icon-plus{ ng: { click: "setVisibility(hub_id,variant.id,true)" } } = t('admin.inventory.add') diff --git a/app/views/admin/variant_overrides/_products.html.haml b/app/views/admin/variant_overrides/_products.html.haml index f255d86e0b..f0b2310fdc 100644 --- a/app/views/admin/variant_overrides/_products.html.haml +++ b/app/views/admin/variant_overrides/_products.html.haml @@ -22,6 +22,6 @@ %th.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } }=t('admin.inventory.enable_reset') %th.inheritance{ ng: { show: 'columns.inheritance.visible' } }=t('admin.inventory.inherit') %th.visibility{ ng: { show: 'columns.visibility.visible' } }=t('admin.inventory.hide') - %tbody{bindonce: true, ng: {repeat: 'product in filteredProducts = (products | hubPermissions:hubPermissions:hub_id | inventoryProducts:hub_id:views | attrFilter:{producer_id:producerFilter} | filter:query) | limitTo:productLimit' } } + %tbody{ ng: {repeat: 'product in filteredProducts = (products | hubPermissions:hubPermissions:hub_id | inventoryProducts:hub_id:views | attrFilter:{producer_id:producerFilter} | filter:query) | limitTo:productLimit' } } = render 'admin/variant_overrides/products_product' = render 'admin/variant_overrides/products_variants' diff --git a/app/views/admin/variant_overrides/_products_product.html.haml b/app/views/admin/variant_overrides/_products_product.html.haml index 70b48e3909..b15840dd49 100644 --- a/app/views/admin/variant_overrides/_products_product.html.haml +++ b/app/views/admin/variant_overrides/_products_product.html.haml @@ -1,6 +1,6 @@ %tr.product.even - %td.producer{ ng: { show: 'columns.producer.visible' }, bo: { bind: 'producersByID[product.producer_id].name'} } - %td.product{ ng: { show: 'columns.product.visible' }, bo: { bind: 'product.name'} } + %td.producer{ ng: { show: 'columns.producer.visible' }, ng: { bind: '::producersByID[product.producer_id].name'} } + %td.product{ ng: { show: 'columns.product.visible' }, ng: { bind: '::product.name'} } %td.sku{ ng: { show: 'columns.sku.visible' } } %td.price{ ng: { show: 'columns.price.visible' } } %td.on_hand{ ng: { show: 'columns.on_hand.visible' } } diff --git a/app/views/admin/variant_overrides/_products_variants.html.haml b/app/views/admin/variant_overrides/_products_variants.html.haml index c26be92697..a7a94437be 100644 --- a/app/views/admin/variant_overrides/_products_variants.html.haml +++ b/app/views/admin/variant_overrides/_products_variants.html.haml @@ -1,8 +1,8 @@ %tr.variant{ id: "v_{{variant.id}}", ng: {repeat: 'variant in product.variants | inventoryVariants:hub_id:views'}} %td.producer{ ng: { show: 'columns.producer.visible' } } %td.product{ ng: { show: 'columns.product.visible' } } - %span{ bo: { bind: 'variant.display_name || ""'} } - .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } + %span{ ng: { bind: '::variant.display_name || ""'} } + .variant-override-unit{ ng: { bind: '::variant.unit_to_display'} } %td.sku{ ng: { show: 'columns.sku.visible' } } %input{name: 'variant-overrides-{{ variant.id }}-sku', type: 'text', ng: {model: 'variantOverrides[hub_id][variant.id].sku'}, placeholder: '{{ variant.sku }}', 'ofn-track-variant-override' => 'sku'} %td.price{ ng: { show: 'columns.price.visible' } } From 7bc118b598b4d77952451757d3a1b0dec8b31d36 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 17 Apr 2016 20:32:04 +1000 Subject: [PATCH 011/110] Final steps to remove bindonce --- app/assets/javascripts/shared/bindonce.min.js | 1 - config/ng-test.conf.js | 1 - 2 files changed, 2 deletions(-) delete mode 100644 app/assets/javascripts/shared/bindonce.min.js diff --git a/app/assets/javascripts/shared/bindonce.min.js b/app/assets/javascripts/shared/bindonce.min.js deleted file mode 100644 index 0edd3d57d4..0000000000 --- a/app/assets/javascripts/shared/bindonce.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(){"use strict";var e=angular.module("pasvaz.bindonce",[]);e.directive("bindonce",function(){var e=function(e){if(e&&0!==e.length){var t=angular.lowercase(""+e);e=!("f"===t||"0"===t||"false"===t||"no"===t||"n"===t||"[]"===t)}else e=!1;return e},t=parseInt((/msie (\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10);isNaN(t)&&(t=parseInt((/trident\/.*; rv:(\d+)/.exec(angular.lowercase(navigator.userAgent))||[])[1],10));var r={restrict:"AM",controller:["$scope","$element","$attrs","$interpolate",function(r,a,i,n){var c=function(t,r,a){var i="show"===r?"":"none",n="hide"===r?"":"none";t.css("display",e(a)?i:n)},o=function(e,t){if(angular.isObject(t)&&!angular.isArray(t)){var r=[];angular.forEach(t,function(e,t){e&&r.push(t)}),t=r}t&&e.addClass(angular.isArray(t)?t.join(" "):t)},s=function(e,t){e.transclude(t,function(t){var r=e.element.parent(),a=e.element&&e.element[e.element.length-1],i=r&&r[0]||a&&a.parentNode,n=a&&a.nextSibling||null;angular.forEach(t,function(e){i.insertBefore(e,n)})})},l={watcherRemover:void 0,binders:[],group:i.boName,element:a,ran:!1,addBinder:function(e){this.binders.push(e),this.ran&&this.runBinders()},setupWatcher:function(e){var t=this;this.watcherRemover=r.$watch(e,function(e){void 0!==e&&(t.removeWatcher(),t.checkBindonce(e))},!0)},checkBindonce:function(e){var t=this,r=e.$promise?e.$promise.then:e.then;"function"==typeof r?r(function(){t.runBinders()}):t.runBinders()},removeWatcher:function(){void 0!==this.watcherRemover&&(this.watcherRemover(),this.watcherRemover=void 0)},runBinders:function(){for(;this.binders.length>0;){var r=this.binders.shift();if(!this.group||this.group==r.group){var a=r.scope.$eval(r.interpolate?n(r.value):r.value);switch(r.attr){case"boIf":e(a)&&s(r,r.scope.$new());break;case"boSwitch":var i,l=r.controller[0];(i=l.cases["!"+a]||l.cases["?"])&&(r.scope.$eval(r.attrs.change),angular.forEach(i,function(e){s(e,r.scope.$new())}));break;case"boSwitchWhen":var u=r.controller[0];u.cases["!"+r.attrs.boSwitchWhen]=u.cases["!"+r.attrs.boSwitchWhen]||[],u.cases["!"+r.attrs.boSwitchWhen].push({transclude:r.transclude,element:r.element});break;case"boSwitchDefault":var u=r.controller[0];u.cases["?"]=u.cases["?"]||[],u.cases["?"].push({transclude:r.transclude,element:r.element});break;case"hide":case"show":c(r.element,r.attr,a);break;case"class":o(r.element,a);break;case"text":r.element.text(a);break;case"html":r.element.html(a);break;case"style":r.element.css(a);break;case"src":r.element.attr(r.attr,a),t&&r.element.prop("src",a);break;case"attr":angular.forEach(r.attrs,function(e,t){var a,i;t.match(/^boAttr./)&&r.attrs[t]&&(a=t.replace(/^boAttr/,"").replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),i=r.scope.$eval(r.attrs[t]),r.element.attr(a,i))});break;case"href":case"alt":case"title":case"id":case"value":r.element.attr(r.attr,a)}}}this.ran=!0}};return l}],link:function(e,t,r,a){var i=r.bindonce&&e.$eval(r.bindonce);void 0!==i?a.checkBindonce(i):(a.setupWatcher(r.bindonce),t.bind("$destroy",a.removeWatcher))}};return r}),angular.forEach([{directiveName:"boShow",attribute:"show"},{directiveName:"boHide",attribute:"hide"},{directiveName:"boClass",attribute:"class"},{directiveName:"boText",attribute:"text"},{directiveName:"boBind",attribute:"text"},{directiveName:"boHtml",attribute:"html"},{directiveName:"boSrcI",attribute:"src",interpolate:!0},{directiveName:"boSrc",attribute:"src"},{directiveName:"boHrefI",attribute:"href",interpolate:!0},{directiveName:"boHref",attribute:"href"},{directiveName:"boAlt",attribute:"alt"},{directiveName:"boTitle",attribute:"title"},{directiveName:"boId",attribute:"id"},{directiveName:"boStyle",attribute:"style"},{directiveName:"boValue",attribute:"value"},{directiveName:"boAttr",attribute:"attr"},{directiveName:"boIf",transclude:"element",terminal:!0,priority:1e3},{directiveName:"boSwitch",require:"boSwitch",controller:function(){this.cases={}}},{directiveName:"boSwitchWhen",transclude:"element",priority:800,require:"^boSwitch"},{directiveName:"boSwitchDefault",transclude:"element",priority:800,require:"^boSwitch"}],function(t){var r=200;return e.directive(t.directiveName,function(){var e={priority:t.priority||r,transclude:t.transclude||!1,terminal:t.terminal||!1,require:["^bindonce"].concat(t.require||[]),controller:t.controller,compile:function(e,r,a){return function(e,r,i,n){var c=n[0],o=i.boParent;if(o&&c.group!==o){var s=c.element.parent();c=void 0;for(var l;9!==s[0].nodeType&&s.length;){if((l=s.data("$bindonceController"))&&l.group===o){c=l;break}s=s.parent()}if(!c)throw new Error("No bindonce controller: "+o)}c.addBinder({element:r,attr:t.attribute||t.directiveName,attrs:i,value:i[t.directiveName],interpolate:t.interpolate,group:o,transclude:a,controller:n.slice(1),scope:e})}}};return e})})}(); \ No newline at end of file diff --git a/config/ng-test.conf.js b/config/ng-test.conf.js index 0cc4fc385e..20ec36622c 100644 --- a/config/ng-test.conf.js +++ b/config/ng-test.conf.js @@ -9,7 +9,6 @@ module.exports = function(config) { 'app/assets/javascripts/shared/jquery-1.8.0.js', // TODO: Can we link to Rails' jquery? 'app/assets/javascripts/shared/jquery.timeago.js', 'app/assets/javascripts/shared/angular-local-storage.js', - 'app/assets/javascripts/shared/bindonce.min.js', 'app/assets/javascripts/shared/ng-infinite-scroll.min.js', 'app/assets/javascripts/shared/angular-slideables.js', From 9fc7908af51afe236b0daf261f429fc05efd15c3 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 17 Apr 2016 20:59:36 +1000 Subject: [PATCH 012/110] Removing '?' to meet expectations of js spec --- spec/javascripts/unit/order_cycle_spec.js.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index b66acfcb1d..025a01a6b2 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -336,7 +336,7 @@ describe 'OrderCycle services', -> inject ($injector, _$httpBackend_)-> Enterprise = $injector.get('Enterprise') $httpBackend = _$httpBackend_ - $httpBackend.whenGET('/admin/enterprises/for_order_cycle.json?').respond [ + $httpBackend.whenGET('/admin/enterprises/for_order_cycle.json').respond [ {id: 1, name: 'One', supplied_products: [1, 2], is_primary_producer: true} {id: 2, name: 'Two', supplied_products: [3, 4]} {id: 3, name: 'Three', supplied_products: [5, 6], sells: 'any'} @@ -412,7 +412,7 @@ describe 'OrderCycle services', -> inject ($injector, _$httpBackend_)-> EnterpriseFee = $injector.get('EnterpriseFee') $httpBackend = _$httpBackend_ - $httpBackend.whenGET('/admin/enterprise_fees/for_order_cycle.json?').respond [ + $httpBackend.whenGET('/admin/enterprise_fees/for_order_cycle.json').respond [ {id: 1, name: "Yayfee", enterprise_id: 1} {id: 2, name: "FeeTwo", enterprise_id: 2} ] From 97e53900ad259594405f09e2e396abea92fad6e1 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 17 Apr 2016 22:04:33 +1000 Subject: [PATCH 013/110] Updating translation to start with a capital letter --- app/views/admin/order_cycles/set_coordinator.html.haml | 2 +- config/locales/en.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/admin/order_cycles/set_coordinator.html.haml b/app/views/admin/order_cycles/set_coordinator.html.haml index db9bbb926c..f4c06e0a98 100644 --- a/app/views/admin/order_cycles/set_coordinator.html.haml +++ b/app/views/admin/order_cycles/set_coordinator.html.haml @@ -1,5 +1,5 @@ %h4.text-center - =t'select_a_coordinator_for_your_order_cycle' + =t 'select_a_coordinator_for_your_order_cycle' %br diff --git a/config/locales/en.yml b/config/locales/en.yml index e895522903..79f6e471b1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -869,7 +869,7 @@ Please follow the instructions there to make your enterprise visible on the Open calculator: "Calculator" calculator_values: "Calculator values" new_order_cycles: "New Order Cycles" - select_a_coordinator_for_your_order_cycle: "select a coordinator for your order cycle" + select_a_coordinator_for_your_order_cycle: "Select a coordinator for your order cycle" edit_order_cycle: "Edit Order Cycle" roles: "Roles" update: "Update" From cf94d67caf079bf5278470cf7737b91094ff444c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 17 Apr 2016 22:05:41 +1000 Subject: [PATCH 014/110] Preventing default submit behaviour for order cycle form submission buttons --- .../admin/order_cycles/controllers/create.js.coffee | 3 ++- .../admin/order_cycles/controllers/edit.js.coffee | 5 +++++ .../order_cycles/controllers/simple_create.js.coffee | 3 ++- .../admin/order_cycles/controllers/simple_edit.js.coffee | 3 ++- app/assets/javascripts/templates/admin/save_bar.html.haml | 2 +- app/views/admin/order_cycles/_form.html.haml | 2 +- app/views/admin/order_cycles/_simple_form.html.haml | 2 +- spec/javascripts/unit/order_cycle_spec.js.coffee | 8 ++++++-- 8 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/create.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/create.js.coffee index 2c98d60f0e..c837ff84ec 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/create.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/create.js.coffee @@ -79,5 +79,6 @@ angular.module('admin.orderCycles') $scope.removeDistributionOfVariant = (variant_id) -> OrderCycle.removeDistributionOfVariant(variant_id) - $scope.submit = (destination) -> + $scope.submit = ($event, destination) -> + $event.preventDefault() OrderCycle.create(destination) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee index 227c6a3045..d4c63121fe 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee @@ -85,6 +85,11 @@ angular.module('admin.orderCycles') OrderCycle.removeDistributionOfVariant(variant_id) $scope.submit = (destination) -> + $event.preventDefault() + StatusMessage.display 'progress', "Saving..." + + $scope.submit = ($event, destination) -> + $event.preventDefault() StatusMessage.display 'progress', "Saving..." OrderCycle.update(destination) $scope.order_cycle_form.$setPristine() diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee index 53f00cdc36..a6d1e18535 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee @@ -41,6 +41,7 @@ angular.module('admin.orderCycles').controller "AdminSimpleCreateOrderCycleCtrl" $scope.enterpriseFeesForEnterprise = (enterprise_id) -> EnterpriseFee.forEnterprise(parseInt(enterprise_id)) - $scope.submit = (destination) -> + $scope.submit = ($event, destination) -> + $event.preventDefault() OrderCycle.mirrorIncomingToOutgoingProducts() OrderCycle.create(destination) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee index 99c2e2649c..d3eeb1c9d8 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee @@ -37,7 +37,8 @@ angular.module('admin.orderCycles').controller "AdminSimpleEditOrderCycleCtrl", $event.preventDefault() OrderCycle.removeCoordinatorFee(index) - $scope.submit = (destination) -> + $scope.submit = ($event, destination) -> + $event.preventDefault() StatusMessage.display 'progress', "Saving..." OrderCycle.mirrorIncomingToOutgoingProducts() OrderCycle.update(destination) diff --git a/app/assets/javascripts/templates/admin/save_bar.html.haml b/app/assets/javascripts/templates/admin/save_bar.html.haml index 08b7cbdf3d..b710c070ca 100644 --- a/app/assets/javascripts/templates/admin/save_bar.html.haml +++ b/app/assets/javascripts/templates/admin/save_bar.html.haml @@ -4,4 +4,4 @@ %h5#status-message{ ng: { style: 'StatusMessage.statusMessage.style' } } {{ StatusMessage.statusMessage.text || " " }} .eight.columns.omega.text-right - %input{"ng-repeat" => "button in buttons", type: "button", value: "{{button.text}}", ng: { class: "button.class", click: "button.action(button.param)" } } + %input{"ng-repeat" => "button in buttons", type: "button", value: "{{button.text}}", ng: { class: "button.class", click: "button.action($event, button.param)" } } diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index ed2d3acbc2..f61c1ad91f 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -51,7 +51,7 @@ .actions - if @order_cycle.new_record? - = f.submit 'Create', 'ng-click' => "submit('#{main_app.admin_order_cycles_path}')", 'ng-disabled' => '!loaded()' + = f.submit 'Create', 'ng-click' => "submit($event, '#{main_app.admin_order_cycles_path}')", 'ng-disabled' => '!loaded()' %span{'ng-show' => 'loaded()'} = link_to 'Cancel', main_app.admin_order_cycles_path diff --git a/app/views/admin/order_cycles/_simple_form.html.haml b/app/views/admin/order_cycles/_simple_form.html.haml index 9364d080ff..7fcf0ea80c 100644 --- a/app/views/admin/order_cycles/_simple_form.html.haml +++ b/app/views/admin/order_cycles/_simple_form.html.haml @@ -22,7 +22,7 @@ .actions - if @order_cycle.new_record? - = f.submit 'Create', 'ng-click' => "submit('#{main_app.admin_order_cycles_path}')", 'ng-disabled' => '!loaded()' + = f.submit 'Create', 'ng-click' => "submit($event, '#{main_app.admin_order_cycles_path}')", 'ng-disabled' => '!loaded()' %span{'ng-show' => 'loaded()'} = link_to 'Cancel', main_app.admin_order_cycles_path diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index 025a01a6b2..45dea6d36e 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -156,7 +156,9 @@ describe 'OrderCycle controllers', -> expect(OrderCycle.removeDistributionOfVariant).toHaveBeenCalledWith('variant') it 'Submits the order cycle via OrderCycle create', -> - scope.submit('/admin/order_cycles') + eventMock = {preventDefault: jasmine.createSpy()} + scope.submit(eventMock,'/admin/order_cycles') + expect(eventMock.preventDefault).toHaveBeenCalled() expect(OrderCycle.create).toHaveBeenCalledWith('/admin/order_cycles') describe 'AdminEditOrderCycleCtrl', -> @@ -321,7 +323,9 @@ describe 'OrderCycle controllers', -> expect(OrderCycle.removeDistributionOfVariant).toHaveBeenCalledWith('variant') it 'Submits the order cycle via OrderCycle update', -> - scope.submit('/admin/order_cycles') + eventMock = {preventDefault: jasmine.createSpy()} + scope.submit(eventMock,'/admin/order_cycles') + expect(eventMock.preventDefault).toHaveBeenCalled() expect(OrderCycle.update).toHaveBeenCalledWith('/admin/order_cycles') expect(scope.order_cycle_form.$setPristine.calls.length).toEqual 1 From 8838a89ecca5db54cb9e0753c5f6d60178483655 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 17 Apr 2016 23:15:11 +1000 Subject: [PATCH 015/110] Don't typecast product and variant on_hand from DOM when it is 'On demand' --- .../admin/products/bulk_edit/_products_product.html.haml | 4 ++-- .../admin/products/bulk_edit/_products_variant.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml b/app/views/spree/admin/products/bulk_edit/_products_product.html.haml index 6ac25ae286..01af8014d1 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_product.html.haml @@ -18,8 +18,8 @@ %td.price{ 'ng-show' => 'columns.price.visible' } %input{ 'ng-model' => 'product.price', 'ofn-decimal' => :true, :name => 'price', 'ofn-track-product' => 'price', :type => 'text', 'ng-hide' => 'hasVariants(product)' } %td.on_hand{ 'ng-show' => 'columns.on_hand.visible' } - %span{ 'ng-bind' => 'product.on_hand', :name => 'on_hand', 'ng-show' => '!hasOnDemandVariants(product) && (hasVariants(product) || product.on_demand)' } - %input.field{ 'ng-model' => 'product.on_hand', :name => 'on_hand', 'ofn-track-product' => 'on_hand', 'ng-hide' => 'hasVariants(product) || product.on_demand', :type => 'number' } + %span{ 'ng-bind' => 'product.on_hand', :name => 'on_hand', 'ng-if' => '!hasOnDemandVariants(product) && (hasVariants(product) || product.on_demand)' } + %input.field{ 'ng-model' => 'product.on_hand', :name => 'on_hand', 'ofn-track-product' => 'on_hand', 'ng-if' => '!(hasVariants(product) || product.on_demand)', :type => 'number' } %td.on_demand{ 'ng-show' => 'columns.on_demand.visible' } %input.field{ 'ng-model' => 'product.on_demand', :name => 'on_demand', 'ofn-track-product' => 'on_demand', :type => 'checkbox', 'ng-hide' => 'hasVariants(product)' } %td.category{ 'ng-if' => 'columns.category.visible' } diff --git a/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml b/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml index fb68704b79..f2e65d6c88 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml @@ -14,8 +14,8 @@ %td{ 'ng-show' => 'columns.price.visible' } %input{ 'ng-model' => 'variant.price', 'ofn-decimal' => :true, :name => 'variant_price', 'ofn-track-variant' => 'price', :type => 'text' } %td{ 'ng-show' => 'columns.on_hand.visible' } - %input.field{ 'ng-model' => 'variant.on_hand', 'ng-change' => 'updateOnHand(product)', :name => 'variant_on_hand', 'ng-hide' => 'variant.on_demand', 'ofn-track-variant' => 'on_hand', :type => 'number' } - %span{ 'ng-bind' => 'variant.on_hand', :name => 'variant_on_hand', 'ng-show' => 'variant.on_demand' } + %input.field{ 'ng-model' => 'variant.on_hand', 'ng-change' => 'updateOnHand(product)', :name => 'variant_on_hand', 'ng-if' => '!variant.on_demand', 'ofn-track-variant' => 'on_hand', :type => 'number' } + %span{ 'ng-bind' => 'variant.on_hand', :name => 'variant_on_hand', 'ng-if' => 'variant.on_demand' } %td{ 'ng-show' => 'columns.on_demand.visible' } %input.field{ 'ng-model' => 'variant.on_demand', :name => 'variant_on_demand', 'ofn-track-variant' => 'on_demand', :type => 'checkbox' } %td{ 'ng-show' => 'columns.category.visible' } From 10133a13f242f2c09e0e3e9694d73346283a83d1 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Mon, 18 Apr 2016 00:05:37 +1000 Subject: [PATCH 016/110] Make sure that changes are saved before attempting to click link --- spec/features/admin/bulk_order_management_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index 0b23e77f6c..de5bd85d3e 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -571,6 +571,7 @@ feature %q{ # So we save the changes expect(URI.parse(current_url).path).to eq "/admin/orders/bulk_management" click_button "Save Changes" + expect(page).to have_selector "#save-bar", text: "All changes saved" # And try again within "tr#li_#{li1.id}" do From 383f7c57aaa467b5421bc3e66f3b5ea2bef41ffa Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Mon, 18 Apr 2016 01:19:02 +1000 Subject: [PATCH 017/110] Wait for login before visiting CMS --- spec/features/admin/cms_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/features/admin/cms_spec.rb b/spec/features/admin/cms_spec.rb index 2d4056b792..8ab180906e 100644 --- a/spec/features/admin/cms_spec.rb +++ b/spec/features/admin/cms_spec.rb @@ -26,6 +26,7 @@ feature %q{ scenario "non-admin user can't access CMS admin", js: true do login_to_consumer_section + page.should_not have_content "Login" visit cms_admin_path page.should_not have_content "ComfortableMexicanSofa" current_path.should == root_path From 02d093f6b65c129eace927b3ae9d16960b742555 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Mon, 18 Apr 2016 01:20:38 +1000 Subject: [PATCH 018/110] Loading selectors into product modal to display proprties and taxons --- .../controllers/products/product_node_controller.js.coffee | 5 +++-- app/assets/javascripts/templates/product_modal.html.haml | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/products/product_node_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/products/product_node_controller.js.coffee index 07ddb4bc61..a692af3fb5 100644 --- a/app/assets/javascripts/darkswarm/controllers/products/product_node_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/products/product_node_controller.js.coffee @@ -1,6 +1,7 @@ -Darkswarm.controller "ProductNodeCtrl", ($scope, $modal) -> +Darkswarm.controller "ProductNodeCtrl", ($scope, $modal, FilterSelectorsService) -> $scope.enterprise = $scope.product.supplier # For the modal, so it's consistent $scope.triggerProductModal = -> + $scope.productTaxonSelectors = FilterSelectorsService.createSelectors() + $scope.productPropertySelectors = FilterSelectorsService.createSelectors() $modal.open(templateUrl: "product_modal.html", scope: $scope) - diff --git a/app/assets/javascripts/templates/product_modal.html.haml b/app/assets/javascripts/templates/product_modal.html.haml index f4839f6512..4b21c054a4 100644 --- a/app/assets/javascripts/templates/product_modal.html.haml +++ b/app/assets/javascripts/templates/product_modal.html.haml @@ -9,10 +9,10 @@ %br .filter-shopfront.taxon-selectors.inline-block - %filter-selector{ objects: "[product] | taxonsOf" } + %filter-selector{ 'selector-set' => "productTaxonSelectors", objects: "[product] | taxonsOf" } .filter-shopfront.property-selectors.inline-block - %filter-selector{ objects: "[product] | propertiesWithValuesOf" } + %filter-selector{ 'selector-set' => "productPropertySelectors", objects: "[product] | propertiesWithValuesOf" } %div{"ng-if" => "product.description"} %hr From 668bffcd844af74db17f92d12416a38fe9567bcf Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Mon, 18 Apr 2016 01:47:33 +1000 Subject: [PATCH 019/110] Bumping AngularJS to 1.4.8 --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 1a635a80f9..9344aac5e0 100644 --- a/Gemfile +++ b/Gemfile @@ -28,7 +28,7 @@ gem 'comfortable_mexican_sofa' gem 'simple_form', :github => 'RohanM/simple_form' gem 'unicorn' -gem 'angularjs-rails', '1.3.15' +gem 'angularjs-rails', '1.4.8' gem 'bugsnag' gem 'newrelic_rpm' gem 'haml' diff --git a/Gemfile.lock b/Gemfile.lock index ed44002f2a..f4a1915a56 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -153,7 +153,7 @@ GEM sprockets (~> 2) tilt angularjs-file-upload-rails (1.1.0) - angularjs-rails (1.3.15) + angularjs-rails (1.4.8) ansi (1.4.2) arel (3.0.3) atomic (1.1.99) @@ -650,7 +650,7 @@ DEPENDENCIES andand angular-rails-templates (~> 0.2.0) angularjs-file-upload-rails (~> 1.1.0) - angularjs-rails (= 1.3.15) + angularjs-rails (= 1.4.8) atomic awesome_print aws-sdk @@ -734,4 +734,4 @@ DEPENDENCIES wkhtmltopdf-binary BUNDLED WITH - 1.10.6 + 1.11.2 From af5eec70947044034119e67083852776317dd320 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 1 May 2016 18:28:25 +1000 Subject: [PATCH 020/110] WIP (Angular 1.4 Upgrade): Adding filters to ofn-select2 and using on order create/edit UI --- .../directives/ofn-select2.js.coffee | 8 ++++-- .../controllers/orders_controller.js.coffee | 14 ++++++---- .../javascripts/admin/orders/orders.js.coffee | 2 +- .../add_distribution_fields.html.haml.deface | 21 ++++++++++----- spec/features/admin/orders_spec.rb | 26 +++++++++++-------- 5 files changed, 46 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee b/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee index 132480d987..bbb5221682 100644 --- a/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.indexUtils").directive "ofnSelect2", ($sanitize, $timeout) -> +angular.module("admin.indexUtils").directive "ofnSelect2", ($sanitize, $timeout, $filter) -> require: 'ngModel' restrict: 'C' scope: @@ -6,15 +6,19 @@ angular.module("admin.indexUtils").directive "ofnSelect2", ($sanitize, $timeout) minSearch: "@?" text: "@?" blank: "=?" + filter: "=?" link: (scope, element, attrs, ngModel) -> $timeout -> scope.text ||= 'name' + scope.filter ||= -> true scope.data.unshift(scope.blank) if scope.blank? && typeof scope.blank is "object" item.name = $sanitize(item.name) for item in scope.data element.select2 minimumResultsForSearch: scope.minSearch || 0 - data: { results: scope.data, text: scope.text } + data: -> + filtered = $filter('filter')(scope.data,scope.filter) + { results: filtered, text: scope.text } formatSelection: (item) -> item[scope.text] formatResult: (item) -> diff --git a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee index 6ad7f0bfb9..060b23bed9 100644 --- a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee +++ b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee @@ -2,13 +2,11 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, $compile, $attr $scope.$compile = $compile $scope.shops = shops $scope.orderCycles = orderCycles - for oc in $scope.orderCycles - oc.name_and_status = "#{oc.name} (#{oc.status})" - $scope.distributor_id = $attrs.ofnDistributorId - $scope.order_cycle_id = $attrs.ofnOrderCycleId + $scope.distributor_id = parseInt($attrs.ofnDistributorId) + $scope.order_cycle_id = parseInt($attrs.ofnOrderCycleId) - $scope.validOrderCycle = (oc, index, array) -> + $scope.validOrderCycle = (oc) -> $scope.orderCycleHasDistributor oc, parseInt($scope.distributor_id) $scope.distributorHasOrderCycles = (distributor) -> @@ -20,3 +18,9 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, $compile, $attr $scope.distributionChosen = -> $scope.distributor_id && $scope.order_cycle_id + + for oc in $scope.orderCycles + oc.name_and_status = "#{oc.name} (#{oc.status})" + + for shop in $scope.shops + shop.disabled = !$scope.distributorHasOrderCycles(shop) diff --git a/app/assets/javascripts/admin/orders/orders.js.coffee b/app/assets/javascripts/admin/orders/orders.js.coffee index abfe576095..4e1ec754e3 100644 --- a/app/assets/javascripts/admin/orders/orders.js.coffee +++ b/app/assets/javascripts/admin/orders/orders.js.coffee @@ -1 +1 @@ -angular.module("admin.orders", ['ngResource']) +angular.module("admin.orders", ['admin.indexUtils', 'ngResource']) diff --git a/app/overrides/spree/admin/orders/_form/add_distribution_fields.html.haml.deface b/app/overrides/spree/admin/orders/_form/add_distribution_fields.html.haml.deface index 3fa7d3ea83..a6c51d517c 100644 --- a/app/overrides/spree/admin/orders/_form/add_distribution_fields.html.haml.deface +++ b/app/overrides/spree/admin/orders/_form/add_distribution_fields.html.haml.deface @@ -19,11 +19,20 @@ .alpha.six.columns .field %label{for: "order_distributor_id"} Distributor - %select.fullwidth{id: "order_distributor_id", name: "order[distributor_id]", 'ng-model' => 'distributor_id'} - %option{"ng-repeat" => "shop in shops", "ng-value" => "shop.id", "ng-selected" => "distributor_id == shop.id", "ng-disabled" => "!distributorHasOrderCycles(shop)", "ng-bind" => "shop.name"} + %input.ofn-select2.fullwidth{id: "order_distributor_id", + type: 'number', + name: "order[distributor_id]", + "ng-model" => 'distributor_id', + data: "shops" } .omega.six.columns - .field{"ng-show" => "distributor_id"} - %label{for: "order_order_cycle_id"} Order Cycle - %select.select2.fullwidth{id: "order_order_cycle_id", name: "order[order_cycle_id]", 'ng-model' => 'order_cycle_id'} - %option{"ng-repeat" => "oc in orderCycles | filter:validOrderCycle", "ng-value" => "oc.id", "ng-selected" => "order_cycle_id == oc.id", "ng-bind" => "oc.name_and_status"} + .field + %label{ for: "order_order_cycle_id"} Order Cycle + %input.ofn-select2.fullwidth{id: "order_order_cycle_id", + type: 'number', + name: "order[order_cycle_id]", + "ng-model" => 'order_cycle_id', + "ng-disabled" => "!distributor_id", + data: "orderCycles", + text: "name_and_status", + filter: "validOrderCycle" } diff --git a/spec/features/admin/orders_spec.rb b/spec/features/admin/orders_spec.rb index 725495b74b..33f91eb850 100644 --- a/spec/features/admin/orders_spec.rb +++ b/spec/features/admin/orders_spec.rb @@ -21,7 +21,7 @@ feature %q{ create :check_payment, order: @order, amount: @order.total end - scenario "creating an order with distributor and order cycle", retry: 3 do + scenario "creating an order with distributor and order cycle" do distributor_disabled = create(:distributor_enterprise) create(:simple_order_cycle, name: 'Two') @@ -31,12 +31,16 @@ feature %q{ click_link 'New Order' # Distributors without an order cycle should be shown as disabled - page.should have_selector "option[value='#{distributor_disabled.id}'][disabled='disabled']" + open_select2('#s2id_order_distributor_id') + page.should have_selector "ul.select2-results li.select2-result.select2-disabled", text: distributor_disabled.name + close_select2('#s2id_order_distributor_id') + + # Order cycle selector should be disabled + page.should have_selector "#s2id_order_order_cycle_id.select2-container-disabled" # When we select a distributor, it should limit order cycle selection to those for that distributor - page.should_not have_select2 'order_order_cycle_id' - select @distributor.name, from: 'order_distributor_id' - page.should have_select2 'order_order_cycle_id', options: ['', 'One (open)'] + select2_select @distributor.name, from: 'order_distributor_id' + page.should have_select2 'order_order_cycle_id', options: ['One (open)'] select2_select @order_cycle.name, from: 'order_order_cycle_id' page.should have_content 'ADD PRODUCT' @@ -80,7 +84,7 @@ feature %q{ click_edit - select d.name, from: 'order_distributor_id' + select2_select d.name, from: 'order_distributor_id' select2_select oc.name, from: 'order_order_cycle_id' click_button 'Update And Recalculate Fees' @@ -106,7 +110,7 @@ feature %q{ visit '/admin/orders' page.find('td.actions a.icon-edit').click - page.should have_no_select 'order_distributor_id' + page.should_not have_select2 'order_distributor_id' page.should_not have_select2 'order_order_cycle_id' page.should have_selector 'p', text: "Distributor: #{@order.distributor.name}" @@ -124,7 +128,7 @@ feature %q{ login_to_admin_section visit '/admin/orders' click_link 'New Order' - select @distributor.name, from: 'order_distributor_id' + select2_select @distributor.name, from: 'order_distributor_id' select2_select @order_cycle.name, from: 'order_order_cycle_id' targetted_select2_search @product.name, from: '#add_variant_id', dropdown_css: '.select2-drop' click_link 'Add' @@ -208,7 +212,7 @@ feature %q{ visit '/admin/orders' click_link 'New Order' - select distributor1.name, from: 'order_distributor_id' + select2_select distributor1.name, from: 'order_distributor_id' select2_select order_cycle1.name, from: 'order_order_cycle_id' expect(page).to have_content 'ADD PRODUCT' @@ -218,8 +222,8 @@ feature %q{ page.has_selector? "table.index tbody[data-hook='admin_order_form_line_items'] tr" # Wait for JS expect(page).to have_selector 'td', text: product.name - expect(page).to have_select 'order_distributor_id', with_options: [distributor1.name] - expect(page).to_not have_select 'order_distributor_id', with_options: [distributor2.name] + expect(page).to have_select2 'order_distributor_id', with_options: [distributor1.name] + expect(page).to_not have_select2 'order_distributor_id', with_options: [distributor2.name] expect(page).to have_select2 'order_order_cycle_id', with_options: ["#{order_cycle1.name} (open)"] expect(page).to_not have_select2 'order_order_cycle_id', with_options: ["#{order_cycle2.name} (open)"] From 39a062e90a87718777d9699fe823b705a2c5e716 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 1 May 2016 18:29:09 +1000 Subject: [PATCH 021/110] WIP (Angular 1.4 Upgrade): Moving controllers for login tabs to prevent multiple directives asking for isolate scope --- app/assets/javascripts/templates/forgot.html.haml | 9 ++------- app/assets/javascripts/templates/login.html.haml | 7 ++----- app/assets/javascripts/templates/signup.html.haml | 7 ++----- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/templates/forgot.html.haml b/app/assets/javascripts/templates/forgot.html.haml index f85b91ab0a..3e88350321 100644 --- a/app/assets/javascripts/templates/forgot.html.haml +++ b/app/assets/javascripts/templates/forgot.html.haml @@ -1,10 +1,5 @@ -%tab#forgot{"ng-controller" => "ForgotCtrl", - heading: "{{'forgot_password' | t}}", - active: "active", - select: "select(path)"} - - %form{"ng-submit" => "submit()"} - +%tab#forgot{ heading: "{{'forgot_password' | t}}", active: "active", select: "select(path)"} + %form{ ng: { controller: "ForgotCtrl", submit: "submit()" } } .row .large-12.columns .alert-box.success.radius{"ng-show" => "sent"} diff --git a/app/assets/javascripts/templates/login.html.haml b/app/assets/javascripts/templates/login.html.haml index 3d2f9a3e06..8b3e64cfb1 100644 --- a/app/assets/javascripts/templates/login.html.haml +++ b/app/assets/javascripts/templates/login.html.haml @@ -1,8 +1,5 @@ -%tab#login-content{"ng-controller" => "LoginCtrl", - heading: "{{'label_login' | t}}", - active: "active", - select: "select(path)"} - %form{"ng-submit" => "submit()"} +%tab#login-content{ heading: "{{'label_login' | t}}", active: "active", select: "select(path)"} + %form{ ng: { controller: "LoginCtrl", submit: "submit()" } } .row .large-12.columns .alert-box.alert{"ng-show" => "errors != null"} diff --git a/app/assets/javascripts/templates/signup.html.haml b/app/assets/javascripts/templates/signup.html.haml index c71d9453bd..1c13985f38 100644 --- a/app/assets/javascripts/templates/signup.html.haml +++ b/app/assets/javascripts/templates/signup.html.haml @@ -1,8 +1,5 @@ -%tab#sign-up-content{"ng-controller" => "SignupCtrl", - heading: "{{'label_signup' | t}}", - active: 'active', - select: "select(path)"} - %form{"ng-submit" => "submit()"} +%tab#sign-up-content{ heading: "{{'label_signup' | t}}", active: 'active', select: "select(path)"} + %form{ ng: { controller: "SignupCtrl", submit: "submit()" } } .row .large-12.columns %label{for: "email"} {{'signup_email' | t}} From 9d1ca6eaca70c9f15340cd18d8b2e0eaa706fb3c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 1 May 2016 18:49:17 +1000 Subject: [PATCH 022/110] Renaming customers form to prevent conflict with customers array --- app/views/admin/customers/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index 0a348d4a0a..dc38304f8e 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -30,7 +30,7 @@ .row{ ng: { show: "loaded && filteredCustomers.length > 0" } } - %form{ name: "customers" } + %form{ name: "customers_form" } %table.index#customers %col.email{ width: "20%"} %col.code{ width: "20%"} From 1b0897d53aaa41a723969097d2f29e4ce40ee5c3 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 1 May 2016 18:52:13 +1000 Subject: [PATCH 023/110] Bump to AngularJS to 1.5.5 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 9344aac5e0..eb49f89c23 100644 --- a/Gemfile +++ b/Gemfile @@ -28,7 +28,7 @@ gem 'comfortable_mexican_sofa' gem 'simple_form', :github => 'RohanM/simple_form' gem 'unicorn' -gem 'angularjs-rails', '1.4.8' +gem 'angularjs-rails', '1.5.5' gem 'bugsnag' gem 'newrelic_rpm' gem 'haml' diff --git a/Gemfile.lock b/Gemfile.lock index f4a1915a56..494882481c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -153,7 +153,7 @@ GEM sprockets (~> 2) tilt angularjs-file-upload-rails (1.1.0) - angularjs-rails (1.4.8) + angularjs-rails (1.5.5) ansi (1.4.2) arel (3.0.3) atomic (1.1.99) @@ -650,7 +650,7 @@ DEPENDENCIES andand angular-rails-templates (~> 0.2.0) angularjs-file-upload-rails (~> 1.1.0) - angularjs-rails (= 1.4.8) + angularjs-rails (= 1.5.5) atomic awesome_print aws-sdk From cfbfe8416fc3493046743d04d265400d8a8da37f Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 1 May 2016 21:09:44 +1000 Subject: [PATCH 024/110] WIP (AngularJS 1.5.5 upgrade): updating npm karma packages for travis --- .travis.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 56d94e5952..279eecb77c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,11 +28,11 @@ before_script: - RAILS_ENV=test bundle exec rake db:create db:schema:load - > if [ "$KARMA" = "true" ]; then - npm install karma@0.12.31 - npm install karma-jasmine@0.1.5 - npm install karma-phantomjs-launcher@0.1.4 - npm install karma-coffee-preprocessor@0.2.1 - npm install -g karma-cli@0.0.4 + npm install karma@0.13.22 + npm install karma-jasmine@0.3.8 + npm install karma-phantomjs-launcher@1.0.0 + npm install karma-coffee-preprocessor@0.3.0 + npm install -g karma-cli@0.1.2 fi script: From 7a498362b36d50d3c9f507d88b6ef2084cd82f1b Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 1 May 2016 21:10:28 +1000 Subject: [PATCH 025/110] Upgrading AngularJS specs to Jasmine 2 syntax --- .../customers_controller_spec.js.coffee | 17 ++--- .../enterprise_controller_spec.js.coffee | 2 +- .../enterprises_controller_spec.js.coffee | 2 +- .../index_panel_controller_spec.js.coffee | 4 +- .../permalink_controller_spec.js.coffee | 8 +-- .../side_menu_controller_spec.js.coffee | 4 +- .../enterprise_payment_methods_spec.js.coffee | 6 +- ...enterprise_shipping_methods_spec.js.coffee | 6 +- .../services/enterprises_spec.js.coffee | 11 ++-- .../services/panels_spec.js.coffee | 6 +- .../services/pending_changes_spec.js.coffee | 16 ++--- .../services/switch_class_spec.js.coffee | 6 +- .../index_utils/services/views_spec.js.coffee | 2 +- .../line_items_controller_spec.js.coffee | 27 ++++---- .../services/line_items_spec.js.coffee | 11 ++-- .../controllers/simple_create.js.coffee | 6 +- .../services/order_cycles_spec.js.coffee | 11 ++-- .../orders/services/orders_spec.js.coffee | 11 ++-- .../option_value_namer_spec.js.coffee | 20 +++--- .../side_menu/services/side_menu.js.coffee | 8 +-- .../tag_rules_controller_spec.js.coffee | 2 +- .../unit/bulk_order_management_spec.js.coffee | 38 +++++------ .../unit/bulk_product_update_spec.js.coffee | 26 ++++---- .../checkout_controller_spec.js.coffee | 2 +- .../darkswarm/services/cart_spec.js.coffee | 20 +++--- .../services/checkout_spec.js.coffee | 3 +- .../darkswarm/services/sidebar_spec.js.coffee | 9 +-- .../unit/order_cycle_spec.js.coffee | 66 +++++++++---------- 28 files changed, 177 insertions(+), 173 deletions(-) diff --git a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee index 6981ef4e4f..1da89f2053 100644 --- a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee @@ -8,9 +8,10 @@ describe "CustomersCtrl", -> scope = $rootScope http = $httpBackend $controller 'customersCtrl', {$scope: scope, CustomerResource: _CustomerResource_, shops: {}} - this.addMatchers - toAngularEqual: (expected) -> - return angular.equals(this.actual, expected) + jasmine.addMatchers + toDeepEqual: (util, customEqualityTesters) -> + compare: (actual, expected) -> + { pass: angular.equals(actual, expected) } it "has no shop pre-selected", -> expect(scope.shop).toEqual {} @@ -26,7 +27,7 @@ describe "CustomersCtrl", -> http.flush() it "retrievs the list of customers", -> - expect(scope.customers).toAngularEqual customers + expect(scope.customers).toDeepEqual customers describe "scope.add", -> it "creates a new customer", -> @@ -36,7 +37,7 @@ describe "CustomersCtrl", -> http.expectPOST('/admin/customers.json?email=' + email + '&enterprise_id=1').respond 200, newCustomer scope.add(email) http.flush() - expect(scope.customers).toAngularEqual customers + expect(scope.customers).toDeepEqual customers describe "scope.deleteCustomer", -> it "deletes a customer", -> @@ -46,7 +47,7 @@ describe "CustomersCtrl", -> scope.deleteCustomer(customer) http.flush() expect(scope.customers.length).toBe 1 - expect(scope.customers[0]).not.toAngularEqual customer + expect(scope.customers[0]).not.toDeepEqual customer describe "scope.findTags", -> tags = [ @@ -63,7 +64,7 @@ describe "CustomersCtrl", -> promise.then (data) -> result = data http.flush() - expect(result).toAngularEqual tags + expect(result).toDeepEqual tags it "filters the tag list", -> filtered_tags = [ @@ -75,4 +76,4 @@ describe "CustomersCtrl", -> promise.then (data) -> result = data http.flush() - expect(result).toAngularEqual filtered_tags + expect(result).toDeepEqual filtered_tags diff --git a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee index b504ff8f90..450876c9e6 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee @@ -53,7 +53,7 @@ describe "enterpriseCtrl", -> expect(enterprise.users).not.toContain u4 it "ignores objects that are already in the list, and alerts the user", -> - spyOn(window, "alert").andCallThrough() + spyOn(window, "alert").and.callThrough() u4 = { id: 3, email: "email-doesn't-matter.com" } scope.addManager u4 expect(enterprise.users).not.toContain u4 diff --git a/spec/javascripts/unit/admin/enterprises/controllers/enterprises_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/enterprises_controller_spec.js.coffee index 6e8bdd8040..573ca33d9f 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/enterprises_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/enterprises_controller_spec.js.coffee @@ -8,7 +8,7 @@ describe "EnterprisesCtrl", -> inject ($controller, $rootScope, _Enterprises_) -> scope = $rootScope Enterprises = _Enterprises_ - spyOn(Enterprises, "index").andReturn "list of enterprises" + spyOn(Enterprises, "index").and.returnValue "list of enterprises" ctrl = $controller 'enterprisesCtrl', {$scope: scope, Enterprises: Enterprises} describe "setting the shop on scope", -> diff --git a/spec/javascripts/unit/admin/enterprises/controllers/index_panel_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/index_panel_controller_spec.js.coffee index b74595b775..8fca1c6ab5 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/index_panel_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/index_panel_controller_spec.js.coffee @@ -20,10 +20,10 @@ describe "indexPanelCtrl", -> deferred = null beforeEach inject ($q) -> - spyOn(scope, "saved").andReturn false + spyOn(scope, "saved").and.returnValue false spyOn(scope, "$emit") deferred = $q.defer() - spyOn(Enterprises, "save").andReturn(deferred.promise) + spyOn(Enterprises, "save").and.returnValue(deferred.promise) scope.save() it "sets scope.saving to true", -> diff --git a/spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee index 888a42daa3..bcecc1c5a6 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/permalink_controller_spec.js.coffee @@ -28,21 +28,21 @@ describe "permalinkCtrl", -> it "sends a request to PermalinkChecker when permalink is changed", -> deferred.resolve("") promise = deferred.promise - spyOn(PermalinkChecker, "check").andReturn promise + spyOn(PermalinkChecker, "check").and.returnValue promise $scope.$apply Enterprise.permalink = "somethingelse" # Change the permalink expect(PermalinkChecker.check).toHaveBeenCalled() it "sets available to '' when PermalinkChecker resolves permalink to the existing permalink on Enterprise ", -> deferred.resolve({permalink: "something"}) promise = deferred.promise - spyOn(PermalinkChecker, "check").andReturn promise + spyOn(PermalinkChecker, "check").and.returnValue promise $scope.$apply Enterprise.permalink = "somethingelse" # Change the permalink expect($scope.availability).toEqual "" it "sets available and permalink when PermalinkChecker resolves", -> deferred.resolve({ available: "Available", permalink: "permalink"}) promise = deferred.promise - spyOn(PermalinkChecker, "check").andReturn promise + spyOn(PermalinkChecker, "check").and.returnValue promise $scope.$apply Enterprise.permalink = "somethingelse" # Change the permalink expect(Enterprise.permalink).toEqual "permalink" expect($scope.availability).toEqual "Available" @@ -51,7 +51,7 @@ describe "permalinkCtrl", -> $scope.availability = "Some Availability" deferred.reject() promise = deferred.promise - spyOn(PermalinkChecker, "check").andReturn promise + spyOn(PermalinkChecker, "check").and.returnValue promise $scope.$apply Enterprise.permalink = "somethingelse" # Change the permalink expect($scope.availability).toEqual "Some Availability" expect(Enterprise.permalink).toEqual "somethingelse" diff --git a/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee index e88aeb44f2..c4a724a137 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee @@ -17,8 +17,8 @@ describe "menuCtrl", -> inject ($rootScope, $controller, _SideMenu_) -> scope = $rootScope SideMenu = _SideMenu_ - spyOn(SideMenu, "select").andCallThrough() - spyOn(SideMenu, "setItems").andCallThrough() + spyOn(SideMenu, "select").and.callThrough() + spyOn(SideMenu, "setItems").and.callThrough() ctrl = $controller 'sideMenuCtrl', {$scope: scope, enterprise: enterprise, SideMenu: SideMenu, enterprisePermissions: {}} describe "initialisation", -> diff --git a/spec/javascripts/unit/admin/enterprises/services/enterprise_payment_methods_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/services/enterprise_payment_methods_spec.js.coffee index 0a719b203a..c4e83cd283 100644 --- a/spec/javascripts/unit/admin/enterprises/services/enterprise_payment_methods_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/services/enterprise_payment_methods_spec.js.coffee @@ -27,16 +27,16 @@ describe "EnterprisePaymentMethods service", -> describe "determining payment method colour", -> it "returns 'blue' when at least one payment method is selected", -> - spyOn(EnterprisePaymentMethods, "selectedCount").andReturn 1 + spyOn(EnterprisePaymentMethods, "selectedCount").and.returnValue 1 expect(EnterprisePaymentMethods.displayColor()).toBe "blue" it "returns 'red' when no payment methods are selected", -> - spyOn(EnterprisePaymentMethods, "selectedCount").andReturn 0 + spyOn(EnterprisePaymentMethods, "selectedCount").and.returnValue 0 expect(EnterprisePaymentMethods.displayColor()).toBe "red" it "returns 'red' when no payment methods exist", -> EnterprisePaymentMethods.paymentMethods = [] - spyOn(EnterprisePaymentMethods, "selectedCount").andReturn 1 + spyOn(EnterprisePaymentMethods, "selectedCount").and.returnValue 1 expect(EnterprisePaymentMethods.displayColor()).toBe "red" describe "counting selected payment methods", -> diff --git a/spec/javascripts/unit/admin/enterprises/services/enterprise_shipping_methods_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/services/enterprise_shipping_methods_spec.js.coffee index 4b857023b8..d8b83cf6c7 100644 --- a/spec/javascripts/unit/admin/enterprises/services/enterprise_shipping_methods_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/services/enterprise_shipping_methods_spec.js.coffee @@ -27,16 +27,16 @@ describe "EnterpriseShippingMethods service", -> describe "determining shipping method colour", -> it "returns 'blue' when at least one shipping method is selected", -> - spyOn(EnterpriseShippingMethods, "selectedCount").andReturn 1 + spyOn(EnterpriseShippingMethods, "selectedCount").and.returnValue 1 expect(EnterpriseShippingMethods.displayColor()).toBe "blue" it "returns 'red' when no shipping methods are selected", -> - spyOn(EnterpriseShippingMethods, "selectedCount").andReturn 0 + spyOn(EnterpriseShippingMethods, "selectedCount").and.returnValue 0 expect(EnterpriseShippingMethods.displayColor()).toBe "red" it "returns 'red' when no shipping methods exist", -> EnterpriseShippingMethods.shippingMethods = [] - spyOn(EnterpriseShippingMethods, "selectedCount").andReturn 1 + spyOn(EnterpriseShippingMethods, "selectedCount").and.returnValue 1 expect(EnterpriseShippingMethods.displayColor()).toBe "red" describe "counting selected shipping methods", -> diff --git a/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee index fb653d2df4..702ef2cc1c 100644 --- a/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee @@ -4,9 +4,10 @@ describe "Enterprises service", -> beforeEach -> module 'admin.enterprises' - this.addMatchers - toDeepEqual: (expected) -> - return angular.equals(this.actual, expected) + jasmine.addMatchers + toDeepEqual: (util, customEqualityTesters) -> + compare: (actual, expected) -> + { pass: angular.equals(actual, expected) } inject ($q, _$httpBackend_, _Enterprises_, _EnterpriseResource_) -> Enterprises = _Enterprises_ @@ -98,14 +99,14 @@ describe "Enterprises service", -> describe "#saved", -> describe "when attributes of the object have been altered", -> beforeEach -> - spyOn(Enterprises, "diff").andReturn ["attr1", "attr2"] + spyOn(Enterprises, "diff").and.returnValue ["attr1", "attr2"] it "returns false", -> expect(Enterprises.saved({})).toBe false describe "when attributes of the object have not been altered", -> beforeEach -> - spyOn(Enterprises, "diff").andReturn [] + spyOn(Enterprises, "diff").and.returnValue [] it "returns false", -> expect(Enterprises.saved({})).toBe true diff --git a/spec/javascripts/unit/admin/index_utils/services/panels_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/panels_spec.js.coffee index a55d9ffa61..7ee69cf88e 100644 --- a/spec/javascripts/unit/admin/index_utils/services/panels_spec.js.coffee +++ b/spec/javascripts/unit/admin/index_utils/services/panels_spec.js.coffee @@ -29,7 +29,7 @@ describe "Panels service", -> describe "when no panel is currently selected", -> beforeEach -> - scopeMock.getSelected = jasmine.createSpy('getSelected').andReturn(null) + scopeMock.getSelected = jasmine.createSpy('getSelected').and.returnValue(null) Panels.toggle(12, 'panel_name') it "calls #open on the scope", -> @@ -37,7 +37,7 @@ describe "Panels service", -> describe "when #toggle is called for the currently selected panel", -> beforeEach -> - scopeMock.getSelected = jasmine.createSpy('getSelected').andReturn('panel_name') + scopeMock.getSelected = jasmine.createSpy('getSelected').and.returnValue('panel_name') Panels.toggle(12, 'panel_name') it "calls #close on the scope", -> @@ -45,7 +45,7 @@ describe "Panels service", -> describe "when #toggle is called for a different panel", -> beforeEach -> - scopeMock.getSelected = jasmine.createSpy('getSelected').andReturn('some_other_panel_name') + scopeMock.getSelected = jasmine.createSpy('getSelected').and.returnValue('some_other_panel_name') Panels.toggle(12, 'panel_name') it "calls #setSelected on the scope", -> diff --git a/spec/javascripts/unit/admin/index_utils/services/pending_changes_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/pending_changes_spec.js.coffee index 31b85df217..df0eaea495 100644 --- a/spec/javascripts/unit/admin/index_utils/services/pending_changes_spec.js.coffee +++ b/spec/javascripts/unit/admin/index_utils/services/pending_changes_spec.js.coffee @@ -4,7 +4,7 @@ describe "Pending Changes", -> beforeEach -> resourcesMock = - update: jasmine.createSpy('update').andCallFake (change) -> + update: jasmine.createSpy('update').and.callFake (change) -> $promise: then: (successFn, errorFn) -> return successFn({propertyName: "new_value"}) if change.success @@ -88,7 +88,7 @@ describe "Pending Changes", -> it "sends the correct object to dataSubmitter", -> pendingChanges.submit change - expect(resourcesMock.update.calls.length).toEqual 1 + expect(resourcesMock.update.calls.count()).toBe 1 expect(resourcesMock.update).toHaveBeenCalledWith change describe "successful request", -> @@ -96,9 +96,9 @@ describe "Pending Changes", -> change.success = true it "calls remove with id and attribute name", -> - spyOn(pendingChanges, "remove").andCallFake(->) + spyOn(pendingChanges, "remove").and.callFake(->) pendingChanges.submit change - expect(pendingChanges.remove.calls.length).toEqual 1 + expect(pendingChanges.remove.calls.count()).toBe 1 expect(pendingChanges.remove).toHaveBeenCalledWith 1, "propertyName" it "calls reset on the relevant scope", -> @@ -114,7 +114,7 @@ describe "Pending Changes", -> change.success = false it "does not call remove", -> - spyOn(pendingChanges, "remove").andCallFake(->) + spyOn(pendingChanges, "remove").and.callFake(->) pendingChanges.submit change expect(pendingChanges.remove).not.toHaveBeenCalled() @@ -128,13 +128,13 @@ describe "Pending Changes", -> describe "cycling through all changes to submit to server", -> it "sends the correct object to dataSubmitter", -> - spyOn(pendingChanges, "submit").andCallFake(->) + spyOn(pendingChanges, "submit").and.callFake(->) pendingChanges.pendingChanges = 1: { "prop1": { attr: "prop1", value: 1 }, "prop2": { attr: "prop2", value: 2 } } 2: { "prop1": { attr: "prop1", value: 2 }, "prop2": { attr: "prop2", value: 4 } } 7: { "prop2": { attr: "prop2", value: 5 } } pendingChanges.submitAll() - expect(pendingChanges.submit.calls.length).toEqual 5 + expect(pendingChanges.submit.calls.count()).toBe 5 expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop1", value: 1 } expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop2", value: 2 } expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop1", value: 2 } @@ -142,7 +142,7 @@ describe "Pending Changes", -> expect(pendingChanges.submit).toHaveBeenCalledWith { attr: "prop2", value: 5 } it "returns an array of promises representing all sumbit requests", -> - spyOn(pendingChanges, "submit").andCallFake (change) -> change.value + spyOn(pendingChanges, "submit").and.callFake (change) -> change.value pendingChanges.pendingChanges = 1: { "prop1": { attr: "prop1", value: 1 } } 2: { "prop1": { attr: "prop1", value: 2 }, "prop2": { attr: "prop1", value: 4 } } diff --git a/spec/javascripts/unit/admin/index_utils/services/switch_class_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/switch_class_spec.js.coffee index e7dedb4e92..117231ca14 100644 --- a/spec/javascripts/unit/admin/index_utils/services/switch_class_spec.js.coffee +++ b/spec/javascripts/unit/admin/index_utils/services/switch_class_spec.js.coffee @@ -8,7 +8,7 @@ describe "switchClass service", -> elementMock = addClass: addClass removeClass: removeClass - timeoutMock = jasmine.createSpy('timeout').andReturn "new timeout" + timeoutMock = jasmine.createSpy('timeout').and.returnValue "new timeout" timeoutMock.cancel = jasmine.createSpy('timeout.cancel') beforeEach -> @@ -22,14 +22,14 @@ describe "switchClass service", -> it "calls addClass on the element once", -> switchClassService elementMock, "addClass", [], false expect(addClass).toHaveBeenCalledWith "addClass" - expect(addClass.calls.length).toEqual 1 + expect(addClass.calls.count()).toBe 1 it "calls removeClass on the element for ", -> switchClassService elementMock, "", ["remClass1", "remClass2", "remClass3"], false expect(removeClass).toHaveBeenCalledWith "remClass1" expect(removeClass).toHaveBeenCalledWith "remClass2" expect(removeClass).toHaveBeenCalledWith "remClass3" - expect(removeClass.calls.length).toEqual 3 + expect(removeClass.calls.count()).toBe 3 it "call cancel on element.timout only if it exists", -> switchClassService elementMock, "", [], false diff --git a/spec/javascripts/unit/admin/index_utils/services/views_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/views_spec.js.coffee index 7333882d5d..77df49d621 100644 --- a/spec/javascripts/unit/admin/index_utils/services/views_spec.js.coffee +++ b/spec/javascripts/unit/admin/index_utils/services/views_spec.js.coffee @@ -9,7 +9,7 @@ describe "Views service", -> describe "setting views", -> beforeEach -> - spyOn(Views, "selectView").andCallThrough() + spyOn(Views, "selectView").and.callThrough() Views.setViews view1: { name: 'View1', visible: true } view2: { name: 'View2', visible: false } diff --git a/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee b/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee index 7a0b165e2b..5b4e4730b6 100644 --- a/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee @@ -5,9 +5,10 @@ describe "LineItemsCtrl", -> beforeEach -> module "admin.lineItems" - this.addMatchers - toDeepEqual: (expected) -> - return angular.equals(this.actual, expected) + jasmine.addMatchers + toDeepEqual: (util, customEqualityTesters) -> + compare: (actual, expected) -> + { pass: angular.equals(actual, expected) } beforeEach inject(($controller, $rootScope, $httpBackend, _$timeout_, _VariantUnitManager_, _Enterprises_, _Orders_, _LineItems_, _OrderCycles_) -> scope = $rootScope.$new() @@ -19,9 +20,9 @@ describe "LineItemsCtrl", -> LineItems = _LineItems_ OrderCycles = _OrderCycles_ VariantUnitManager = _VariantUnitManager_ - spyOn(window, "daysFromToday").andReturn "SomeDate" - spyOn(window, "formatDate").andReturn "SomeDate" - spyOn(window, "parseDate").andReturn "SomeDate" + spyOn(window, "daysFromToday").and.returnValue "SomeDate" + spyOn(window, "formatDate").and.returnValue "SomeDate" + spyOn(window, "parseDate").and.returnValue "SomeDate" supplier = { id: 1, name: "Supplier" } distributor = { id: 5, name: "Distributor" } @@ -51,7 +52,7 @@ describe "LineItemsCtrl", -> expect(scope.quickSearch).toBeUndefined() it "will not have reset the form state to pristine", -> - expect(scope.bulk_order_form.$setPristine.calls.length).toEqual 0 + expect(scope.bulk_order_form.$setPristine.calls.count()).toBe 0 describe "after data is returned", -> beforeEach -> @@ -87,13 +88,13 @@ describe "LineItemsCtrl", -> expect(scope.quickSearch).toBe = "" it "resets the form state to pristine", -> - expect(scope.bulk_order_form.$setPristine.calls.length).toEqual 1 + expect(scope.bulk_order_form.$setPristine.calls.count()).toBe 1 describe "deleting a line item", -> order = line_item1 = line_item2 = null beforeEach inject((LineItemResource) -> - spyOn(window,"confirm").andReturn true + spyOn(window,"confirm").and.returnValue true order = { number: "R12345678" } line_item1 = new LineItemResource({ id: 1, order: order }) line_item2 = new LineItemResource({ id: 2, order: order }) @@ -247,7 +248,7 @@ describe "LineItemsCtrl", -> # A Units Variant is an API object which holds unit properies of a variant beforeEach -> - spyOn(Math,"round").andCallThrough() + spyOn(Math,"round").and.callThrough() it "returns '' if selectedUnitsVariant has no property 'variant_unit'", -> expect(scope.formattedValueWithUnitName(1,{})).toEqual '' @@ -267,14 +268,14 @@ describe "LineItemsCtrl", -> it "calls Math.round with the quotient of scale and value, multiplied by 1000", -> unitsVariant = { variant_unit: "weight" } - spyOn(VariantUnitManager, "getScale").andReturn 5 + spyOn(VariantUnitManager, "getScale").and.returnValue 5 scope.formattedValueWithUnitName(10, unitsVariant) expect(Math.round).toHaveBeenCalledWith 10/5 * 1000 it "returns the result of Math.round divided by 1000, followed by the result of getUnitName", -> unitsVariant = { variant_unit: "weight" } - spyOn(VariantUnitManager, "getScale").andReturn 1000 - spyOn(VariantUnitManager, "getUnitName").andReturn "kg" + spyOn(VariantUnitManager, "getScale").and.returnValue 1000 + spyOn(VariantUnitManager, "getUnitName").and.returnValue "kg" expect(scope.formattedValueWithUnitName(2000,unitsVariant)).toEqual "2 kg" describe "updating the price upon updating the weight of a line item", -> diff --git a/spec/javascripts/unit/admin/line_items/services/line_items_spec.js.coffee b/spec/javascripts/unit/admin/line_items/services/line_items_spec.js.coffee index 44948ecb37..ef428377c0 100644 --- a/spec/javascripts/unit/admin/line_items/services/line_items_spec.js.coffee +++ b/spec/javascripts/unit/admin/line_items/services/line_items_spec.js.coffee @@ -4,9 +4,10 @@ describe "LineItems service", -> beforeEach -> module 'admin.lineItems' - this.addMatchers - toDeepEqual: (expected) -> - return angular.equals(this.actual, expected) + jasmine.addMatchers + toDeepEqual: (util, customEqualityTesters) -> + compare: (actual, expected) -> + { pass: angular.equals(actual, expected) } inject ($q, _$httpBackend_, _LineItems_, _LineItemResource_) -> LineItems = _LineItems_ @@ -72,14 +73,14 @@ describe "LineItems service", -> describe "#isSaved", -> describe "when attributes of the object have been altered", -> beforeEach -> - spyOn(LineItems, "diff").andReturn ["attr1", "attr2"] + spyOn(LineItems, "diff").and.returnValue ["attr1", "attr2"] it "returns false", -> expect(LineItems.isSaved({})).toBe false describe "when attributes of the object have not been altered", -> beforeEach -> - spyOn(LineItems, "diff").andReturn [] + spyOn(LineItems, "diff").and.returnValue [] it "returns false", -> expect(LineItems.isSaved({})).toBe true diff --git a/spec/javascripts/unit/admin/order_cycles/controllers/simple_create.js.coffee b/spec/javascripts/unit/admin/order_cycles/controllers/simple_create.js.coffee index 09d5524fac..7404bbead2 100644 --- a/spec/javascripts/unit/admin/order_cycles/controllers/simple_create.js.coffee +++ b/spec/javascripts/unit/admin/order_cycles/controllers/simple_create.js.coffee @@ -18,11 +18,11 @@ describe "AdminSimpleCreateOrderCycleCtrl", -> addSupplier: jasmine.createSpy() addDistributor: jasmine.createSpy() setExchangeVariants: jasmine.createSpy() - new: jasmine.createSpy().andReturn order_cycle + new: jasmine.createSpy().and.returnValue order_cycle Enterprise = - get: jasmine.createSpy().andReturn {id: 123} + get: jasmine.createSpy().and.returnValue {id: 123} index: jasmine.createSpy() - suppliedVariants: jasmine.createSpy().andReturn('supplied variants') + suppliedVariants: jasmine.createSpy().and.returnValue('supplied variants') EnterpriseFee = index: jasmine.createSpy() ocInstance = {} diff --git a/spec/javascripts/unit/admin/order_cycles/services/order_cycles_spec.js.coffee b/spec/javascripts/unit/admin/order_cycles/services/order_cycles_spec.js.coffee index 2ddfe92407..aebdb19f6f 100644 --- a/spec/javascripts/unit/admin/order_cycles/services/order_cycles_spec.js.coffee +++ b/spec/javascripts/unit/admin/order_cycles/services/order_cycles_spec.js.coffee @@ -4,9 +4,10 @@ describe "OrderCycles service", -> beforeEach -> module 'admin.orderCycles' - this.addMatchers - toDeepEqual: (expected) -> - return angular.equals(this.actual, expected) + jasmine.addMatchers + toDeepEqual: (util, customEqualityTesters) -> + compare: (actual, expected) -> + { pass: angular.equals(actual, expected) } inject ($q, _$httpBackend_, _OrderCycles_, _OrderCycleResource_) -> OrderCycles = _OrderCycles_ @@ -98,14 +99,14 @@ describe "OrderCycles service", -> describe "#saved", -> describe "when attributes of the object have been altered", -> beforeEach -> - spyOn(OrderCycles, "diff").andReturn ["attr1", "attr2"] + spyOn(OrderCycles, "diff").and.returnValue ["attr1", "attr2"] it "returns false", -> expect(OrderCycles.saved({})).toBe false describe "when attributes of the object have not been altered", -> beforeEach -> - spyOn(OrderCycles, "diff").andReturn [] + spyOn(OrderCycles, "diff").and.returnValue [] it "returns false", -> expect(OrderCycles.saved({})).toBe true diff --git a/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee b/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee index cefb1d7a10..c3d6646144 100644 --- a/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee +++ b/spec/javascripts/unit/admin/orders/services/orders_spec.js.coffee @@ -4,9 +4,10 @@ describe "Orders service", -> beforeEach -> module 'admin.orders' - this.addMatchers - toDeepEqual: (expected) -> - return angular.equals(this.actual, expected) + jasmine.addMatchers + toDeepEqual: (util, customEqualityTesters) -> + compare: (actual, expected) -> + { pass: angular.equals(actual, expected) } inject ($q, _$httpBackend_, _Orders_, _OrderResource_) -> Orders = _Orders_ @@ -74,14 +75,14 @@ describe "Orders service", -> describe "#saved", -> describe "when attributes of the object have been altered", -> beforeEach -> - spyOn(Orders, "diff").andReturn ["attr1", "attr2"] + spyOn(Orders, "diff").and.returnValue ["attr1", "attr2"] it "returns false", -> expect(Orders.saved({})).toBe false describe "when attributes of the object have not been altered", -> beforeEach -> - spyOn(Orders, "diff").andReturn [] + spyOn(Orders, "diff").and.returnValue [] it "returns false", -> expect(Orders.saved({})).toBe true diff --git a/spec/javascripts/unit/admin/services/option_value_namer_spec.js.coffee b/spec/javascripts/unit/admin/services/option_value_namer_spec.js.coffee index 198a09a8b8..19353bf5dd 100644 --- a/spec/javascripts/unit/admin/services/option_value_namer_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/option_value_namer_spec.js.coffee @@ -15,26 +15,26 @@ describe "Option Value Namer", -> it "when description is blank", -> v.unit_description = null - spyOn(namer, "value_scaled").andReturn true - spyOn(namer, "option_value_value_unit").andReturn ["value", "unit"] + spyOn(namer, "value_scaled").and.returnValue true + spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"] expect(namer.name()).toBe "valueunit" it "when description is present", -> v.unit_description = 'desc' - spyOn(namer, "option_value_value_unit").andReturn ["value", "unit"] - spyOn(namer, "value_scaled").andReturn true + spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"] + spyOn(namer, "value_scaled").and.returnValue true expect(namer.name()).toBe "valueunit desc" it "when value is blank and description is present", -> v.unit_description = 'desc' - spyOn(namer, "option_value_value_unit").andReturn [null, null] - spyOn(namer, "value_scaled").andReturn true + spyOn(namer, "option_value_value_unit").and.returnValue [null, null] + spyOn(namer, "value_scaled").and.returnValue true expect(namer.name()).toBe "desc" it "spaces value and unit when value is unscaled", -> v.unit_description = null - spyOn(namer, "option_value_value_unit").andReturn ["value", "unit"] - spyOn(namer, "value_scaled").andReturn false + spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"] + spyOn(namer, "value_scaled").and.returnValue false expect(namer.name()).toBe "value unit" describe "determining if a variant's value is scaled", -> @@ -44,7 +44,7 @@ describe "Option Value Namer", -> p = {} v = { product: p } namer = new OptionValueNamer(v) - + it "returns true when the product has a scale", -> p.variant_unit_scale = 1000 expect(namer.value_scaled()).toBe true @@ -120,4 +120,4 @@ describe "Option Value Namer", -> p.variant_unit_scale = null p.variant_unit_name = 'foo' v.unit_value = null - expect(namer.option_value_value_unit()).toEqual [null, null] \ No newline at end of file + expect(namer.option_value_value_unit()).toEqual [null, null] diff --git a/spec/javascripts/unit/admin/side_menu/services/side_menu.js.coffee b/spec/javascripts/unit/admin/side_menu/services/side_menu.js.coffee index 1c6044b66e..0840d69c28 100644 --- a/spec/javascripts/unit/admin/side_menu/services/side_menu.js.coffee +++ b/spec/javascripts/unit/admin/side_menu/services/side_menu.js.coffee @@ -73,21 +73,21 @@ describe "SideMenu service", -> describe "hiding an item by name", -> it "sets visible to false on the response from find_by_name", -> mockItem = { visible: true } - spyOn(SideMenu, 'find_by_name').andReturn mockItem + spyOn(SideMenu, 'find_by_name').and.returnValue mockItem SideMenu.hide_item_by_name() expect(mockItem.visible).toBe false it "doesn't crash if null is returned from find_by_name", -> - spyOn(SideMenu, 'find_by_name').andReturn null + spyOn(SideMenu, 'find_by_name').and.returnValue null SideMenu.hide_item_by_name() describe "showing an item by name", -> it "sets visible to false on the response from find_by_name", -> mockItem = { visible: false } - spyOn(SideMenu, 'find_by_name').andReturn mockItem + spyOn(SideMenu, 'find_by_name').and.returnValue mockItem SideMenu.show_item_by_name() expect(mockItem.visible).toBe true it "doesn't crash if null is returned from find_by_name", -> - spyOn(SideMenu, 'find_by_name').andReturn null + spyOn(SideMenu, 'find_by_name').and.returnValue null SideMenu.show_item_by_name() diff --git a/spec/javascripts/unit/admin/tag_rules/controllers/tag_rules_controller_spec.js.coffee b/spec/javascripts/unit/admin/tag_rules/controllers/tag_rules_controller_spec.js.coffee index 1e132ec07d..4a0c304ee8 100644 --- a/spec/javascripts/unit/admin/tag_rules/controllers/tag_rules_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/tag_rules/controllers/tag_rules_controller_spec.js.coffee @@ -47,7 +47,7 @@ describe "TagRulesCtrl", -> beforeEach inject ($httpBackend) -> rule = scope.tagGroups[0].rules[0] - spyOn(window, "confirm").andReturn(true) + spyOn(window, "confirm").and.returnValue(true) $httpBackend.expectDELETE('/admin/enterprises/45/tag_rules/1.json').respond(status: 204) scope.deleteTagRule(scope.tagGroups[0], rule) $httpBackend.flush() diff --git a/spec/javascripts/unit/bulk_order_management_spec.js.coffee b/spec/javascripts/unit/bulk_order_management_spec.js.coffee index 3e7b27ae73..60edc958aa 100644 --- a/spec/javascripts/unit/bulk_order_management_spec.js.coffee +++ b/spec/javascripts/unit/bulk_order_management_spec.js.coffee @@ -10,7 +10,7 @@ describe "AdminOrderMgmtCtrl", -> ctrl = $controller httpBackend = $httpBackend VariantUnitManager = _VariantUnitManager_ - spyOn(window, "formatDate").andReturn "SomeDate" + spyOn(window, "formatDate").and.returnValue "SomeDate" ctrl "AdminOrderMgmtCtrl", {$scope: scope} ) @@ -24,8 +24,8 @@ describe "AdminOrderMgmtCtrl", -> httpBackend.expectGET("/api/enterprises/accessible?template=bulk_index&q[is_primary_producer_eq]=true").respond returnedSuppliers httpBackend.expectGET("/api/enterprises/accessible?template=bulk_index&q[sells_in][]=own&q[sells_in][]=any").respond returnedDistributors httpBackend.expectGET("/api/order_cycles/accessible?as=distributor&q[orders_close_at_gt]=SomeDate").respond returnedOrderCycles - spyOn(scope, "initialiseVariables").andCallThrough() - spyOn(scope, "fetchOrders").andReturn "nothing" + spyOn(scope, "initialiseVariables").and.callThrough() + spyOn(scope, "fetchOrders").and.returnValue "nothing" #spyOn(returnedSuppliers, "unshift") #spyOn(returnedDistributors, "unshift") #spyOn(returnedOrderCycles, "unshift") @@ -36,8 +36,8 @@ describe "AdminOrderMgmtCtrl", -> expect(scope.distributors).toEqual [ { id : '0', name : 'All' }, 'list of distributors' ] expect(scope.orderCycles).toEqual [ { id : '0', name : 'All' }, 'oc1', 'oc2', 'oc3' ] - expect(scope.initialiseVariables.calls.length).toBe 1 - expect(scope.fetchOrders.calls.length).toBe 1 + expect(scope.initialiseVariables.calls.count()).toBe 1 + expect(scope.fetchOrders.calls.count()).toBe 1 expect(scope.spree_api_key_ok).toBe true describe "fetching orders", -> @@ -63,8 +63,8 @@ describe "AdminOrderMgmtCtrl", -> describe "resetting orders", -> beforeEach -> - spyOn(scope, "matchObject").andReturn "nothing" - spyOn(scope, "resetLineItems").andReturn "nothing" + spyOn(scope, "matchObject").and.returnValue "nothing" + spyOn(scope, "resetLineItems").and.returnValue "nothing" scope.resetOrders [ "order1", "order2", "order3" ] it "sets the value of $scope.orders to the data received", -> @@ -77,8 +77,8 @@ describe "AdminOrderMgmtCtrl", -> order1 = order2 = order3 = null beforeEach -> - spyOn(scope, "matchObject").andReturn "nothing" - spyOn(scope, "lineItemOrder").andReturn "copied order" + spyOn(scope, "matchObject").and.returnValue "nothing" + spyOn(scope, "lineItemOrder").and.returnValue "copied order" order1 = { name: "order1", line_items: [ { name: "line_item1.1" }, { name: "line_item1.1" }, { name: "line_item1.1" } ] } order2 = { name: "order2", line_items: [ { name: "line_item2.1" }, { name: "line_item2.1" }, { name: "line_item2.1" } ] } order3 = { name: "order3", line_items: [ { name: "line_item3.1" }, { name: "line_item3.1" }, { name: "line_item3.1" } ] } @@ -92,18 +92,18 @@ describe "AdminOrderMgmtCtrl", -> expect(scope.lineItems[6].name).toEqual "line_item3.1" it "adds a reference to a modified parent order object to each line item", -> - expect(scope.lineItemOrder.calls.length).toEqual scope.orders.length + expect(scope.lineItemOrder.calls.count()).toBe scope.orders.length expect("copied order").toEqual line_item.order for line_item in scope.lineItems it "calls matchObject once for each line item", -> - expect(scope.matchObject.calls.length).toEqual scope.lineItems.length + expect(scope.matchObject.calls.count()).toBe scope.lineItems.length describe "copying orders", -> order1copy = null beforeEach -> - spyOn(scope, "lineItemOrder").andCallThrough() - spyOn(scope, "matchObject").andReturn "matched object" + spyOn(scope, "lineItemOrder").and.callThrough() + spyOn(scope, "matchObject").and.returnValue "matched object" order1 = { name: "order1", line_items: [ ] } scope.orders = [ order1 ] order1copy = scope.lineItemOrder order1 @@ -112,7 +112,7 @@ describe "AdminOrderMgmtCtrl", -> expect(order1copy.hasOwnProperty("line_items")).toEqual false it "calls matchObject twice for each order (once for distributor and once for order cycle)", -> - expect(scope.matchObject.calls.length).toEqual scope.lineItemOrder.calls.length * 2 + expect(scope.matchObject.calls.count()).toBe scope.lineItemOrder.calls.count() * 2 expect(order1copy.distributor).toEqual "matched object" expect(order1copy.distributor).toEqual "matched object" @@ -166,7 +166,7 @@ describe "AdminOrderMgmtCtrl", -> beforeEach -> scope.initialiseVariables() - spyOn(window,"confirm").andReturn true + spyOn(window,"confirm").and.returnValue true order = { number: "R12345678", line_items: [] } line_item1 = { id: 1, order: order } line_item2 = { id: 2, order: order } @@ -317,7 +317,7 @@ describe "AdminOrderMgmtCtrl", -> # A Units Variant is an API object which holds unit properies of a variant beforeEach -> - spyOn(Math,"round").andCallThrough() + spyOn(Math,"round").and.callThrough() it "returns '' if selectedUnitsVariant has no property 'variant_unit'", -> expect(scope.formattedValueWithUnitName(1,{})).toEqual '' @@ -337,14 +337,14 @@ describe "AdminOrderMgmtCtrl", -> it "calls Math.round with the quotient of scale and value, multiplied by 1000", -> unitsVariant = { variant_unit: "weight" } - spyOn(VariantUnitManager, "getScale").andReturn 5 + spyOn(VariantUnitManager, "getScale").and.returnValue 5 scope.formattedValueWithUnitName(10, unitsVariant) expect(Math.round).toHaveBeenCalledWith 10/5 * 1000 it "returns the result of Math.round divided by 1000, followed by the result of getUnitName", -> unitsVariant = { variant_unit: "weight" } - spyOn(VariantUnitManager, "getScale").andReturn 1000 - spyOn(VariantUnitManager, "getUnitName").andReturn "kg" + spyOn(VariantUnitManager, "getScale").and.returnValue 1000 + spyOn(VariantUnitManager, "getUnitName").and.returnValue "kg" expect(scope.formattedValueWithUnitName(2000,unitsVariant)).toEqual "2 kg" describe "updating the price upon updating the weight of a line item", -> diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index 1f761418d2..457010da0c 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -258,10 +258,10 @@ describe "AdminProductEditCtrl", -> describe "loading data upon initialisation", -> it "gets a list of producers and then resets products with a list of data", -> $httpBackend.expectGET("/api/users/authorise_api?token=API_KEY").respond success: "Use of API Authorised" - spyOn($scope, "fetchProducts").andReturn "nothing" + spyOn($scope, "fetchProducts").and.returnValue "nothing" $scope.initialise() $httpBackend.flush() - expect($scope.fetchProducts.calls.length).toEqual 1 + expect($scope.fetchProducts.calls.count()).toBe 1 expect($scope.spree_api_key_ok).toEqual true @@ -277,7 +277,7 @@ describe "AdminProductEditCtrl", -> deferred = $q.defer() deferred.resolve() spyOn $scope, "resetProducts" - spyOn(BulkProducts, "fetch").andReturn deferred.promise + spyOn(BulkProducts, "fetch").and.returnValue deferred.promise it "calls resetProducts after data has been received", -> $scope.fetchProducts() @@ -312,13 +312,13 @@ describe "AdminProductEditCtrl", -> describe "updating the product on hand count", -> it "updates when product is not available on demand", -> - spyOn($scope, "onHand").andReturn 123 + spyOn($scope, "onHand").and.returnValue 123 product = {on_demand: false} $scope.updateOnHand(product) expect(product.on_hand).toEqual 123 it "updates when product's variants are not available on demand", -> - spyOn($scope, "onHand").andReturn 123 + spyOn($scope, "onHand").and.returnValue 123 product = {on_demand: false, variants: [{on_demand: false}]} $scope.updateOnHand(product) expect(product.on_hand).toEqual 123 @@ -610,7 +610,7 @@ describe "AdminProductEditCtrl", -> describe "filtering products", -> beforeEach -> spyOn $scope, "packProduct" - spyOn(window, "filterSubmitProducts").andReturn [ + spyOn(window, "filterSubmitProducts").and.returnValue [ { id: 1 value: 3 @@ -632,7 +632,7 @@ describe "AdminProductEditCtrl", -> $scope.submitProducts() it "packs all products and all dirty products", -> - expect($scope.packProduct.calls.length).toEqual 4 + expect($scope.packProduct.calls.count()).toBe 4 it "filters returned dirty products", -> expect(filterSubmitProducts).toHaveBeenCalledWith @@ -734,7 +734,7 @@ describe "AdminProductEditCtrl", -> describe "deleting products", -> it "deletes products with a http delete request to /api/products/id/soft_delete", -> - spyOn(window, "confirm").andReturn true + spyOn(window, "confirm").and.returnValue true $scope.products = [ { id: 9 @@ -751,7 +751,7 @@ describe "AdminProductEditCtrl", -> $httpBackend.flush() it "removes the specified product from both $scope.products and $scope.dirtyProducts (if it exists there)", -> - spyOn(window, "confirm").andReturn true + spyOn(window, "confirm").and.returnValue true $scope.products = [ { id: 9 @@ -790,7 +790,7 @@ describe "AdminProductEditCtrl", -> describe "when the variant has not been saved", -> it "removes the variant from products and dirtyProducts", -> - spyOn(window, "confirm").andReturn true + spyOn(window, "confirm").and.returnValue true $scope.products = [ {id: 1, variants: [{id: -1},{id: -2}]} ] @@ -806,7 +806,7 @@ describe "AdminProductEditCtrl", -> describe "when the variant has been saved", -> it "deletes variants with a http delete request to /api/products/product_permalink/variants/(variant_id)/soft_delete", -> - spyOn(window, "confirm").andReturn true + spyOn(window, "confirm").and.returnValue true $scope.products = [ { id: 9 @@ -832,7 +832,7 @@ describe "AdminProductEditCtrl", -> $httpBackend.flush() it "removes the specified variant from both the variants object and $scope.dirtyProducts (if it exists there)", -> - spyOn(window, "confirm").andReturn true + spyOn(window, "confirm").and.returnValue true $scope.products = [ { id: 9 @@ -927,7 +927,7 @@ describe "converting arrays of objects with ids to an object with ids as keys", it "sends arrays with the key 'variants' to itself", -> - spyOn(window, "toObjectWithIDKeys").andCallThrough() + spyOn(window, "toObjectWithIDKeys").and.callThrough() array = [ { id: 1 diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee index 390a7a1c94..d395ae9cc6 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee @@ -30,7 +30,7 @@ describe "CheckoutCtrl", -> beforeEach -> inject ($controller, $rootScope, _storage_) -> storage = _storage_ - spyOn(storage, "bind").andCallThrough() + spyOn(storage, "bind").and.callThrough() scope = $rootScope.$new() CurrentUser = { id: 1 } ctrl = $controller 'CheckoutCtrl', {$scope: scope, Checkout: Checkout, CurrentUser: CurrentUser } diff --git a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee index d985904f81..ce3fe7eec3 100644 --- a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee @@ -131,7 +131,7 @@ describe 'Cart service', -> it "reduces the quantity in the cart", -> li = {variant: {id: 1}, quantity: 5} stockLevels = {1: {quantity: 0, max_quantity: 0, on_hand: 0}} - spyOn(Cart, 'line_items_present').andReturn [li] + spyOn(Cart, 'line_items_present').and.returnValue [li] Cart.compareAndNotifyStockLevels stockLevels expect(li.quantity).toEqual 0 expect(li.max_quantity).toBeUndefined() @@ -139,14 +139,14 @@ describe 'Cart service', -> it "reduces the max_quantity in the cart", -> li = {variant: {id: 1}, quantity: 5, max_quantity: 6} stockLevels = {1: {quantity: 0, max_quantity: 0, on_hand: 0}} - spyOn(Cart, 'line_items_present').andReturn [li] + spyOn(Cart, 'line_items_present').and.returnValue [li] Cart.compareAndNotifyStockLevels stockLevels expect(li.max_quantity).toEqual 0 it "resets the count on hand available", -> li = {variant: {id: 1, count_on_hand: 10}, quantity: 5} stockLevels = {1: {quantity: 0, max_quantity: 0, on_hand: 0}} - spyOn(Cart, 'line_items_present').andReturn [li] + spyOn(Cart, 'line_items_present').and.returnValue [li] Cart.compareAndNotifyStockLevels stockLevels expect(li.variant.count_on_hand).toEqual 0 @@ -154,7 +154,7 @@ describe 'Cart service', -> it "reduces the quantity in the cart", -> li = {variant: {id: 1}, quantity: 6} stockLevels = {1: {quantity: 5, on_hand: 5}} - spyOn(Cart, 'line_items_present').andReturn [li] + spyOn(Cart, 'line_items_present').and.returnValue [li] Cart.compareAndNotifyStockLevels stockLevels expect(li.quantity).toEqual 5 expect(li.max_quantity).toBeUndefined() @@ -162,14 +162,14 @@ describe 'Cart service', -> it "does not reduce the max_quantity in the cart", -> li = {variant: {id: 1}, quantity: 6, max_quantity: 7} stockLevels = {1: {quantity: 5, max_quantity: 5, on_hand: 5}} - spyOn(Cart, 'line_items_present').andReturn [li] + spyOn(Cart, 'line_items_present').and.returnValue [li] Cart.compareAndNotifyStockLevels stockLevels expect(li.max_quantity).toEqual 7 it "resets the count on hand available", -> li = {variant: {id: 1}, quantity: 6} stockLevels = {1: {quantity: 5, on_hand: 6}} - spyOn(Cart, 'line_items_present').andReturn [li] + spyOn(Cart, 'line_items_present').and.returnValue [li] Cart.compareAndNotifyStockLevels stockLevels expect(li.variant.count_on_hand).toEqual 6 @@ -177,7 +177,7 @@ describe 'Cart service', -> it "does not reset the quantity", -> li = {variant: {id: 1}, quantity: 6} stockLevels = {1: {quantity: 5, on_hand: 6}} - spyOn(Cart, 'line_items_present').andReturn [li] + spyOn(Cart, 'line_items_present').and.returnValue [li] Cart.compareAndNotifyStockLevels stockLevels expect(li.quantity).toEqual 6 expect(li.max_quantity).toBeUndefined() @@ -185,7 +185,7 @@ describe 'Cart service', -> it "does not reset the max_quantity", -> li = {variant: {id: 1}, quantity: 5, max_quantity: 7} stockLevels = {1: {quantity: 5, max_quantity: 6, on_hand: 7}} - spyOn(Cart, 'line_items_present').andReturn [li] + spyOn(Cart, 'line_items_present').and.returnValue [li] Cart.compareAndNotifyStockLevels stockLevels expect(li.quantity).toEqual 5 expect(li.max_quantity).toEqual 7 @@ -193,14 +193,14 @@ describe 'Cart service', -> describe "when the client-side quantity has been changed from 0 to 1 during the request", -> it "does not reset the quantity", -> li = {variant: {id: 1}, quantity: 1} - spyOn(Cart, 'line_items_present').andReturn [li] + spyOn(Cart, 'line_items_present').and.returnValue [li] Cart.compareAndNotifyStockLevels {} expect(li.quantity).toEqual 1 expect(li.max_quantity).toBeUndefined() it "does not reset the max_quantity", -> li = {variant: {id: 1}, quantity: 1, max_quantity: 1} - spyOn(Cart, 'line_items_present').andReturn [li] + spyOn(Cart, 'line_items_present').and.returnValue [li] Cart.compareAndNotifyStockLevels {} expect(li.quantity).toEqual 1 expect(li.max_quantity).toEqual 1 diff --git a/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee index 718504a377..43e83409ad 100644 --- a/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee @@ -30,6 +30,7 @@ describe 'Checkout service', -> beforeEach -> orderData = id: 3102 + shipping_method_id: null payment_method_id: null email: "test@test.com" bill_address: @@ -78,7 +79,7 @@ describe 'Checkout service', -> expect(Checkout.shippingPrice()).toEqual 13 it 'Gets the current payment method', -> - expect(Checkout.paymentMethod()).toEqual null + expect(Checkout.paymentMethod()).toBeUndefined() Checkout.order.payment_method_id = 99 expect(Checkout.paymentMethod()).toEqual paymentMethods[0] diff --git a/spec/javascripts/unit/darkswarm/services/sidebar_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/sidebar_spec.js.coffee index 8551113280..6ace586e30 100644 --- a/spec/javascripts/unit/darkswarm/services/sidebar_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/sidebar_spec.js.coffee @@ -13,13 +13,13 @@ describe "Sidebar", -> it 'is active when a location in paths is set', -> - spyOn(location, "path").andReturn "/test" + spyOn(location, "path").and.returnValue "/test" expect(Sidebar.active()).toEqual true it 'is inactive if location is set', -> - spyOn(location, "path").andReturn null + spyOn(location, "path").and.returnValue null expect(Sidebar.active()).toEqual false - + describe "Toggling on/off", -> it 'toggles the current sidebar path', -> expect(Sidebar.active()).toEqual false @@ -32,6 +32,3 @@ describe "Sidebar", -> spyOn(Navigation, 'navigate') Sidebar.toggle() expect(Navigation.navigate).toHaveBeenCalledWith("/test") - - - diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index 45dea6d36e..599c5530b0 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -13,10 +13,10 @@ describe 'OrderCycle controllers', -> event = preventDefault: jasmine.createSpy('preventDefault') OrderCycle = - exchangeSelectedVariants: jasmine.createSpy('exchangeSelectedVariants').andReturn('variants selected') - productSuppliedToOrderCycle: jasmine.createSpy('productSuppliedToOrderCycle').andReturn('product supplied') - variantSuppliedToOrderCycle: jasmine.createSpy('variantSuppliedToOrderCycle').andReturn('variant supplied') - exchangeDirection: jasmine.createSpy('exchangeDirection').andReturn('exchange direction') + exchangeSelectedVariants: jasmine.createSpy('exchangeSelectedVariants').and.returnValue('variants selected') + productSuppliedToOrderCycle: jasmine.createSpy('productSuppliedToOrderCycle').and.returnValue('product supplied') + variantSuppliedToOrderCycle: jasmine.createSpy('variantSuppliedToOrderCycle').and.returnValue('variant supplied') + exchangeDirection: jasmine.createSpy('exchangeDirection').and.returnValue('exchange direction') toggleProducts: jasmine.createSpy('toggleProducts') setExchangeVariants: jasmine.createSpy('setExchangeVariants') addSupplier: jasmine.createSpy('addSupplier') @@ -28,15 +28,15 @@ describe 'OrderCycle controllers', -> removeExchangeFee: jasmine.createSpy('removeExchangeFee') removeDistributionOfVariant: jasmine.createSpy('removeDistributionOfVariant') create: jasmine.createSpy('create') - new: jasmine.createSpy('new').andReturn "my order cycle" + new: jasmine.createSpy('new').and.returnValue "my order cycle" Enterprise = - index: jasmine.createSpy('index').andReturn('enterprises list') + index: jasmine.createSpy('index').and.returnValue('enterprises list') supplied_products: 'supplied products' - suppliedVariants: jasmine.createSpy('suppliedVariants').andReturn('supplied variants') - totalVariants: jasmine.createSpy('totalVariants').andReturn('variants total') + suppliedVariants: jasmine.createSpy('suppliedVariants').and.returnValue('supplied variants') + totalVariants: jasmine.createSpy('totalVariants').and.returnValue('variants total') EnterpriseFee = - index: jasmine.createSpy('index').andReturn('enterprise fees list') - forEnterprise: jasmine.createSpy('forEnterprise').andReturn('enterprise fees for enterprise') + index: jasmine.createSpy('index').and.returnValue('enterprise fees list') + forEnterprise: jasmine.createSpy('forEnterprise').and.returnValue('enterprise fees for enterprise') ocInstance = {} module('admin.orderCycles') @@ -99,7 +99,7 @@ describe 'OrderCycle controllers', -> 1: {id: 1, name: 'Eaterprises'} 2: {id: 2, name: 'Pepper Tree Place'} 3: {id: 3, name: 'South East'} - OrderCycle.participatingEnterpriseIds = jasmine.createSpy('participatingEnterpriseIds').andReturn([2]) + OrderCycle.participatingEnterpriseIds = jasmine.createSpy('participatingEnterpriseIds').and.returnValue([2]) EnterpriseFee.enterprise_fees = [ {enterprise_id: 2} ] # Pepper Tree Place has a fee expect(scope.enterprisesWithFees()).toEqual([ {id: 2, name: 'Pepper Tree Place'} @@ -181,10 +181,10 @@ describe 'OrderCycle controllers', -> 'example.com/admin/order_cycles/27/edit' OrderCycle = load: jasmine.createSpy('load') - exchangeSelectedVariants: jasmine.createSpy('exchangeSelectedVariants').andReturn('variants selected') - productSuppliedToOrderCycle: jasmine.createSpy('productSuppliedToOrderCycle').andReturn('product supplied') - variantSuppliedToOrderCycle: jasmine.createSpy('variantSuppliedToOrderCycle').andReturn('variant supplied') - exchangeDirection: jasmine.createSpy('exchangeDirection').andReturn('exchange direction') + exchangeSelectedVariants: jasmine.createSpy('exchangeSelectedVariants').and.returnValue('variants selected') + productSuppliedToOrderCycle: jasmine.createSpy('productSuppliedToOrderCycle').and.returnValue('product supplied') + variantSuppliedToOrderCycle: jasmine.createSpy('variantSuppliedToOrderCycle').and.returnValue('variant supplied') + exchangeDirection: jasmine.createSpy('exchangeDirection').and.returnValue('exchange direction') toggleProducts: jasmine.createSpy('toggleProducts') setExchangeVariants: jasmine.createSpy('setExchangeVariants') addSupplier: jasmine.createSpy('addSupplier') @@ -197,13 +197,13 @@ describe 'OrderCycle controllers', -> removeDistributionOfVariant: jasmine.createSpy('removeDistributionOfVariant') update: jasmine.createSpy('update') Enterprise = - index: jasmine.createSpy('index').andReturn('enterprises list') + index: jasmine.createSpy('index').and.returnValue('enterprises list') supplied_products: 'supplied products' - suppliedVariants: jasmine.createSpy('suppliedVariants').andReturn('supplied variants') - totalVariants: jasmine.createSpy('totalVariants').andReturn('variants total') + suppliedVariants: jasmine.createSpy('suppliedVariants').and.returnValue('supplied variants') + totalVariants: jasmine.createSpy('totalVariants').and.returnValue('variants total') EnterpriseFee = - index: jasmine.createSpy('index').andReturn('enterprise fees list') - forEnterprise: jasmine.createSpy('forEnterprise').andReturn('enterprise fees for enterprise') + index: jasmine.createSpy('index').and.returnValue('enterprise fees list') + forEnterprise: jasmine.createSpy('forEnterprise').and.returnValue('enterprise fees for enterprise') module('admin.orderCycles') inject ($controller) -> @@ -265,7 +265,7 @@ describe 'OrderCycle controllers', -> 1: {id: 1, name: 'Eaterprises'} 2: {id: 2, name: 'Pepper Tree Place'} 3: {id: 3, name: 'South East'} - OrderCycle.participatingEnterpriseIds = jasmine.createSpy('participatingEnterpriseIds').andReturn([2]) + OrderCycle.participatingEnterpriseIds = jasmine.createSpy('participatingEnterpriseIds').and.returnValue([2]) EnterpriseFee.enterprise_fees = [ {enterprise_id: 2} ] # Pepper Tree Place has a fee expect(scope.enterprisesWithFees()).toEqual([ {id: 2, name: 'Pepper Tree Place'} @@ -376,7 +376,7 @@ describe 'OrderCycle services', -> expect(Enterprise.supplied_products).toEqual [1, 2, 3, 4, 5, 6] it "finds supplied variants for an enterprise", -> - spyOn(Enterprise, 'variantsOf').andReturn(10) + spyOn(Enterprise, 'variantsOf').and.returnValue(10) Enterprise.index() $httpBackend.flush() expect(Enterprise.suppliedVariants(1)).toEqual [10, 10] @@ -721,7 +721,7 @@ describe 'OrderCycle services', -> master_id: 6 variants: [{id: 7}, {id: 8}] - spyOn(OrderCycle, 'incomingExchangesVariants').andReturn([1, 3]) + spyOn(OrderCycle, 'incomingExchangesVariants').and.returnValue([1, 3]) it 'returns true for products whose master is supplied', -> expect(OrderCycle.productSuppliedToOrderCycle(product_master_present)).toBeTruthy() @@ -738,7 +738,7 @@ describe 'OrderCycle services', -> describe 'checking whether a variant is supplied to the order cycle', -> beforeEach -> - spyOn(OrderCycle, 'incomingExchangesVariants').andReturn([1, 3]) + spyOn(OrderCycle, 'incomingExchangesVariants').and.returnValue([1, 3]) it 'returns true for variants that are supplied', -> expect(OrderCycle.variantSuppliedToOrderCycle({id: 1})).toBeTruthy() @@ -814,11 +814,11 @@ describe 'OrderCycle services', -> describe 'creating an order cycle', -> beforeEach -> - spyOn(OrderCycle, 'confirmNoDistributors').andReturn true + spyOn(OrderCycle, 'confirmNoDistributors').and.returnValue true it 'redirects to the destination page on success', -> OrderCycle.order_cycle = 'this is the order cycle' - spyOn(OrderCycle, 'dataForSubmit').andReturn('this is the submit data') + spyOn(OrderCycle, 'dataForSubmit').and.returnValue('this is the submit data') $httpBackend.expectPOST('/admin/order_cycles.json', { order_cycle: 'this is the submit data' }).respond {success: true} @@ -829,7 +829,7 @@ describe 'OrderCycle services', -> it 'does not redirect on error', -> OrderCycle.order_cycle = 'this is the order cycle' - spyOn(OrderCycle, 'dataForSubmit').andReturn('this is the submit data') + spyOn(OrderCycle, 'dataForSubmit').and.returnValue('this is the submit data') $httpBackend.expectPOST('/admin/order_cycles.json', { order_cycle: 'this is the submit data' }).respond {success: false} @@ -840,11 +840,11 @@ describe 'OrderCycle services', -> describe 'updating an order cycle', -> beforeEach -> - spyOn(OrderCycle, 'confirmNoDistributors').andReturn true + spyOn(OrderCycle, 'confirmNoDistributors').and.returnValue true it 'redirects to the destination page on success', -> OrderCycle.order_cycle = 'this is the order cycle' - spyOn(OrderCycle, 'dataForSubmit').andReturn('this is the submit data') + spyOn(OrderCycle, 'dataForSubmit').and.returnValue('this is the submit data') $httpBackend.expectPUT('/admin/order_cycles.json?reloading=1', { order_cycle: 'this is the submit data' }).respond {success: true} @@ -855,7 +855,7 @@ describe 'OrderCycle services', -> it 'does not redirect on error', -> OrderCycle.order_cycle = 'this is the order cycle' - spyOn(OrderCycle, 'dataForSubmit').andReturn('this is the submit data') + spyOn(OrderCycle, 'dataForSubmit').and.returnValue('this is the submit data') $httpBackend.expectPUT('/admin/order_cycles.json?reloading=1', { order_cycle: 'this is the submit data' }).respond {success: false} @@ -968,19 +968,19 @@ describe 'OrderCycle services', -> outgoing_exchanges: [] it "returns true when there are distributors", -> - spyOn window, 'confirm' + spyOn(window, 'confirm') OrderCycle.order_cycle = order_cycle_with_exchanges expect(OrderCycle.confirmNoDistributors()).toBe true expect(window.confirm).not.toHaveBeenCalled() it "returns true when there are no distributors but the user confirms", -> - spyOn(window, 'confirm').andReturn true + spyOn(window, 'confirm').and.returnValue(true) OrderCycle.order_cycle = order_cycle_without_exchanges expect(OrderCycle.confirmNoDistributors()).toBe true expect(window.confirm).toHaveBeenCalled() it "returns false when there are no distributors and the user does not confirm", -> - spyOn(window, 'confirm').andReturn false + spyOn(window, 'confirm').and.returnValue(false) OrderCycle.order_cycle = order_cycle_without_exchanges expect(OrderCycle.confirmNoDistributors()).toBe false expect(window.confirm).toHaveBeenCalled() From a0254f30cd230ee0769c5360104d0de3fc21de27 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 4 May 2016 11:10:47 +1000 Subject: [PATCH 026/110] Adding package.json with npm dev dependencies for js specs --- .gitignore | 1 + .travis.yml | 6 +- npm-shrinkwrap.json | 2022 +++++++++++++++++++++++++++++++++++++++++++ package.json | 16 + 4 files changed, 2041 insertions(+), 4 deletions(-) create mode 100644 npm-shrinkwrap.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore index 42107a8e2e..13f5a4441c 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ NERD_tree* coverage libpeerconnection.log /config/application.yml +node_modules diff --git a/.travis.yml b/.travis.yml index 279eecb77c..8a66feb11a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,10 +28,8 @@ before_script: - RAILS_ENV=test bundle exec rake db:create db:schema:load - > if [ "$KARMA" = "true" ]; then - npm install karma@0.13.22 - npm install karma-jasmine@0.3.8 - npm install karma-phantomjs-launcher@1.0.0 - npm install karma-coffee-preprocessor@0.3.0 + npm install -g npm@'~3.8.8' + npm install npm install -g karma-cli@0.1.2 fi diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json new file mode 100644 index 0000000000..e1c8b2aa83 --- /dev/null +++ b/npm-shrinkwrap.json @@ -0,0 +1,2022 @@ +{ + "name": "openfoodnetwork", + "version": "1.7.1", + "dependencies": { + "accepts": { + "version": "1.1.4", + "from": "accepts@1.1.4", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.1.4.tgz", + "dependencies": { + "mime-db": { + "version": "1.12.0", + "from": "mime-db@>=1.12.0 <1.13.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz" + }, + "mime-types": { + "version": "2.0.14", + "from": "mime-types@>=2.0.4 <2.1.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz" + } + } + }, + "adm-zip": { + "version": "0.4.7", + "from": "adm-zip@>=0.4.7 <0.5.0", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.7.tgz" + }, + "after": { + "version": "0.8.1", + "from": "after@0.8.1", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.1.tgz" + }, + "ansi-regex": { + "version": "2.0.0", + "from": "ansi-regex@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" + }, + "ansi-styles": { + "version": "2.2.1", + "from": "ansi-styles@>=2.2.1 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" + }, + "anymatch": { + "version": "1.3.0", + "from": "anymatch@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz" + }, + "arr-diff": { + "version": "2.0.0", + "from": "arr-diff@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz" + }, + "arr-flatten": { + "version": "1.0.1", + "from": "arr-flatten@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.0.1.tgz" + }, + "array-slice": { + "version": "0.2.3", + "from": "array-slice@>=0.2.3 <0.3.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz" + }, + "array-unique": { + "version": "0.2.1", + "from": "array-unique@>=0.2.1 <0.3.0", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz" + }, + "arraybuffer.slice": { + "version": "0.0.6", + "from": "arraybuffer.slice@0.0.6", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz" + }, + "arrify": { + "version": "1.0.1", + "from": "arrify@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" + }, + "asn1": { + "version": "0.2.3", + "from": "asn1@>=0.2.3 <0.3.0", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz" + }, + "assert-plus": { + "version": "0.2.0", + "from": "assert-plus@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz" + }, + "async-each": { + "version": "1.0.0", + "from": "async-each@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.0.tgz" + }, + "aws-sign2": { + "version": "0.6.0", + "from": "aws-sign2@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz" + }, + "backo2": { + "version": "1.0.2", + "from": "backo2@1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz" + }, + "balanced-match": { + "version": "0.4.1", + "from": "balanced-match@>=0.4.1 <0.5.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz" + }, + "base64-arraybuffer": { + "version": "0.1.2", + "from": "base64-arraybuffer@0.1.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.2.tgz" + }, + "base64id": { + "version": "0.1.0", + "from": "base64id@0.1.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-0.1.0.tgz" + }, + "batch": { + "version": "0.5.3", + "from": "batch@0.5.3", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.5.3.tgz" + }, + "benchmark": { + "version": "1.0.0", + "from": "benchmark@1.0.0", + "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-1.0.0.tgz" + }, + "better-assert": { + "version": "1.0.2", + "from": "better-assert@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz" + }, + "binary-extensions": { + "version": "1.4.0", + "from": "binary-extensions@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.4.0.tgz" + }, + "bl": { + "version": "1.0.3", + "from": "bl@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz", + "dependencies": { + "readable-stream": { + "version": "2.0.6", + "from": "readable-stream@2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz" + } + } + }, + "blob": { + "version": "0.0.4", + "from": "blob@0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz" + }, + "bluebird": { + "version": "2.10.2", + "from": "bluebird@>=2.9.27 <3.0.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.10.2.tgz" + }, + "body-parser": { + "version": "1.15.0", + "from": "body-parser@>=1.12.4 <2.0.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.15.0.tgz" + }, + "boom": { + "version": "2.10.1", + "from": "boom@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" + }, + "brace-expansion": { + "version": "1.1.4", + "from": "brace-expansion@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz" + }, + "braces": { + "version": "1.8.4", + "from": "braces@>=1.8.2 <2.0.0", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.4.tgz" + }, + "bytes": { + "version": "2.2.0", + "from": "bytes@2.2.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.2.0.tgz" + }, + "callsite": { + "version": "1.0.0", + "from": "callsite@1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz" + }, + "caseless": { + "version": "0.11.0", + "from": "caseless@>=0.11.0 <0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz" + }, + "chalk": { + "version": "1.1.3", + "from": "chalk@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" + }, + "charenc": { + "version": "0.0.1", + "from": "charenc@>=0.0.1 <0.1.0", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.1.tgz" + }, + "chokidar": { + "version": "1.4.3", + "from": "chokidar@>=0.8.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.4.3.tgz" + }, + "coffee-script": { + "version": "1.10.0", + "from": "coffee-script@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.10.0.tgz" + }, + "colors": { + "version": "1.1.2", + "from": "colors@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz" + }, + "combined-stream": { + "version": "1.0.5", + "from": "combined-stream@>=1.0.5 <1.1.0", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz" + }, + "component-bind": { + "version": "1.0.0", + "from": "component-bind@1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz" + }, + "component-emitter": { + "version": "1.1.2", + "from": "component-emitter@1.1.2", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz" + }, + "component-inherit": { + "version": "0.0.3", + "from": "component-inherit@0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz" + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + }, + "concat-stream": { + "version": "1.5.0", + "from": "concat-stream@1.5.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.0.tgz", + "dependencies": { + "readable-stream": { + "version": "2.0.6", + "from": "readable-stream@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz" + } + } + }, + "connect": { + "version": "3.4.1", + "from": "connect@>=3.3.5 <4.0.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.4.1.tgz" + }, + "content-type": { + "version": "1.0.1", + "from": "content-type@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.1.tgz" + }, + "core-js": { + "version": "2.3.0", + "from": "core-js@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz" + }, + "core-util-is": { + "version": "1.0.2", + "from": "core-util-is@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" + }, + "crypt": { + "version": "0.0.1", + "from": "crypt@>=0.0.1 <0.1.0", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.1.tgz" + }, + "cryptiles": { + "version": "2.0.5", + "from": "cryptiles@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz" + }, + "custom-event": { + "version": "1.0.0", + "from": "custom-event@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.0.tgz" + }, + "d": { + "version": "0.1.1", + "from": "d@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz" + }, + "dashdash": { + "version": "1.13.1", + "from": "dashdash@>=1.12.0 <2.0.0", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.13.1.tgz", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "from": "assert-plus@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + } + } + }, + "debug": { + "version": "2.2.0", + "from": "debug@>=2.2.0 <2.3.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz" + }, + "delayed-stream": { + "version": "1.0.0", + "from": "delayed-stream@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + }, + "depd": { + "version": "1.1.0", + "from": "depd@>=1.1.0 <1.2.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz" + }, + "di": { + "version": "0.0.1", + "from": "di@>=0.0.1 <0.1.0", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz" + }, + "dom-serialize": { + "version": "2.2.1", + "from": "dom-serialize@>=2.2.0 <3.0.0", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz" + }, + "ecc-jsbn": { + "version": "0.1.1", + "from": "ecc-jsbn@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz" + }, + "ee-first": { + "version": "1.1.1", + "from": "ee-first@1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + }, + "engine.io": { + "version": "1.6.9", + "from": "engine.io@1.6.9", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.6.9.tgz" + }, + "engine.io-client": { + "version": "1.6.9", + "from": "engine.io-client@1.6.9", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.6.9.tgz" + }, + "engine.io-parser": { + "version": "1.2.4", + "from": "engine.io-parser@1.2.4", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.2.4.tgz", + "dependencies": { + "has-binary": { + "version": "0.1.6", + "from": "has-binary@0.1.6", + "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.6.tgz" + }, + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1" + } + } + }, + "ent": { + "version": "2.2.0", + "from": "ent@>=2.2.0 <2.3.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz" + }, + "es5-ext": { + "version": "0.10.11", + "from": "es5-ext@>=0.10.11 <0.11.0", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.11.tgz" + }, + "es6-iterator": { + "version": "2.0.0", + "from": "es6-iterator@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.0.tgz" + }, + "es6-symbol": { + "version": "3.0.2", + "from": "es6-symbol@>=3.0.2 <3.1.0", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.0.2.tgz" + }, + "escape-html": { + "version": "1.0.3", + "from": "escape-html@>=1.0.3 <1.1.0", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + }, + "escape-string-regexp": { + "version": "1.0.5", + "from": "escape-string-regexp@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + }, + "eventemitter3": { + "version": "1.2.0", + "from": "eventemitter3@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz" + }, + "expand-braces": { + "version": "0.1.2", + "from": "expand-braces@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz", + "dependencies": { + "braces": { + "version": "0.1.5", + "from": "braces@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz" + }, + "expand-range": { + "version": "0.1.1", + "from": "expand-range@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz" + }, + "is-number": { + "version": "0.1.1", + "from": "is-number@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz" + }, + "repeat-string": { + "version": "0.2.2", + "from": "repeat-string@>=0.2.2 <0.3.0", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz" + } + } + }, + "expand-brackets": { + "version": "0.1.5", + "from": "expand-brackets@>=0.1.4 <0.2.0", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz" + }, + "expand-range": { + "version": "1.8.1", + "from": "expand-range@>=1.8.1 <2.0.0", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.1.tgz" + }, + "extend": { + "version": "3.0.0", + "from": "extend@>=3.0.0 <3.1.0", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz" + }, + "extglob": { + "version": "0.3.2", + "from": "extglob@>=0.3.1 <0.4.0", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz" + }, + "extract-zip": { + "version": "1.5.0", + "from": "extract-zip@>=1.5.0 <1.6.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.5.0.tgz", + "dependencies": { + "debug": { + "version": "0.7.4", + "from": "debug@0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz" + }, + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + }, + "mkdirp": { + "version": "0.5.0", + "from": "mkdirp@0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz" + } + } + }, + "extsprintf": { + "version": "1.0.2", + "from": "extsprintf@1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz" + }, + "fd-slicer": { + "version": "1.0.1", + "from": "fd-slicer@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz" + }, + "filename-regex": { + "version": "2.0.0", + "from": "filename-regex@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.0.tgz" + }, + "fill-range": { + "version": "2.2.3", + "from": "fill-range@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz" + }, + "finalhandler": { + "version": "0.4.1", + "from": "finalhandler@0.4.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.4.1.tgz" + }, + "for-in": { + "version": "0.1.5", + "from": "for-in@>=0.1.5 <0.2.0", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.5.tgz" + }, + "for-own": { + "version": "0.1.4", + "from": "for-own@>=0.1.3 <0.2.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.4.tgz" + }, + "forever-agent": { + "version": "0.6.1", + "from": "forever-agent@>=0.6.1 <0.7.0", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + }, + "form-data": { + "version": "1.0.0-rc4", + "from": "form-data@>=1.0.0-rc3 <1.1.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz", + "dependencies": { + "async": { + "version": "1.5.2", + "from": "async@>=1.5.2 <2.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" + }, + "mime-db": { + "version": "1.22.0", + "from": "mime-db@>=1.22.0 <1.23.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.22.0.tgz" + }, + "mime-types": { + "version": "2.1.10", + "from": "mime-types@2.1.10", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.10.tgz" + } + } + }, + "fs-extra": { + "version": "0.26.7", + "from": "fs-extra@>=0.26.4 <0.27.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz", + "dependencies": { + "graceful-fs": { + "version": "4.1.3", + "from": "graceful-fs@>=4.1.2 <5.0.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.3.tgz" + } + } + }, + "fsevents": { + "version": "1.0.12", + "from": "fsevents@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.0.12.tgz", + "dependencies": { + "ansi": { + "version": "0.3.1", + "from": "ansi@~0.3.1", + "resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz" + }, + "ansi-regex": { + "version": "2.0.0", + "from": "ansi-regex@^2.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" + }, + "ansi-styles": { + "version": "2.2.1", + "from": "ansi-styles@^2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" + }, + "are-we-there-yet": { + "version": "1.1.2", + "from": "are-we-there-yet@~1.1.2", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz" + }, + "asn1": { + "version": "0.2.3", + "from": "asn1@>=0.2.3 <0.3.0", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz" + }, + "assert-plus": { + "version": "0.2.0", + "from": "assert-plus@^0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz" + }, + "async": { + "version": "1.5.2", + "from": "async@^1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" + }, + "aws-sign2": { + "version": "0.6.0", + "from": "aws-sign2@~0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz" + }, + "aws4": { + "version": "1.3.2", + "from": "aws4@^1.2.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.3.2.tgz", + "dependencies": { + "lru-cache": { + "version": "4.0.1", + "from": "lru-cache@^4.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.1.tgz", + "dependencies": { + "pseudomap": { + "version": "1.0.2", + "from": "pseudomap@^1.0.1", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz" + }, + "yallist": { + "version": "2.0.0", + "from": "yallist@^2.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.0.0.tgz" + } + } + } + } + }, + "bl": { + "version": "1.0.3", + "from": "bl@~1.0.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz" + }, + "block-stream": { + "version": "0.0.8", + "from": "block-stream@*", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.8.tgz" + }, + "boom": { + "version": "2.10.1", + "from": "boom@2.x.x", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" + }, + "caseless": { + "version": "0.11.0", + "from": "caseless@~0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz" + }, + "chalk": { + "version": "1.1.3", + "from": "chalk@^1.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" + }, + "combined-stream": { + "version": "1.0.5", + "from": "combined-stream@~1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz" + }, + "commander": { + "version": "2.9.0", + "from": "commander@^2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz" + }, + "core-util-is": { + "version": "1.0.2", + "from": "core-util-is@~1.0.0", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" + }, + "cryptiles": { + "version": "2.0.5", + "from": "cryptiles@2.x.x", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz" + }, + "dashdash": { + "version": "1.13.0", + "from": "dashdash@>=1.10.1 <2.0.0", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.13.0.tgz", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "from": "assert-plus@^1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + } + } + }, + "debug": { + "version": "2.2.0", + "from": "debug@~2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz" + }, + "deep-extend": { + "version": "0.4.1", + "from": "deep-extend@~0.4.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz" + }, + "delayed-stream": { + "version": "1.0.0", + "from": "delayed-stream@~1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + }, + "delegates": { + "version": "1.0.0", + "from": "delegates@^1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" + }, + "ecc-jsbn": { + "version": "0.1.1", + "from": "ecc-jsbn@>=0.0.1 <1.0.0", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz" + }, + "escape-string-regexp": { + "version": "1.0.5", + "from": "escape-string-regexp@^1.0.2", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + }, + "extend": { + "version": "3.0.0", + "from": "extend@~3.0.0", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz" + }, + "extsprintf": { + "version": "1.0.2", + "from": "extsprintf@1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz" + }, + "forever-agent": { + "version": "0.6.1", + "from": "forever-agent@~0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + }, + "form-data": { + "version": "1.0.0-rc4", + "from": "form-data@~1.0.0-rc3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz" + }, + "fstream": { + "version": "1.0.8", + "from": "fstream@^1.0.2", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.8.tgz" + }, + "fstream-ignore": { + "version": "1.0.3", + "from": "fstream-ignore@~1.0.3", + "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.3.tgz", + "dependencies": { + "minimatch": { + "version": "3.0.0", + "from": "minimatch@^3.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz", + "dependencies": { + "brace-expansion": { + "version": "1.1.3", + "from": "brace-expansion@^1.0.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.3.tgz", + "dependencies": { + "balanced-match": { + "version": "0.3.0", + "from": "balanced-match@^0.3.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.3.0.tgz" + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + } + } + } + } + } + } + }, + "gauge": { + "version": "1.2.7", + "from": "gauge@~1.2.5", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-1.2.7.tgz" + }, + "generate-function": { + "version": "2.0.0", + "from": "generate-function@^2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" + }, + "generate-object-property": { + "version": "1.2.0", + "from": "generate-object-property@^1.1.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz" + }, + "graceful-fs": { + "version": "4.1.3", + "from": "graceful-fs@^4.1.2", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.3.tgz" + }, + "graceful-readlink": { + "version": "1.0.1", + "from": "graceful-readlink@>= 1.0.0", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" + }, + "har-validator": { + "version": "2.0.6", + "from": "har-validator@~2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz" + }, + "has-ansi": { + "version": "2.0.0", + "from": "has-ansi@^2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz" + }, + "has-unicode": { + "version": "2.0.0", + "from": "has-unicode@^2.0.0", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.0.tgz" + }, + "hawk": { + "version": "3.1.3", + "from": "hawk@~3.1.0", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz" + }, + "hoek": { + "version": "2.16.3", + "from": "hoek@2.x.x", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" + }, + "http-signature": { + "version": "1.1.1", + "from": "http-signature@~1.1.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz" + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@*", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "ini": { + "version": "1.3.4", + "from": "ini@~1.3.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz" + }, + "is-my-json-valid": { + "version": "2.13.1", + "from": "is-my-json-valid@^2.12.4", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz" + }, + "is-property": { + "version": "1.0.2", + "from": "is-property@^1.0.0", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" + }, + "is-typedarray": { + "version": "1.0.0", + "from": "is-typedarray@~1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + }, + "isarray": { + "version": "1.0.0", + "from": "isarray@~1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + }, + "isstream": { + "version": "0.1.2", + "from": "isstream@~0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + }, + "jodid25519": { + "version": "1.0.2", + "from": "jodid25519@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz" + }, + "jsbn": { + "version": "0.1.0", + "from": "jsbn@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz" + }, + "json-schema": { + "version": "0.2.2", + "from": "json-schema@0.2.2", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz" + }, + "json-stringify-safe": { + "version": "5.0.1", + "from": "json-stringify-safe@~5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + }, + "jsonpointer": { + "version": "2.0.0", + "from": "jsonpointer@2.0.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz" + }, + "jsprim": { + "version": "1.2.2", + "from": "jsprim@^1.2.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz" + }, + "lodash.pad": { + "version": "4.1.0", + "from": "lodash.pad@^4.1.0", + "resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.1.0.tgz" + }, + "lodash.padend": { + "version": "4.2.0", + "from": "lodash.padend@^4.1.0", + "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.2.0.tgz" + }, + "lodash.padstart": { + "version": "4.2.0", + "from": "lodash.padstart@^4.1.0", + "resolved": "https://registry.npmjs.org/lodash.padstart/-/lodash.padstart-4.2.0.tgz" + }, + "lodash.repeat": { + "version": "4.0.0", + "from": "lodash.repeat@^4.0.0", + "resolved": "https://registry.npmjs.org/lodash.repeat/-/lodash.repeat-4.0.0.tgz" + }, + "lodash.tostring": { + "version": "4.1.2", + "from": "lodash.tostring@^4.0.0", + "resolved": "https://registry.npmjs.org/lodash.tostring/-/lodash.tostring-4.1.2.tgz" + }, + "mime-db": { + "version": "1.22.0", + "from": "mime-db@~1.22.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.22.0.tgz" + }, + "mime-types": { + "version": "2.1.10", + "from": "mime-types@~2.1.7", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.10.tgz" + }, + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + }, + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.3.0 <0.4.0||>=0.4.0 <0.5.0||>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + }, + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + }, + "node-pre-gyp": { + "version": "0.6.25", + "from": "node-pre-gyp@0.6.25", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.25.tgz", + "dependencies": { + "nopt": { + "version": "3.0.6", + "from": "nopt@~3.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "dependencies": { + "abbrev": { + "version": "1.0.7", + "from": "abbrev@1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz" + } + } + } + } + }, + "node-uuid": { + "version": "1.4.7", + "from": "node-uuid@~1.4.7", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz" + }, + "npmlog": { + "version": "2.0.3", + "from": "npmlog@~2.0.0", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-2.0.3.tgz" + }, + "oauth-sign": { + "version": "0.8.1", + "from": "oauth-sign@~0.8.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.1.tgz" + }, + "once": { + "version": "1.3.3", + "from": "once@~1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz" + }, + "pinkie": { + "version": "2.0.4", + "from": "pinkie@^2.0.0", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" + }, + "pinkie-promise": { + "version": "2.0.0", + "from": "pinkie-promise@^2.0.0", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.0.tgz" + }, + "process-nextick-args": { + "version": "1.0.6", + "from": "process-nextick-args@~1.0.6", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.6.tgz" + }, + "qs": { + "version": "6.0.2", + "from": "qs@~6.0.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.0.2.tgz" + }, + "rc": { + "version": "1.1.6", + "from": "rc@~1.1.0", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz", + "dependencies": { + "minimist": { + "version": "1.2.0", + "from": "minimist@^1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz" + } + } + }, + "readable-stream": { + "version": "2.0.6", + "from": "readable-stream@^2.0.0 || ^1.1.13", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz" + }, + "request": { + "version": "2.69.0", + "from": "request@2.x", + "resolved": "https://registry.npmjs.org/request/-/request-2.69.0.tgz" + }, + "rimraf": { + "version": "2.5.2", + "from": "rimraf@~2.5.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.2.tgz", + "dependencies": { + "glob": { + "version": "7.0.3", + "from": "glob@^7.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz", + "dependencies": { + "inflight": { + "version": "1.0.4", + "from": "inflight@^1.0.4", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.4.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.1", + "from": "wrappy@1", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz" + } + } + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "minimatch": { + "version": "3.0.0", + "from": "minimatch@2 || 3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz", + "dependencies": { + "brace-expansion": { + "version": "1.1.3", + "from": "brace-expansion@^1.0.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.3.tgz", + "dependencies": { + "balanced-match": { + "version": "0.3.0", + "from": "balanced-match@^0.3.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.3.0.tgz" + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + } + } + } + } + }, + "once": { + "version": "1.3.3", + "from": "once@^1.3.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.1", + "from": "wrappy@1", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz" + } + } + }, + "path-is-absolute": { + "version": "1.0.0", + "from": "path-is-absolute@^1.0.0", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" + } + } + } + } + }, + "semver": { + "version": "5.1.0", + "from": "semver@~5.1.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz" + }, + "sntp": { + "version": "1.0.9", + "from": "sntp@1.x.x", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" + }, + "sshpk": { + "version": "1.7.4", + "from": "sshpk@^1.7.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.7.4.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@~0.10.x", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + }, + "stringstream": { + "version": "0.0.5", + "from": "stringstream@~0.0.4", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz" + }, + "strip-ansi": { + "version": "3.0.1", + "from": "strip-ansi@^3.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" + }, + "strip-json-comments": { + "version": "1.0.4", + "from": "strip-json-comments@~1.0.4", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz" + }, + "supports-color": { + "version": "2.0.0", + "from": "supports-color@^2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" + }, + "tar": { + "version": "2.2.1", + "from": "tar@~2.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz" + }, + "tar-pack": { + "version": "3.1.3", + "from": "tar-pack@~3.1.0", + "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.1.3.tgz" + }, + "tough-cookie": { + "version": "2.2.2", + "from": "tough-cookie@~2.2.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz" + }, + "tunnel-agent": { + "version": "0.4.2", + "from": "tunnel-agent@~0.4.1", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.2.tgz" + }, + "tweetnacl": { + "version": "0.14.3", + "from": "tweetnacl@>=0.13.0 <1.0.0", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.3.tgz" + }, + "uid-number": { + "version": "0.0.6", + "from": "uid-number@~0.0.6", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz" + }, + "util-deprecate": { + "version": "1.0.2", + "from": "util-deprecate@~1.0.1", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + }, + "verror": { + "version": "1.3.6", + "from": "verror@1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz" + }, + "wrappy": { + "version": "1.0.1", + "from": "wrappy@1", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz" + }, + "xtend": { + "version": "4.0.1", + "from": "xtend@^4.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" + } + } + }, + "generate-function": { + "version": "2.0.0", + "from": "generate-function@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" + }, + "generate-object-property": { + "version": "1.2.0", + "from": "generate-object-property@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz" + }, + "getpass": { + "version": "0.1.6", + "from": "getpass@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "from": "assert-plus@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + } + } + }, + "glob": { + "version": "7.0.3", + "from": "glob@>=7.0.0 <8.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz" + }, + "glob-base": { + "version": "0.3.0", + "from": "glob-base@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz" + }, + "glob-parent": { + "version": "2.0.0", + "from": "glob-parent@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz" + }, + "graceful-fs": { + "version": "4.1.3", + "from": "graceful-fs@>=4.1.2 <5.0.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.3.tgz" + }, + "graceful-readlink": { + "version": "1.0.1", + "from": "graceful-readlink@>=1.0.0", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" + }, + "har-validator": { + "version": "2.0.6", + "from": "har-validator@>=2.0.2 <2.1.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "dependencies": { + "commander": { + "version": "2.9.0", + "from": "commander@>=2.9.0 <3.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz" + } + } + }, + "has-ansi": { + "version": "2.0.0", + "from": "has-ansi@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz" + }, + "has-binary": { + "version": "0.1.7", + "from": "has-binary@0.1.7", + "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1" + } + } + }, + "has-cors": { + "version": "1.1.0", + "from": "has-cors@1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz" + }, + "hasha": { + "version": "2.2.0", + "from": "hasha@>=2.2.0 <3.0.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz" + }, + "hawk": { + "version": "3.1.3", + "from": "hawk@>=3.1.0 <3.2.0", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz" + }, + "hoek": { + "version": "2.16.3", + "from": "hoek@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" + }, + "http-errors": { + "version": "1.4.0", + "from": "http-errors@>=1.4.0 <1.5.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.4.0.tgz" + }, + "http-proxy": { + "version": "1.13.2", + "from": "http-proxy@>=1.13.0 <2.0.0", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.13.2.tgz" + }, + "http-signature": { + "version": "1.1.1", + "from": "http-signature@>=1.1.0 <1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz" + }, + "iconv-lite": { + "version": "0.4.13", + "from": "iconv-lite@0.4.13", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz" + }, + "indexof": { + "version": "0.0.1", + "from": "indexof@0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz" + }, + "inflight": { + "version": "1.0.4", + "from": "inflight@>=1.0.4 <2.0.0", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.4.tgz" + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "is-absolute": { + "version": "0.1.7", + "from": "is-absolute@>=0.1.7 <0.2.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.1.7.tgz" + }, + "is-binary-path": { + "version": "1.0.1", + "from": "is-binary-path@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz" + }, + "is-buffer": { + "version": "1.1.3", + "from": "is-buffer@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.3.tgz" + }, + "is-dotfile": { + "version": "1.0.2", + "from": "is-dotfile@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.2.tgz" + }, + "is-equal-shallow": { + "version": "0.1.3", + "from": "is-equal-shallow@>=0.1.3 <0.2.0", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz" + }, + "is-extendable": { + "version": "0.1.1", + "from": "is-extendable@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz" + }, + "is-extglob": { + "version": "1.0.0", + "from": "is-extglob@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz" + }, + "is-glob": { + "version": "2.0.1", + "from": "is-glob@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz" + }, + "is-my-json-valid": { + "version": "2.13.1", + "from": "is-my-json-valid@>=2.12.4 <3.0.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz" + }, + "is-number": { + "version": "2.1.0", + "from": "is-number@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz" + }, + "is-posix-bracket": { + "version": "0.1.1", + "from": "is-posix-bracket@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz" + }, + "is-primitive": { + "version": "2.0.0", + "from": "is-primitive@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz" + }, + "is-property": { + "version": "1.0.2", + "from": "is-property@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" + }, + "is-relative": { + "version": "0.1.3", + "from": "is-relative@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.1.3.tgz" + }, + "is-stream": { + "version": "1.1.0", + "from": "is-stream@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" + }, + "is-typedarray": { + "version": "1.0.0", + "from": "is-typedarray@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + }, + "isarray": { + "version": "1.0.0", + "from": "isarray@1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + }, + "isbinaryfile": { + "version": "3.0.0", + "from": "isbinaryfile@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.0.tgz" + }, + "isexe": { + "version": "1.1.2", + "from": "isexe@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-1.1.2.tgz" + }, + "isobject": { + "version": "2.1.0", + "from": "isobject@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz" + }, + "isstream": { + "version": "0.1.2", + "from": "isstream@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + }, + "jasmine-core": { + "version": "2.4.1", + "from": "jasmine-core@>=2.4.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.4.1.tgz" + }, + "jodid25519": { + "version": "1.0.2", + "from": "jodid25519@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz" + }, + "jsbn": { + "version": "0.1.0", + "from": "jsbn@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz" + }, + "json-schema": { + "version": "0.2.2", + "from": "json-schema@0.2.2", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz" + }, + "json-stringify-safe": { + "version": "5.0.1", + "from": "json-stringify-safe@>=5.0.1 <5.1.0", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + }, + "json3": { + "version": "3.2.6", + "from": "json3@3.2.6", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.2.6.tgz" + }, + "jsonfile": { + "version": "2.3.0", + "from": "jsonfile@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.3.0.tgz" + }, + "jsonpointer": { + "version": "2.0.0", + "from": "jsonpointer@2.0.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz" + }, + "jsprim": { + "version": "1.2.2", + "from": "jsprim@>=1.2.2 <2.0.0", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz" + }, + "karma": { + "version": "0.13.22", + "from": "karma@>=0.13.22", + "resolved": "https://registry.npmjs.org/karma/-/karma-0.13.22.tgz", + "dependencies": { + "rimraf": { + "version": "2.5.2", + "from": "rimraf@>=2.3.3 <3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.2.tgz" + } + } + }, + "karma-coffee-preprocessor": { + "version": "0.3.0", + "from": "karma-coffee-preprocessor@0.3.0", + "resolved": "https://registry.npmjs.org/karma-coffee-preprocessor/-/karma-coffee-preprocessor-0.3.0.tgz" + }, + "karma-jasmine": { + "version": "0.3.8", + "from": "karma-jasmine@>=0.3.8 <1.0.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-0.3.8.tgz" + }, + "karma-phantomjs-launcher": { + "version": "1.0.0", + "from": "karma-phantomjs-launcher@1.0.0", + "resolved": "https://registry.npmjs.org/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.0.tgz", + "dependencies": { + "lodash": { + "version": "4.11.1", + "from": "lodash@>=4.0.1 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.11.1.tgz" + } + } + }, + "kew": { + "version": "0.7.0", + "from": "kew@>=0.7.0 <0.8.0", + "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz" + }, + "kind-of": { + "version": "3.0.3", + "from": "kind-of@>=3.0.2 <4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.0.3.tgz" + }, + "klaw": { + "version": "1.2.0", + "from": "klaw@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.2.0.tgz" + }, + "lodash": { + "version": "3.10.1", + "from": "lodash@>=3.8.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz" + }, + "log4js": { + "version": "0.6.35", + "from": "log4js@>=0.6.3 <0.7.0", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.35.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.0.34", + "from": "readable-stream@>=1.0.2 <1.1.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + } + } + }, + "lru-cache": { + "version": "2.2.4", + "from": "lru-cache@>=2.2.0 <2.3.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.2.4.tgz" + }, + "md5": { + "version": "2.0.0", + "from": "md5@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.0.0.tgz", + "dependencies": { + "is-buffer": { + "version": "1.0.2", + "from": "is-buffer@>=1.0.2 <1.1.0", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.0.2.tgz" + } + } + }, + "media-typer": { + "version": "0.3.0", + "from": "media-typer@0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + }, + "micromatch": { + "version": "2.3.8", + "from": "micromatch@>=2.1.5 <3.0.0", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.8.tgz" + }, + "mime": { + "version": "1.3.4", + "from": "mime@>=1.3.4 <2.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz" + }, + "mime-db": { + "version": "1.23.0", + "from": "mime-db@>=1.23.0 <1.24.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz" + }, + "mime-types": { + "version": "2.1.11", + "from": "mime-types@>=2.1.10 <2.2.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz" + }, + "minimatch": { + "version": "3.0.0", + "from": "minimatch@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz" + }, + "minimist": { + "version": "0.0.10", + "from": "minimist@>=0.0.1 <0.1.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz" + }, + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + }, + "nan": { + "version": "2.3.3", + "from": "nan@>=2.3.0 <3.0.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.3.tgz" + }, + "negotiator": { + "version": "0.4.9", + "from": "negotiator@0.4.9", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.4.9.tgz" + }, + "node-uuid": { + "version": "1.4.7", + "from": "node-uuid@>=1.4.7 <1.5.0", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz" + }, + "normalize-path": { + "version": "2.0.1", + "from": "normalize-path@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.0.1.tgz" + }, + "oauth-sign": { + "version": "0.8.1", + "from": "oauth-sign@>=0.8.0 <0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.1.tgz" + }, + "object-component": { + "version": "0.0.3", + "from": "object-component@0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz" + }, + "object.omit": { + "version": "2.0.0", + "from": "object.omit@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.0.tgz" + }, + "on-finished": { + "version": "2.3.0", + "from": "on-finished@>=2.3.0 <2.4.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" + }, + "once": { + "version": "1.3.3", + "from": "once@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz" + }, + "optimist": { + "version": "0.6.1", + "from": "optimist@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz" + }, + "options": { + "version": "0.0.6", + "from": "options@>=0.0.5", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz" + }, + "parse-glob": { + "version": "3.0.4", + "from": "parse-glob@>=3.0.4 <4.0.0", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz" + }, + "parsejson": { + "version": "0.0.1", + "from": "parsejson@0.0.1", + "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.1.tgz" + }, + "parseqs": { + "version": "0.0.2", + "from": "parseqs@0.0.2", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.2.tgz" + }, + "parseuri": { + "version": "0.0.4", + "from": "parseuri@0.0.4", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.4.tgz" + }, + "parseurl": { + "version": "1.3.1", + "from": "parseurl@>=1.3.0 <1.4.0", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz" + }, + "path-is-absolute": { + "version": "1.0.0", + "from": "path-is-absolute@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" + }, + "pend": { + "version": "1.2.0", + "from": "pend@>=1.2.0 <1.3.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" + }, + "phantomjs": { + "version": "2.1.1", + "from": "phantomjs@2.1.1", + "resolved": "https://registry.npmjs.org/phantomjs/-/phantomjs-2.1.1.tgz" + }, + "phantomjs-prebuilt": { + "version": "2.1.7", + "from": "phantomjs-prebuilt@2.1.7", + "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.7.tgz" + }, + "pinkie": { + "version": "2.0.4", + "from": "pinkie@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" + }, + "pinkie-promise": { + "version": "2.0.1", + "from": "pinkie-promise@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" + }, + "preserve": { + "version": "0.2.0", + "from": "preserve@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz" + }, + "process-nextick-args": { + "version": "1.0.6", + "from": "process-nextick-args@>=1.0.6 <1.1.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.6.tgz" + }, + "progress": { + "version": "1.1.8", + "from": "progress@>=1.1.8 <1.2.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz" + }, + "qs": { + "version": "6.1.0", + "from": "qs@6.1.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz" + }, + "randomatic": { + "version": "1.1.5", + "from": "randomatic@>=1.1.3 <2.0.0", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.5.tgz" + }, + "raw-body": { + "version": "2.1.6", + "from": "raw-body@>=2.1.5 <2.2.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.6.tgz", + "dependencies": { + "bytes": { + "version": "2.3.0", + "from": "bytes@2.3.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.3.0.tgz" + } + } + }, + "readable-stream": { + "version": "2.1.2", + "from": "readable-stream@>=2.0.2 <3.0.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.2.tgz" + }, + "readdirp": { + "version": "2.0.0", + "from": "readdirp@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.0.0.tgz", + "dependencies": { + "minimatch": { + "version": "2.0.10", + "from": "minimatch@>=2.0.10 <3.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz" + } + } + }, + "regex-cache": { + "version": "0.4.3", + "from": "regex-cache@>=0.4.2 <0.5.0", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz" + }, + "repeat-element": { + "version": "1.1.2", + "from": "repeat-element@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz" + }, + "repeat-string": { + "version": "1.5.4", + "from": "repeat-string@>=1.5.2 <2.0.0", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.5.4.tgz" + }, + "request": { + "version": "2.67.0", + "from": "request@>=2.67.0 <2.68.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.67.0.tgz", + "dependencies": { + "mime-db": { + "version": "1.22.0", + "from": "mime-db@1.22.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.22.0.tgz" + }, + "mime-types": { + "version": "2.1.10", + "from": "mime-types@>=2.1.7 <2.2.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.10.tgz" + }, + "qs": { + "version": "5.2.0", + "from": "qs@>=5.2.0 <5.3.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.0.tgz" + } + } + }, + "request-progress": { + "version": "2.0.1", + "from": "request-progress@>=2.0.1 <2.1.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz" + }, + "requires-port": { + "version": "1.0.0", + "from": "requires-port@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" + }, + "rimraf": { + "version": "2.2.8", + "from": "rimraf@>=2.2.5 <2.3.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz" + }, + "semver": { + "version": "4.3.6", + "from": "semver@>=4.3.3 <4.4.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz" + }, + "sntp": { + "version": "1.0.9", + "from": "sntp@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" + }, + "socket.io": { + "version": "1.4.6", + "from": "socket.io@>=1.4.5 <2.0.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.4.6.tgz" + }, + "socket.io-adapter": { + "version": "0.4.0", + "from": "socket.io-adapter@0.4.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.4.0.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1" + }, + "socket.io-parser": { + "version": "2.2.2", + "from": "socket.io-parser@2.2.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.2.2.tgz", + "dependencies": { + "debug": { + "version": "0.7.4", + "from": "debug@0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz" + } + } + } + } + }, + "socket.io-client": { + "version": "1.4.6", + "from": "socket.io-client@1.4.6", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.4.6.tgz", + "dependencies": { + "component-emitter": { + "version": "1.2.0", + "from": "component-emitter@1.2.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.0.tgz" + } + } + }, + "socket.io-parser": { + "version": "2.2.6", + "from": "socket.io-parser@2.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.2.6.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1" + }, + "json3": { + "version": "3.3.2", + "from": "json3@3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz" + } + } + }, + "source-map": { + "version": "0.5.6", + "from": "source-map@>=0.5.3 <0.6.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" + }, + "sshpk": { + "version": "1.8.3", + "from": "sshpk@>=1.7.0 <2.0.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.8.3.tgz", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "from": "assert-plus@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + } + } + }, + "statuses": { + "version": "1.2.1", + "from": "statuses@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@>=0.10.0 <0.11.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + }, + "stringstream": { + "version": "0.0.5", + "from": "stringstream@>=0.0.4 <0.1.0", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz" + }, + "strip-ansi": { + "version": "3.0.1", + "from": "strip-ansi@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" + }, + "supports-color": { + "version": "2.0.0", + "from": "supports-color@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" + }, + "throttleit": { + "version": "1.0.0", + "from": "throttleit@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz" + }, + "to-array": { + "version": "0.1.4", + "from": "to-array@0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz" + }, + "tough-cookie": { + "version": "2.2.2", + "from": "tough-cookie@>=2.2.0 <2.3.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz" + }, + "tunnel-agent": { + "version": "0.4.2", + "from": "tunnel-agent@>=0.4.1 <0.5.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.2.tgz" + }, + "tweetnacl": { + "version": "0.13.3", + "from": "tweetnacl@>=0.13.0 <0.14.0", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.13.3.tgz" + }, + "type-is": { + "version": "1.6.12", + "from": "type-is@>=1.6.11 <1.7.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.12.tgz" + }, + "typedarray": { + "version": "0.0.6", + "from": "typedarray@>=0.0.5 <0.1.0", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" + }, + "ultron": { + "version": "1.0.2", + "from": "ultron@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz" + }, + "unpipe": { + "version": "1.0.0", + "from": "unpipe@1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + }, + "useragent": { + "version": "2.1.9", + "from": "useragent@>=2.1.6 <3.0.0", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.1.9.tgz" + }, + "utf8": { + "version": "2.1.0", + "from": "utf8@2.1.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.0.tgz" + }, + "util-deprecate": { + "version": "1.0.2", + "from": "util-deprecate@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + }, + "utils-merge": { + "version": "1.0.0", + "from": "utils-merge@1.0.0", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz" + }, + "verror": { + "version": "1.3.6", + "from": "verror@1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz" + }, + "void-elements": { + "version": "2.0.1", + "from": "void-elements@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz" + }, + "which": { + "version": "1.2.4", + "from": "which@>=1.2.2 <1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.2.4.tgz" + }, + "wordwrap": { + "version": "0.0.3", + "from": "wordwrap@>=0.0.2 <0.1.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" + }, + "wrappy": { + "version": "1.0.1", + "from": "wrappy@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz" + }, + "ws": { + "version": "1.0.1", + "from": "ws@1.0.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.0.1.tgz" + }, + "xmlhttprequest-ssl": { + "version": "1.5.1", + "from": "xmlhttprequest-ssl@1.5.1", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.1.tgz" + }, + "xtend": { + "version": "4.0.1", + "from": "xtend@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" + }, + "yauzl": { + "version": "2.4.1", + "from": "yauzl@2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz" + }, + "yeast": { + "version": "0.1.2", + "from": "yeast@0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000..0e70fc1c6f --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "openfoodnetwork", + "version": "1.7.1", + "repository": { + "type": "git", + "url": "https://github.com/openfoodfoundation/openfoodnetwork" + }, + "devDependencies": { + "karma": "~0.13.22", + "karma-jasmine": "~0.3.8", + "jasmine-core": "~2.4.1", + "karma-phantomjs-launcher": "~1.0.0", + "karma-coffee-preprocessor": "~0.3.0" + }, + "license": "AGPL-1.0" +} From 2146a55c3bb12e58bb58d7a2028cad289ed72318 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 4 May 2016 15:28:35 +1000 Subject: [PATCH 027/110] Linking to PhantomJS installed on PATH --- .travis.yml | 13 +- npm-shrinkwrap.json | 2022 ------------------------------------------- package.json | 1 + 3 files changed, 12 insertions(+), 2024 deletions(-) delete mode 100644 npm-shrinkwrap.json diff --git a/.travis.yml b/.travis.yml index 8a66feb11a..04e7100f44 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,9 @@ language: ruby sudo: false -cache: bundler +cache: + - bundler + - directories: + - travis-phantomjs bundler_args: --without development rvm: - "2.1.5" @@ -23,12 +26,18 @@ env: - CI_NODE_INDEX=4 KARMA="true" GITHUB_DEPLOY="true" before_script: + - mkdir travis-phantomjs || true + - wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 -O $PWD/travis-phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2 + - tar -xvf $PWD/travis-phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2 -C $PWD/travis-phantomjs + - export PATH=$PWD/travis-phantomjs/phantomjs-2.1.1-linux-x86_64/bin:$PATH + - cp config/database.travis.yml config/database.yml - cp config/application.yml.example config/application.yml - RAILS_ENV=test bundle exec rake db:create db:schema:load + - > if [ "$KARMA" = "true" ]; then - npm install -g npm@'~3.8.8' + npm install -g npm@'3.8.8' npm install npm install -g karma-cli@0.1.2 fi diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json deleted file mode 100644 index e1c8b2aa83..0000000000 --- a/npm-shrinkwrap.json +++ /dev/null @@ -1,2022 +0,0 @@ -{ - "name": "openfoodnetwork", - "version": "1.7.1", - "dependencies": { - "accepts": { - "version": "1.1.4", - "from": "accepts@1.1.4", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.1.4.tgz", - "dependencies": { - "mime-db": { - "version": "1.12.0", - "from": "mime-db@>=1.12.0 <1.13.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz" - }, - "mime-types": { - "version": "2.0.14", - "from": "mime-types@>=2.0.4 <2.1.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz" - } - } - }, - "adm-zip": { - "version": "0.4.7", - "from": "adm-zip@>=0.4.7 <0.5.0", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.7.tgz" - }, - "after": { - "version": "0.8.1", - "from": "after@0.8.1", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.1.tgz" - }, - "ansi-regex": { - "version": "2.0.0", - "from": "ansi-regex@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" - }, - "ansi-styles": { - "version": "2.2.1", - "from": "ansi-styles@>=2.2.1 <3.0.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" - }, - "anymatch": { - "version": "1.3.0", - "from": "anymatch@>=1.3.0 <2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz" - }, - "arr-diff": { - "version": "2.0.0", - "from": "arr-diff@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz" - }, - "arr-flatten": { - "version": "1.0.1", - "from": "arr-flatten@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.0.1.tgz" - }, - "array-slice": { - "version": "0.2.3", - "from": "array-slice@>=0.2.3 <0.3.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz" - }, - "array-unique": { - "version": "0.2.1", - "from": "array-unique@>=0.2.1 <0.3.0", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz" - }, - "arraybuffer.slice": { - "version": "0.0.6", - "from": "arraybuffer.slice@0.0.6", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz" - }, - "arrify": { - "version": "1.0.1", - "from": "arrify@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" - }, - "asn1": { - "version": "0.2.3", - "from": "asn1@>=0.2.3 <0.3.0", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz" - }, - "assert-plus": { - "version": "0.2.0", - "from": "assert-plus@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz" - }, - "async-each": { - "version": "1.0.0", - "from": "async-each@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.0.tgz" - }, - "aws-sign2": { - "version": "0.6.0", - "from": "aws-sign2@>=0.6.0 <0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz" - }, - "backo2": { - "version": "1.0.2", - "from": "backo2@1.0.2", - "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz" - }, - "balanced-match": { - "version": "0.4.1", - "from": "balanced-match@>=0.4.1 <0.5.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz" - }, - "base64-arraybuffer": { - "version": "0.1.2", - "from": "base64-arraybuffer@0.1.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.2.tgz" - }, - "base64id": { - "version": "0.1.0", - "from": "base64id@0.1.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-0.1.0.tgz" - }, - "batch": { - "version": "0.5.3", - "from": "batch@0.5.3", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.5.3.tgz" - }, - "benchmark": { - "version": "1.0.0", - "from": "benchmark@1.0.0", - "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-1.0.0.tgz" - }, - "better-assert": { - "version": "1.0.2", - "from": "better-assert@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz" - }, - "binary-extensions": { - "version": "1.4.0", - "from": "binary-extensions@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.4.0.tgz" - }, - "bl": { - "version": "1.0.3", - "from": "bl@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz", - "dependencies": { - "readable-stream": { - "version": "2.0.6", - "from": "readable-stream@2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz" - } - } - }, - "blob": { - "version": "0.0.4", - "from": "blob@0.0.4", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz" - }, - "bluebird": { - "version": "2.10.2", - "from": "bluebird@>=2.9.27 <3.0.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.10.2.tgz" - }, - "body-parser": { - "version": "1.15.0", - "from": "body-parser@>=1.12.4 <2.0.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.15.0.tgz" - }, - "boom": { - "version": "2.10.1", - "from": "boom@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" - }, - "brace-expansion": { - "version": "1.1.4", - "from": "brace-expansion@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz" - }, - "braces": { - "version": "1.8.4", - "from": "braces@>=1.8.2 <2.0.0", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.4.tgz" - }, - "bytes": { - "version": "2.2.0", - "from": "bytes@2.2.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.2.0.tgz" - }, - "callsite": { - "version": "1.0.0", - "from": "callsite@1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz" - }, - "caseless": { - "version": "0.11.0", - "from": "caseless@>=0.11.0 <0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz" - }, - "chalk": { - "version": "1.1.3", - "from": "chalk@>=1.1.1 <2.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" - }, - "charenc": { - "version": "0.0.1", - "from": "charenc@>=0.0.1 <0.1.0", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.1.tgz" - }, - "chokidar": { - "version": "1.4.3", - "from": "chokidar@>=0.8.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.4.3.tgz" - }, - "coffee-script": { - "version": "1.10.0", - "from": "coffee-script@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.10.0.tgz" - }, - "colors": { - "version": "1.1.2", - "from": "colors@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz" - }, - "combined-stream": { - "version": "1.0.5", - "from": "combined-stream@>=1.0.5 <1.1.0", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz" - }, - "component-bind": { - "version": "1.0.0", - "from": "component-bind@1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz" - }, - "component-emitter": { - "version": "1.1.2", - "from": "component-emitter@1.1.2", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz" - }, - "component-inherit": { - "version": "0.0.3", - "from": "component-inherit@0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz" - }, - "concat-map": { - "version": "0.0.1", - "from": "concat-map@0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - }, - "concat-stream": { - "version": "1.5.0", - "from": "concat-stream@1.5.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.0.tgz", - "dependencies": { - "readable-stream": { - "version": "2.0.6", - "from": "readable-stream@>=2.0.0 <2.1.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz" - } - } - }, - "connect": { - "version": "3.4.1", - "from": "connect@>=3.3.5 <4.0.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.4.1.tgz" - }, - "content-type": { - "version": "1.0.1", - "from": "content-type@>=1.0.1 <1.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.1.tgz" - }, - "core-js": { - "version": "2.3.0", - "from": "core-js@>=2.1.0 <3.0.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz" - }, - "core-util-is": { - "version": "1.0.2", - "from": "core-util-is@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" - }, - "crypt": { - "version": "0.0.1", - "from": "crypt@>=0.0.1 <0.1.0", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.1.tgz" - }, - "cryptiles": { - "version": "2.0.5", - "from": "cryptiles@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz" - }, - "custom-event": { - "version": "1.0.0", - "from": "custom-event@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.0.tgz" - }, - "d": { - "version": "0.1.1", - "from": "d@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz" - }, - "dashdash": { - "version": "1.13.1", - "from": "dashdash@>=1.12.0 <2.0.0", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.13.1.tgz", - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "from": "assert-plus@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" - } - } - }, - "debug": { - "version": "2.2.0", - "from": "debug@>=2.2.0 <2.3.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz" - }, - "delayed-stream": { - "version": "1.0.0", - "from": "delayed-stream@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - }, - "depd": { - "version": "1.1.0", - "from": "depd@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz" - }, - "di": { - "version": "0.0.1", - "from": "di@>=0.0.1 <0.1.0", - "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz" - }, - "dom-serialize": { - "version": "2.2.1", - "from": "dom-serialize@>=2.2.0 <3.0.0", - "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz" - }, - "ecc-jsbn": { - "version": "0.1.1", - "from": "ecc-jsbn@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz" - }, - "ee-first": { - "version": "1.1.1", - "from": "ee-first@1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" - }, - "engine.io": { - "version": "1.6.9", - "from": "engine.io@1.6.9", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.6.9.tgz" - }, - "engine.io-client": { - "version": "1.6.9", - "from": "engine.io-client@1.6.9", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.6.9.tgz" - }, - "engine.io-parser": { - "version": "1.2.4", - "from": "engine.io-parser@1.2.4", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.2.4.tgz", - "dependencies": { - "has-binary": { - "version": "0.1.6", - "from": "has-binary@0.1.6", - "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.6.tgz" - }, - "isarray": { - "version": "0.0.1", - "from": "isarray@0.0.1" - } - } - }, - "ent": { - "version": "2.2.0", - "from": "ent@>=2.2.0 <2.3.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz" - }, - "es5-ext": { - "version": "0.10.11", - "from": "es5-ext@>=0.10.11 <0.11.0", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.11.tgz" - }, - "es6-iterator": { - "version": "2.0.0", - "from": "es6-iterator@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.0.tgz" - }, - "es6-symbol": { - "version": "3.0.2", - "from": "es6-symbol@>=3.0.2 <3.1.0", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.0.2.tgz" - }, - "escape-html": { - "version": "1.0.3", - "from": "escape-html@>=1.0.3 <1.1.0", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" - }, - "escape-string-regexp": { - "version": "1.0.5", - "from": "escape-string-regexp@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" - }, - "eventemitter3": { - "version": "1.2.0", - "from": "eventemitter3@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz" - }, - "expand-braces": { - "version": "0.1.2", - "from": "expand-braces@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz", - "dependencies": { - "braces": { - "version": "0.1.5", - "from": "braces@>=0.1.2 <0.2.0", - "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz" - }, - "expand-range": { - "version": "0.1.1", - "from": "expand-range@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz" - }, - "is-number": { - "version": "0.1.1", - "from": "is-number@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz" - }, - "repeat-string": { - "version": "0.2.2", - "from": "repeat-string@>=0.2.2 <0.3.0", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz" - } - } - }, - "expand-brackets": { - "version": "0.1.5", - "from": "expand-brackets@>=0.1.4 <0.2.0", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz" - }, - "expand-range": { - "version": "1.8.1", - "from": "expand-range@>=1.8.1 <2.0.0", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.1.tgz" - }, - "extend": { - "version": "3.0.0", - "from": "extend@>=3.0.0 <3.1.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz" - }, - "extglob": { - "version": "0.3.2", - "from": "extglob@>=0.3.1 <0.4.0", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz" - }, - "extract-zip": { - "version": "1.5.0", - "from": "extract-zip@>=1.5.0 <1.6.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.5.0.tgz", - "dependencies": { - "debug": { - "version": "0.7.4", - "from": "debug@0.7.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz" - }, - "minimist": { - "version": "0.0.8", - "from": "minimist@0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" - }, - "mkdirp": { - "version": "0.5.0", - "from": "mkdirp@0.5.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz" - } - } - }, - "extsprintf": { - "version": "1.0.2", - "from": "extsprintf@1.0.2", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz" - }, - "fd-slicer": { - "version": "1.0.1", - "from": "fd-slicer@>=1.0.1 <1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz" - }, - "filename-regex": { - "version": "2.0.0", - "from": "filename-regex@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.0.tgz" - }, - "fill-range": { - "version": "2.2.3", - "from": "fill-range@>=2.1.0 <3.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz" - }, - "finalhandler": { - "version": "0.4.1", - "from": "finalhandler@0.4.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.4.1.tgz" - }, - "for-in": { - "version": "0.1.5", - "from": "for-in@>=0.1.5 <0.2.0", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.5.tgz" - }, - "for-own": { - "version": "0.1.4", - "from": "for-own@>=0.1.3 <0.2.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.4.tgz" - }, - "forever-agent": { - "version": "0.6.1", - "from": "forever-agent@>=0.6.1 <0.7.0", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" - }, - "form-data": { - "version": "1.0.0-rc4", - "from": "form-data@>=1.0.0-rc3 <1.1.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz", - "dependencies": { - "async": { - "version": "1.5.2", - "from": "async@>=1.5.2 <2.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" - }, - "mime-db": { - "version": "1.22.0", - "from": "mime-db@>=1.22.0 <1.23.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.22.0.tgz" - }, - "mime-types": { - "version": "2.1.10", - "from": "mime-types@2.1.10", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.10.tgz" - } - } - }, - "fs-extra": { - "version": "0.26.7", - "from": "fs-extra@>=0.26.4 <0.27.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz", - "dependencies": { - "graceful-fs": { - "version": "4.1.3", - "from": "graceful-fs@>=4.1.2 <5.0.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.3.tgz" - } - } - }, - "fsevents": { - "version": "1.0.12", - "from": "fsevents@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.0.12.tgz", - "dependencies": { - "ansi": { - "version": "0.3.1", - "from": "ansi@~0.3.1", - "resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz" - }, - "ansi-regex": { - "version": "2.0.0", - "from": "ansi-regex@^2.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" - }, - "ansi-styles": { - "version": "2.2.1", - "from": "ansi-styles@^2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" - }, - "are-we-there-yet": { - "version": "1.1.2", - "from": "are-we-there-yet@~1.1.2", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz" - }, - "asn1": { - "version": "0.2.3", - "from": "asn1@>=0.2.3 <0.3.0", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz" - }, - "assert-plus": { - "version": "0.2.0", - "from": "assert-plus@^0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz" - }, - "async": { - "version": "1.5.2", - "from": "async@^1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" - }, - "aws-sign2": { - "version": "0.6.0", - "from": "aws-sign2@~0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz" - }, - "aws4": { - "version": "1.3.2", - "from": "aws4@^1.2.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.3.2.tgz", - "dependencies": { - "lru-cache": { - "version": "4.0.1", - "from": "lru-cache@^4.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.1.tgz", - "dependencies": { - "pseudomap": { - "version": "1.0.2", - "from": "pseudomap@^1.0.1", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz" - }, - "yallist": { - "version": "2.0.0", - "from": "yallist@^2.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.0.0.tgz" - } - } - } - } - }, - "bl": { - "version": "1.0.3", - "from": "bl@~1.0.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz" - }, - "block-stream": { - "version": "0.0.8", - "from": "block-stream@*", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.8.tgz" - }, - "boom": { - "version": "2.10.1", - "from": "boom@2.x.x", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" - }, - "caseless": { - "version": "0.11.0", - "from": "caseless@~0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz" - }, - "chalk": { - "version": "1.1.3", - "from": "chalk@^1.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" - }, - "combined-stream": { - "version": "1.0.5", - "from": "combined-stream@~1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz" - }, - "commander": { - "version": "2.9.0", - "from": "commander@^2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz" - }, - "core-util-is": { - "version": "1.0.2", - "from": "core-util-is@~1.0.0", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" - }, - "cryptiles": { - "version": "2.0.5", - "from": "cryptiles@2.x.x", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz" - }, - "dashdash": { - "version": "1.13.0", - "from": "dashdash@>=1.10.1 <2.0.0", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.13.0.tgz", - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "from": "assert-plus@^1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" - } - } - }, - "debug": { - "version": "2.2.0", - "from": "debug@~2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz" - }, - "deep-extend": { - "version": "0.4.1", - "from": "deep-extend@~0.4.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz" - }, - "delayed-stream": { - "version": "1.0.0", - "from": "delayed-stream@~1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - }, - "delegates": { - "version": "1.0.0", - "from": "delegates@^1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" - }, - "ecc-jsbn": { - "version": "0.1.1", - "from": "ecc-jsbn@>=0.0.1 <1.0.0", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz" - }, - "escape-string-regexp": { - "version": "1.0.5", - "from": "escape-string-regexp@^1.0.2", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" - }, - "extend": { - "version": "3.0.0", - "from": "extend@~3.0.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz" - }, - "extsprintf": { - "version": "1.0.2", - "from": "extsprintf@1.0.2", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz" - }, - "forever-agent": { - "version": "0.6.1", - "from": "forever-agent@~0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" - }, - "form-data": { - "version": "1.0.0-rc4", - "from": "form-data@~1.0.0-rc3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz" - }, - "fstream": { - "version": "1.0.8", - "from": "fstream@^1.0.2", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.8.tgz" - }, - "fstream-ignore": { - "version": "1.0.3", - "from": "fstream-ignore@~1.0.3", - "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.3.tgz", - "dependencies": { - "minimatch": { - "version": "3.0.0", - "from": "minimatch@^3.0.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz", - "dependencies": { - "brace-expansion": { - "version": "1.1.3", - "from": "brace-expansion@^1.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.3.tgz", - "dependencies": { - "balanced-match": { - "version": "0.3.0", - "from": "balanced-match@^0.3.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.3.0.tgz" - }, - "concat-map": { - "version": "0.0.1", - "from": "concat-map@0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - } - } - } - } - } - } - }, - "gauge": { - "version": "1.2.7", - "from": "gauge@~1.2.5", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-1.2.7.tgz" - }, - "generate-function": { - "version": "2.0.0", - "from": "generate-function@^2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" - }, - "generate-object-property": { - "version": "1.2.0", - "from": "generate-object-property@^1.1.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz" - }, - "graceful-fs": { - "version": "4.1.3", - "from": "graceful-fs@^4.1.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.3.tgz" - }, - "graceful-readlink": { - "version": "1.0.1", - "from": "graceful-readlink@>= 1.0.0", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" - }, - "har-validator": { - "version": "2.0.6", - "from": "har-validator@~2.0.6", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz" - }, - "has-ansi": { - "version": "2.0.0", - "from": "has-ansi@^2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz" - }, - "has-unicode": { - "version": "2.0.0", - "from": "has-unicode@^2.0.0", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.0.tgz" - }, - "hawk": { - "version": "3.1.3", - "from": "hawk@~3.1.0", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz" - }, - "hoek": { - "version": "2.16.3", - "from": "hoek@2.x.x", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" - }, - "http-signature": { - "version": "1.1.1", - "from": "http-signature@~1.1.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz" - }, - "inherits": { - "version": "2.0.1", - "from": "inherits@*", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" - }, - "ini": { - "version": "1.3.4", - "from": "ini@~1.3.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz" - }, - "is-my-json-valid": { - "version": "2.13.1", - "from": "is-my-json-valid@^2.12.4", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz" - }, - "is-property": { - "version": "1.0.2", - "from": "is-property@^1.0.0", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" - }, - "is-typedarray": { - "version": "1.0.0", - "from": "is-typedarray@~1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" - }, - "isarray": { - "version": "1.0.0", - "from": "isarray@~1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - }, - "isstream": { - "version": "0.1.2", - "from": "isstream@~0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" - }, - "jodid25519": { - "version": "1.0.2", - "from": "jodid25519@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz" - }, - "jsbn": { - "version": "0.1.0", - "from": "jsbn@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz" - }, - "json-schema": { - "version": "0.2.2", - "from": "json-schema@0.2.2", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz" - }, - "json-stringify-safe": { - "version": "5.0.1", - "from": "json-stringify-safe@~5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" - }, - "jsonpointer": { - "version": "2.0.0", - "from": "jsonpointer@2.0.0", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz" - }, - "jsprim": { - "version": "1.2.2", - "from": "jsprim@^1.2.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz" - }, - "lodash.pad": { - "version": "4.1.0", - "from": "lodash.pad@^4.1.0", - "resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.1.0.tgz" - }, - "lodash.padend": { - "version": "4.2.0", - "from": "lodash.padend@^4.1.0", - "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.2.0.tgz" - }, - "lodash.padstart": { - "version": "4.2.0", - "from": "lodash.padstart@^4.1.0", - "resolved": "https://registry.npmjs.org/lodash.padstart/-/lodash.padstart-4.2.0.tgz" - }, - "lodash.repeat": { - "version": "4.0.0", - "from": "lodash.repeat@^4.0.0", - "resolved": "https://registry.npmjs.org/lodash.repeat/-/lodash.repeat-4.0.0.tgz" - }, - "lodash.tostring": { - "version": "4.1.2", - "from": "lodash.tostring@^4.0.0", - "resolved": "https://registry.npmjs.org/lodash.tostring/-/lodash.tostring-4.1.2.tgz" - }, - "mime-db": { - "version": "1.22.0", - "from": "mime-db@~1.22.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.22.0.tgz" - }, - "mime-types": { - "version": "2.1.10", - "from": "mime-types@~2.1.7", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.10.tgz" - }, - "minimist": { - "version": "0.0.8", - "from": "minimist@0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" - }, - "mkdirp": { - "version": "0.5.1", - "from": "mkdirp@>=0.3.0 <0.4.0||>=0.4.0 <0.5.0||>=0.5.0 <0.6.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" - }, - "ms": { - "version": "0.7.1", - "from": "ms@0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" - }, - "node-pre-gyp": { - "version": "0.6.25", - "from": "node-pre-gyp@0.6.25", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.25.tgz", - "dependencies": { - "nopt": { - "version": "3.0.6", - "from": "nopt@~3.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "dependencies": { - "abbrev": { - "version": "1.0.7", - "from": "abbrev@1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz" - } - } - } - } - }, - "node-uuid": { - "version": "1.4.7", - "from": "node-uuid@~1.4.7", - "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz" - }, - "npmlog": { - "version": "2.0.3", - "from": "npmlog@~2.0.0", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-2.0.3.tgz" - }, - "oauth-sign": { - "version": "0.8.1", - "from": "oauth-sign@~0.8.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.1.tgz" - }, - "once": { - "version": "1.3.3", - "from": "once@~1.3.3", - "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz" - }, - "pinkie": { - "version": "2.0.4", - "from": "pinkie@^2.0.0", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" - }, - "pinkie-promise": { - "version": "2.0.0", - "from": "pinkie-promise@^2.0.0", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.0.tgz" - }, - "process-nextick-args": { - "version": "1.0.6", - "from": "process-nextick-args@~1.0.6", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.6.tgz" - }, - "qs": { - "version": "6.0.2", - "from": "qs@~6.0.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.0.2.tgz" - }, - "rc": { - "version": "1.1.6", - "from": "rc@~1.1.0", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz", - "dependencies": { - "minimist": { - "version": "1.2.0", - "from": "minimist@^1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz" - } - } - }, - "readable-stream": { - "version": "2.0.6", - "from": "readable-stream@^2.0.0 || ^1.1.13", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz" - }, - "request": { - "version": "2.69.0", - "from": "request@2.x", - "resolved": "https://registry.npmjs.org/request/-/request-2.69.0.tgz" - }, - "rimraf": { - "version": "2.5.2", - "from": "rimraf@~2.5.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.2.tgz", - "dependencies": { - "glob": { - "version": "7.0.3", - "from": "glob@^7.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz", - "dependencies": { - "inflight": { - "version": "1.0.4", - "from": "inflight@^1.0.4", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.4.tgz", - "dependencies": { - "wrappy": { - "version": "1.0.1", - "from": "wrappy@1", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz" - } - } - }, - "inherits": { - "version": "2.0.1", - "from": "inherits@2", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" - }, - "minimatch": { - "version": "3.0.0", - "from": "minimatch@2 || 3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz", - "dependencies": { - "brace-expansion": { - "version": "1.1.3", - "from": "brace-expansion@^1.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.3.tgz", - "dependencies": { - "balanced-match": { - "version": "0.3.0", - "from": "balanced-match@^0.3.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.3.0.tgz" - }, - "concat-map": { - "version": "0.0.1", - "from": "concat-map@0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - } - } - } - } - }, - "once": { - "version": "1.3.3", - "from": "once@^1.3.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", - "dependencies": { - "wrappy": { - "version": "1.0.1", - "from": "wrappy@1", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz" - } - } - }, - "path-is-absolute": { - "version": "1.0.0", - "from": "path-is-absolute@^1.0.0", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" - } - } - } - } - }, - "semver": { - "version": "5.1.0", - "from": "semver@~5.1.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz" - }, - "sntp": { - "version": "1.0.9", - "from": "sntp@1.x.x", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" - }, - "sshpk": { - "version": "1.7.4", - "from": "sshpk@^1.7.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.7.4.tgz" - }, - "string_decoder": { - "version": "0.10.31", - "from": "string_decoder@~0.10.x", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - }, - "stringstream": { - "version": "0.0.5", - "from": "stringstream@~0.0.4", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz" - }, - "strip-ansi": { - "version": "3.0.1", - "from": "strip-ansi@^3.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" - }, - "strip-json-comments": { - "version": "1.0.4", - "from": "strip-json-comments@~1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz" - }, - "supports-color": { - "version": "2.0.0", - "from": "supports-color@^2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" - }, - "tar": { - "version": "2.2.1", - "from": "tar@~2.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz" - }, - "tar-pack": { - "version": "3.1.3", - "from": "tar-pack@~3.1.0", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.1.3.tgz" - }, - "tough-cookie": { - "version": "2.2.2", - "from": "tough-cookie@~2.2.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz" - }, - "tunnel-agent": { - "version": "0.4.2", - "from": "tunnel-agent@~0.4.1", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.2.tgz" - }, - "tweetnacl": { - "version": "0.14.3", - "from": "tweetnacl@>=0.13.0 <1.0.0", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.3.tgz" - }, - "uid-number": { - "version": "0.0.6", - "from": "uid-number@~0.0.6", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz" - }, - "util-deprecate": { - "version": "1.0.2", - "from": "util-deprecate@~1.0.1", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - }, - "verror": { - "version": "1.3.6", - "from": "verror@1.3.6", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz" - }, - "wrappy": { - "version": "1.0.1", - "from": "wrappy@1", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz" - }, - "xtend": { - "version": "4.0.1", - "from": "xtend@^4.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" - } - } - }, - "generate-function": { - "version": "2.0.0", - "from": "generate-function@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" - }, - "generate-object-property": { - "version": "1.2.0", - "from": "generate-object-property@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz" - }, - "getpass": { - "version": "0.1.6", - "from": "getpass@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz", - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "from": "assert-plus@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" - } - } - }, - "glob": { - "version": "7.0.3", - "from": "glob@>=7.0.0 <8.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz" - }, - "glob-base": { - "version": "0.3.0", - "from": "glob-base@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz" - }, - "glob-parent": { - "version": "2.0.0", - "from": "glob-parent@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz" - }, - "graceful-fs": { - "version": "4.1.3", - "from": "graceful-fs@>=4.1.2 <5.0.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.3.tgz" - }, - "graceful-readlink": { - "version": "1.0.1", - "from": "graceful-readlink@>=1.0.0", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" - }, - "har-validator": { - "version": "2.0.6", - "from": "har-validator@>=2.0.2 <2.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", - "dependencies": { - "commander": { - "version": "2.9.0", - "from": "commander@>=2.9.0 <3.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz" - } - } - }, - "has-ansi": { - "version": "2.0.0", - "from": "has-ansi@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz" - }, - "has-binary": { - "version": "0.1.7", - "from": "has-binary@0.1.7", - "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz", - "dependencies": { - "isarray": { - "version": "0.0.1", - "from": "isarray@0.0.1" - } - } - }, - "has-cors": { - "version": "1.1.0", - "from": "has-cors@1.1.0", - "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz" - }, - "hasha": { - "version": "2.2.0", - "from": "hasha@>=2.2.0 <3.0.0", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz" - }, - "hawk": { - "version": "3.1.3", - "from": "hawk@>=3.1.0 <3.2.0", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz" - }, - "hoek": { - "version": "2.16.3", - "from": "hoek@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" - }, - "http-errors": { - "version": "1.4.0", - "from": "http-errors@>=1.4.0 <1.5.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.4.0.tgz" - }, - "http-proxy": { - "version": "1.13.2", - "from": "http-proxy@>=1.13.0 <2.0.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.13.2.tgz" - }, - "http-signature": { - "version": "1.1.1", - "from": "http-signature@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz" - }, - "iconv-lite": { - "version": "0.4.13", - "from": "iconv-lite@0.4.13", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz" - }, - "indexof": { - "version": "0.0.1", - "from": "indexof@0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz" - }, - "inflight": { - "version": "1.0.4", - "from": "inflight@>=1.0.4 <2.0.0", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.4.tgz" - }, - "inherits": { - "version": "2.0.1", - "from": "inherits@>=2.0.1 <3.0.0", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" - }, - "is-absolute": { - "version": "0.1.7", - "from": "is-absolute@>=0.1.7 <0.2.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.1.7.tgz" - }, - "is-binary-path": { - "version": "1.0.1", - "from": "is-binary-path@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz" - }, - "is-buffer": { - "version": "1.1.3", - "from": "is-buffer@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.3.tgz" - }, - "is-dotfile": { - "version": "1.0.2", - "from": "is-dotfile@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.2.tgz" - }, - "is-equal-shallow": { - "version": "0.1.3", - "from": "is-equal-shallow@>=0.1.3 <0.2.0", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz" - }, - "is-extendable": { - "version": "0.1.1", - "from": "is-extendable@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz" - }, - "is-extglob": { - "version": "1.0.0", - "from": "is-extglob@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz" - }, - "is-glob": { - "version": "2.0.1", - "from": "is-glob@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz" - }, - "is-my-json-valid": { - "version": "2.13.1", - "from": "is-my-json-valid@>=2.12.4 <3.0.0", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz" - }, - "is-number": { - "version": "2.1.0", - "from": "is-number@>=2.1.0 <3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz" - }, - "is-posix-bracket": { - "version": "0.1.1", - "from": "is-posix-bracket@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz" - }, - "is-primitive": { - "version": "2.0.0", - "from": "is-primitive@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz" - }, - "is-property": { - "version": "1.0.2", - "from": "is-property@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" - }, - "is-relative": { - "version": "0.1.3", - "from": "is-relative@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.1.3.tgz" - }, - "is-stream": { - "version": "1.1.0", - "from": "is-stream@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" - }, - "is-typedarray": { - "version": "1.0.0", - "from": "is-typedarray@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" - }, - "isarray": { - "version": "1.0.0", - "from": "isarray@1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - }, - "isbinaryfile": { - "version": "3.0.0", - "from": "isbinaryfile@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.0.tgz" - }, - "isexe": { - "version": "1.1.2", - "from": "isexe@>=1.1.1 <2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-1.1.2.tgz" - }, - "isobject": { - "version": "2.1.0", - "from": "isobject@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz" - }, - "isstream": { - "version": "0.1.2", - "from": "isstream@>=0.1.2 <0.2.0", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" - }, - "jasmine-core": { - "version": "2.4.1", - "from": "jasmine-core@>=2.4.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.4.1.tgz" - }, - "jodid25519": { - "version": "1.0.2", - "from": "jodid25519@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz" - }, - "jsbn": { - "version": "0.1.0", - "from": "jsbn@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz" - }, - "json-schema": { - "version": "0.2.2", - "from": "json-schema@0.2.2", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz" - }, - "json-stringify-safe": { - "version": "5.0.1", - "from": "json-stringify-safe@>=5.0.1 <5.1.0", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" - }, - "json3": { - "version": "3.2.6", - "from": "json3@3.2.6", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.2.6.tgz" - }, - "jsonfile": { - "version": "2.3.0", - "from": "jsonfile@>=2.1.0 <3.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.3.0.tgz" - }, - "jsonpointer": { - "version": "2.0.0", - "from": "jsonpointer@2.0.0", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz" - }, - "jsprim": { - "version": "1.2.2", - "from": "jsprim@>=1.2.2 <2.0.0", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz" - }, - "karma": { - "version": "0.13.22", - "from": "karma@>=0.13.22", - "resolved": "https://registry.npmjs.org/karma/-/karma-0.13.22.tgz", - "dependencies": { - "rimraf": { - "version": "2.5.2", - "from": "rimraf@>=2.3.3 <3.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.2.tgz" - } - } - }, - "karma-coffee-preprocessor": { - "version": "0.3.0", - "from": "karma-coffee-preprocessor@0.3.0", - "resolved": "https://registry.npmjs.org/karma-coffee-preprocessor/-/karma-coffee-preprocessor-0.3.0.tgz" - }, - "karma-jasmine": { - "version": "0.3.8", - "from": "karma-jasmine@>=0.3.8 <1.0.0", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-0.3.8.tgz" - }, - "karma-phantomjs-launcher": { - "version": "1.0.0", - "from": "karma-phantomjs-launcher@1.0.0", - "resolved": "https://registry.npmjs.org/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.0.tgz", - "dependencies": { - "lodash": { - "version": "4.11.1", - "from": "lodash@>=4.0.1 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.11.1.tgz" - } - } - }, - "kew": { - "version": "0.7.0", - "from": "kew@>=0.7.0 <0.8.0", - "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz" - }, - "kind-of": { - "version": "3.0.3", - "from": "kind-of@>=3.0.2 <4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.0.3.tgz" - }, - "klaw": { - "version": "1.2.0", - "from": "klaw@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.2.0.tgz" - }, - "lodash": { - "version": "3.10.1", - "from": "lodash@>=3.8.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz" - }, - "log4js": { - "version": "0.6.35", - "from": "log4js@>=0.6.3 <0.7.0", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.35.tgz", - "dependencies": { - "isarray": { - "version": "0.0.1", - "from": "isarray@0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" - }, - "readable-stream": { - "version": "1.0.34", - "from": "readable-stream@>=1.0.2 <1.1.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" - } - } - }, - "lru-cache": { - "version": "2.2.4", - "from": "lru-cache@>=2.2.0 <2.3.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.2.4.tgz" - }, - "md5": { - "version": "2.0.0", - "from": "md5@>=2.0.0 <2.1.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.0.0.tgz", - "dependencies": { - "is-buffer": { - "version": "1.0.2", - "from": "is-buffer@>=1.0.2 <1.1.0", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.0.2.tgz" - } - } - }, - "media-typer": { - "version": "0.3.0", - "from": "media-typer@0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" - }, - "micromatch": { - "version": "2.3.8", - "from": "micromatch@>=2.1.5 <3.0.0", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.8.tgz" - }, - "mime": { - "version": "1.3.4", - "from": "mime@>=1.3.4 <2.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz" - }, - "mime-db": { - "version": "1.23.0", - "from": "mime-db@>=1.23.0 <1.24.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz" - }, - "mime-types": { - "version": "2.1.11", - "from": "mime-types@>=2.1.10 <2.2.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz" - }, - "minimatch": { - "version": "3.0.0", - "from": "minimatch@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz" - }, - "minimist": { - "version": "0.0.10", - "from": "minimist@>=0.0.1 <0.1.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz" - }, - "ms": { - "version": "0.7.1", - "from": "ms@0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" - }, - "nan": { - "version": "2.3.3", - "from": "nan@>=2.3.0 <3.0.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.3.tgz" - }, - "negotiator": { - "version": "0.4.9", - "from": "negotiator@0.4.9", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.4.9.tgz" - }, - "node-uuid": { - "version": "1.4.7", - "from": "node-uuid@>=1.4.7 <1.5.0", - "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz" - }, - "normalize-path": { - "version": "2.0.1", - "from": "normalize-path@>=2.0.1 <3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.0.1.tgz" - }, - "oauth-sign": { - "version": "0.8.1", - "from": "oauth-sign@>=0.8.0 <0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.1.tgz" - }, - "object-component": { - "version": "0.0.3", - "from": "object-component@0.0.3", - "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz" - }, - "object.omit": { - "version": "2.0.0", - "from": "object.omit@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.0.tgz" - }, - "on-finished": { - "version": "2.3.0", - "from": "on-finished@>=2.3.0 <2.4.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" - }, - "once": { - "version": "1.3.3", - "from": "once@>=1.3.0 <2.0.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz" - }, - "optimist": { - "version": "0.6.1", - "from": "optimist@>=0.6.0 <0.7.0", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz" - }, - "options": { - "version": "0.0.6", - "from": "options@>=0.0.5", - "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz" - }, - "parse-glob": { - "version": "3.0.4", - "from": "parse-glob@>=3.0.4 <4.0.0", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz" - }, - "parsejson": { - "version": "0.0.1", - "from": "parsejson@0.0.1", - "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.1.tgz" - }, - "parseqs": { - "version": "0.0.2", - "from": "parseqs@0.0.2", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.2.tgz" - }, - "parseuri": { - "version": "0.0.4", - "from": "parseuri@0.0.4", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.4.tgz" - }, - "parseurl": { - "version": "1.3.1", - "from": "parseurl@>=1.3.0 <1.4.0", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz" - }, - "path-is-absolute": { - "version": "1.0.0", - "from": "path-is-absolute@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" - }, - "pend": { - "version": "1.2.0", - "from": "pend@>=1.2.0 <1.3.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" - }, - "phantomjs": { - "version": "2.1.1", - "from": "phantomjs@2.1.1", - "resolved": "https://registry.npmjs.org/phantomjs/-/phantomjs-2.1.1.tgz" - }, - "phantomjs-prebuilt": { - "version": "2.1.7", - "from": "phantomjs-prebuilt@2.1.7", - "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.7.tgz" - }, - "pinkie": { - "version": "2.0.4", - "from": "pinkie@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" - }, - "pinkie-promise": { - "version": "2.0.1", - "from": "pinkie-promise@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" - }, - "preserve": { - "version": "0.2.0", - "from": "preserve@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz" - }, - "process-nextick-args": { - "version": "1.0.6", - "from": "process-nextick-args@>=1.0.6 <1.1.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.6.tgz" - }, - "progress": { - "version": "1.1.8", - "from": "progress@>=1.1.8 <1.2.0", - "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz" - }, - "qs": { - "version": "6.1.0", - "from": "qs@6.1.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz" - }, - "randomatic": { - "version": "1.1.5", - "from": "randomatic@>=1.1.3 <2.0.0", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.5.tgz" - }, - "raw-body": { - "version": "2.1.6", - "from": "raw-body@>=2.1.5 <2.2.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.6.tgz", - "dependencies": { - "bytes": { - "version": "2.3.0", - "from": "bytes@2.3.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.3.0.tgz" - } - } - }, - "readable-stream": { - "version": "2.1.2", - "from": "readable-stream@>=2.0.2 <3.0.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.2.tgz" - }, - "readdirp": { - "version": "2.0.0", - "from": "readdirp@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.0.0.tgz", - "dependencies": { - "minimatch": { - "version": "2.0.10", - "from": "minimatch@>=2.0.10 <3.0.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz" - } - } - }, - "regex-cache": { - "version": "0.4.3", - "from": "regex-cache@>=0.4.2 <0.5.0", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz" - }, - "repeat-element": { - "version": "1.1.2", - "from": "repeat-element@>=1.1.2 <2.0.0", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz" - }, - "repeat-string": { - "version": "1.5.4", - "from": "repeat-string@>=1.5.2 <2.0.0", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.5.4.tgz" - }, - "request": { - "version": "2.67.0", - "from": "request@>=2.67.0 <2.68.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.67.0.tgz", - "dependencies": { - "mime-db": { - "version": "1.22.0", - "from": "mime-db@1.22.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.22.0.tgz" - }, - "mime-types": { - "version": "2.1.10", - "from": "mime-types@>=2.1.7 <2.2.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.10.tgz" - }, - "qs": { - "version": "5.2.0", - "from": "qs@>=5.2.0 <5.3.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.0.tgz" - } - } - }, - "request-progress": { - "version": "2.0.1", - "from": "request-progress@>=2.0.1 <2.1.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz" - }, - "requires-port": { - "version": "1.0.0", - "from": "requires-port@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" - }, - "rimraf": { - "version": "2.2.8", - "from": "rimraf@>=2.2.5 <2.3.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz" - }, - "semver": { - "version": "4.3.6", - "from": "semver@>=4.3.3 <4.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz" - }, - "sntp": { - "version": "1.0.9", - "from": "sntp@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" - }, - "socket.io": { - "version": "1.4.6", - "from": "socket.io@>=1.4.5 <2.0.0", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.4.6.tgz" - }, - "socket.io-adapter": { - "version": "0.4.0", - "from": "socket.io-adapter@0.4.0", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.4.0.tgz", - "dependencies": { - "isarray": { - "version": "0.0.1", - "from": "isarray@0.0.1" - }, - "socket.io-parser": { - "version": "2.2.2", - "from": "socket.io-parser@2.2.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.2.2.tgz", - "dependencies": { - "debug": { - "version": "0.7.4", - "from": "debug@0.7.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz" - } - } - } - } - }, - "socket.io-client": { - "version": "1.4.6", - "from": "socket.io-client@1.4.6", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.4.6.tgz", - "dependencies": { - "component-emitter": { - "version": "1.2.0", - "from": "component-emitter@1.2.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.0.tgz" - } - } - }, - "socket.io-parser": { - "version": "2.2.6", - "from": "socket.io-parser@2.2.6", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.2.6.tgz", - "dependencies": { - "isarray": { - "version": "0.0.1", - "from": "isarray@0.0.1" - }, - "json3": { - "version": "3.3.2", - "from": "json3@3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz" - } - } - }, - "source-map": { - "version": "0.5.6", - "from": "source-map@>=0.5.3 <0.6.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" - }, - "sshpk": { - "version": "1.8.3", - "from": "sshpk@>=1.7.0 <2.0.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.8.3.tgz", - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "from": "assert-plus@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" - } - } - }, - "statuses": { - "version": "1.2.1", - "from": "statuses@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz" - }, - "string_decoder": { - "version": "0.10.31", - "from": "string_decoder@>=0.10.0 <0.11.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - }, - "stringstream": { - "version": "0.0.5", - "from": "stringstream@>=0.0.4 <0.1.0", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz" - }, - "strip-ansi": { - "version": "3.0.1", - "from": "strip-ansi@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" - }, - "supports-color": { - "version": "2.0.0", - "from": "supports-color@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" - }, - "throttleit": { - "version": "1.0.0", - "from": "throttleit@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz" - }, - "to-array": { - "version": "0.1.4", - "from": "to-array@0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz" - }, - "tough-cookie": { - "version": "2.2.2", - "from": "tough-cookie@>=2.2.0 <2.3.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz" - }, - "tunnel-agent": { - "version": "0.4.2", - "from": "tunnel-agent@>=0.4.1 <0.5.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.2.tgz" - }, - "tweetnacl": { - "version": "0.13.3", - "from": "tweetnacl@>=0.13.0 <0.14.0", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.13.3.tgz" - }, - "type-is": { - "version": "1.6.12", - "from": "type-is@>=1.6.11 <1.7.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.12.tgz" - }, - "typedarray": { - "version": "0.0.6", - "from": "typedarray@>=0.0.5 <0.1.0", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" - }, - "ultron": { - "version": "1.0.2", - "from": "ultron@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz" - }, - "unpipe": { - "version": "1.0.0", - "from": "unpipe@1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" - }, - "useragent": { - "version": "2.1.9", - "from": "useragent@>=2.1.6 <3.0.0", - "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.1.9.tgz" - }, - "utf8": { - "version": "2.1.0", - "from": "utf8@2.1.0", - "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.0.tgz" - }, - "util-deprecate": { - "version": "1.0.2", - "from": "util-deprecate@>=1.0.1 <1.1.0", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - }, - "utils-merge": { - "version": "1.0.0", - "from": "utils-merge@1.0.0", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz" - }, - "verror": { - "version": "1.3.6", - "from": "verror@1.3.6", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz" - }, - "void-elements": { - "version": "2.0.1", - "from": "void-elements@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz" - }, - "which": { - "version": "1.2.4", - "from": "which@>=1.2.2 <1.3.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.2.4.tgz" - }, - "wordwrap": { - "version": "0.0.3", - "from": "wordwrap@>=0.0.2 <0.1.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" - }, - "wrappy": { - "version": "1.0.1", - "from": "wrappy@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz" - }, - "ws": { - "version": "1.0.1", - "from": "ws@1.0.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-1.0.1.tgz" - }, - "xmlhttprequest-ssl": { - "version": "1.5.1", - "from": "xmlhttprequest-ssl@1.5.1", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.1.tgz" - }, - "xtend": { - "version": "4.0.1", - "from": "xtend@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" - }, - "yauzl": { - "version": "2.4.1", - "from": "yauzl@2.4.1", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz" - }, - "yeast": { - "version": "0.1.2", - "from": "yeast@0.1.2", - "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz" - } - } -} diff --git a/package.json b/package.json index 0e70fc1c6f..bb174e883d 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "url": "https://github.com/openfoodfoundation/openfoodnetwork" }, "devDependencies": { + "phantomjs-prebuilt": "~2.1.7", "karma": "~0.13.22", "karma-jasmine": "~0.3.8", "jasmine-core": "~2.4.1", From 0586a710711aa4b6d98cb5b2a92f83aee95e8bbe Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 5 May 2016 12:33:06 +1000 Subject: [PATCH 028/110] Bumping Poltergeist and Capybara versions --- Gemfile.lock | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 494882481c..07931312ab 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -176,7 +176,8 @@ GEM columnize (~> 0.3) debugger-linecache (~> 1.2) cancan (1.6.8) - capybara (2.5.0) + capybara (2.7.1) + addressable mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) @@ -455,7 +456,7 @@ GEM railties (>= 3.1) money (5.1.1) i18n (~> 0.6.0) - multi_json (1.11.2) + multi_json (1.12.0) multi_xml (0.5.5) newrelic_rpm (3.12.0.288) nokogiri (1.6.7.2) @@ -479,7 +480,7 @@ GEM paypal-sdk-merchant (1.106.1) paypal-sdk-core (~> 0.2.3) pg (0.13.2) - poltergeist (1.7.0) + poltergeist (1.9.0) capybara (~> 2.1) cliver (~> 0.3.1) multi_json (~> 1.0) @@ -626,7 +627,7 @@ GEM webmock (1.13.0) addressable (>= 2.2.7) crack (>= 0.3.2) - websocket-driver (0.6.2) + websocket-driver (0.6.3) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) whenever (0.9.2) From 245db8971ae5be87eb0c1623e67b883ec81fa27e Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 5 May 2016 13:50:58 +1000 Subject: [PATCH 029/110] Fixing specs broken by faster PhantomJS --- .../order_cycles/controllers/create.js.coffee | 2 +- .../controllers/simple_create.js.coffee | 2 +- .../_name_and_timing_form.html.haml | 6 +- .../admin/bulk_order_management_spec.rb | 316 +++++++++--------- .../admin/bulk_product_update_spec.rb | 89 ++--- spec/features/admin/order_cycles_spec.rb | 28 +- .../unit/order_cycle_spec.js.coffee | 3 +- 7 files changed, 240 insertions(+), 206 deletions(-) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/create.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/create.js.coffee index c837ff84ec..3077aa8a11 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/create.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/create.js.coffee @@ -12,7 +12,7 @@ angular.module('admin.orderCycles') $scope.StatusMessage = StatusMessage $scope.loaded = -> - Enterprise.loaded && EnterpriseFee.loaded + Enterprise.loaded && EnterpriseFee.loaded && OrderCycle.loaded $scope.suppliedVariants = (enterprise_id) -> Enterprise.suppliedVariants(enterprise_id) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee index a6d1e18535..dfa8011167 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee @@ -20,7 +20,7 @@ angular.module('admin.orderCycles').controller "AdminSimpleCreateOrderCycleCtrl" OrderCycle.order_cycle.coordinator_id = enterprise.id $scope.loaded = -> - Enterprise.loaded && EnterpriseFee.loaded + Enterprise.loaded && EnterpriseFee.loaded && OrderCycle.loaded $scope.removeDistributionOfVariant = angular.noop diff --git a/app/views/admin/order_cycles/_name_and_timing_form.html.haml b/app/views/admin/order_cycles/_name_and_timing_form.html.haml index 703d8094b9..39fa19f287 100644 --- a/app/views/admin/order_cycles/_name_and_timing_form.html.haml +++ b/app/views/admin/order_cycles/_name_and_timing_form.html.haml @@ -3,14 +3,14 @@ = f.label :name .six.columns.omega - if viewing_as_coordinator_of?(@order_cycle) - = f.text_field :name, 'ng-model' => 'order_cycle.name', 'required' => true + = f.text_field :name, 'ng-model' => 'order_cycle.name', 'required' => true, 'ng-disabled' => '!loaded()' - else {{ order_cycle.name }} .two.columns = f.label :orders_open_at, 'Orders open' .omega.six.columns - if viewing_as_coordinator_of?(@order_cycle) - = f.text_field :orders_open_at, 'datetimepicker' => 'order_cycle.orders_open_at', 'ng-model' => 'order_cycle.orders_open_at' + = f.text_field :orders_open_at, 'datetimepicker' => 'order_cycle.orders_open_at', 'ng-model' => 'order_cycle.orders_open_at', 'ng-disabled' => '!loaded()' - else {{ order_cycle.orders_open_at }} @@ -23,6 +23,6 @@ = f.label :orders_close_at, 'Orders close' .six.columns.omega - if viewing_as_coordinator_of?(@order_cycle) - = f.text_field :orders_close_at, 'datetimepicker' => 'order_cycle.orders_close_at', 'ng-model' => 'order_cycle.orders_close_at' + = f.text_field :orders_close_at, 'datetimepicker' => 'order_cycle.orders_close_at', 'ng-model' => 'order_cycle.orders_close_at', 'ng-disabled' => '!loaded()' - else {{ order_cycle.orders_close_at }} diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index de5bd85d3e..ef4fc13e39 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -18,12 +18,12 @@ feature %q{ end context "displaying the list of line items" do - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:o2) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:o3) { FactoryGirl.create(:order_with_distributor, state: 'address', completed_at: nil ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1 ) } - let!(:li2) { FactoryGirl.create(:line_item, order: o2 ) } - let!(:li3) { FactoryGirl.create(:line_item, order: o3 ) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:o2) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:o3) { create(:order_with_distributor, state: 'address', completed_at: nil ) } + let!(:li1) { create(:line_item, order: o1 ) } + let!(:li2) { create(:line_item, order: o2 ) } + let!(:li3) { create(:line_item, order: o3 ) } before :each do visit '/admin/orders/bulk_management' @@ -32,15 +32,15 @@ feature %q{ it "displays a list of line items" do expect(page).to have_selector "tr#li_#{li1.id}" expect(page).to have_selector "tr#li_#{li2.id}" - expect(page).to_not have_selector "tr#li_#{li3.id}" + expect(page).to have_no_selector "tr#li_#{li3.id}" end end context "displaying individual columns" do - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, bill_address: FactoryGirl.create(:address) ) } - let!(:o2) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, bill_address: nil ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1 ) } - let!(:li2) { FactoryGirl.create(:line_item, order: o2, product: FactoryGirl.create(:product_with_option_types) ) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, bill_address: create(:address) ) } + let!(:o2) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, bill_address: nil ) } + let!(:li1) { create(:line_item, order: o1 ) } + let!(:li2) { create(:line_item, order: o2, product: create(:product_with_option_types) ) } before :each do visit '/admin/orders/bulk_management' @@ -90,8 +90,8 @@ feature %q{ end context "tracking changes" do - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1, :quantity => 5 ) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:li1) { create(:line_item, order: o1, :quantity => 5 ) } before :each do visit '/admin/orders/bulk_management' @@ -105,8 +105,8 @@ feature %q{ end context "submitting data to the server" do - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1, :quantity => 5 ) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:li1) { create(:line_item, order: o1, :quantity => 5 ) } before :each do Spree::Config.set(allow_backorders: false) @@ -116,19 +116,19 @@ feature %q{ context "when acceptable data is sent to the server" do it "displays an update button which submits pending changes" do - expect(page).to_not have_selector "#save-bar" + expect(page).to have_no_selector "#save-bar" fill_in "quantity", :with => 2 expect(page).to have_selector "input[name='quantity'].ng-dirty" expect(page).to have_selector "#save-bar", text: "You have unsaved changes" click_button "Save Changes" expect(page).to have_selector "#save-bar", text: "All changes saved" - expect(page).to_not have_selector "input[name='quantity'].ng-dirty" + expect(page).to have_no_selector "input[name='quantity'].ng-dirty" end end context "when unacceptable data is sent to the server" do it "displays an update button which submits pending changes" do - expect(page).to_not have_selector "#save-bar" + expect(page).to have_no_selector "#save-bar" fill_in "quantity", :with => li1.variant.on_hand + li1.quantity + 10 expect(page).to have_selector "input[name='quantity'].ng-dirty" expect(page).to have_selector "#save-bar", text: "You have unsaved changes" @@ -146,28 +146,28 @@ feature %q{ admin_user = quick_login_as_admin end - let!(:p1) { FactoryGirl.create(:product_with_option_types, group_buy: true, group_buy_unit_size: 5000, variant_unit: "weight", variants: [FactoryGirl.create(:variant, unit_value: 1000)] ) } + let!(:p1) { create(:product_with_option_types, group_buy: true, group_buy_unit_size: 5000, variant_unit: "weight", variants: [create(:variant, unit_value: 1000)] ) } let!(:v1) { p1.variants.first } - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1, variant: v1, :quantity => 5, :final_weight_volume => 1000, price: 10.00 ) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:li1) { create(:line_item, order: o1, variant: v1, :quantity => 5, :final_weight_volume => 1000, price: 10.00 ) } before { v1.update_attribute(:on_hand, 100)} context "modifying the weight/volume of a line item" do it "price is altered" do visit '/admin/orders/bulk_management' - first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "Weight/Volume").click - first("div#columns-dropdown div.menu div.menu_item", text: "Price").click + find("div#columns-dropdown", :text => "COLUMNS").click + find("div#columns-dropdown div.menu div.menu_item", text: "Weight/Volume").click + find("div#columns-dropdown div.menu div.menu_item", text: "Price").click # hide dropdown - first("div#columns-dropdown", :text => "COLUMNS").click + find("div#columns-dropdown", :text => "COLUMNS").click within "tr#li_#{li1.id}" do expect(page).to have_field "price", with: "$50.00" fill_in "final_weight_volume", :with => 2000 expect(page).to have_field "price", with: "$100.00" end click_button "Save Changes" - expect(page).to_not have_selector "#save-bar" + expect(page).to have_no_selector "#save-bar" li1.reload expect(li1.final_weight_volume).to eq 2000 expect(li1.price).to eq 20.00 @@ -177,9 +177,9 @@ feature %q{ context "modifying the quantity of a line item" do it "price is altered" do visit '/admin/orders/bulk_management' - first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "Price").click - first("div#columns-dropdown", :text => "COLUMNS").click + find("div#columns-dropdown", :text => "COLUMNS").click + find("div#columns-dropdown div.menu div.menu_item", text: "Price").click + find("div#columns-dropdown", :text => "COLUMNS").click within "tr#li_#{li1.id}" do expect(page).to have_field "price", with: "$#{format("%.2f",li1.price * 5)}" fill_in "quantity", :with => 6 @@ -191,9 +191,9 @@ feature %q{ context "modifying the quantity of a line item" do it "weight/volume is altered" do visit '/admin/orders/bulk_management' - first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "Weight/Volume").click - first("div#columns-dropdown", :text => "COLUMNS").click + find("div#columns-dropdown", :text => "COLUMNS").click + find("div#columns-dropdown div.menu div.menu_item", text: "Weight/Volume").click + find("div#columns-dropdown", :text => "COLUMNS").click within "tr#li_#{li1.id}" do expect(page).to have_field "final_weight_volume", with: "#{li1.final_weight_volume.round}" fill_in "quantity", :with => 6 @@ -213,11 +213,11 @@ feature %q{ expect(page).to have_selector "th", :text => "QUANTITY" expect(page).to have_selector "th", :text => "MAX" - first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "Producer").click - first("div#columns-dropdown", :text => "COLUMNS").click + find("div#columns-dropdown", :text => "COLUMNS").click + find("div#columns-dropdown div.menu div.menu_item", text: "Producer").click + find("div#columns-dropdown", :text => "COLUMNS").click - expect(page).to_not have_selector "th", :text => "PRODUCER" + expect(page).to have_no_selector "th", :text => "PRODUCER" expect(page).to have_selector "th", :text => "NAME" expect(page).to have_selector "th", :text => "ORDER DATE" expect(page).to have_selector "th", :text => "PRODUCT: UNIT" @@ -230,60 +230,64 @@ feature %q{ context "supplier filter" do let!(:s1) { create(:supplier_enterprise) } let!(:s2) { create(:supplier_enterprise) } - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, order_cycle: create(:simple_order_cycle) ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1, product: create(:product, supplier: s1) ) } - let!(:li2) { FactoryGirl.create(:line_item, order: o1, product: create(:product, supplier: s2) ) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, order_cycle: create(:simple_order_cycle) ) } + let!(:li1) { create(:line_item, order: o1, product: create(:product, supplier: s1) ) } + let!(:li2) { create(:line_item, order: o1, product: create(:product, supplier: s2) ) } before :each do visit '/admin/orders/bulk_management' end it "displays a select box for producers, which filters line items by the selected supplier" do - supplier_names = ["All"] - Enterprise.is_primary_producer.each{ |e| supplier_names << e.name } + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_selector "tr#li_#{li2.id}" open_select2 "div.select2-container#s2id_supplier_filter" - supplier_names.each { |sn| expect(page).to have_selector "div.select2-drop-active ul.select2-results li", text: sn } + expect(page).to have_selector "div.select2-drop-active ul.select2-results li", text: "All" + Enterprise.is_primary_producer.map(&:name).each do |sn| + expect(page).to have_selector "div.select2-drop-active ul.select2-results li", text: sn + end close_select2 "div.select2-container#s2id_supplier_filter" - expect(page).to have_selector "tr#li_#{li1.id}", visible: true - expect(page).to have_selector "tr#li_#{li2.id}", visible: true select2_select s1.name, from: "supplier_filter" - expect(page).to have_selector "tr#li_#{li1.id}", visible: true - expect(page).to_not have_selector "tr#li_#{li2.id}" + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_no_selector "tr#li_#{li2.id}" end it "displays all line items when 'All' is selected from supplier filter" do + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_selector "tr#li_#{li2.id}" select2_select s1.name, from: "supplier_filter" - expect(page).to have_selector "tr#li_#{li1.id}", visible: true - expect(page).to_not have_selector "tr#li_#{li2.id}", visible: true + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_no_selector "tr#li_#{li2.id}" select2_select "All", from: "supplier_filter" - expect(page).to have_selector "tr#li_#{li1.id}", visible: true - expect(page).to have_selector "tr#li_#{li2.id}", visible: true + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_selector "tr#li_#{li2.id}" end end context "distributor filter" do let!(:d1) { create(:distributor_enterprise) } let!(:d2) { create(:distributor_enterprise) } - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, distributor: d1, order_cycle: create(:simple_order_cycle) ) } - let!(:o2) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, distributor: d2, order_cycle: create(:simple_order_cycle) ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1 ) } - let!(:li2) { FactoryGirl.create(:line_item, order: o2 ) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, distributor: d1, order_cycle: create(:simple_order_cycle) ) } + let!(:o2) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, distributor: d2, order_cycle: create(:simple_order_cycle) ) } + let!(:li1) { create(:line_item, order: o1 ) } + let!(:li2) { create(:line_item, order: o2 ) } before :each do visit '/admin/orders/bulk_management' end it "displays a select box for distributors, which filters line items by the selected distributor" do - distributor_names = ["All"] - Enterprise.is_distributor.each{ |e| distributor_names << e.name } + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_selector "tr#li_#{li2.id}" open_select2 "div.select2-container#s2id_distributor_filter" - distributor_names.each { |dn| expect(page).to have_selector "div.select2-drop-active ul.select2-results li", text: dn } + expect(page).to have_selector "div.select2-drop-active ul.select2-results li", text: "All" + Enterprise.is_distributor.map(&:name).each do |dn| + expect(page).to have_selector "div.select2-drop-active ul.select2-results li", text: dn + end close_select2 "div.select2-container#s2id_distributor_filter" - expect(page).to have_selector "tr#li_#{li1.id}", visible: true - expect(page).to have_selector "tr#li_#{li2.id}", visible: true select2_select d1.name, from: "distributor_filter" - expect(page).to have_selector "tr#li_#{li1.id}", visible: true - expect(page).to_not have_selector "tr#li_#{li2.id}", visible: true + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_no_selector "tr#li_#{li2.id}" end it "displays all line items when 'All' is selected from distributor filter" do @@ -291,7 +295,7 @@ feature %q{ expect(page).to have_selector "tr#li_#{li2.id}" select2_select d1.name, from: "distributor_filter" expect(page).to have_selector "tr#li_#{li1.id}" - expect(page).to_not have_selector "tr#li_#{li2.id}" + expect(page).to have_no_selector "tr#li_#{li2.id}" select2_select "All", from: "distributor_filter" expect(page).to have_selector "tr#li_#{li1.id}" expect(page).to have_selector "tr#li_#{li2.id}" @@ -300,25 +304,25 @@ feature %q{ context "order_cycle filter" do let!(:distributor) { create(:distributor_enterprise) } - let!(:oc1) { FactoryGirl.create(:simple_order_cycle, distributors: [distributor]) } - let!(:oc2) { FactoryGirl.create(:simple_order_cycle, distributors: [distributor]) } - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, order_cycle: oc1 ) } - let!(:o2) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, order_cycle: oc2 ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1 ) } - let!(:li2) { FactoryGirl.create(:line_item, order: o2 ) } + let!(:oc1) { create(:simple_order_cycle, distributors: [distributor]) } + let!(:oc2) { create(:simple_order_cycle, distributors: [distributor]) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, order_cycle: oc1 ) } + let!(:o2) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, order_cycle: oc2 ) } + let!(:li1) { create(:line_item, order: o1 ) } + let!(:li2) { create(:line_item, order: o2 ) } before do visit '/admin/orders/bulk_management' end it "displays a select box for order cycles, which filters line items by the selected order cycle" do - expect(page).to have_select2 'order_cycle_filter', with_options: OrderCycle.pluck(:name).unshift("All") expect(page).to have_selector "tr#li_#{li1.id}" expect(page).to have_selector "tr#li_#{li2.id}" + expect(page).to have_select2 'order_cycle_filter', with_options: OrderCycle.pluck(:name).unshift("All") select2_select oc1.name, from: "order_cycle_filter" - expect(page).to_not have_selector "#loading img.spinner" + expect(page).to have_no_selector "#loading img.spinner" expect(page).to have_selector "tr#li_#{li1.id}" - expect(page).to_not have_selector "tr#li_#{li2.id}" + expect(page).to have_no_selector "tr#li_#{li2.id}" end it "displays all line items when 'All' is selected from order_cycle filter" do @@ -326,7 +330,7 @@ feature %q{ expect(page).to have_selector "tr#li_#{li2.id}" select2_select oc1.name, from: "order_cycle_filter" expect(page).to have_selector "tr#li_#{li1.id}" - expect(page).to_not have_selector "tr#li_#{li2.id}" + expect(page).to have_no_selector "tr#li_#{li2.id}" select2_select "All", from: "order_cycle_filter" expect(page).to have_selector "tr#li_#{li1.id}" expect(page).to have_selector "tr#li_#{li2.id}" @@ -338,42 +342,46 @@ feature %q{ let!(:s2) { create(:supplier_enterprise) } let!(:d1) { create(:distributor_enterprise) } let!(:d2) { create(:distributor_enterprise) } - let!(:oc1) { FactoryGirl.create(:simple_order_cycle, suppliers: [s1], distributors: [d1] ) } - let!(:oc2) { FactoryGirl.create(:simple_order_cycle, suppliers: [s2], distributors: [d2] ) } - let!(:p1) { FactoryGirl.create(:product, supplier: s1) } - let!(:p2) { FactoryGirl.create(:product, supplier: s2) } - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, distributor: d1, order_cycle: oc1 ) } - let!(:o2) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, distributor: d2, order_cycle: oc2 ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1, product: p1 ) } - let!(:li2) { FactoryGirl.create(:line_item, order: o2, product: p2 ) } + let!(:oc1) { create(:simple_order_cycle, suppliers: [s1], distributors: [d1] ) } + let!(:oc2) { create(:simple_order_cycle, suppliers: [s2], distributors: [d2] ) } + let!(:p1) { create(:product, supplier: s1) } + let!(:p2) { create(:product, supplier: s2) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, distributor: d1, order_cycle: oc1 ) } + let!(:o2) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, distributor: d2, order_cycle: oc2 ) } + let!(:li1) { create(:line_item, order: o1, product: p1 ) } + let!(:li2) { create(:line_item, order: o2, product: p2 ) } before :each do visit '/admin/orders/bulk_management' end it "allows filters to be used in combination" do + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_selector "tr#li_#{li2.id}" select2_select oc1.name, from: "order_cycle_filter" - expect(page).to have_selector "tr#li_#{li1.id}", visible: true - expect(page).to_not have_selector "tr#li_#{li2.id}", visible: true + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_no_selector "tr#li_#{li2.id}" select2_select d1.name, from: "distributor_filter" select2_select s1.name, from: "supplier_filter" - expect(page).to have_selector "tr#li_#{li1.id}", visible: true - expect(page).to_not have_selector "tr#li_#{li2.id}", visible: true + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_no_selector "tr#li_#{li2.id}" select2_select d2.name, from: "distributor_filter" select2_select s2.name, from: "supplier_filter" - expect(page).to_not have_selector "tr#li_#{li1.id}", visible: true - expect(page).to_not have_selector "tr#li_#{li2.id}", visible: true + expect(page).to have_no_selector "tr#li_#{li1.id}" + expect(page).to have_no_selector "tr#li_#{li2.id}" select2_select oc2.name, from: "order_cycle_filter" - expect(page).to_not have_selector "tr#li_#{li1.id}", visible: true - expect(page).to have_selector "tr#li_#{li2.id}", visible: true + expect(page).to have_no_selector "tr#li_#{li1.id}" + expect(page).to have_selector "tr#li_#{li2.id}" end it "displays a 'Clear All' button which sets all select filters to 'All'" do + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_selector "tr#li_#{li2.id}" select2_select oc1.name, from: "order_cycle_filter" select2_select d1.name, from: "distributor_filter" select2_select s1.name, from: "supplier_filter" expect(page).to have_selector "tr#li_#{li1.id}" - expect(page).to_not have_selector "tr#li_#{li2.id}" + expect(page).to have_no_selector "tr#li_#{li2.id}" expect(page).to have_button "Clear All" click_button "Clear All" expect(page).to have_selector "div#s2id_order_cycle_filter a.select2-choice", text: "All" @@ -386,12 +394,12 @@ feature %q{ end context "using quick search" do - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:o2) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:o3) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1 ) } - let!(:li2) { FactoryGirl.create(:line_item, order: o2 ) } - let!(:li3) { FactoryGirl.create(:line_item, order: o3 ) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:o2) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:o3) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:li1) { create(:line_item, order: o1 ) } + let!(:li2) { create(:line_item, order: o2 ) } + let!(:li3) { create(:line_item, order: o3 ) } before :each do visit '/admin/orders/bulk_management' @@ -402,23 +410,23 @@ feature %q{ end it "filters line items based on their attributes and the contents of the quick search input" do - expect(page).to have_selector "tr#li_#{li1.id}", visible: true - expect(page).to have_selector "tr#li_#{li2.id}", visible: true - expect(page).to have_selector "tr#li_#{li3.id}", visible: true + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_selector "tr#li_#{li2.id}" + expect(page).to have_selector "tr#li_#{li3.id}" fill_in "quick_search", :with => o1.email - expect(page).to have_selector "tr#li_#{li1.id}", visible: true - expect(page).to_not have_selector "tr#li_#{li2.id}", visible: true - expect(page).to_not have_selector "tr#li_#{li3.id}", visible: true + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_no_selector "tr#li_#{li2.id}", true + expect(page).to have_no_selector "tr#li_#{li3.id}" end end context "using date restriction controls" do - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: (Date.current - 8).strftime("%F %T") ) } - let!(:o2) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:o3) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: (Date.current + 2).strftime("%F %T") ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1, :quantity => 1 ) } - let!(:li2) { FactoryGirl.create(:line_item, order: o2, :quantity => 2 ) } - let!(:li3) { FactoryGirl.create(:line_item, order: o3, :quantity => 3 ) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: (Date.current - 8).strftime("%F %T") ) } + let!(:o2) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:o3) { create(:order_with_distributor, state: 'complete', completed_at: (Date.current + 2).strftime("%F %T") ) } + let!(:li1) { create(:line_item, order: o1, :quantity => 1 ) } + let!(:li2) { create(:line_item, order: o2, :quantity => 2 ) } + let!(:li3) { create(:line_item, order: o3, :quantity => 3 ) } before :each do visit '/admin/orders/bulk_management' @@ -434,21 +442,21 @@ feature %q{ end it "only loads line items whose orders meet the date restriction criteria" do - expect(page).to_not have_selector "tr#li_#{li1.id}", visible: true - expect(page).to have_selector "tr#li_#{li2.id}", visible: true - expect(page).to_not have_selector "tr#li_#{li3.id}", visible: true + expect(page).to have_no_selector "tr#li_#{li1.id}" + expect(page).to have_selector "tr#li_#{li2.id}" + expect(page).to have_no_selector "tr#li_#{li3.id}" end it "displays only line items whose orders meet the date restriction criteria, when changed" do fill_in "start_date_filter", :with => (Date.current - 9).strftime("%F") - expect(page).to have_selector "tr#li_#{li1.id}", visible: true - expect(page).to have_selector "tr#li_#{li2.id}", visible: true - expect(page).to_not have_selector "tr#li_#{li3.id}", visible: true + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_selector "tr#li_#{li2.id}" + expect(page).to have_no_selector "tr#li_#{li3.id}" fill_in "end_date_filter", :with => (Date.current + 3).strftime("%F") - expect(page).to have_selector "tr#li_#{li1.id}", visible: true - expect(page).to have_selector "tr#li_#{li2.id}", visible: true - expect(page).to have_selector "tr#li_#{li3.id}", visible: true + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_selector "tr#li_#{li2.id}" + expect(page).to have_selector "tr#li_#{li3.id}" end context "when the form is dirty" do @@ -481,10 +489,10 @@ feature %q{ end context "bulk action controls" do - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:o2) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1 ) } - let!(:li2) { FactoryGirl.create(:line_item, order: o2 ) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:o2) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:li1) { create(:line_item, order: o1 ) } + let!(:li2) { create(:line_item, order: o2 ) } before :each do visit '/admin/orders/bulk_management' @@ -512,15 +520,15 @@ feature %q{ context "performing actions" do it "deletes selected items" do - expect(page).to have_selector "tr#li_#{li1.id}", visible: true - expect(page).to have_selector "tr#li_#{li2.id}", visible: true + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_selector "tr#li_#{li2.id}" within("tr#li_#{li2.id} td.bulk") do check "bulk" end find("div#bulk-actions-dropdown").click find("div#bulk-actions-dropdown div.menu_item", :text => "Delete Selected" ).click - expect(page).to have_selector "tr#li_#{li1.id}", visible: true - expect(page).to_not have_selector "tr#li_#{li2.id}", visible: true + expect(page).to have_selector "tr#li_#{li1.id}" + expect(page).to have_no_selector "tr#li_#{li2.id}" end end @@ -540,18 +548,18 @@ feature %q{ find("div#bulk-actions-dropdown").click find("div#bulk-actions-dropdown div.menu_item", :text => "Delete Selected" ).click fill_in "quick_search", with: '' - expect(page).to_not have_selector "tr#li_#{li1.id}", visible: true - expect(page).to have_selector "tr#li_#{li2.id}", visible: true + expect(page).to have_no_selector "tr#li_#{li1.id}" + expect(page).to have_selector "tr#li_#{li2.id}" end end end context "using action buttons" do context "using edit buttons" do - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:o2) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1 ) } - let!(:li2) { FactoryGirl.create(:line_item, order: o2 ) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:o2) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:li1) { create(:line_item, order: o1 ) } + let!(:li2) { create(:line_item, order: o2 ) } before :each do visit '/admin/orders/bulk_management' @@ -564,7 +572,7 @@ feature %q{ page.driver.dismiss_modal :confirm, text: "Unsaved changes exist and will be lost if you continue." do within "tr#li_#{li1.id}" do fill_in "quantity", with: (li1.quantity + 1) - first("a.edit-order").click + find("a.edit-order").click end end @@ -575,17 +583,17 @@ feature %q{ # And try again within "tr#li_#{li1.id}" do - first("a.edit-order").click + find("a.edit-order").click end expect(URI.parse(current_url).path).to eq "/admin/orders/#{o1.number}/edit" end end context "using delete buttons" do - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:o2) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1 ) } - let!(:li2) { FactoryGirl.create(:line_item, order: o2 ) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:o2) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:li1) { create(:line_item, order: o1 ) } + let!(:li2) { create(:line_item, order: o2 ) } before :each do visit '/admin/orders/bulk_management' @@ -593,8 +601,8 @@ feature %q{ it "removes a line item when the relevant delete button is clicked" do expect(page).to have_selector "a.delete-line-item", :count => 2 - first("a.delete-line-item").click - expect(page).to_not have_selector "a.delete-line-item", :count => 2 + find("tr#li_#{li1.id} a.delete-line-item").click + expect(page).to have_no_selector "a.delete-line-item", :count => 2 expect(page).to have_selector "a.delete-line-item", :count => 1 visit '/admin/orders/bulk_management' expect(page).to have_selector "a.delete-line-item", :count => 1 @@ -603,15 +611,15 @@ feature %q{ end context "clicking the link on variant name" do - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:o2) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1 ) } - let!(:li2) { FactoryGirl.create(:line_item, order: o2 ) } - let!(:p3) { FactoryGirl.create(:product_with_option_types, group_buy: true, group_buy_unit_size: 5000, variant_unit: "weight", variants: [FactoryGirl.create(:variant, unit_value: 1000)] ) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:o2) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:li1) { create(:line_item, order: o1 ) } + let!(:li2) { create(:line_item, order: o2 ) } + let!(:p3) { create(:product_with_option_types, group_buy: true, group_buy_unit_size: 5000, variant_unit: "weight", variants: [create(:variant, unit_value: 1000)] ) } let!(:v3) { p3.variants.first } - let!(:o3) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } - let!(:li3) { FactoryGirl.create(:line_item, order: o3, variant: v3, quantity: 3, max_quantity: 6 ) } - let!(:li4) { FactoryGirl.create(:line_item, order: o2, variant: v3, quantity: 1, max_quantity: 3 ) } + let!(:o3) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now ) } + let!(:li3) { create(:line_item, order: o3, variant: v3, quantity: 3, max_quantity: 6 ) } + let!(:li4) { create(:line_item, order: o2, variant: v3, quantity: 1, max_quantity: 3 ) } before :each do visit '/admin/orders/bulk_management' @@ -643,8 +651,8 @@ feature %q{ end it "all line items of the same variant" do - expect(page).to_not have_selector "tr#li_#{li1.id}", :visible => true - expect(page).to_not have_selector "tr#li_#{li2.id}", :visible => true + expect(page).to have_no_selector "tr#li_#{li1.id}", :visible => true + expect(page).to have_no_selector "tr#li_#{li2.id}", :visible => true expect(page).to have_selector "tr#li_#{li3.id}", :visible => true expect(page).to have_selector "tr#li_#{li4.id}", :visible => true end @@ -655,7 +663,7 @@ feature %q{ end it "shows all products and clears group buy box" do - expect(page).to_not have_selector "div#group_buy_calculation", :visible => true + expect(page).to have_no_selector "div#group_buy_calculation", :visible => true expect(page).to have_selector "tr#li_#{li1.id}", :visible => true expect(page).to have_selector "tr#li_#{li2.id}", :visible => true expect(page).to have_selector "tr#li_#{li3.id}", :visible => true @@ -669,10 +677,10 @@ feature %q{ let(:s1) { create(:supplier_enterprise, name: 'First Supplier') } let(:d1) { create(:distributor_enterprise, name: 'First Distributor') } let(:d2) { create(:distributor_enterprise, name: 'Another Distributor') } - let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, distributor: d1 ) } - let!(:o2) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, distributor: d2 ) } - let!(:line_item_distributed) { FactoryGirl.create(:line_item, order: o1, product: create(:product, supplier: s1) ) } - let!(:line_item_not_distributed) { FactoryGirl.create(:line_item, order: o2, product: create(:product, supplier: s1) ) } + let!(:o1) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, distributor: d1 ) } + let!(:o2) { create(:order_with_distributor, state: 'complete', completed_at: Time.zone.now, distributor: d2 ) } + let!(:line_item_distributed) { create(:line_item, order: o1, product: create(:product, supplier: s1) ) } + let!(:line_item_not_distributed) { create(:line_item, order: o2, product: create(:product, supplier: s1) ) } before(:each) do @enterprise_user = create_enterprise_user @@ -693,7 +701,7 @@ feature %q{ visit '/admin/orders/bulk_management' expect(page).to have_selector "tr#li_#{line_item_distributed.id}", :visible => true - expect(page).to_not have_selector "tr#li_#{line_item_not_distributed.id}", :visible => true + expect(page).to have_no_selector "tr#li_#{line_item_not_distributed.id}", :visible => true end end end diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 9dabf93028..260caf207e 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -46,8 +46,9 @@ feature %q{ p2 = FactoryGirl.create(:product, available_on: Date.current-1) visit '/admin/products/bulk_edit' - first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "Available On").click + find("div#columns-dropdown", :text => "COLUMNS").click + find("div#columns-dropdown div.menu div.menu_item", text: "Available On").click + find("div#columns-dropdown", :text => "COLUMNS").click expect(page).to have_field "available_on", with: p1.available_on.strftime("%F %T") expect(page).to have_field "available_on", with: p2.available_on.strftime("%F %T") @@ -73,7 +74,8 @@ feature %q{ v2 = FactoryGirl.create(:variant, product: p1, is_master: false, on_hand: 0, on_demand: true) visit '/admin/products/bulk_edit' - first("a.view-variants").trigger('click') + expect(page).to have_selector "a.view-variants", count: 1 + find("a.view-variants").trigger('click') expect(page).to have_no_selector "span[name='on_hand']", text: "On demand", visible: true expect(page).to have_field "variant_on_hand", with: "4" @@ -109,7 +111,7 @@ feature %q{ v2 = FactoryGirl.create(:variant, display_name: "something2" ) visit '/admin/products/bulk_edit' - expect(page).to have_selector "a.view-variants" + expect(page).to have_selector "a.view-variants", count: 2 all("a.view-variants").each { |e| e.trigger('click') } expect(page).to have_field "product_name", with: v1.product.name @@ -124,6 +126,7 @@ feature %q{ v2 = FactoryGirl.create(:variant, product: p1, is_master: false, on_hand: 6) visit '/admin/products/bulk_edit' + expect(page).to have_selector "a.view-variants", count: 1 all("a.view-variants").each { |e| e.trigger('click') } expect(page).to have_selector "span[name='on_hand']", text: p1.variants.sum{ |v| v.on_hand }.to_s @@ -138,6 +141,7 @@ feature %q{ v2 = FactoryGirl.create(:variant, product: p1, is_master: false, price: 2.50) visit '/admin/products/bulk_edit' + expect(page).to have_selector "a.view-variants", count: 1 all("a.view-variants").each { |e| e.trigger('click') } expect(page).to have_field "price", with: "2.0", visible: false @@ -151,6 +155,7 @@ feature %q{ v2 = FactoryGirl.create(:variant, product: p1, is_master: false, price: 2.50, unit_value: 4800, unit_description: "(large bag)", display_as: "bin") visit '/admin/products/bulk_edit' + expect(page).to have_selector "a.view-variants", count: 1 all("a.view-variants").each { |e| e.trigger('click') } expect(page).to have_field "variant_unit_value_with_description", with: "1.2 (small bag)" @@ -217,7 +222,7 @@ feature %q{ fill_in "variant_price", with: "4.0" fill_in "variant_on_hand", with: "10" - first(:button, 'Save Changes').click + click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "Changes saved." updated_variant = Spree::Variant.where(deleted_at: nil).last @@ -243,11 +248,12 @@ feature %q{ visit '/admin/products/bulk_edit' - first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "Available On").click - first("div#columns-dropdown div.menu div.menu_item", text: "Category").click - first("div#columns-dropdown div.menu div.menu_item", text: "Inherits Properties?").click - first("div#columns-dropdown div.menu div.menu_item", text: "SKU").click + find("div#columns-dropdown", :text => "COLUMNS").click + find("div#columns-dropdown div.menu div.menu_item", text: "Available On").click + find("div#columns-dropdown div.menu div.menu_item", text: "Category").click + find("div#columns-dropdown div.menu div.menu_item", text: "Inherits Properties?").click + find("div#columns-dropdown div.menu div.menu_item", text: "SKU").click + find("div#columns-dropdown", :text => "COLUMNS").click within "tr#p_#{p.id}" do expect(page).to have_field "product_name", with: p.name @@ -267,7 +273,7 @@ feature %q{ fill_in "product_sku", with: "NEW SKU" end - first(:button, 'Save Changes').click + click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "Changes saved." p.reload @@ -293,7 +299,7 @@ feature %q{ select "Items", from: "variant_unit_with_scale" fill_in "variant_unit_name", with: "loaf" - first(:button, 'Save Changes').click + click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "Changes saved." p.reload @@ -313,12 +319,12 @@ feature %q{ login_to_admin_section visit '/admin/products/bulk_edit' - expect(page).to have_selector "a.view-variants" - first("a.view-variants").trigger('click') + expect(page).to have_selector "a.view-variants", count: 1 + find("a.view-variants").trigger('click') - first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "SKU").click - first("div#columns-dropdown", :text => "COLUMNS").click + find("div#columns-dropdown", :text => "COLUMNS").click + find("div#columns-dropdown div.menu div.menu_item", text: "SKU").click + find("div#columns-dropdown", :text => "COLUMNS").click expect(page).to have_field "variant_sku", with: "VARIANTSKU" expect(page).to have_field "variant_price", with: "3.0" @@ -334,7 +340,7 @@ feature %q{ expect(page).to have_selector "span[name='on_hand']", text: "10" - first(:button, 'Save Changes').click + click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "Changes saved." v.reload @@ -352,8 +358,8 @@ feature %q{ login_to_admin_section visit '/admin/products/bulk_edit' - expect(page).to have_selector "a.view-variants" - first("a.view-variants").trigger('click') + expect(page).to have_selector "a.view-variants", count: 1 + find("a.view-variants").trigger('click') expect(page).to have_field "variant_price", with: "3.0" @@ -361,7 +367,7 @@ feature %q{ fill_in "variant_price", with: "10.0" end - first(:button, 'Save Changes').click + click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "Changes saved." v.reload @@ -378,21 +384,21 @@ feature %q{ fill_in "product_name", with: "new name 1" - first(:button, 'Save Changes').click + click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "new name 1" fill_in "product_name", with: "new name 2" - first(:button, 'Save Changes').click + click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "new name 2" fill_in "product_name", with: "original name" - first(:button, 'Save Changes').click + click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "original name" @@ -404,11 +410,12 @@ feature %q{ visit '/admin/products/bulk_edit' - first("a.clone-product").click + expect(page).to have_selector "a.clone-product", count: 1 + find("a.clone-product").click fill_in "product_name", :with => "new product name" - first(:button, 'Save Changes').click + click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "new product name" @@ -421,7 +428,7 @@ feature %q{ visit '/admin/products/bulk_edit' - first(:button, 'Save Changes').click + click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "No changes to save." end end @@ -440,7 +447,7 @@ feature %q{ expect(page).to have_no_field "product_name", with: p2.name fill_in "product_name", :with => "new product1" - first(:button, 'Save Changes').click + click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "Changes saved." p1.reload expect(p1.name).to eq "new product1" @@ -464,7 +471,7 @@ feature %q{ expect(page).to have_selector "a.delete-product", :count => 2 within "tr#p_#{p1.id}" do - first("a.delete-product").click + find("a.delete-product").click end expect(page).to have_selector "a.delete-product", :count => 1 @@ -481,7 +488,7 @@ feature %q{ expect(page).to have_selector "a.delete-variant", :count => 3 within "tr#v_#{v3.id}" do - first("a.delete-variant").click + find("a.delete-variant").click end expect(page).to have_selector "a.delete-variant", :count => 2 @@ -509,7 +516,7 @@ feature %q{ expect(page).to have_selector "a.edit-product", :count => 2 within "tr#p_#{p1.id}" do - first("a.edit-product").click + find("a.edit-product").click end expect(URI.parse(current_url).path).to eq "/admin/products/#{p1.permalink}/edit" @@ -522,7 +529,7 @@ feature %q{ expect(page).to have_selector "a.edit-variant", :count => 2 within "tr#v_#{v1.id}" do - first("a.edit-variant").click + find("a.edit-variant").click end expect(URI.parse(current_url).path).to eq "/admin/products/#{v1.product.permalink}/variants/#{v1.id}/edit" @@ -541,7 +548,7 @@ feature %q{ expect(page).to have_selector "a.clone-product", :count => 3 within "tr#p_#{p1.id}" do - first("a.clone-product").click + find("a.clone-product").click end expect(page).to have_selector "a.clone-product", :count => 4 expect(page).to have_field "product_name", with: "COPY OF #{p1.name}" @@ -564,8 +571,9 @@ feature %q{ visit '/admin/products/bulk_edit' - first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "Available On").click + find("div#columns-dropdown", :text => "COLUMNS").click + find("div#columns-dropdown div.menu div.menu_item", text: "Available On").click + find("div#columns-dropdown", :text => "COLUMNS").click expect(page).to have_selector "th", :text => "NAME" expect(page).to have_selector "th", :text => "PRODUCER" @@ -573,7 +581,9 @@ feature %q{ expect(page).to have_selector "th", :text => "ON HAND" expect(page).to have_selector "th", :text => "AV. ON" - first("div#columns-dropdown div.menu div.menu_item", text: /^.{0,1}Producer$/).click + find("div#columns-dropdown", :text => "COLUMNS").click + find("div#columns-dropdown div.menu div.menu_item", text: /^.{0,1}Producer$/).click + find("div#columns-dropdown", :text => "COLUMNS").click expect(page).to have_no_selector "th", :text => "PRODUCER" expect(page).to have_selector "th", :text => "NAME" @@ -696,8 +706,9 @@ feature %q{ v = p.variants.first visit '/admin/products/bulk_edit' - first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "Available On").click + find("div#columns-dropdown", :text => "COLUMNS").click + find("div#columns-dropdown div.menu div.menu_item", text: "Available On").click + find("div#columns-dropdown", :text => "COLUMNS").click within "tr#p_#{p.id}" do expect(page).to have_field "product_name", with: p.name @@ -718,7 +729,7 @@ feature %q{ fill_in "variant_display_as", with: "Big Bag" end - first(:button, 'Save Changes').click + click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "Changes saved." p.reload diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index cd5ef0a07e..22402222fe 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -608,9 +608,16 @@ feature %q{ visit edit_admin_order_cycle_path(oc) - # I should not see exchanges for supplier_unmanaged or distributor_unmanaged - page.all('tr.supplier').count.should == 3 - page.all('tr.distributor').count.should == 3 + # I should be able to see but not edit exchanges for supplier_unmanaged or distributor_unmanaged + expect(page).to have_selector "tr.supplier-#{supplier_managed.id}" + expect(page).to have_selector "tr.supplier-#{supplier_permitted.id}" + expect(page).to have_selector "tr.supplier-#{supplier_unmanaged.id}" + expect(page.all('tr.supplier').count).to be 3 + + expect(page).to have_selector "tr.distributor-#{distributor_managed.id}" + expect(page).to have_selector "tr.distributor-#{distributor_permitted.id}" + expect(page).to have_selector "tr.distributor-#{distributor_unmanaged.id}" + expect(page.all('tr.distributor').count).to be 3 oc.reload oc.suppliers.should match_array [supplier_managed, supplier_permitted, supplier_unmanaged] @@ -689,8 +696,12 @@ feature %q{ # I should only see exchanges for supplier_managed AND # distributor_managed and distributor_permitted (who I have given permission to) AND # and distributor_unmanaged (who distributes my products) - page.all('tr.supplier').count.should == 1 - page.all('tr.distributor').count.should == 2 + expect(page).to have_selector "tr.supplier-#{supplier_managed.id}" + expect(page.all('tr.supplier').count).to be 1 + + expect(page).to have_selector "tr.distributor-#{distributor_managed.id}" + expect(page).to have_selector "tr.distributor-#{distributor_permitted.id}" + expect(page.all('tr.distributor').count).to be 2 # Open the products list for managed_supplier's incoming exchange within "tr.distributor-#{distributor_managed.id}" do @@ -738,8 +749,11 @@ feature %q{ visit edit_admin_order_cycle_path(oc) # I should see exchanges for my_distributor, and the incoming exchanges supplying the variants in it - page.all('tr.supplier').count.should == 1 - page.all('tr.distributor').count.should == 1 + expect(page).to have_selector "tr.supplier-#{supplier_managed.id}" + expect(page.all('tr.supplier').count).to be 1 + + expect(page).to have_selector "tr.distributor-#{my_distributor.id}" + expect(page.all('tr.distributor').count).to be 1 # Open the products list for managed_supplier's incoming exchange within "tr.supplier-#{supplier_managed.id}" do diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index 599c5530b0..511051d39f 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -58,12 +58,13 @@ describe 'OrderCycle controllers', -> describe 'Reporting when all resources are loaded', -> it 'returns true when Enterprise and EnterpriseFee are loaded', -> - Enterprise.loaded = EnterpriseFee.loaded = true + Enterprise.loaded = EnterpriseFee.loaded = OrderCycle.loaded = true expect(scope.loaded()).toBe(true) it 'returns false otherwise', -> Enterprise.loaded = true EnterpriseFee.loaded = false + OrderCycle.loaded = true expect(scope.loaded()).toBe(false) it "delegates suppliedVariants to Enterprise", -> From 9e0b97dc9c22d2262160f7dac12abd5c7f50eb94 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 5 May 2016 16:46:07 +1000 Subject: [PATCH 030/110] Upgrading textAngular --- app/assets/javascripts/admin/all.js | 3 +- app/assets/stylesheets/admin/all.css | 2 +- app/assets/stylesheets/shared/textAngular.css | 193 ++++++++++++++++++ .../stylesheets/shared/textAngular.min.css | 1 - .../javascripts/textAngular-rangy.min.js | 2 + .../javascripts/textAngular-sanitize.min.js | 7 +- vendor/assets/javascripts/textAngular.min.js | 5 +- 7 files changed, 207 insertions(+), 6 deletions(-) create mode 100644 app/assets/stylesheets/shared/textAngular.css delete mode 100644 app/assets/stylesheets/shared/textAngular.min.css create mode 100644 vendor/assets/javascripts/textAngular-rangy.min.js diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 73b51a20be..0923e5721c 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -43,8 +43,9 @@ //= require ./utils/utils //= require ./users/users //= require ./variant_overrides/variant_overrides -//= require textAngular.min.js +//= require textAngular-rangy.min.js //= require textAngular-sanitize.min.js +//= require textAngular.min.js //= require darkswarm/i18n.js //= require darkswarm/i18n.translate.js diff --git a/app/assets/stylesheets/admin/all.css b/app/assets/stylesheets/admin/all.css index e0d668b95b..e913e9e710 100644 --- a/app/assets/stylesheets/admin/all.css +++ b/app/assets/stylesheets/admin/all.css @@ -9,7 +9,7 @@ *= require admin/spree_promo *= require shared/jquery-ui-timepicker-addon - *= require shared/textAngular.min + *= require shared/textAngular *= require shared/ng-tags-input.min *= require_self diff --git a/app/assets/stylesheets/shared/textAngular.css b/app/assets/stylesheets/shared/textAngular.css new file mode 100644 index 0000000000..a2f76234dc --- /dev/null +++ b/app/assets/stylesheets/shared/textAngular.css @@ -0,0 +1,193 @@ +.ta-hidden-input { + width: 1px; + height: 1px; + border: none; + margin: 0; + padding: 0; + position: absolute; + top: -10000px; + left: -10000px; + opacity: 0; + overflow: hidden; +} + +/* add generic styling for the editor */ +.ta-root.focussed > .ta-scroll-window.form-control { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); +} + +.ta-editor.ta-html, .ta-scroll-window.form-control { + min-height: 300px; + height: auto; + overflow: auto; + font-family: inherit; + font-size: 100%; +} + +.ta-scroll-window.form-control { + position: relative; + padding: 0; +} + +.ta-scroll-window > .ta-bind { + height: auto; + min-height: 300px; + padding: 6px 12px; +} + +.ta-editor:focus { + user-select: text; +} + +/* add the styling for the awesomness of the resizer */ +.ta-resizer-handle-overlay { + z-index: 100; + position: absolute; + display: none; +} + +.ta-resizer-handle-overlay > .ta-resizer-handle-info { + position: absolute; + bottom: 16px; + right: 16px; + border: 1px solid black; + background-color: #FFF; + padding: 0 4px; + opacity: 0.7; +} + +.ta-resizer-handle-overlay > .ta-resizer-handle-background { + position: absolute; + bottom: 5px; + right: 5px; + left: 5px; + top: 5px; + border: 1px solid black; + background-color: rgba(0, 0, 0, 0.2); +} + +.ta-resizer-handle-overlay > .ta-resizer-handle-corner { + width: 10px; + height: 10px; + position: absolute; +} + +.ta-resizer-handle-overlay > .ta-resizer-handle-corner-tl{ + top: 0; + left: 0; + border-left: 1px solid black; + border-top: 1px solid black; +} + +.ta-resizer-handle-overlay > .ta-resizer-handle-corner-tr{ + top: 0; + right: 0; + border-right: 1px solid black; + border-top: 1px solid black; +} + +.ta-resizer-handle-overlay > .ta-resizer-handle-corner-bl{ + bottom: 0; + left: 0; + border-left: 1px solid black; + border-bottom: 1px solid black; +} + +.ta-resizer-handle-overlay > .ta-resizer-handle-corner-br{ + bottom: 0; + right: 0; + border: 1px solid black; + cursor: se-resize; + background-color: white; +} + +/* copy the popover code from bootstrap so this will work even without it */ +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: none; + max-width: 276px; + padding: 1px; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: left; + white-space: normal; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); + box-shadow: 0 5px 10px rgba(0, 0, 0, .2); +} +.popover.top { + margin-top: -10px; +} +.popover.bottom { + margin-top: 10px; +} +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; +} +.popover-content { + padding: 9px 14px; +} +.popover > .arrow, +.popover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.popover > .arrow { + border-width: 11px; +} +.popover > .arrow:after { + content: ""; + border-width: 10px; +} +.popover.top > .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, .25); + border-bottom-width: 0; +} +.popover.top > .arrow:after { + bottom: 1px; + margin-left: -10px; + content: " "; + border-top-color: #fff; + border-bottom-width: 0; +} +.popover.bottom > .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, .25); +} +.popover.bottom > .arrow:after { + top: 1px; + margin-left: -10px; + content: " "; + border-top-width: 0; + border-bottom-color: #fff; +} diff --git a/app/assets/stylesheets/shared/textAngular.min.css b/app/assets/stylesheets/shared/textAngular.min.css deleted file mode 100644 index 6f132953bb..0000000000 --- a/app/assets/stylesheets/shared/textAngular.min.css +++ /dev/null @@ -1 +0,0 @@ -.ta-scroll-window.form-control{height:auto;min-height:300px;overflow:auto;font-family:inherit;font-size:100%;position:relative;padding:0}.ta-root.focussed .ta-scroll-window.form-control{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.ta-editor.ta-html{min-height:300px;height:auto;overflow:auto;font-family:inherit;font-size:100%}.ta-scroll-window>.ta-bind{height:auto;min-height:300px;padding:6px 12px}.ta-root .ta-resizer-handle-overlay{z-index:100;position:absolute;display:none}.ta-root .ta-resizer-handle-overlay>.ta-resizer-handle-info{position:absolute;bottom:16px;right:16px;border:1px solid #000;background-color:#FFF;padding:0 4px;opacity:.7}.ta-root .ta-resizer-handle-overlay>.ta-resizer-handle-background{position:absolute;bottom:5px;right:5px;left:5px;top:5px;border:1px solid #000;background-color:rgba(0,0,0,.2)}.ta-root .ta-resizer-handle-overlay>.ta-resizer-handle-corner{width:10px;height:10px;position:absolute}.ta-root .ta-resizer-handle-overlay>.ta-resizer-handle-corner-tl{top:0;left:0;border-left:1px solid #000;border-top:1px solid #000}.ta-root .ta-resizer-handle-overlay>.ta-resizer-handle-corner-tr{top:0;right:0;border-right:1px solid #000;border-top:1px solid #000}.ta-root .ta-resizer-handle-overlay>.ta-resizer-handle-corner-bl{bottom:0;left:0;border-left:1px solid #000;border-bottom:1px solid #000}.ta-root .ta-resizer-handle-overlay>.ta-resizer-handle-corner-br{bottom:0;right:0;border:1px solid #000;cursor:se-resize;background-color:#fff} \ No newline at end of file diff --git a/vendor/assets/javascripts/textAngular-rangy.min.js b/vendor/assets/javascripts/textAngular-rangy.min.js new file mode 100644 index 0000000000..d2de46f10e --- /dev/null +++ b/vendor/assets/javascripts/textAngular-rangy.min.js @@ -0,0 +1,2 @@ +!function(a,b){"function"==typeof define&&define.amd?define(a):"undefined"!=typeof module&&"object"==typeof exports?module.exports=a():b.rangy=a()}(function(){function a(a,b){var c=typeof a[b];return c==u||!(c!=t||!a[b])||"unknown"==c}function b(a,b){return!(typeof a[b]!=t||!a[b])}function c(a,b){return typeof a[b]!=v}function d(a){return function(b,c){for(var d=c.length;d--;)if(!a(b,c[d]))return!1;return!0}}function e(a){return a&&A(a,z)&&C(a,y)}function f(a){return b(a,"body")?a.body:a.getElementsByTagName("body")[0]}function g(b){typeof console!=v&&a(console,"log")&&console.log(b)}function h(a,b){F&&b?alert(a):g(a)}function i(a){H.initialized=!0,H.supported=!1,h("Rangy is not supported in this environment. Reason: "+a,H.config.alertOnFail)}function j(a){h("Rangy warning: "+a,H.config.alertOnWarn)}function k(a){return a.message||a.description||String(a)}function l(){if(F&&!H.initialized){var b,c=!1,d=!1;a(document,"createRange")&&(b=document.createRange(),A(b,x)&&C(b,w)&&(c=!0));var h=f(document);if(!h||"body"!=h.nodeName.toLowerCase())return void i("No body element found");if(h&&a(h,"createTextRange")&&(b=h.createTextRange(),e(b)&&(d=!0)),!c&&!d)return void i("Neither Range nor TextRange are available");H.initialized=!0,H.features={implementsDomRange:c,implementsTextRange:d};var j,l;for(var m in E)(j=E[m])instanceof p&&j.init(j,H);for(var n=0,o=K.length;o>n;++n)try{K[n](H)}catch(q){l="Rangy init listener threw an exception. Continuing. Detail: "+k(q),g(l)}}}function m(a,b,c){c&&(a+=" in module "+c.name),H.warn("DEPRECATED: "+a+" is deprecated. Please use "+b+" instead.")}function n(a,b,c,d){a[b]=function(){return m(b,c,d),a[c].apply(a,G.toArray(arguments))}}function o(a){a=a||window,l();for(var b=0,c=L.length;c>b;++b)L[b](a)}function p(a,b,c){this.name=a,this.dependencies=b,this.initialized=!1,this.supported=!1,this.initializer=c}function q(a,b,c){var d=new p(a,b,function(b){if(!b.initialized){b.initialized=!0;try{c(H,b),b.supported=!0}catch(d){var e="Module '"+a+"' failed to load: "+k(d);g(e),d.stack&&g(d.stack)}}});return E[a]=d,d}function r(){}function s(){}var t="object",u="function",v="undefined",w=["startContainer","startOffset","endContainer","endOffset","collapsed","commonAncestorContainer"],x=["setStart","setStartBefore","setStartAfter","setEnd","setEndBefore","setEndAfter","collapse","selectNode","selectNodeContents","compareBoundaryPoints","deleteContents","extractContents","cloneContents","insertNode","surroundContents","cloneRange","toString","detach"],y=["boundingHeight","boundingLeft","boundingTop","boundingWidth","htmlText","text"],z=["collapse","compareEndPoints","duplicate","moveToElementText","parentElement","select","setEndPoint","getBoundingClientRect"],A=d(a),B=d(b),C=d(c),D=[].forEach?function(a,b){a.forEach(b)}:function(a,b){for(var c=0,d=a.length;d>c;++c)b(a[c],c)},E={},F=typeof window!=v&&typeof document!=v,G={isHostMethod:a,isHostObject:b,isHostProperty:c,areHostMethods:A,areHostObjects:B,areHostProperties:C,isTextRange:e,getBody:f,forEach:D},H={version:"1.3.0",initialized:!1,isBrowser:F,supported:!0,util:G,features:{},modules:E,config:{alertOnFail:!1,alertOnWarn:!1,preferTextRange:!1,autoInitialize:typeof rangyAutoInitialize==v?!0:rangyAutoInitialize}};H.fail=i,H.warn=j;var I;({}).hasOwnProperty?(G.extend=I=function(a,b,c){var d,e;for(var f in b)b.hasOwnProperty(f)&&(d=a[f],e=b[f],c&&null!==d&&"object"==typeof d&&null!==e&&"object"==typeof e&&I(d,e,!0),a[f]=e);return b.hasOwnProperty("toString")&&(a.toString=b.toString),a},G.createOptions=function(a,b){var c={};return I(c,b),a&&I(c,a),c}):i("hasOwnProperty not supported"),F||i("Rangy can only run in a browser"),function(){var a;if(F){var b=document.createElement("div");b.appendChild(document.createElement("span"));var c=[].slice;try{1==c.call(b.childNodes,0)[0].nodeType&&(a=function(a){return c.call(a,0)})}catch(d){}}a||(a=function(a){for(var b=[],c=0,d=a.length;d>c;++c)b[c]=a[c];return b}),G.toArray=a}();var J;F&&(a(document,"addEventListener")?J=function(a,b,c){a.addEventListener(b,c,!1)}:a(document,"attachEvent")?J=function(a,b,c){a.attachEvent("on"+b,c)}:i("Document does not have required addEventListener or attachEvent method"),G.addListener=J);var K=[];G.deprecationNotice=m,G.createAliasForDeprecatedMethod=n,H.init=l,H.addInitListener=function(a){H.initialized?a(H):K.push(a)};var L=[];H.addShimListener=function(a){L.push(a)},F&&(H.shim=H.createMissingNativeApi=o,n(H,"createMissingNativeApi","shim")),p.prototype={init:function(){for(var a,b,c=this.dependencies||[],d=0,e=c.length;e>d;++d){if(b=c[d],a=E[b],!(a&&a instanceof p))throw new Error("required module '"+b+"' not found");if(a.init(),!a.supported)throw new Error("required module '"+b+"' not supported")}this.initializer(this)},fail:function(a){throw this.initialized=!0,this.supported=!1,new Error(a)},warn:function(a){H.warn("Module "+this.name+": "+a)},deprecationNotice:function(a,b){H.warn("DEPRECATED: "+a+" in module "+this.name+" is deprecated. Please use "+b+" instead")},createError:function(a){return new Error("Error in Rangy "+this.name+" module: "+a)}},H.createModule=function(a){var b,c;2==arguments.length?(b=arguments[1],c=[]):(b=arguments[2],c=arguments[1]);var d=q(a,c,b);H.initialized&&H.supported&&d.init()},H.createCoreModule=function(a,b,c){q(a,b,c)},H.RangePrototype=r,H.rangePrototype=new r,H.selectionPrototype=new s,H.createCoreModule("DomUtil",[],function(a,b){function c(a){var b;return typeof a.namespaceURI==F||null===(b=a.namespaceURI)||"http://www.w3.org/1999/xhtml"==b}function d(a){var b=a.parentNode;return 1==b.nodeType?b:null}function e(a){for(var b=0;a=a.previousSibling;)++b;return b}function f(a){switch(a.nodeType){case 7:case 10:return 0;case 3:case 8:return a.length;default:return a.childNodes.length}}function g(a,b){var c,d=[];for(c=a;c;c=c.parentNode)d.push(c);for(c=b;c;c=c.parentNode)if(K(d,c))return c;return null}function h(a,b,c){for(var d=c?b:b.parentNode;d;){if(d===a)return!0;d=d.parentNode}return!1}function i(a,b){return h(a,b,!0)}function j(a,b,c){for(var d,e=c?a:a.parentNode;e;){if(d=e.parentNode,d===b)return e;e=d}return null}function k(a){var b=a.nodeType;return 3==b||4==b||8==b}function l(a){if(!a)return!1;var b=a.nodeType;return 3==b||8==b}function m(a,b){var c=b.nextSibling,d=b.parentNode;return c?d.insertBefore(a,c):d.appendChild(a),a}function n(a,b,c){var d=a.cloneNode(!1);if(d.deleteData(0,b),a.deleteData(b,a.length-b),m(d,a),c)for(var f,g=0;f=c[g++];)f.node==a&&f.offset>b?(f.node=d,f.offset-=b):f.node==a.parentNode&&f.offset>e(a)&&++f.offset;return d}function o(a){if(9==a.nodeType)return a;if(typeof a.ownerDocument!=F)return a.ownerDocument;if(typeof a.document!=F)return a.document;if(a.parentNode)return o(a.parentNode);throw b.createError("getDocument: no document found for node")}function p(a){var c=o(a);if(typeof c.defaultView!=F)return c.defaultView;if(typeof c.parentWindow!=F)return c.parentWindow;throw b.createError("Cannot get a window object for node")}function q(a){if(typeof a.contentDocument!=F)return a.contentDocument;if(typeof a.contentWindow!=F)return a.contentWindow.document;throw b.createError("getIframeDocument: No Document object found for iframe element")}function r(a){if(typeof a.contentWindow!=F)return a.contentWindow;if(typeof a.contentDocument!=F)return a.contentDocument.defaultView;throw b.createError("getIframeWindow: No Window object found for iframe element")}function s(a){return a&&G.isHostMethod(a,"setTimeout")&&G.isHostObject(a,"document")}function t(a,b,c){var d;if(a?G.isHostProperty(a,"nodeType")?d=1==a.nodeType&&"iframe"==a.tagName.toLowerCase()?q(a):o(a):s(a)&&(d=a.document):d=document,!d)throw b.createError(c+"(): Parameter must be a Window object or DOM node");return d}function u(a){for(var b;b=a.parentNode;)a=b;return a}function v(a,c,d,f){var h,i,k,l,m;if(a==d)return c===f?0:f>c?-1:1;if(h=j(d,a,!0))return c<=e(h)?-1:1;if(h=j(a,d,!0))return e(h)[index:"+e(a)+",length:"+a.childNodes.length+"]["+(a.innerHTML||"[innerHTML not supported]").slice(0,25)+"]"}return a.nodeName}function y(a){for(var b,c=o(a).createDocumentFragment();b=a.firstChild;)c.appendChild(b);return c}function z(a,b,c){var d=H(a),e=a.createElement("div");e.contentEditable=""+!!c,b&&(e.innerHTML=b);var f=d.firstChild;return f?d.insertBefore(e,f):d.appendChild(e),e}function A(a){return a.parentNode.removeChild(a)}function B(a){this.root=a,this._next=a}function C(a){return new B(a)}function D(a,b){this.node=a,this.offset=b}function E(a){this.code=this[a],this.codeName=a,this.message="DOMException: "+this.codeName}var F="undefined",G=a.util,H=G.getBody;G.areHostMethods(document,["createDocumentFragment","createElement","createTextNode"])||b.fail("document missing a Node creation method"),G.isHostMethod(document,"getElementsByTagName")||b.fail("document missing getElementsByTagName method");var I=document.createElement("div");G.areHostMethods(I,["insertBefore","appendChild","cloneNode"]||!G.areHostObjects(I,["previousSibling","nextSibling","childNodes","parentNode"]))||b.fail("Incomplete Element implementation"),G.isHostProperty(I,"innerHTML")||b.fail("Element is missing innerHTML property");var J=document.createTextNode("test");G.areHostMethods(J,["splitText","deleteData","insertData","appendData","cloneNode"]||!G.areHostObjects(I,["previousSibling","nextSibling","childNodes","parentNode"])||!G.areHostProperties(J,["data"]))||b.fail("Incomplete Text Node implementation");var K=function(a,b){for(var c=a.length;c--;)if(a[c]===b)return!0;return!1},L=!1;!function(){var b=document.createElement("b");b.innerHTML="1";var c=b.firstChild;b.innerHTML="
    ",L=w(c),a.features.crashyTextNodes=L}();var M;typeof window.getComputedStyle!=F?M=function(a,b){return p(a).getComputedStyle(a,null)[b]}:typeof document.documentElement.currentStyle!=F?M=function(a,b){return a.currentStyle?a.currentStyle[b]:""}:b.fail("No means of obtaining computed style properties found"),B.prototype={_current:null,hasNext:function(){return!!this._next},next:function(){var a,b,c=this._current=this._next;if(this._current)if(a=c.firstChild)this._next=a;else{for(b=null;c!==this.root&&!(b=c.nextSibling);)c=c.parentNode;this._next=b}return this._current},detach:function(){this._current=this._next=this.root=null}},D.prototype={equals:function(a){return!!a&&this.node===a.node&&this.offset==a.offset},inspect:function(){return"[DomPosition("+x(this.node)+":"+this.offset+")]"},toString:function(){return this.inspect()}},E.prototype={INDEX_SIZE_ERR:1,HIERARCHY_REQUEST_ERR:3,WRONG_DOCUMENT_ERR:4,NO_MODIFICATION_ALLOWED_ERR:7,NOT_FOUND_ERR:8,NOT_SUPPORTED_ERR:9,INVALID_STATE_ERR:11,INVALID_NODE_TYPE_ERR:24},E.prototype.toString=function(){return this.message},a.dom={arrayContains:K,isHtmlNamespace:c,parentElement:d,getNodeIndex:e,getNodeLength:f,getCommonAncestor:g,isAncestorOf:h,isOrIsAncestorOf:i,getClosestAncestorIn:j,isCharacterDataNode:k,isTextOrCommentNode:l,insertAfter:m,splitDataNode:n,getDocument:o,getWindow:p,getIframeWindow:r,getIframeDocument:q,getBody:H,isWindow:s,getContentDocument:t,getRootContainer:u,comparePoints:v,isBrokenNode:w,inspectNode:x,getComputedStyleProperty:M,createTestElement:z,removeNode:A,fragmentFromNodeChildren:y,createIterator:C,DomPosition:D},a.DOMException=E}),H.createCoreModule("DomRange",["DomUtil"],function(a,b){function c(a,b){return 3!=a.nodeType&&(P(a,b.startContainer)||P(a,b.endContainer))}function d(a){return a.document||Q(a.startContainer)}function e(a){return W(a.startContainer)}function f(a){return new L(a.parentNode,O(a))}function g(a){return new L(a.parentNode,O(a)+1)}function h(a,b,c){var d=11==a.nodeType?a.firstChild:a;return N(b)?c==b.length?J.insertAfter(a,b):b.parentNode.insertBefore(a,0==c?b:S(b,c)):c>=b.childNodes.length?b.appendChild(a):b.insertBefore(a,b.childNodes[c]),d}function i(a,b,c){if(z(a),z(b),d(b)!=d(a))throw new M("WRONG_DOCUMENT_ERR");var e=R(a.startContainer,a.startOffset,b.endContainer,b.endOffset),f=R(a.endContainer,a.endOffset,b.startContainer,b.startOffset);return c?0>=e&&f>=0:0>e&&f>0}function j(a){for(var b,c,e,f=d(a.range).createDocumentFragment();c=a.next();){if(b=a.isPartiallySelectedSubtree(),c=c.cloneNode(!b),b&&(e=a.getSubtreeIterator(),c.appendChild(j(e)),e.detach()),10==c.nodeType)throw new M("HIERARCHY_REQUEST_ERR");f.appendChild(c)}return f}function k(a,b,c){var d,e;c=c||{stop:!1};for(var f,g;f=a.next();)if(a.isPartiallySelectedSubtree()){if(b(f)===!1)return void(c.stop=!0);if(g=a.getSubtreeIterator(),k(g,b,c),g.detach(),c.stop)return}else for(d=J.createIterator(f);e=d.next();)if(b(e)===!1)return void(c.stop=!0)}function l(a){for(var b;a.next();)a.isPartiallySelectedSubtree()?(b=a.getSubtreeIterator(),l(b),b.detach()):a.remove()}function m(a){for(var b,c,e=d(a.range).createDocumentFragment();b=a.next();){if(a.isPartiallySelectedSubtree()?(b=b.cloneNode(!1),c=a.getSubtreeIterator(),b.appendChild(m(c)),c.detach()):a.remove(),10==b.nodeType)throw new M("HIERARCHY_REQUEST_ERR");e.appendChild(b)}return e}function n(a,b,c){var d,e=!(!b||!b.length),f=!!c;e&&(d=new RegExp("^("+b.join("|")+")$"));var g=[];return k(new p(a,!1),function(b){if((!e||d.test(b.nodeType))&&(!f||c(b))){var h=a.startContainer;if(b!=h||!N(h)||a.startOffset!=h.length){var i=a.endContainer;b==i&&N(i)&&0==a.endOffset||g.push(b)}}}),g}function o(a){var b="undefined"==typeof a.getName?"Range":a.getName();return"["+b+"("+J.inspectNode(a.startContainer)+":"+a.startOffset+", "+J.inspectNode(a.endContainer)+":"+a.endOffset+")]"}function p(a,b){if(this.range=a,this.clonePartiallySelectedTextNodes=b,!a.collapsed){this.sc=a.startContainer,this.so=a.startOffset,this.ec=a.endContainer,this.eo=a.endOffset;var c=a.commonAncestorContainer;this.sc===this.ec&&N(this.sc)?(this.isSingleCharacterDataNode=!0,this._first=this._last=this._next=this.sc):(this._first=this._next=this.sc!==c||N(this.sc)?T(this.sc,c,!0):this.sc.childNodes[this.so],this._last=this.ec!==c||N(this.ec)?T(this.ec,c,!0):this.ec.childNodes[this.eo-1])}}function q(a){return function(b,c){for(var d,e=c?b:b.parentNode;e;){if(d=e.nodeType,V(a,d))return e;e=e.parentNode}return null}}function r(a,b){if(ea(a,b))throw new M("INVALID_NODE_TYPE_ERR")}function s(a,b){if(!V(b,a.nodeType))throw new M("INVALID_NODE_TYPE_ERR")}function t(a,b){if(0>b||b>(N(a)?a.length:a.childNodes.length))throw new M("INDEX_SIZE_ERR")}function u(a,b){if(ca(a,!0)!==ca(b,!0))throw new M("WRONG_DOCUMENT_ERR")}function v(a){if(da(a,!0))throw new M("NO_MODIFICATION_ALLOWED_ERR")}function w(a,b){if(!a)throw new M(b)}function x(a,b){return b<=(N(a)?a.length:a.childNodes.length)}function y(a){return!!a.startContainer&&!!a.endContainer&&!(X&&(J.isBrokenNode(a.startContainer)||J.isBrokenNode(a.endContainer)))&&W(a.startContainer)==W(a.endContainer)&&x(a.startContainer,a.startOffset)&&x(a.endContainer,a.endOffset)}function z(a){if(!y(a))throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: ("+a.inspect()+")")}function A(a,b){z(a);var c=a.startContainer,d=a.startOffset,e=a.endContainer,f=a.endOffset,g=c===e;N(e)&&f>0&&f0&&d=O(c)&&f++,d=0),a.setStartAndEnd(c,d,e,f)}function B(a){z(a);var b=a.commonAncestorContainer.parentNode.cloneNode(!1);return b.appendChild(a.cloneContents()),b.innerHTML}function C(a){a.START_TO_START=ka,a.START_TO_END=la,a.END_TO_END=ma,a.END_TO_START=na,a.NODE_BEFORE=oa,a.NODE_AFTER=pa,a.NODE_BEFORE_AND_AFTER=qa,a.NODE_INSIDE=ra}function D(a){C(a),C(a.prototype)}function E(a,b){return function(){z(this);var c,d,e=this.startContainer,f=this.startOffset,h=this.commonAncestorContainer,i=new p(this,!0);e!==h&&(c=T(e,h,!0),d=g(c),e=d.node,f=d.offset),k(i,v),i.reset();var j=a(i);return i.detach(),b(this,e,f,e,f),j}}function F(b,d){function e(a,b){return function(c){s(c,Z),s(W(c),$);var d=(a?f:g)(c);(b?h:i)(this,d.node,d.offset)}}function h(a,b,c){var e=a.endContainer,f=a.endOffset;(b!==a.startContainer||c!==a.startOffset)&&((W(b)!=W(e)||1==R(b,c,e,f))&&(e=b,f=c),d(a,b,c,e,f))}function i(a,b,c){var e=a.startContainer,f=a.startOffset;(b!==a.endContainer||c!==a.endOffset)&&((W(b)!=W(e)||-1==R(b,c,e,f))&&(e=b,f=c),d(a,e,f,b,c))}var j=function(){};j.prototype=a.rangePrototype,b.prototype=new j,K.extend(b.prototype,{setStart:function(a,b){r(a,!0),t(a,b),h(this,a,b)},setEnd:function(a,b){r(a,!0),t(a,b),i(this,a,b)},setStartAndEnd:function(){var a=arguments,b=a[0],c=a[1],e=b,f=c;switch(a.length){case 3:f=a[2];break;case 4:e=a[2],f=a[3]}d(this,b,c,e,f)},setBoundary:function(a,b,c){this["set"+(c?"Start":"End")](a,b)},setStartBefore:e(!0,!0),setStartAfter:e(!1,!0),setEndBefore:e(!0,!1),setEndAfter:e(!1,!1),collapse:function(a){z(this),a?d(this,this.startContainer,this.startOffset,this.startContainer,this.startOffset):d(this,this.endContainer,this.endOffset,this.endContainer,this.endOffset)},selectNodeContents:function(a){r(a,!0),d(this,a,0,a,U(a))},selectNode:function(a){r(a,!1),s(a,Z);var b=f(a),c=g(a);d(this,b.node,b.offset,c.node,c.offset)},extractContents:E(m,d),deleteContents:E(l,d),canSurroundContents:function(){z(this),v(this.startContainer),v(this.endContainer);var a=new p(this,!0),b=a._first&&c(a._first,this)||a._last&&c(a._last,this);return a.detach(),!b},splitBoundaries:function(){A(this)},splitBoundariesPreservingPositions:function(a){A(this,a)},normalizeBoundaries:function(){z(this);var a,b=this.startContainer,c=this.startOffset,e=this.endContainer,f=this.endOffset,g=function(a){var b=a.nextSibling;b&&b.nodeType==a.nodeType&&(e=a,f=a.length,a.appendData(b.data),Y(b))},h=function(a){var d=a.previousSibling;if(d&&d.nodeType==a.nodeType){b=a;var g=a.length;if(c=d.length,a.insertData(0,d.data),Y(d),b==e)f+=c,e=b;else if(e==a.parentNode){var h=O(a);f==h?(e=a,f=g):f>h&&f--}}},i=!0;if(N(e))f==e.length?g(e):0==f&&(a=e.previousSibling,a&&a.nodeType==e.nodeType&&(f=a.length,b==e&&(i=!1),a.appendData(e.data),Y(e),e=a));else{if(f>0){var j=e.childNodes[f-1];j&&N(j)&&g(j)}i=!this.collapsed}if(i){if(N(b))0==c?h(b):c==b.length&&(a=b.nextSibling,a&&a.nodeType==b.nodeType&&(e==a&&(e=b,f+=b.length),b.appendData(a.data),Y(a)));else if(cx
    ",ga=3==fa.firstChild.nodeType}catch(ha){}a.features.htmlParsingConforms=ga;var ia=ga?function(a){var b=this.startContainer,c=Q(b);if(!b)throw new M("INVALID_STATE_ERR");var d=null;return 1==b.nodeType?d=b:N(b)&&(d=J.parentElement(b)),d=null===d||"HTML"==d.nodeName&&J.isHtmlNamespace(Q(d).documentElement)&&J.isHtmlNamespace(d)?c.createElement("body"):d.cloneNode(!1),d.innerHTML=a,J.fragmentFromNodeChildren(d)}:function(a){var b=d(this),c=b.createElement("body");return c.innerHTML=a,J.fragmentFromNodeChildren(c)},ja=["startContainer","startOffset","endContainer","endOffset","collapsed","commonAncestorContainer"],ka=0,la=1,ma=2,na=3,oa=0,pa=1,qa=2,ra=3;K.extend(a.rangePrototype,{compareBoundaryPoints:function(a,b){z(this),u(this.startContainer,b.startContainer);var c,d,e,f,g=a==na||a==ka?"start":"end",h=a==la||a==ka?"start":"end";return c=this[g+"Container"],d=this[g+"Offset"],e=b[h+"Container"],f=b[h+"Offset"],R(c,d,e,f)},insertNode:function(a){if(z(this),s(a,aa),v(this.startContainer),P(a,this.startContainer))throw new M("HIERARCHY_REQUEST_ERR");var b=h(a,this.startContainer,this.startOffset);this.setStartBefore(b)},cloneContents:function(){z(this);var a,b;if(this.collapsed)return d(this).createDocumentFragment();if(this.startContainer===this.endContainer&&N(this.startContainer))return a=this.startContainer.cloneNode(!0),a.data=a.data.slice(this.startOffset,this.endOffset),b=d(this).createDocumentFragment(),b.appendChild(a),b;var c=new p(this,!0);return a=j(c),c.detach(),a},canSurroundContents:function(){z(this),v(this.startContainer),v(this.endContainer);var a=new p(this,!0),b=a._first&&c(a._first,this)||a._last&&c(a._last,this);return a.detach(),!b},surroundContents:function(a){if(s(a,ba),!this.canSurroundContents())throw new M("INVALID_STATE_ERR");var b=this.extractContents();if(a.hasChildNodes())for(;a.lastChild;)a.removeChild(a.lastChild);h(a,this.startContainer,this.startOffset),a.appendChild(b),this.selectNode(a)},cloneRange:function(){z(this);for(var a,b=new I(d(this)),c=ja.length;c--;)a=ja[c],b[a]=this[a];return b},toString:function(){z(this);var a=this.startContainer;if(a===this.endContainer&&N(a))return 3==a.nodeType||4==a.nodeType?a.data.slice(this.startOffset,this.endOffset):"";var b=[],c=new p(this,!0);return k(c,function(a){(3==a.nodeType||4==a.nodeType)&&b.push(a.data)}),c.detach(),b.join("")},compareNode:function(a){z(this);var b=a.parentNode,c=O(a);if(!b)throw new M("NOT_FOUND_ERR");var d=this.comparePoint(b,c),e=this.comparePoint(b,c+1);return 0>d?e>0?qa:oa:e>0?pa:ra},comparePoint:function(a,b){return z(this),w(a,"HIERARCHY_REQUEST_ERR"),u(a,this.startContainer),R(a,b,this.startContainer,this.startOffset)<0?-1:R(a,b,this.endContainer,this.endOffset)>0?1:0},createContextualFragment:ia,toHtml:function(){return B(this)},intersectsNode:function(a,b){if(z(this),W(a)!=e(this))return!1;var c=a.parentNode,d=O(a);if(!c)return!0;var f=R(c,d,this.endContainer,this.endOffset),g=R(c,d+1,this.startContainer,this.startOffset);return b?0>=f&&g>=0:0>f&&g>0},isPointInRange:function(a,b){return z(this),w(a,"HIERARCHY_REQUEST_ERR"),u(a,this.startContainer),R(a,b,this.startContainer,this.startOffset)>=0&&R(a,b,this.endContainer,this.endOffset)<=0},intersectsRange:function(a){return i(this,a,!1)},intersectsOrTouchesRange:function(a){return i(this,a,!0)},intersection:function(a){if(this.intersectsRange(a)){var b=R(this.startContainer,this.startOffset,a.startContainer,a.startOffset),c=R(this.endContainer,this.endOffset,a.endContainer,a.endOffset),d=this.cloneRange();return-1==b&&d.setStart(a.startContainer,a.startOffset),1==c&&d.setEnd(a.endContainer,a.endOffset),d}return null},union:function(a){if(this.intersectsOrTouchesRange(a)){var b=this.cloneRange();return-1==R(a.startContainer,a.startOffset,this.startContainer,this.startOffset)&&b.setStart(a.startContainer,a.startOffset),1==R(a.endContainer,a.endOffset,this.endContainer,this.endOffset)&&b.setEnd(a.endContainer,a.endOffset),b}throw new M("Ranges do not intersect")},containsNode:function(a,b){return b?this.intersectsNode(a,!1):this.compareNode(a)==ra},containsNodeContents:function(a){return this.comparePoint(a,0)>=0&&this.comparePoint(a,U(a))<=0},containsRange:function(a){var b=this.intersection(a);return null!==b&&a.equals(b)},containsNodeText:function(a){var b=this.cloneRange();b.selectNode(a);var c=b.getNodes([3]);if(c.length>0){b.setStart(c[0],0);var d=c.pop();return b.setEnd(d,d.length),this.containsRange(b)}return this.containsNodeContents(a)},getNodes:function(a,b){return z(this),n(this,a,b)},getDocument:function(){return d(this)},collapseBefore:function(a){this.setEndBefore(a),this.collapse(!1)},collapseAfter:function(a){this.setStartAfter(a),this.collapse(!0)},getBookmark:function(b){var c=d(this),e=a.createRange(c);b=b||J.getBody(c),e.selectNodeContents(b);var f=this.intersection(e),g=0,h=0;return f&&(e.setEnd(f.startContainer,f.startOffset),g=e.toString().length,h=g+f.toString().length),{start:g,end:h,containerNode:b}},moveToBookmark:function(a){var b=a.containerNode,c=0;this.setStart(b,0),this.collapse(!0);for(var d,e,f,g,h=[b],i=!1,j=!1;!j&&(d=h.pop());)if(3==d.nodeType)e=c+d.length,!i&&a.start>=c&&a.start<=e&&(this.setStart(d,a.start-c),i=!0),i&&a.end>=c&&a.end<=e&&(this.setEnd(d,a.end-c),j=!0),c=e;else for(g=d.childNodes,f=g.length;f--;)h.push(g[f])},getName:function(){return"DomRange"},equals:function(a){return I.rangesEqual(this,a)},isValid:function(){return y(this)},inspect:function(){return o(this)},detach:function(){}}),F(I,H),K.extend(I,{rangeProperties:ja,RangeIterator:p,copyComparisonConstants:D,createPrototypeRange:F,inspect:o,toHtml:B,getRangeDocument:d,rangesEqual:function(a,b){return a.startContainer===b.startContainer&&a.startOffset===b.startOffset&&a.endContainer===b.endContainer&&a.endOffset===b.endOffset}}),a.DomRange=I}),H.createCoreModule("WrappedRange",["DomRange"],function(a,b){var c,d,e=a.dom,f=a.util,g=e.DomPosition,h=a.DomRange,i=e.getBody,j=e.getContentDocument,k=e.isCharacterDataNode;if(a.features.implementsDomRange&&!function(){function d(a){for(var b,c=m.length;c--;)b=m[c],a[b]=a.nativeRange[b];a.collapsed=a.startContainer===a.endContainer&&a.startOffset===a.endOffset}function g(a,b,c,d,e){var f=a.startContainer!==b||a.startOffset!=c,g=a.endContainer!==d||a.endOffset!=e,h=!a.equals(a.nativeRange);(f||g||h)&&(a.setEnd(d,e),a.setStart(b,c))}var k,l,m=h.rangeProperties;c=function(a){if(!a)throw b.createError("WrappedRange: Range must be specified");this.nativeRange=a,d(this)},h.createPrototypeRange(c,g),k=c.prototype,k.selectNode=function(a){this.nativeRange.selectNode(a),d(this)},k.cloneContents=function(){return this.nativeRange.cloneContents()},k.surroundContents=function(a){this.nativeRange.surroundContents(a),d(this)},k.collapse=function(a){this.nativeRange.collapse(a),d(this)},k.cloneRange=function(){return new c(this.nativeRange.cloneRange())},k.refresh=function(){d(this)},k.toString=function(){return this.nativeRange.toString()};var n=document.createTextNode("test");i(document).appendChild(n);var o=document.createRange();o.setStart(n,0),o.setEnd(n,0);try{o.setStart(n,1),k.setStart=function(a,b){this.nativeRange.setStart(a,b),d(this)},k.setEnd=function(a,b){this.nativeRange.setEnd(a,b),d(this)},l=function(a){return function(b){this.nativeRange[a](b),d(this)}}}catch(p){k.setStart=function(a,b){try{this.nativeRange.setStart(a,b)}catch(c){this.nativeRange.setEnd(a,b),this.nativeRange.setStart(a,b)}d(this)},k.setEnd=function(a,b){try{this.nativeRange.setEnd(a,b)}catch(c){this.nativeRange.setStart(a,b),this.nativeRange.setEnd(a,b)}d(this)},l=function(a,b){return function(c){try{this.nativeRange[a](c)}catch(e){this.nativeRange[b](c),this.nativeRange[a](c)}d(this)}}}k.setStartBefore=l("setStartBefore","setEndBefore"),k.setStartAfter=l("setStartAfter","setEndAfter"),k.setEndBefore=l("setEndBefore","setStartBefore"),k.setEndAfter=l("setEndAfter","setStartAfter"),k.selectNodeContents=function(a){this.setStartAndEnd(a,0,e.getNodeLength(a))},o.selectNodeContents(n),o.setEnd(n,3);var q=document.createRange();q.selectNodeContents(n),q.setEnd(n,4),q.setStart(n,2),-1==o.compareBoundaryPoints(o.START_TO_END,q)&&1==o.compareBoundaryPoints(o.END_TO_START,q)?k.compareBoundaryPoints=function(a,b){return b=b.nativeRange||b,a==b.START_TO_END?a=b.END_TO_START:a==b.END_TO_START&&(a=b.START_TO_END),this.nativeRange.compareBoundaryPoints(a,b)}:k.compareBoundaryPoints=function(a,b){return this.nativeRange.compareBoundaryPoints(a,b.nativeRange||b)};var r=document.createElement("div");r.innerHTML="123";var s=r.firstChild,t=i(document);t.appendChild(r),o.setStart(s,1),o.setEnd(s,2),o.deleteContents(),"13"==s.data&&(k.deleteContents=function(){this.nativeRange.deleteContents(),d(this)},k.extractContents=function(){var a=this.nativeRange.extractContents();return d(this),a}),t.removeChild(r),t=null,f.isHostMethod(o,"createContextualFragment")&&(k.createContextualFragment=function(a){return this.nativeRange.createContextualFragment(a)}),i(document).removeChild(n),k.getName=function(){return"WrappedRange"},a.WrappedRange=c,a.createNativeRange=function(a){return a=j(a,b,"createNativeRange"),a.createRange()}}(),a.features.implementsTextRange){var l=function(a){var b=a.parentElement(),c=a.duplicate();c.collapse(!0);var d=c.parentElement();c=a.duplicate(),c.collapse(!1);var f=c.parentElement(),g=d==f?d:e.getCommonAncestor(d,f);return g==b?g:e.getCommonAncestor(b,g)},m=function(a){return 0==a.compareEndPoints("StartToEnd",a)},n=function(a,b,c,d,f){var h=a.duplicate();h.collapse(c);var i=h.parentElement();if(e.isOrIsAncestorOf(b,i)||(i=b),!i.canHaveHTML){var j=new g(i.parentNode,e.getNodeIndex(i));return{boundaryPosition:j,nodeInfo:{nodeIndex:j.offset,containerElement:j.node}}}var l=e.getDocument(i).createElement("span");l.parentNode&&e.removeNode(l);for(var m,n,o,p,q,r=c?"StartToStart":"StartToEnd",s=f&&f.containerElement==i?f.nodeIndex:0,t=i.childNodes.length,u=t,v=u;;){if(v==t?i.appendChild(l):i.insertBefore(l,i.childNodes[v]),h.moveToElementText(l),m=h.compareEndPoints(r,a),0==m||s==u)break;if(-1==m){if(u==s+1)break;s=v}else u=u==s+1?s:v;v=Math.floor((s+u)/2),i.removeChild(l)}if(q=l.nextSibling,-1==m&&q&&k(q)){h.setEndPoint(c?"EndToStart":"EndToEnd",a);var w;if(/[\r\n]/.test(q.data)){var x=h.duplicate(),y=x.text.replace(/\r\n/g,"\r").length;for(w=x.moveStart("character",y);-1==(m=x.compareEndPoints("StartToEnd",x));)w++,x.moveStart("character",1)}else w=h.text.length;p=new g(q,w)}else n=(d||!c)&&l.previousSibling,o=(d||c)&&l.nextSibling,p=o&&k(o)?new g(o,0):n&&k(n)?new g(n,n.data.length):new g(i,e.getNodeIndex(l));return e.removeNode(l),{boundaryPosition:p,nodeInfo:{nodeIndex:v,containerElement:i}}},o=function(a,b){var c,d,f,g,h=a.offset,j=e.getDocument(a.node),l=i(j).createTextRange(),m=k(a.node);return m?(c=a.node,d=c.parentNode):(g=a.node.childNodes,c=hb;++b)if(!C.isAncestorOf(a[0],a[b]))return!1;return!0}function m(a){var c=a.getNodes();if(!l(c))throw b.createError("getSingleElementFromRange: range "+a.inspect()+" did not consist of a single element");return c[0]}function n(a){return!!a&&"undefined"!=typeof a.text}function o(a,b){var c=new G(b);a._ranges=[c],h(a,c,!1),a.rangeCount=1,a.isCollapsed=c.collapsed}function p(b){if(b._ranges.length=0,"None"==b.docSelection.type)j(b);else{var c=b.docSelection.createRange();if(n(c))o(b,c);else{b.rangeCount=c.length;for(var d,e=L(c.item(0)),f=0;fh;++h)g.add(d.item(h));try{g.add(e)}catch(j){throw b.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)")}g.select(),p(a)}function r(a,b,c){this.nativeSelection=a,this.docSelection=b,this._ranges=[],this.win=c,this.refresh()}function s(a){a.win=a.anchorNode=a.focusNode=a._ranges=null,a.rangeCount=a.anchorOffset=a.focusOffset=0,a.detached=!0}function t(a,b){for(var c,d,e=ba.length;e--;)if(c=ba[e],d=c.selection,"deleteAll"==b)s(d);else if(c.win==a)return"delete"==b?(ba.splice(e,1),!0):d;return"deleteAll"==b&&(ba.length=0),null}function u(a,c){for(var d,e=L(c[0].startContainer),f=M(e).createControlRange(),g=0,h=c.length;h>g;++g){d=m(c[g]);try{f.add(d)}catch(i){throw b.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)")}}f.select(),p(a)}function v(a,b){if(a.win.document!=L(b))throw new H("WRONG_DOCUMENT_ERR")}function w(b){return function(c,d){var e;this.rangeCount?(e=this.getRangeAt(0),e["set"+(b?"Start":"End")](c,d)):(e=a.createRange(this.win.document),e.setStartAndEnd(c,d)),this.setSingleRange(e,this.isBackward())}}function x(a){var b=[],c=new I(a.anchorNode,a.anchorOffset),d=new I(a.focusNode,a.focusOffset),e="function"==typeof a.getName?a.getName():"Selection";if("undefined"!=typeof a.rangeCount)for(var f=0,g=a.rangeCount;g>f;++f)b[f]=F.inspect(a.getRangeAt(f));return"["+e+"(Ranges: "+b.join(", ")+")(anchor: "+c.inspect()+", focus: "+d.inspect()+"]"}a.config.checkSelectionRanges=!0;var y,z,A="boolean",B="number",C=a.dom,D=a.util,E=D.isHostMethod,F=a.DomRange,G=a.WrappedRange,H=a.DOMException,I=C.DomPosition,J=a.features,K="Control",L=C.getDocument,M=C.getBody,N=F.rangesEqual,O=E(window,"getSelection"),P=D.isHostObject(document,"selection");J.implementsWinGetSelection=O,J.implementsDocSelection=P;var Q=P&&(!O||a.config.preferTextRange);if(Q)y=f,a.isSelectionValid=function(a){var b=d(a,"isSelectionValid").document,c=b.selection;return"None"!=c.type||L(c.createRange().parentElement())==b};else{if(!O)return b.fail("Neither document.selection or window.getSelection() detected."),!1;y=e,a.isSelectionValid=function(){return!0}}a.getNativeSelection=y;var R=y();if(!R)return b.fail("Native selection was null (possibly issue 138?)"),!1;var S=a.createNativeRange(document),T=M(document),U=D.areHostProperties(R,["anchorNode","focusNode","anchorOffset","focusOffset"]);J.selectionHasAnchorAndFocus=U;var V=E(R,"extend");J.selectionHasExtend=V;var W=typeof R.rangeCount==B;J.selectionHasRangeCount=W;var X=!1,Y=!0,Z=V?function(b,c){var d=F.getRangeDocument(c),e=a.createRange(d);e.collapseToPoint(c.endContainer,c.endOffset),b.addRange(k(e)),b.extend(c.startContainer,c.startOffset)}:null;D.areHostMethods(R,["addRange","getRangeAt","removeAllRanges"])&&typeof R.rangeCount==B&&J.implementsDomRange&&!function(){var b=window.getSelection();if(b){for(var c=b.rangeCount,d=c>1,e=[],f=g(b),h=0;c>h;++h)e[h]=b.getRangeAt(h);var i=C.createTestElement(document,"",!1),j=i.appendChild(document.createTextNode(" ")),k=document.createRange();if(k.setStart(j,1),k.collapse(!0),b.removeAllRanges(),b.addRange(k),Y=1==b.rangeCount,b.removeAllRanges(),!d){var l=window.navigator.appVersion.match(/Chrome\/(.*?) /);if(l&&parseInt(l[1])>=36)X=!1;else{var m=k.cloneRange();k.setStart(j,0),m.setEnd(j,3),m.setStart(j,2),b.addRange(k),b.addRange(m),X=2==b.rangeCount}}for(C.removeNode(i),b.removeAllRanges(),h=0;c>h;++h)0==h&&f?Z?Z(b,e[h]):(a.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend"),b.addRange(e[h])):b.addRange(e[h])}}(),J.selectionSupportsMultipleRanges=X,J.collapsedNonEditableSelectionsSupported=Y;var $,_=!1;T&&E(T,"createControlRange")&&($=T.createControlRange(),D.areHostProperties($,["item","add"])&&(_=!0)),J.implementsControlRange=_,z=U?function(a){return a.anchorNode===a.focusNode&&a.anchorOffset===a.focusOffset}:function(a){return a.rangeCount?a.getRangeAt(a.rangeCount-1).collapsed:!1};var aa;E(R,"getRangeAt")?aa=function(a,b){try{return a.getRangeAt(b)}catch(c){return null}}:U&&(aa=function(b){var c=L(b.anchorNode),d=a.createRange(c);return d.setStartAndEnd(b.anchorNode,b.anchorOffset,b.focusNode,b.focusOffset),d.collapsed!==this.isCollapsed&&d.setStartAndEnd(b.focusNode,b.focusOffset,b.anchorNode,b.anchorOffset),d}),r.prototype=a.selectionPrototype;var ba=[],ca=function(a){if(a&&a instanceof r)return a.refresh(),a;a=d(a,"getNativeSelection");var b=t(a),c=y(a),e=P?f(a):null;return b?(b.nativeSelection=c,b.docSelection=e,b.refresh()):(b=new r(c,e,a),ba.push({win:a,selection:b})),b};a.getSelection=ca,D.createAliasForDeprecatedMethod(a,"getIframeSelection","getSelection");var da=r.prototype;if(!Q&&U&&D.areHostMethods(R,["removeAllRanges","addRange"])){da.removeAllRanges=function(){this.nativeSelection.removeAllRanges(),j(this)};var ea=function(a,b){Z(a.nativeSelection,b),a.refresh()};W?da.addRange=function(b,d){if(_&&P&&this.docSelection.type==K)q(this,b);else if(c(d)&&V)ea(this,b);else{var e;X?e=this.rangeCount:(this.removeAllRanges(),e=0);var f=k(b).cloneRange();try{this.nativeSelection.addRange(f)}catch(g){}if(this.rangeCount=this.nativeSelection.rangeCount,this.rangeCount==e+1){if(a.config.checkSelectionRanges){var i=aa(this.nativeSelection,this.rangeCount-1);i&&!N(i,b)&&(b=new G(i))}this._ranges[this.rangeCount-1]=b,h(this,b,ha(this.nativeSelection)),this.isCollapsed=z(this)}else this.refresh()}}:da.addRange=function(a,b){c(b)&&V?ea(this,a):(this.nativeSelection.addRange(k(a)),this.refresh())},da.setRanges=function(a){if(_&&P&&a.length>1)u(this,a);else{this.removeAllRanges();for(var b=0,c=a.length;c>b;++b)this.addRange(a[b])}}}else{if(!(E(R,"empty")&&E(S,"select")&&_&&Q))return b.fail("No means of selecting a Range or TextRange was found"),!1;da.removeAllRanges=function(){try{if(this.docSelection.empty(),"None"!=this.docSelection.type){var a;if(this.anchorNode)a=L(this.anchorNode);else if(this.docSelection.type==K){var b=this.docSelection.createRange();b.length&&(a=L(b.item(0)))}if(a){var c=M(a).createTextRange();c.select(),this.docSelection.empty()}}}catch(d){}j(this)},da.addRange=function(b){this.docSelection.type==K?q(this,b):(a.WrappedTextRange.rangeToTextRange(b).select(),this._ranges[0]=b,this.rangeCount=1,this.isCollapsed=this._ranges[0].collapsed,h(this,b,!1))},da.setRanges=function(a){this.removeAllRanges();var b=a.length;b>1?u(this,a):b&&this.addRange(a[0])}}da.getRangeAt=function(a){if(0>a||a>=this.rangeCount)throw new H("INDEX_SIZE_ERR");return this._ranges[a].cloneRange()};var fa;if(Q)fa=function(b){var c;a.isSelectionValid(b.win)?c=b.docSelection.createRange():(c=M(b.win.document).createTextRange(),c.collapse(!0)),b.docSelection.type==K?p(b):n(c)?o(b,c):j(b)};else if(E(R,"getRangeAt")&&typeof R.rangeCount==B)fa=function(b){if(_&&P&&b.docSelection.type==K)p(b);else if(b._ranges.length=b.rangeCount=b.nativeSelection.rangeCount,b.rangeCount){for(var c=0,d=b.rangeCount;d>c;++c)b._ranges[c]=new a.WrappedRange(b.nativeSelection.getRangeAt(c));h(b,b._ranges[b.rangeCount-1],ha(b.nativeSelection)),b.isCollapsed=z(b)}else j(b)};else{if(!U||typeof R.isCollapsed!=A||typeof S.collapsed!=A||!J.implementsDomRange)return b.fail("No means of obtaining a Range or TextRange from the user's selection was found"),!1;fa=function(a){var b,c=a.nativeSelection;c.anchorNode?(b=aa(c,0),a._ranges=[b],a.rangeCount=1,i(a),a.isCollapsed=z(a)):j(a)}}da.refresh=function(a){var b=a?this._ranges.slice(0):null,c=this.anchorNode,d=this.anchorOffset;if(fa(this),a){var e=b.length;if(e!=this._ranges.length)return!0;if(this.anchorNode!=c||this.anchorOffset!=d)return!0;for(;e--;)if(!N(b[e],this._ranges[e]))return!0;return!1}};var ga=function(a,b){var c=a.getAllRanges();a.removeAllRanges();for(var d=0,e=c.length;e>d;++d)N(b,c[d])||a.addRange(c[d]);a.rangeCount||j(a)};_&&P?da.removeRange=function(a){if(this.docSelection.type==K){for(var b,c=this.docSelection.createRange(),d=m(a),e=L(c.item(0)),f=M(e).createControlRange(),g=!1,h=0,i=c.length;i>h;++h)b=c.item(h),b!==d||g?f.add(c.item(h)):g=!0;f.select(),p(this)}else ga(this,a)}:da.removeRange=function(a){ga(this,a)};var ha;!Q&&U&&J.implementsDomRange?(ha=g,da.isBackward=function(){return ha(this)}):ha=da.isBackward=function(){return!1},da.isBackwards=da.isBackward,da.toString=function(){for(var a=[],b=0,c=this.rangeCount;c>b;++b)a[b]=""+this._ranges[b];return a.join("")},da.collapse=function(b,c){v(this,b);var d=a.createRange(b);d.collapseToPoint(b,c),this.setSingleRange(d),this.isCollapsed=!0},da.collapseToStart=function(){if(!this.rangeCount)throw new H("INVALID_STATE_ERR");var a=this._ranges[0];this.collapse(a.startContainer,a.startOffset)},da.collapseToEnd=function(){if(!this.rangeCount)throw new H("INVALID_STATE_ERR");var a=this._ranges[this.rangeCount-1];this.collapse(a.endContainer,a.endOffset)},da.selectAllChildren=function(b){v(this,b);var c=a.createRange(b);c.selectNodeContents(b),this.setSingleRange(c)},da.deleteFromDocument=function(){if(_&&P&&this.docSelection.type==K){for(var a,b=this.docSelection.createRange();b.length;)a=b.item(0),b.remove(a),C.removeNode(a);this.refresh()}else if(this.rangeCount){var c=this.getAllRanges();if(c.length){this.removeAllRanges();for(var d=0,e=c.length;e>d;++d)c[d].deleteContents();this.addRange(c[e-1])}}},da.eachRange=function(a,b){for(var c=0,d=this._ranges.length;d>c;++c)if(a(this.getRangeAt(c)))return b},da.getAllRanges=function(){var a=[];return this.eachRange(function(b){a.push(b)}),a},da.setSingleRange=function(a,b){this.removeAllRanges(),this.addRange(a,b)},da.callMethodOnEachRange=function(a,b){var c=[];return this.eachRange(function(d){c.push(d[a].apply(d,b||[]))}),c},da.setStart=w(!0),da.setEnd=w(!1),a.rangePrototype.select=function(a){ca(this.getDocument()).setSingleRange(this,a)},da.changeEachRange=function(a){var b=[],c=this.isBackward();this.eachRange(function(c){a(c),b.push(c)}),this.removeAllRanges(),c&&1==b.length?this.addRange(b[0],"backward"):this.setRanges(b)},da.containsNode=function(a,b){return this.eachRange(function(c){return c.containsNode(a,b)},!0)||!1},da.getBookmark=function(a){return{backward:this.isBackward(),rangeBookmarks:this.callMethodOnEachRange("getBookmark",[a])}},da.moveToBookmark=function(b){for(var c,d,e=[],f=0;c=b.rangeBookmarks[f++];)d=a.createRange(this.win),d.moveToBookmark(c),e.push(d);b.backward?this.setSingleRange(e[0],"backward"):this.setRanges(e)},da.saveRanges=function(){return{backward:this.isBackward(),ranges:this.callMethodOnEachRange("cloneRange")}},da.restoreRanges=function(a){this.removeAllRanges();for(var b,c=0;b=a.ranges[c];++c)this.addRange(b,a.backward&&0==c)},da.toHtml=function(){var a=[];return this.eachRange(function(b){a.push(F.toHtml(b))}),a.join("")},J.implementsTextRange&&(da.getNativeTextRange=function(){var c;if(c=this.docSelection){var d=c.createRange();if(n(d))return d;throw b.createError("getNativeTextRange: selection is a control selection")}if(this.rangeCount>0)return a.WrappedTextRange.rangeToTextRange(this.getRangeAt(0));throw b.createError("getNativeTextRange: selection contains no range")}),da.getName=function(){return"WrappedSelection"},da.inspect=function(){return x(this)},da.detach=function(){t(this.win,"delete"),s(this)},r.detachAll=function(){t(null,"deleteAll")},r.inspect=x,r.isDirectionBackward=c,a.Selection=r,a.selectionPrototype=da,a.addShimListener(function(a){"undefined"==typeof a.getSelection&&(a.getSelection=function(){return ca(a)}),a=null})});var M=!1,N=function(a){M||(M=!0,!H.initialized&&H.config.autoInitialize&&l())};return F&&("complete"==document.readyState?N():(a(document,"addEventListener")&&document.addEventListener("DOMContentLoaded",N,!1),J(window,"load",N))),H},this),function(a,b){"function"==typeof define&&define.amd?define(["./rangy-core"],a):"undefined"!=typeof module&&"object"==typeof exports?module.exports=a(require("rangy")):a(b.rangy)}(function(a){return a.createModule("SaveRestore",["WrappedRange"],function(a,b){function c(a,b){return(b||document).getElementById(a)}function d(a,b){var c,d="selectionBoundary_"+ +new Date+"_"+(""+Math.random()).slice(2),e=o.getDocument(a.startContainer),f=a.cloneRange();return f.collapse(b),c=e.createElement("span"),c.id=d,c.style.lineHeight="0",c.style.display="none",c.className="rangySelectionBoundary",c.appendChild(e.createTextNode(r)),f.insertNode(c),c}function e(a,d,e,f){var g=c(e,a);g?(d[f?"setStartBefore":"setEndBefore"](g),p(g)):b.warn("Marker element has been removed. Cannot restore selection.")}function f(a,b){return b.compareBoundaryPoints(a.START_TO_START,a)}function g(b,c){var e,f,g=a.DomRange.getRangeDocument(b),h=b.toString(),i=q(c);return b.collapsed?(f=d(b,!1),{document:g,markerId:f.id,collapsed:!0}):(f=d(b,!1),e=d(b,!0),{document:g,startMarkerId:e.id,endMarkerId:f.id,collapsed:!1,backward:i,toString:function(){return"original text: '"+h+"', new text: '"+b.toString()+"'"}})}function h(d,f){var g=d.document;"undefined"==typeof f&&(f=!0);var h=a.createRange(g);if(d.collapsed){var i=c(d.markerId,g);if(i){i.style.display="inline";var j=i.previousSibling;j&&3==j.nodeType?(p(i),h.collapseToPoint(j,j.length)):(h.collapseBefore(i),p(i))}else b.warn("Marker element has been removed. Cannot restore selection.")}else e(g,h,d.startMarkerId,!0),e(g,h,d.endMarkerId,!1);return f&&h.normalizeBoundaries(),h}function i(b,d){var e,h,i=[],j=q(d);b=b.slice(0),b.sort(f);for(var k=0,l=b.length;l>k;++k)i[k]=g(b[k],j);for(k=l-1;k>=0;--k)e=b[k],h=a.DomRange.getRangeDocument(e),e.collapsed?e.collapseAfter(c(i[k].markerId,h)):(e.setEndBefore(c(i[k].endMarkerId,h)),e.setStartAfter(c(i[k].startMarkerId,h)));return i}function j(c){if(!a.isSelectionValid(c))return b.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus."),null;var d=a.getSelection(c),e=d.getAllRanges(),f=1==e.length&&d.isBackward(),g=i(e,f);return f?d.setSingleRange(e[0],f):d.setRanges(e),{win:c,rangeInfos:g,restored:!1}}function k(a){for(var b=[],c=a.length,d=c-1;d>=0;d--)b[d]=h(a[d],!0);return b}function l(b,c){if(!b.restored){var d=b.rangeInfos,e=a.getSelection(b.win),f=k(d),g=d.length;1==g&&c&&a.features.selectionHasExtend&&d[0].backward?(e.removeAllRanges(),e.addRange(f[0],!0)):e.setRanges(f),b.restored=!0}}function m(a,b){var d=c(b,a);d&&p(d)}function n(a){for(var b,c=a.rangeInfos,d=0,e=c.length;e>d;++d)b=c[d],b.collapsed?m(a.doc,b.markerId):(m(a.doc,b.startMarkerId),m(a.doc,b.endMarkerId))}var o=a.dom,p=o.removeNode,q=a.Selection.isDirectionBackward,r="\ufeff";a.util.extend(a,{saveRange:g,restoreRange:h,saveRanges:i,restoreRanges:k,saveSelection:j,restoreSelection:l,removeMarkerElement:m,removeMarkers:n})}),a},this); diff --git a/vendor/assets/javascripts/textAngular-sanitize.min.js b/vendor/assets/javascripts/textAngular-sanitize.min.js index 75534aaa01..b3ebd34363 100644 --- a/vendor/assets/javascripts/textAngular-sanitize.min.js +++ b/vendor/assets/javascripts/textAngular-sanitize.min.js @@ -1 +1,6 @@ -!function(a,b){b["true"]=a,function(a,b){"use strict";function c(){this.$get=["$$sanitizeUri",function(a){return function(b){var c=[];return f(b,k(c,function(b,c){return!/^unsafe/.test(a(b,c))})),c.join("")}}]}function d(a){var c=[],d=k(c,b.noop);return d.chars(a),c.join("")}function e(a){var b,c={},d=a.split(",");for(b=0;b=0&&j[f]!=d;f--);if(f>=0){for(e=j.length-1;e>=f;e--)c.end&&c.end(j[e]);j.length=f}}var f,h,i,j=[],k=a;for(j.last=function(){return j[j.length-1]};a;){if(h=!0,j.last()&&C[j.last()])a=a.replace(new RegExp("(.*)<\\s*\\/\\s*"+j.last()+"[^>]*>","i"),function(a,b){return b=b.replace(r,"$1").replace(t,"$1"),c.chars&&c.chars(g(b)),""}),e("",j.last());else if(0===a.indexOf("",f)===f&&(c.comment&&c.comment(a.substring(4,f)),a=a.substring(f+3),h=!1)):s.test(a)?(i=a.match(s),i&&(a=a.replace(i[0],""),h=!1)):q.test(a)?(i=a.match(n),i&&(a=a.substring(i[0].length),i[0].replace(n,e),h=!1)):p.test(a)&&(i=a.match(m),i&&(a=a.substring(i[0].length),i[0].replace(m,d),h=!1)),h){f=a.indexOf("<");var u=0>f?a:a.substring(0,f);a=0>f?"":a.substring(f),c.chars&&c.chars(g(u))}if(a==k)throw l("badparse","The sanitizer was unable to parse the following block of html: {0}",a);k=a}e()}function g(a){if(!a)return"";var b=H.exec(a),c=b[1],d=b[3],e=b[2];return e&&(G.innerHTML=e.replace(/=b||173==b||b>=1536&&1540>=b||1807==b||6068==b||6069==b||b>=8204&&8207>=b||b>=8232&&8239>=b||b>=8288&&8303>=b||65279==b||b>=65520&&65535>=b?"&#"+b+";":a}).replace(//g,">")}function i(a){var c="",d=a.split(";");return b.forEach(d,function(a){var d=a.split(":");if(2==d.length){var e=I(b.lowercase(d[0])),a=I(b.lowercase(d[1]));("color"===e&&(a.match(/^rgb\([0-9%,\. ]*\)$/i)||a.match(/^rgba\([0-9%,\. ]*\)$/i)||a.match(/^hsl\([0-9%,\. ]*\)$/i)||a.match(/^hsla\([0-9%,\. ]*\)$/i)||a.match(/^#[0-9a-f]{3,6}$/i)||a.match(/^[a-z]*$/i))||"text-align"===e&&("left"===a||"right"===a||"center"===a||"justify"===a)||"float"===e&&("left"===a||"right"===a||"none"===a)||("width"===e||"height"===e)&&a.match(/[0-9\.]*(px|em|rem|%)/))&&(c+=e+": "+a+";")}}),c}function j(a,b,c,d){return"img"===a&&b["ta-insert-video"]&&("ta-insert-video"===c||"allowfullscreen"===c||"frameborder"===c||"contenteditble"===c&&"false"===d)?!0:!1}function k(a,c){var d=!1,e=b.bind(a,a.push);return{start:function(a,f,g){a=b.lowercase(a),!d&&C[a]&&(d=a),d||D[a]!==!0||(e("<"),e(a),b.forEach(f,function(d,g){var k=b.lowercase(g),l="img"===a&&"src"===k||"background"===k;("style"===k&&""!==(d=i(d))||j(a,f,k,d)||F[k]===!0&&(E[k]!==!0||c(d,l)))&&(e(" "),e(g),e('="'),e(h(d)),e('"'))}),e(g?"/>":">"))},end:function(a){a=b.lowercase(a),d||D[a]!==!0||(e("")),a==d&&(d=!1)},chars:function(a){d||e(h(a))}}}var l=b.$$minErr("$sanitize"),m=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,n=/^<\s*\/\s*([\w:-]+)[^>]*>/,o=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,p=/^/g,s=/]*?)>/i,t=//g,u=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,v=/([^\#-~| |!])/g,w=e("area,br,col,hr,img,wbr"),x=e("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),y=e("rp,rt"),z=b.extend({},y,x),A=b.extend({},x,e("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),B=b.extend({},y,e("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")),C=e("script,style"),D=b.extend({},w,A,B,z),E=e("background,cite,href,longdesc,src,usemap"),F=b.extend({},E,e("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,size,span,start,summary,target,title,type,valign,value,vspace,width")),G=document.createElement("pre"),H=/^(\s*)([\s\S]*?)(\s*)$/,I=function(){return String.prototype.trim?function(a){return b.isString(a)?a.trim():a}:function(a){return b.isString(a)?a.replace(/^\s\s*/,"").replace(/\s\s*$/,""):a}}();b.module("ngSanitize",[]).provider("$sanitize",c),b.module("ngSanitize").filter("linky",["$sanitize",function(a){var c=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/,e=/^mailto:/;return function(f,g){function h(a){a&&n.push(d(a))}function i(a,c){n.push("'),h(c),n.push("")}if(!f)return f;for(var j,k,l,m=f,n=[];j=m.match(c);)k=j[0],j[2]==j[3]&&(k="mailto:"+k),l=j.index,h(m.substr(0,l)),i(k,j[0].replace(e,"")),m=m.substring(l+j[0].length);return h(m),a(n.join(""))}}])}(window,window.angular)}({},function(){return this}()); \ No newline at end of file +/** + * @license AngularJS v1.3.10 + * (c) 2010-2014 Google, Inc. http://angularjs.org + * License: MIT + */ +!function(a,b,c){"use strict";function d(){this.$get=["$$sanitizeUri",function(a){return function(b){"undefined"!=typeof arguments[1]&&(arguments[1].version="taSanitize");var c=[];return g(b,l(c,function(b,c){return!/^unsafe/.test(a(b,c))})),c.join("")}}]}function e(a){var c=[],d=l(c,b.noop);return d.chars(a),c.join("")}function f(a){var b,c={},d=a.split(",");for(b=0;b=0&&k[f]!=d;f--);if(f>=0){for(e=k.length-1;e>=f;e--)c.end&&c.end(k[e]);k.length=f}}"string"!=typeof a&&(a=null===a||"undefined"==typeof a?"":""+a);var f,g,i,j,k=[],l=a;for(k.last=function(){return k[k.length-1]};a;){if(j="",g=!0,k.last()&&G[k.last()])a=a.replace(new RegExp("([^]*)<\\s*\\/\\s*"+k.last()+"[^>]*>","i"),function(a,b){return b=b.replace(s,"$1").replace(v,"$1"),c.chars&&c.chars(h(b)),""}),e("",k.last());else{if(y.test(a)){if(i=a.match(y)){i[0];c.whitespace&&c.whitespace(i[0]),a=a.replace(i[0],""),g=!1}}else t.test(a)?(i=a.match(t),i&&(c.comment&&c.comment(i[1]),a=a.replace(i[0],""),g=!1)):u.test(a)?(i=a.match(u),i&&(a=a.replace(i[0],""),g=!1)):r.test(a)?(i=a.match(o),i&&(a=a.substring(i[0].length),i[0].replace(o,e),g=!1)):q.test(a)&&(i=a.match(n),i?(i[4]&&(a=a.substring(i[0].length),i[0].replace(n,d)),g=!1):(j+="<",a=a.substring(1)));g&&(f=a.indexOf("<"),j+=0>f?a:a.substring(0,f),a=0>f?"":a.substring(f),c.chars&&c.chars(h(j)))}if(a==l)throw m("badparse","The sanitizer was unable to parse the following block of html: {0}",a);l=a}e()}function h(a){if(!a)return"";var b=N.exec(a),c=b[1],d=b[3],e=b[2];return e&&(M.innerHTML=e.replace(/=b||173==b||b>=1536&&1540>=b||1807==b||6068==b||6069==b||b>=8204&&8207>=b||b>=8232&&8239>=b||b>=8288&&8303>=b||65279==b||b>=65520&&65535>=b?"&#"+b+";":a}).replace(//g,">")}function j(a){var c="",d=a.split(";");return b.forEach(d,function(a){var d=a.split(":");if(2==d.length){var e=O(b.lowercase(d[0])),a=O(b.lowercase(d[1]));(("color"===e||"background-color"===e)&&(a.match(/^rgb\([0-9%,\. ]*\)$/i)||a.match(/^rgba\([0-9%,\. ]*\)$/i)||a.match(/^hsl\([0-9%,\. ]*\)$/i)||a.match(/^hsla\([0-9%,\. ]*\)$/i)||a.match(/^#[0-9a-f]{3,6}$/i)||a.match(/^[a-z]*$/i))||"text-align"===e&&("left"===a||"right"===a||"center"===a||"justify"===a)||"text-decoration"===e&&("underline"===a||"line-through"===a)||"font-weight"===e&&"bold"===a||"float"===e&&("left"===a||"right"===a||"none"===a)||("width"===e||"height"===e)&&a.match(/[0-9\.]*(px|em|rem|%)/)||"direction"===e&&a.match(/^ltr|rtl|initial|inherit$/))&&(c+=e+": "+a+";")}}),c}function k(a,b,c,d){return"img"===a&&b["ta-insert-video"]&&("ta-insert-video"===c||"allowfullscreen"===c||"frameborder"===c||"contenteditable"===c&&"false"===d)?!0:!1}function l(a,c){var d=!1,e=b.bind(a,a.push);return{start:function(a,f,g){a=b.lowercase(a),!d&&G[a]&&(d=a),d||H[a]!==!0||(e("<"),e(a),b.forEach(f,function(d,g){var h=b.lowercase(g),l="img"===a&&"src"===h||"background"===h;("style"===h&&""!==(d=j(d))||k(a,f,h,d)||L[h]===!0&&(I[h]!==!0||c(d,l)))&&(e(" "),e(g),e('="'),e(i(d)),e('"'))}),e(g?"/>":">"))},comment:function(a){e(a)},whitespace:function(a){e(i(a))},end:function(a){a=b.lowercase(a),d||H[a]!==!0||(e("")),a==d&&(d=!1)},chars:function(a){d||e(i(a))}}}var m=b.$$minErr("$sanitize"),n=/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,o=/^<\/\s*([\w:-]+)[^>]*>/,p=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,q=/^/g,t=/(^)/,u=/]*?)>/i,v=//g,w=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,x=/([^\#-~| |!])/g,y=/^(\s+)/,z=f("area,br,col,hr,img,wbr,input"),A=f("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),B=f("rp,rt"),C=b.extend({},B,A),D=b.extend({},A,f("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),E=b.extend({},B,f("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")),F=f("animate,animateColor,animateMotion,animateTransform,circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set,stop,svg,switch,text,title,tspan,use"),G=f("script,style"),H=b.extend({},z,D,E,C,F),I=f("background,cite,href,longdesc,src,usemap,xlink:href"),J=f("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,id,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,size,span,start,summary,target,title,type,valign,value,vspace,width"),K=f("accent-height,accumulate,additive,alphabetic,arabic-form,ascent,attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan"),L=b.extend({},I,K,J),M=document.createElement("pre"),N=/^(\s*)([\s\S]*?)(\s*)$/,O=function(){return String.prototype.trim?function(a){return b.isString(a)?a.trim():a}:function(a){return b.isString(a)?a.replace(/^\s\s*/,"").replace(/\s\s*$/,""):a}}();b.module("ngSanitize",[]).provider("$sanitize",d),b.module("ngSanitize").filter("linky",["$sanitize",function(a){var c=/((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/,d=/^mailto:/;return function(f,g){function h(a){a&&n.push(e(a))}function i(a,c){n.push("'),h(c),n.push("")}if(!f)return f;for(var j,k,l,m=f,n=[];j=m.match(c);)k=j[0],j[2]||j[4]||(k=(j[3]?"http://":"mailto:")+k),l=j.index,h(m.substr(0,l)),i(k,j[0].replace(d,"")),m=m.substring(l+j[0].length);return h(m),a(n.join(""))}}])}(window,window.angular); diff --git a/vendor/assets/javascripts/textAngular.min.js b/vendor/assets/javascripts/textAngular.min.js index aba313ad9f..f293a823f5 100644 --- a/vendor/assets/javascripts/textAngular.min.js +++ b/vendor/assets/javascripts/textAngular.min.js @@ -1,2 +1,3 @@ -!function(a,b){b["true"]=a,angular.module("textAngularSetup",[]).value("taOptions",{toolbar:[["h1","h2","h3","h4","h5","h6","p","pre","quote"],["bold","italics","underline","ul","ol","redo","undo","clear"],["justifyLeft","justifyCenter","justifyRight","indent","outdent"],["html","insertImage","insertLink","insertVideo"]],classes:{focussed:"focussed",toolbar:"btn-toolbar",toolbarGroup:"btn-group",toolbarButton:"btn btn-default",toolbarButtonActive:"active",disabled:"disabled",textEditor:"form-control",htmlEditor:"form-control"},setup:{textEditorSetup:function(){},htmlEditorSetup:function(){}},defaultFileDropHandler:function(a,b){var c=new FileReader;return"image"===a.type.substring(0,5)?(c.onload=function(){""!==c.result&&b("insertImage",c.result,!0)},c.readAsDataURL(a),!0):!1}}).value("taSelectableElements",["a","img"]).value("taCustomRenderers",[{selector:"img",customAttribute:"ta-insert-video",renderLogic:function(a){var b=angular.element(""),c=a.prop("attributes");angular.forEach(c,function(a){b.attr(a.name,a.value)}),b.attr("src",b.attr("ta-insert-video")),a.replaceWith(b)}}]).constant("taTranslations",{html:{buttontext:"Toggle HTML",tooltip:"Toggle html / Rich Text"},heading:{tooltip:"Heading "},p:{tooltip:"Paragraph"},pre:{tooltip:"Preformatted text"},ul:{tooltip:"Unordered List"},ol:{tooltip:"Ordered List"},quote:{tooltip:"Quote/unqoute selection or paragraph"},undo:{tooltip:"Undo"},redo:{tooltip:"Redo"},bold:{tooltip:"Bold"},italic:{tooltip:"Italic"},underline:{tooltip:"Underline"},justifyLeft:{tooltip:"Align text left"},justifyRight:{tooltip:"Align text right"},justifyCenter:{tooltip:"Center"},indent:{tooltip:"Increase indent"},outdent:{tooltip:"Decrease indent"},clear:{tooltip:"Clear formatting"},insertImage:{dialogPrompt:"Please enter an image URL to insert",tooltip:"Insert image",hotkey:"the - possibly language dependent hotkey ... for some future implementation"},insertVideo:{tooltip:"Insert video",dialogPrompt:"Please enter a youtube URL to embed"},insertLink:{tooltip:"Insert / edit link",dialogPrompt:"Please enter a URL to insert"}}).run(["taRegisterTool","$window","taTranslations","taSelection",function(a,b,c,d){a("html",{buttontext:c.html.buttontext,tooltiptext:c.html.tooltip,action:function(){this.$editor().switchView()},activeState:function(){return this.$editor().showHtml}});var e=function(a){return function(){return this.$editor().queryFormatBlockState(a)}},f=function(){return this.$editor().wrapSelection("formatBlock","<"+this.name.toUpperCase()+">")};angular.forEach(["h1","h2","h3","h4","h5","h6"],function(b){a(b.toLowerCase(),{buttontext:b.toUpperCase(),tooltiptext:c.heading.tooltip+b.charAt(1),action:f,activeState:e(b.toLowerCase())})}),a("p",{buttontext:"P",tooltiptext:c.p.tooltip,action:function(){return this.$editor().wrapSelection("formatBlock","

    ")},activeState:function(){return this.$editor().queryFormatBlockState("p")}}),a("pre",{buttontext:"pre",tooltiptext:c.pre.tooltip,action:function(){return this.$editor().wrapSelection("formatBlock","

    ")},activeState:function(){return this.$editor().queryFormatBlockState("pre")}}),a("ul",{iconclass:"fa fa-list-ul",tooltiptext:c.ul.tooltip,action:function(){return this.$editor().wrapSelection("insertUnorderedList",null)},activeState:function(){return this.$editor().queryCommandState("insertUnorderedList")}}),a("ol",{iconclass:"fa fa-list-ol",tooltiptext:c.ol.tooltip,action:function(){return this.$editor().wrapSelection("insertOrderedList",null)},activeState:function(){return this.$editor().queryCommandState("insertOrderedList")}}),a("quote",{iconclass:"fa fa-quote-right",tooltiptext:c.quote.tooltip,action:function(){return this.$editor().wrapSelection("formatBlock","
    ")},activeState:function(){return this.$editor().queryFormatBlockState("blockquote")}}),a("undo",{iconclass:"fa fa-undo",tooltiptext:c.undo.tooltip,action:function(){return this.$editor().wrapSelection("undo",null)}}),a("redo",{iconclass:"fa fa-repeat",tooltiptext:c.redo.tooltip,action:function(){return this.$editor().wrapSelection("redo",null)}}),a("bold",{iconclass:"fa fa-bold",tooltiptext:c.bold.tooltip,action:function(){return this.$editor().wrapSelection("bold",null)},activeState:function(){return this.$editor().queryCommandState("bold")},commandKeyCode:98}),a("justifyLeft",{iconclass:"fa fa-align-left",tooltiptext:c.justifyLeft.tooltip,action:function(){return this.$editor().wrapSelection("justifyLeft",null)},activeState:function(a){var b=!1;return a&&(b="left"===a.css("text-align")||"left"===a.attr("align")||"right"!==a.css("text-align")&&"center"!==a.css("text-align")&&!this.$editor().queryCommandState("justifyRight")&&!this.$editor().queryCommandState("justifyCenter")),b=b||this.$editor().queryCommandState("justifyLeft")}}),a("justifyRight",{iconclass:"fa fa-align-right",tooltiptext:c.justifyRight.tooltip,action:function(){return this.$editor().wrapSelection("justifyRight",null)},activeState:function(a){var b=!1;return a&&(b="right"===a.css("text-align")),b=b||this.$editor().queryCommandState("justifyRight")}}),a("justifyCenter",{iconclass:"fa fa-align-center",tooltiptext:c.justifyCenter.tooltip,action:function(){return this.$editor().wrapSelection("justifyCenter",null)},activeState:function(a){var b=!1;return a&&(b="center"===a.css("text-align")),b=b||this.$editor().queryCommandState("justifyCenter")}}),a("indent",{iconclass:"fa fa-indent",tooltiptext:c.indent.tooltip,action:function(){return this.$editor().wrapSelection("indent",null)},activeState:function(){return this.$editor().queryFormatBlockState("blockquote")}}),a("outdent",{iconclass:"fa fa-outdent",tooltiptext:c.outdent.tooltip,action:function(){return this.$editor().wrapSelection("outdent",null)},activeState:function(){return!1}}),a("italics",{iconclass:"fa fa-italic",tooltiptext:c.italic.tooltip,action:function(){return this.$editor().wrapSelection("italic",null)},activeState:function(){return this.$editor().queryCommandState("italic")},commandKeyCode:105}),a("underline",{iconclass:"fa fa-underline",tooltiptext:c.underline.tooltip,action:function(){return this.$editor().wrapSelection("underline",null)},activeState:function(){return this.$editor().queryCommandState("underline")},commandKeyCode:117}),a("clear",{iconclass:"fa fa-ban",tooltiptext:c.clear.tooltip,action:function(a,b){this.$editor().wrapSelection("removeFormat",null);var c=angular.element(d.getSelectionElement()),e=function(a){a=angular.element(a);var b=a;angular.forEach(a.children(),function(a){var c=angular.element("

    ");c.html(angular.element(a).html()),b.after(c),b=c}),a.remove()};angular.forEach(c.find("ul"),e),angular.forEach(c.find("ol"),e);var f=this.$editor(),g=function(a){a=angular.element(a),a[0]!==f.displayElements.text[0]&&a.removeAttr("class"),angular.forEach(a.children(),g)};angular.forEach(c,g),"li"!==c[0].tagName.toLowerCase()&&"ol"!==c[0].tagName.toLowerCase()&&"ul"!==c[0].tagName.toLowerCase()&&this.$editor().wrapSelection("formatBlock","

    "),b()}});var g=function(a,b,c){var d=function(){c.updateTaBindtaTextElement(),c.hidePopover()};a.preventDefault(),c.displayElements.popover.css("width","375px");var e=c.displayElements.popoverContainer;e.empty();var f=angular.element('

    '),g=angular.element('');g.on("click",function(a){a.preventDefault(),b.css({width:"100%",height:""}),d()});var h=angular.element('');h.on("click",function(a){a.preventDefault(),b.css({width:"50%",height:""}),d()});var i=angular.element('');i.on("click",function(a){a.preventDefault(),b.css({width:"25%",height:""}),d()});var j=angular.element('');j.on("click",function(a){a.preventDefault(),b.css({width:"",height:""}),d()}),f.append(g),f.append(h),f.append(i),f.append(j),e.append(f),f=angular.element('
    ');var k=angular.element('');k.on("click",function(a){a.preventDefault(),b.css("float","left"),d()});var l=angular.element('');l.on("click",function(a){a.preventDefault(),b.css("float","right"),d()});var m=angular.element('');m.on("click",function(a){a.preventDefault(),b.css("float",""),d()}),f.append(k),f.append(m),f.append(l),e.append(f),f=angular.element('
    ');var n=angular.element('');n.on("click",function(a){a.preventDefault(),b.remove(),d()}),f.append(n),e.append(f),c.showPopover(b),c.showResizeOverlay(b)};a("insertImage",{iconclass:"fa fa-picture-o",tooltiptext:c.insertImage.tooltip,action:function(){var a;return a=b.prompt(c.insertImage.dialogPrompt,"http://"),a&&""!==a&&"http://"!==a?this.$editor().wrapSelection("insertImage",a,!0):void 0},onElementSelect:{element:"img",action:g}}),a("insertVideo",{iconclass:"fa fa-youtube-play",tooltiptext:c.insertVideo.tooltip,action:function(){var a;if(a=b.prompt(c.insertVideo.dialogPrompt,"http://"),a&&""!==a&&"http://"!==a){var d=a.match(/(\?|&)v=[^&]*/);if(d.length>0){var e="http://www.youtube.com/embed/"+d[0].substring(3),f='';return this.$editor().wrapSelection("insertHTML",f,!0)}}},onElementSelect:{element:"img",onlyWithAttrs:["ta-insert-video"],action:g}}),a("insertLink",{tooltiptext:c.insertLink.tooltip,iconclass:"fa fa-link",action:function(){var a;return a=b.prompt(c.insertLink.dialogPrompt,"http://"),a&&""!==a&&"http://"!==a?this.$editor().wrapSelection("createLink",a,!0):void 0},activeState:function(a){return a?"A"===a[0].tagName:!1},onElementSelect:{element:"a",action:function(a,d,e){a.preventDefault(),e.displayElements.popover.css("width","435px");var f=e.displayElements.popoverContainer;f.empty(),f.css("line-height","28px");var g=angular.element(''+d.attr("href")+"");g.css({display:"inline-block","max-width":"200px",overflow:"hidden","text-overflow":"ellipsis","white-space":"nowrap","vertical-align":"middle"}),f.append(g);var h=angular.element('
    '),i=angular.element('');i.on("click",function(a){a.preventDefault();var f=b.prompt(c.insertLink.dialogPrompt,d.attr("href"));f&&""!==f&&"http://"!==f&&(d.attr("href",f),e.updateTaBindtaTextElement()),e.hidePopover()}),h.append(i);var j=angular.element('');j.on("click",function(a){a.preventDefault(),d.replaceWith(d.contents()),e.updateTaBindtaTextElement(),e.hidePopover()}),h.append(j);var k=angular.element('');"_blank"===d.attr("target")&&k.addClass("active"),k.on("click",function(a){a.preventDefault(),d.attr("target","_blank"===d.attr("target")?"":"_blank"),k.toggleClass("active"),e.updateTaBindtaTextElement()}),h.append(k),f.append(h),e.showPopover(d)}}})}]),function(){"Use Strict";function a(a){try{return 0!==angular.element(a).length}catch(b){return!1}}function b(a,c){var d=[],e=a.children();return e.length&&angular.forEach(e,function(a){d=d.concat(b(angular.element(a),c))}),void 0!==a.attr(c)&&d.push(a),d}function c(b,c){if(!b||""===b||n.hasOwnProperty(b))throw"textAngular Error: A unique name is required for a Tool Definition";if(c.display&&(""===c.display||!a(c.display))||!c.display&&!c.buttontext&&!c.iconclass)throw'textAngular Error: Tool Definition for "'+b+'" does not have a valid display/iconclass/buttontext value';n[b]=c}var d=!1;/AppleWebKit\/([\d.]+)/.exec(navigator.userAgent)&&(document.addEventListener("click",function(){var a=window.event.target;if(d&&null!==a){for(var b=!1,c=a;null!==c&&"html"!==c.tagName.toLowerCase()&&!b;)b="true"===c.contentEditable,c=c.parentNode;b||(document.getElementById("textAngular-editableFix-010203040506070809").setSelectionRange(0,0),a.focus())}d=!1},!1),angular.element(document).ready(function(){angular.element(document.body).append(angular.element(''))}));var e=function(){var a,b=-1,c=window.navigator.userAgent,d=c.indexOf("MSIE "),e=c.indexOf("Trident/");if(d>0)b=parseInt(c.substring(d+5,c.indexOf(".",d)),10);else if(e>0){var f=c.indexOf("rv:");b=parseInt(c.substring(f+3,c.indexOf(".",f)),10)}return b>-1?b:a}();"function"!=typeof String.prototype.trim&&(String.prototype.trim=function(){return this.replace(/^\s\s*/,"").replace(/\s\s*$/,"")});var f,g,h,i,j;if(e>8||void 0===e){var k=function(){var a=document.createElement("style");return/AppleWebKit\/([\d.]+)/.exec(navigator.userAgent)&&a.appendChild(document.createTextNode("")),document.head.insertBefore(a,document.head.firstChild),a.sheet}();f=function(){var a=document.createElement("style");return/AppleWebKit\/([\d.]+)/.exec(navigator.userAgent)&&a.appendChild(document.createTextNode("")),document.head.appendChild(a),a.sheet}(),g=function(a,b){i(f,a,b)},i=function(a,b,c){var d;return a.rules?d=Math.max(a.rules.length-1,0):a.cssRules&&(d=Math.max(a.cssRules.length-1,0)),a.insertRule?a.insertRule(b+"{"+c+"}",d):a.addRule(b,c,d),d},h=function(a){j(f,a)},j=function(a,b){a.removeRule?a.removeRule(b):a.deleteRule(b)},i(k,".ta-scroll-window.form-control","height: auto; min-height: 300px; overflow: auto; font-family: inherit; font-size: 100%; position: relative; padding: 0;"),i(k,".ta-root.focussed .ta-scroll-window.form-control","border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);"),i(k,".ta-editor.ta-html","min-height: 300px; height: auto; overflow: auto; font-family: inherit; font-size: 100%;"),i(k,".ta-scroll-window > .ta-bind","height: auto; min-height: 300px; padding: 6px 12px;"),i(k,".ta-root .ta-resizer-handle-overlay","z-index: 100; position: absolute; display: none;"),i(k,".ta-root .ta-resizer-handle-overlay > .ta-resizer-handle-info","position: absolute; bottom: 16px; right: 16px; border: 1px solid black; background-color: #FFF; padding: 0 4px; opacity: 0.7;"),i(k,".ta-root .ta-resizer-handle-overlay > .ta-resizer-handle-background","position: absolute; bottom: 5px; right: 5px; left: 5px; top: 5px; border: 1px solid black; background-color: rgba(0, 0, 0, 0.2);"),i(k,".ta-root .ta-resizer-handle-overlay > .ta-resizer-handle-corner","width: 10px; height: 10px; position: absolute;"),i(k,".ta-root .ta-resizer-handle-overlay > .ta-resizer-handle-corner-tl","top: 0; left: 0; border-left: 1px solid black; border-top: 1px solid black;"),i(k,".ta-root .ta-resizer-handle-overlay > .ta-resizer-handle-corner-tr","top: 0; right: 0; border-right: 1px solid black; border-top: 1px solid black;"),i(k,".ta-root .ta-resizer-handle-overlay > .ta-resizer-handle-corner-bl","bottom: 0; left: 0; border-left: 1px solid black; border-bottom: 1px solid black;"),i(k,".ta-root .ta-resizer-handle-overlay > .ta-resizer-handle-corner-br","bottom: 0; right: 0; border: 1px solid black; cursor: se-resize; background-color: white;")}var l=!1,m=angular.module("textAngular",["ngSanitize","textAngularSetup"]),n={};m.constant("taRegisterTool",c),m.value("taTools",n),m.config([function(){angular.forEach(n,function(a,b){delete n[b]})}]),m.directive("textAngular",["$compile","$timeout","taOptions","taSelection","taExecCommand","textAngularManager","$window","$document","$animate","$log",function(a,b,c,d,e,f,g,h,i,j){return{require:"?ngModel",scope:{},restrict:"EA",link:function(k,l,m,n){var o,p,q,r,s,t,u,v,w,x=m.serial?m.serial:Math.floor(1e16*Math.random()),y=m.name?m.name:"textAngularEditor"+x,z=function(a,c,d){b(function(){var b=function(){a.off(c,b),d()};a.on(c,b)},100)};w=e(m.taDefaultWrap),angular.extend(k,angular.copy(c),{wrapSelection:function(a,b,c){w(a,!1,b),c&&k["reApplyOnSelectorHandlerstaTextElement"+x](),k.displayElements.text[0].focus()},showHtml:!1}),m.taFocussedClass&&(k.classes.focussed=m.taFocussedClass),m.taTextEditorClass&&(k.classes.textEditor=m.taTextEditorClass),m.taHtmlEditorClass&&(k.classes.htmlEditor=m.taHtmlEditorClass),m.taTextEditorSetup&&(k.setup.textEditorSetup=k.$parent.$eval(m.taTextEditorSetup)),m.taHtmlEditorSetup&&(k.setup.htmlEditorSetup=k.$parent.$eval(m.taHtmlEditorSetup)),k.fileDropHandler=m.taFileDrop?k.$parent.$eval(m.taFileDrop):k.defaultFileDropHandler,u=l[0].innerHTML,l[0].innerHTML="",k.displayElements={forminput:angular.element(""),html:angular.element(""),text:angular.element("
    "),scrollWindow:angular.element("
    "),popover:angular.element('
    '),popoverArrow:angular.element('
    '),popoverContainer:angular.element('
    '),resize:{overlay:angular.element('
    '),background:angular.element('
    '),anchors:[angular.element('
    '),angular.element('
    '),angular.element('
    '),angular.element('
    ')],info:angular.element('
    ')}},k.displayElements.popover.append(k.displayElements.popoverArrow),k.displayElements.popover.append(k.displayElements.popoverContainer),k.displayElements.scrollWindow.append(k.displayElements.popover),k.displayElements.popover.on("mousedown",function(a,b){return b&&angular.extend(a,b),a.preventDefault(),!1}),k.showPopover=function(a){k.displayElements.popover.css("display","block"),k.reflowPopover(a),i.addClass(k.displayElements.popover,"in"),z(l,"click keyup",function(){k.hidePopover()})},k.reflowPopover=function(a){k.displayElements.text[0].offsetHeight-51>a[0].offsetTop?(k.displayElements.popover.css("top",a[0].offsetTop+a[0].offsetHeight+"px"),k.displayElements.popover.removeClass("top").addClass("bottom")):(k.displayElements.popover.css("top",a[0].offsetTop-54+"px"),k.displayElements.popover.removeClass("bottom").addClass("top"));var b=k.displayElements.text[0].offsetWidth-k.displayElements.popover[0].offsetWidth,c=a[0].offsetLeft+a[0].offsetWidth/2-k.displayElements.popover[0].offsetWidth/2;k.displayElements.popover.css("left",Math.max(0,Math.min(b,c))+"px"),k.displayElements.popoverArrow.css("margin-left",Math.min(c,Math.max(0,c-b))-11+"px")},k.hidePopover=function(){i.removeClass(k.displayElements.popover,"in",function(){k.displayElements.popover.css("display",""),k.displayElements.popoverContainer.attr("style",""),k.displayElements.popoverContainer.attr("class","popover-content")})},k.displayElements.resize.overlay.append(k.displayElements.resize.background),angular.forEach(k.displayElements.resize.anchors,function(a){k.displayElements.resize.overlay.append(a)}),k.displayElements.resize.overlay.append(k.displayElements.resize.info),k.displayElements.scrollWindow.append(k.displayElements.resize.overlay),k.reflowResizeOverlay=function(a){a=angular.element(a)[0],k.displayElements.resize.overlay.css({display:"block",left:a.offsetLeft-5+"px",top:a.offsetTop-5+"px",width:a.offsetWidth+10+"px",height:a.offsetHeight+10+"px"}),k.displayElements.resize.info.text(a.offsetWidth+" x "+a.offsetHeight)},k.showResizeOverlay=function(a){var b=function(b){var c={width:parseInt(a.attr("width")),height:parseInt(a.attr("height")),x:b.clientX,y:b.clientY};void 0===c.width&&(c.width=a[0].offsetWidth),void 0===c.height&&(c.height=a[0].offsetHeight),k.hidePopover();var d=c.height/c.width,e=function(b){var e={x:Math.max(0,c.width+(b.clientX-c.x)),y:Math.max(0,c.height+(b.clientY-c.y))},f=function(a,b){a=angular.element(a),"img"===a[0].tagName.toLowerCase()&&(b.height&&(a.attr("height",b.height),delete b.height),b.width&&(a.attr("width",b.width),delete b.width)),a.css(b)};if(b.shiftKey){var g=e.y/e.x;f(a,{width:d>g?e.x:e.y/d,height:d>g?e.x*d:e.y})}else f(a,{width:e.x,height:e.y});k.reflowResizeOverlay(a)};h.find("body").on("mousemove",e),z(k.displayElements.resize.overlay,"mouseup",function(){h.find("body").off("mousemove",e),k.showPopover(a)}),b.stopPropagation(),b.preventDefault()};k.displayElements.resize.anchors[3].on("mousedown",b),k.reflowResizeOverlay(a),z(l,"click",function(){k.hideResizeOverlay()})},k.hideResizeOverlay=function(){k.displayElements.resize.overlay.css("display","")},k.setup.htmlEditorSetup(k.displayElements.html),k.setup.textEditorSetup(k.displayElements.text),k.displayElements.html.attr({id:"taHtmlElement"+x,"ng-show":"showHtml","ta-bind":"ta-bind","ng-model":"html"}),k.displayElements.text.attr({id:"taTextElement"+x,contentEditable:"true","ta-bind":"ta-bind","ng-model":"html"}),k.displayElements.scrollWindow.attr({"ng-hide":"showHtml"}),m.taDefaultWrap&&k.displayElements.text.attr("ta-default-wrap",m.taDefaultWrap),m.taUnsafeSanitizer&&(k.displayElements.text.attr("ta-unsafe-sanitizer",m.taUnsafeSanitizer),k.displayElements.html.attr("ta-unsafe-sanitizer",m.taUnsafeSanitizer)),k.displayElements.scrollWindow.append(k.displayElements.text),l.append(k.displayElements.scrollWindow),l.append(k.displayElements.html),k.displayElements.forminput.attr("name",y),l.append(k.displayElements.forminput),m.tabindex&&(l.removeAttr("tabindex"),k.displayElements.text.attr("tabindex",m.tabindex),k.displayElements.html.attr("tabindex",m.tabindex)),m.placeholder&&(k.displayElements.text.attr("placeholder",m.placeholder),k.displayElements.html.attr("placeholder",m.placeholder)),m.taDisabled&&(k.displayElements.text.attr("ta-readonly","disabled"),k.displayElements.html.attr("ta-readonly","disabled"),k.disabled=k.$parent.$eval(m.taDisabled),k.$parent.$watch(m.taDisabled,function(a){k.disabled=a,k.disabled?l.addClass(k.classes.disabled):l.removeClass(k.classes.disabled)})),a(k.displayElements.scrollWindow)(k),a(k.displayElements.html)(k),k.updateTaBindtaTextElement=k["updateTaBindtaTextElement"+x],k.updateTaBindtaHtmlElement=k["updateTaBindtaHtmlElement"+x],l.addClass("ta-root"),k.displayElements.scrollWindow.addClass("ta-text ta-editor "+k.classes.textEditor),k.displayElements.html.addClass("ta-html ta-editor "+k.classes.htmlEditor),k._actionRunning=!1;var A=!1;if(k.startAction=function(){return k._actionRunning=!0,g.rangy&&g.rangy.saveSelection?(A=g.rangy.saveSelection(),function(){A&&g.rangy.restoreSelection(A)}):void 0},k.endAction=function(){k._actionRunning=!1,A&&g.rangy.removeMarkers(A),A=!1,k.updateSelectedStyles(),k.showHtml||k["updateTaBindtaTextElement"+x]()},s=function(){l.addClass(k.classes.focussed),v.focus()},k.displayElements.html.on("focus",s),k.displayElements.text.on("focus",s),t=function(a){return k._actionRunning||h[0].activeElement===k.displayElements.html[0]||h[0].activeElement===k.displayElements.text[0]||(l.removeClass(k.classes.focussed),v.unfocus(),b(function(){l.triggerHandler("blur")},0)),a.preventDefault(),!1},k.displayElements.html.on("blur",t),k.displayElements.text.on("blur",t),k.queryFormatBlockState=function(a){return!k.showHtml&&a.toLowerCase()===h[0].queryCommandValue("formatBlock").toLowerCase()},k.queryCommandState=function(a){return k.showHtml?"":h[0].queryCommandState(a)},k.switchView=function(){k.showHtml=!k.showHtml,k.showHtml?b(function(){return k.displayElements.html[0].focus()},100):b(function(){return k.displayElements.text[0].focus()},100)},m.ngModel){var B=!0;n.$render=function(){if(B){B=!1;var a=k.$parent.$eval(m.ngModel);void 0!==a&&null!==a||!u||""===u||n.$setViewValue(u)}k.displayElements.forminput.val(n.$viewValue),k._elementSelectTriggered||h[0].activeElement===k.displayElements.html[0]||h[0].activeElement===k.displayElements.text[0]||(k.html=n.$viewValue||"")};var C=function(a){return m.required&&n.$setValidity("required",!(!a||""===a.trim())),a};n.$parsers.push(C),n.$formatters.push(C)}else k.displayElements.forminput.val(u),k.html=u;if(k.$watch("html",function(a,b){a!==b&&(m.ngModel&&n.$viewValue!==a&&n.$setViewValue(a),k.displayElements.forminput.val(a))}),m.taTargetToolbars)v=f.registerEditor(y,k,m.taTargetToolbars.split(","));else{var D=angular.element('
    ');m.taToolbar&&D.attr("ta-toolbar",m.taToolbar),m.taToolbarClass&&D.attr("ta-toolbar-class",m.taToolbarClass),m.taToolbarGroupClass&&D.attr("ta-toolbar-group-class",m.taToolbarGroupClass),m.taToolbarButtonClass&&D.attr("ta-toolbar-button-class",m.taToolbarButtonClass),m.taToolbarActiveButtonClass&&D.attr("ta-toolbar-active-button-class",m.taToolbarActiveButtonClass),m.taFocussedClass&&D.attr("ta-focussed-class",m.taFocussedClass),l.prepend(D),a(D)(k.$parent),v=f.registerEditor(y,k,["textAngularToolbar"+x])}k.$on("$destroy",function(){f.unregisterEditor(y)}),k.$on("ta-element-select",function(a,b){v.triggerElementSelect(a,b)}),k.$on("ta-drop-event",function(a,b,c,d){k.displayElements.text[0].focus(),d&&d.files&&d.files.length>0&&(angular.forEach(d.files,function(a){try{return k.fileDropHandler(a,k.wrapSelection)||k.fileDropHandler!==k.defaultFileDropHandler&&k.defaultFileDropHandler(a,k.wrapSelection)}catch(b){j.error(b)}}),c.preventDefault(),c.stopPropagation())}),k._bUpdateSelectedStyles=!1,k.updateSelectedStyles=function(){var a;void 0!==(a=d.getSelectionElement())&&a.parentNode!==k.displayElements.text[0]?v.updateSelectedStyles(angular.element(a)):v.updateSelectedStyles(),k._bUpdateSelectedStyles&&b(k.updateSelectedStyles,200)},o=function(){k._bUpdateSelectedStyles||(k._bUpdateSelectedStyles=!0,k.$apply(function(){k.updateSelectedStyles()}))},k.displayElements.html.on("keydown",o),k.displayElements.text.on("keydown",o),p=function(){k._bUpdateSelectedStyles=!1},k.displayElements.html.on("keyup",p),k.displayElements.text.on("keyup",p),q=function(a,b){b&&angular.extend(a,b),k.$apply(function(){return v.sendKeyCommand(a)?(k._bUpdateSelectedStyles||k.updateSelectedStyles(),a.preventDefault(),!1):void 0})},k.displayElements.html.on("keypress",q),k.displayElements.text.on("keypress",q),r=function(){k._bUpdateSelectedStyles=!1,k.$apply(function(){k.updateSelectedStyles()})},k.displayElements.html.on("mouseup",r),k.displayElements.text.on("mouseup",r)}}}]).factory("taBrowserTag",[function(){return function(a){return a?""===a?void 0===e?"div":8>=e?"P":"p":8>=e?a.toUpperCase():a:8>=e?"P":"p"}}]).factory("taExecCommand",["taSelection","taBrowserTag","$document",function(a,b,c){var d=/^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video)$/gi,e=/^(ul|li|ol)$/gi,f=function(b,c){var d,e,f=b.find("li");for(e=f.length-1;e>=0;e--)d=angular.element("<"+c+">"+f[e].innerHTML+""),b.after(d);b.remove(),a.setSelectionToElementEnd(d[0])},g=function(b,c){var d=angular.element("<"+c+">"+b[0].innerHTML+"");b.after(d),b.remove(),a.setSelectionToElementEnd(d.find("li")[0])},h=function(c,d,e){for(var f="",g=0;g"+c[g].innerHTML+"";var h=angular.element("<"+e+">"+f+"");d.after(h),d.remove(),a.setSelectionToElementEnd(h.find("li")[0])};return function(i){return i=b(i),function(j,k,l){var m,n,o,p,q,r=angular.element("<"+i+">"),s=a.getSelectionElement(),t=angular.element(s);if(void 0!==s){var u=s.tagName.toLowerCase();if("insertorderedlist"===j.toLowerCase()||"insertunorderedlist"===j.toLowerCase()){var v=b("insertorderedlist"===j.toLowerCase()?"ol":"ul");if(u===v)return f(t,i);if("li"===u&&t.parent()[0].tagName.toLowerCase()===v&&1===t.parent().children().length)return f(t.parent(),i);if("li"===u&&t.parent()[0].tagName.toLowerCase()!==v&&1===t.parent().children().length)return g(t.parent(),v);if(u.match(d)&&!t.hasClass("ta-bind")){if("ol"===u||"ul"===u)return g(t,v);var w=!1;return angular.forEach(t.children(),function(a){a.tagName.match(d)&&(w=!0)}),w?h(t.children(),t,v):h([angular.element("
    "+s.innerHTML+"
    ")[0]],t,v)}if(u.match(d)){if(p=a.getOnlySelectedElements(),1===p.length&&("ol"===p[0].tagName.toLowerCase()||"ul"===p[0].tagName.toLowerCase()))return p[0].tagName.toLowerCase()===v?f(angular.element(p[0]),i):g(angular.element(p[0]),v);o="";var x=[];for(m=0;m"+y[0].innerHTML+"",x.unshift(y)}return n=angular.element("<"+v+">"+o+""),x.pop().replaceWith(n),angular.forEach(x,function(a){a.remove()}),void a.setSelectionToElementEnd(n[0])}}else if("formatblock"===j.toLowerCase()){var z=l.toLowerCase().replace(/[<>]/gi,"");for(n="li"===u?t.parent():t;!n[0].tagName.match(d);)n=n.parent(),u=n[0].tagName.toLowerCase();if(u===z){p=n.children();var A=!1;for(m=0;m"),r[0].innerHTML=D[m].outerHTML,D[m]=r[0]),C.parent()[0].insertBefore(D[m],C[0]);C.remove()}return void a.setSelectionToElementEnd(n[0])}}try{c[0].execCommand(j,k,l)}catch(E){}}}}]).directive("taBind",["taSanitize","$timeout","$window","$document","taFixChrome","taBrowserTag","taSelection","taSelectableElements","taApplyCustomRenderers","taOptions",function(a,b,c,f,i,j,k,m,n,o){return{require:"ngModel",scope:{},link:function(j,p,q,r){var s,t,u=void 0!==p.attr("contenteditable")&&p.attr("contenteditable"),v=u||"textarea"===p[0].tagName.toLowerCase()||"input"===p[0].tagName.toLowerCase(),w=!1,x=!1,y=q.taUnsafeSanitizer||o.disableSanitizer;void 0===q.taDefaultWrap&&(q.taDefaultWrap="p"),""===q.taDefaultWrap?(s="",t=void 0===e?"

    ":e>=11?"


    ":8>=e?"

     

    ":"

     

    "):(s=void 0===e||e>=11?"<"+q.taDefaultWrap+">
    ":8>=e?"<"+q.taDefaultWrap.toUpperCase()+">":"<"+q.taDefaultWrap+">",t=void 0===e||e>=11?"<"+q.taDefaultWrap+">
    ":8>=e?"<"+q.taDefaultWrap.toUpperCase()+"> ":"<"+q.taDefaultWrap+"> "),p.addClass("ta-bind"); -var z=function(){if(u)return p[0].innerHTML;if(v)return p.val();throw"textAngular Error: attempting to update non-editable taBind"},A=function(a){a||(a=z()),a===t?""!==r.$viewValue&&r.$setViewValue(""):r.$viewValue!==a&&r.$setViewValue(a)};if(j.$parent["updateTaBind"+(q.id||"")]=function(){w||A()},v)if(u){if(p.on("cut",function(a){w?a.preventDefault():b(function(){A()},0)}),p.on("paste",function(a,b){b&&angular.extend(a,b);var d;if(a.clipboardData||a.originalEvent&&a.originalEvent.clipboardData?d=(a.originalEvent||a).clipboardData.getData("text/plain"):c.clipboardData&&(d=c.clipboardData.getData("Text")),!d&&!w)return!0;if(a.preventDefault(),!w){var e=angular.element("
    ");if(e[0].innerHTML=d,d=e.text(),f[0].selection){var g=f[0].selection.createRange();g.pasteHTML(d)}else f[0].execCommand("insertText",!1,d);A()}}),p.on("keyup",function(a,b){if(b&&angular.extend(a,b),!w){if(""!==s&&13===a.keyCode&&!a.shiftKey){var c=k.getSelectionElement();if(c.tagName.toLowerCase()!==q.taDefaultWrap&&"li"!==c.tagName.toLowerCase()&&(""===c.innerHTML.trim()||"
    "===c.innerHTML.trim())){var d=angular.element(s);angular.element(c).replaceWith(d),k.setSelectionToElementStart(d[0])}}var e=z();""!==s&&""===e.trim()&&(p[0].innerHTML=s,k.setSelectionToElementStart(p.children()[0])),A(e)}}),p.on("blur",function(){x=!1,w||A(),r.$render()}),q.placeholder&&(e>8||void 0===e)){var B;if(!q.id)throw"textAngular Error: An unique ID is required for placeholders to work";B=g("#"+q.id+".placeholder-text:before",'content: "'+q.placeholder+'"'),j.$on("$destroy",function(){h(B)})}p.on("focus",function(){x=!0,r.$render()}),p.on("mousedown",function(a,b){b&&angular.extend(a,b),a.stopPropagation()})}else p.on("paste cut",function(){w||b(function(){r.$setViewValue(z())},0)}),p.on("change blur",function(){w||r.$setViewValue(z())});var C=function(b){return r.$oldViewValue=a(i(b),r.$oldViewValue,y)},D=function(a){return q.required&&r.$setValidity("required",!(!a||a.trim()===t||""===a.trim())),a};r.$parsers.push(C),r.$parsers.push(D),r.$formatters.push(C),r.$formatters.push(D);var E=function(a){return j.$emit("ta-element-select",this),a.preventDefault(),!1},F=function(a,c){if(c&&angular.extend(a,c),!l&&!w){l=!0;var d;d=a.originalEvent?a.originalEvent.dataTransfer:a.dataTransfer,j.$emit("ta-drop-event",this,a,d),b(function(){l=!1},100)}};j.$parent["reApplyOnSelectorHandlers"+(q.id||"")]=function(){w||angular.forEach(m,function(a){p.find(a).off("click",E).on("click",E)})};var G=function(a){p[0].innerHTML=a};r.$render=function(){var a=r.$viewValue||"";f[0].activeElement!==p[0]?u?(q.placeholder?""===a?(x?p.removeClass("placeholder-text"):p.addClass("placeholder-text"),G(s)):(p.removeClass("placeholder-text"),G(a)):G(""===a?s:a),w?p.off("drop",F):(angular.forEach(m,function(a){p.find(a).on("click",E)}),p.on("drop",F))):"textarea"!==p[0].tagName.toLowerCase()&&"input"!==p[0].tagName.toLowerCase()?G(n(a)):p.val(a):u&&p.removeClass("placeholder-text")},q.taReadonly&&(w=j.$parent.$eval(q.taReadonly),w?(p.addClass("ta-readonly"),("textarea"===p[0].tagName.toLowerCase()||"input"===p[0].tagName.toLowerCase())&&p.attr("disabled","disabled"),void 0!==p.attr("contenteditable")&&p.attr("contenteditable")&&p.removeAttr("contenteditable")):(p.removeClass("ta-readonly"),"textarea"===p[0].tagName.toLowerCase()||"input"===p[0].tagName.toLowerCase()?p.removeAttr("disabled"):u&&p.attr("contenteditable","true")),j.$parent.$watch(q.taReadonly,function(a,b){b!==a&&(a?(p.addClass("ta-readonly"),("textarea"===p[0].tagName.toLowerCase()||"input"===p[0].tagName.toLowerCase())&&p.attr("disabled","disabled"),void 0!==p.attr("contenteditable")&&p.attr("contenteditable")&&p.removeAttr("contenteditable"),angular.forEach(m,function(a){p.find(a).on("click",E)}),p.off("drop",F)):(p.removeClass("ta-readonly"),"textarea"===p[0].tagName.toLowerCase()||"input"===p[0].tagName.toLowerCase()?p.removeAttr("disabled"):u&&p.attr("contenteditable","true"),angular.forEach(m,function(a){p.find(a).off("click",E)}),p.on("drop",F)),w=a)})),u&&!w&&(angular.forEach(m,function(a){p.find(a).on("click",E)}),p.on("drop",F),p.on("blur",function(){/AppleWebKit\/([\d.]+)/.exec(navigator.userAgent)&&(d=!0)}))}}}]).factory("taApplyCustomRenderers",["taCustomRenderers",function(a){return function(c){var d=angular.element("
    ");return d[0].innerHTML=c,angular.forEach(a,function(a){var c=[];a.selector&&""!==a.selector?c=d.find(a.selector):a.customAttribute&&""!==a.customAttribute&&(c=b(d,a.customAttribute)),angular.forEach(c,function(b){b=angular.element(b),a.selector&&""!==a.selector&&a.customAttribute&&""!==a.customAttribute?void 0!==b.attr(a.customAttribute)&&a.renderLogic(b):a.renderLogic(b)})}),d[0].innerHTML}}]).directive("taMaxText",function(){return{restrict:"A",require:"ngModel",link:function(a,b,c,d){function e(a){var b=angular.element("
    ");b.html(a);var c=b.text().length;return f>=c?(d.$setValidity("taMaxText",!0),a):void d.$setValidity("taMaxText",!1)}var f=parseInt(a.$eval(c.taMaxText));if(isNaN(f))throw"Max text must be an integer";c.$observe("taMaxText",function(a){if(f=parseInt(a),isNaN(f))throw"Max text must be an integer";d.$dirty&&d.$setViewValue(d.$viewValue)}),d.$parsers.unshift(e)}}}).directive("taMinText",function(){return{restrict:"A",require:"ngModel",link:function(a,b,c,d){function e(a){var b=angular.element("
    ");b.html(a);var c=b.text().length;return!c||c>=f?(d.$setValidity("taMinText",!0),a):void d.$setValidity("taMinText",!1)}var f=parseInt(a.$eval(c.taMinText));if(isNaN(f))throw"Min text must be an integer";c.$observe("taMinText",function(a){if(f=parseInt(a),isNaN(f))throw"Min text must be an integer";d.$dirty&&d.$setViewValue(d.$viewValue)}),d.$parsers.unshift(e)}}}).factory("taFixChrome",function(){var a=function(a){for(var b=angular.element("
    "+a+"
    "),c=angular.element(b).find("span"),d=0;d0&&"BR"===e.next()[0].tagName&&e.next().remove(),e.replaceWith(e[0].innerHTML)))}var f=b[0].innerHTML.replace(/style="[^"]*?(line-height: 1.428571429;|color: inherit; line-height: 1.1;)[^"]*"/gi,"");return f!==b[0].innerHTML&&(b[0].innerHTML=f),b[0].innerHTML};return a}).factory("taSanitize",["$sanitize",function(a){return function(c,d,e){var f=angular.element("
    "+c+"
    ");angular.forEach(b(f,"align"),function(a){a.css("text-align",a.attr("align")),a.removeAttr("align")});var g;c=f[0].innerHTML;try{g=a(c),e&&(g=c)}catch(h){g=d||""}return g}}]).directive("textAngularToolbar",["$compile","textAngularManager","taOptions","taTools","taToolExecuteAction","$window",function(a,b,c,d,e,f){return{scope:{name:"@"},restrict:"EA",link:function(g,h,i){if(!g.name||""===g.name)throw"textAngular Error: A toolbar requires a name";angular.extend(g,angular.copy(c)),i.taToolbar&&(g.toolbar=g.$parent.$eval(i.taToolbar)),i.taToolbarClass&&(g.classes.toolbar=i.taToolbarClass),i.taToolbarGroupClass&&(g.classes.toolbarGroup=i.taToolbarGroupClass),i.taToolbarButtonClass&&(g.classes.toolbarButton=i.taToolbarButtonClass),i.taToolbarActiveButtonClass&&(g.classes.toolbarButtonActive=i.taToolbarActiveButtonClass),i.taFocussedClass&&(g.classes.focussed=i.taFocussedClass),g.disabled=!0,g.focussed=!1,g._$element=h,h[0].innerHTML="",h.addClass("ta-toolbar "+g.classes.toolbar),g.$watch("focussed",function(){g.focussed?h.addClass(g.classes.focussed):h.removeClass(g.classes.focussed)});var j=function(b,c){var d;if(d=angular.element(b&&b.display?b.display:"');g.on("click",function(a){a.preventDefault(),b.css({width:"100%",height:""}),d()});var h=angular.element('');h.on("click",function(a){a.preventDefault(),b.css({width:"50%",height:""}),d()});var i=angular.element('');i.on("click",function(a){a.preventDefault(),b.css({width:"25%",height:""}),d()});var j=angular.element('');j.on("click",function(a){a.preventDefault(),b.css({width:"",height:""}),d()}),f.append(g),f.append(h),f.append(i),f.append(j),e.append(f),f=angular.element('
    ');var k=angular.element('');k.on("click",function(a){a.preventDefault(),b.css("float","left"),b.css("cssFloat","left"),b.css("styleFloat","left"),d()});var l=angular.element('');l.on("click",function(a){a.preventDefault(),b.css("float","right"),b.css("cssFloat","right"),b.css("styleFloat","right"),d()});var m=angular.element('');m.on("click",function(a){a.preventDefault(),b.css("float",""),b.css("cssFloat",""),b.css("styleFloat",""),d()}),f.append(k),f.append(m),f.append(l),e.append(f),f=angular.element('
    ');var n=angular.element('');n.on("click",function(a){a.preventDefault(),b.remove(),d()}),f.append(n),e.append(f),c.showPopover(b),c.showResizeOverlay(b)},aOnSelectAction:function(c,d,e){c.preventDefault(),e.displayElements.popover.css("width","436px");var f=e.displayElements.popoverContainer;f.empty(),f.css("line-height","28px");var g=angular.element(''+d.attr("href")+"");g.css({display:"inline-block","max-width":"200px",overflow:"hidden","text-overflow":"ellipsis","white-space":"nowrap","vertical-align":"middle"}),f.append(g);var h=angular.element('
    '),i=angular.element('');i.on("click",function(c){c.preventDefault();var f=a.prompt(b.insertLink.dialogPrompt,d.attr("href"));f&&""!==f&&"http://"!==f&&(d.attr("href",f),e.updateTaBindtaTextElement()),e.hidePopover()}),h.append(i);var j=angular.element('');j.on("click",function(a){a.preventDefault(),d.replaceWith(d.contents()),e.updateTaBindtaTextElement(),e.hidePopover()}),h.append(j);var k=angular.element('");"_blank"===d.attr("target")&&k.addClass("active"),k.on("click",function(a){a.preventDefault(),d.attr("target","_blank"===d.attr("target")?"":"_blank"),k.toggleClass("active"),e.updateTaBindtaTextElement()}),h.append(k),f.append(h),e.showPopover(d)},extractYoutubeVideoId:function(a){var b=/(?:youtube(?:-nocookie)?\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/i,c=a.match(b);return c&&c[1]||null}}}]).run(["taRegisterTool","$window","taTranslations","taSelection","taToolFunctions","$sanitize","taOptions",function(a,b,c,d,e,f,g){var h={};if(f("",h),g.forceTextAngularSanitize===!0&&"taSanitize"!==h.version)throw angular.$$minErr("textAngular")("textAngularSetup","The textAngular-sanitize provider has been replaced by another -- have you included angular-sanitize by mistake?");a("html",{iconclass:"fa fa-code",tooltiptext:c.html.tooltip,action:function(){this.$editor().switchView()},activeState:function(){return this.$editor().showHtml}});var i=function(a){return function(){return this.$editor().queryFormatBlockState(a)}},j=function(){return this.$editor().wrapSelection("formatBlock","<"+this.name.toUpperCase()+">")};angular.forEach(["h1","h2","h3","h4","h5","h6"],function(b){a(b.toLowerCase(),{buttontext:b.toUpperCase(),tooltiptext:c.heading.tooltip+b.charAt(1),action:j,activeState:i(b.toLowerCase())})}),a("p",{buttontext:"P",tooltiptext:c.p.tooltip,action:function(){return this.$editor().wrapSelection("formatBlock","

    ")},activeState:function(){return this.$editor().queryFormatBlockState("p")}}),a("pre",{buttontext:"pre",tooltiptext:c.pre.tooltip,action:function(){return this.$editor().wrapSelection("formatBlock","

    ")},activeState:function(){return this.$editor().queryFormatBlockState("pre")}}),a("ul",{iconclass:"fa fa-list-ul",tooltiptext:c.ul.tooltip,action:function(){return this.$editor().wrapSelection("insertUnorderedList",null)},activeState:function(){return this.$editor().queryCommandState("insertUnorderedList")}}),a("ol",{iconclass:"fa fa-list-ol",tooltiptext:c.ol.tooltip,action:function(){return this.$editor().wrapSelection("insertOrderedList",null)},activeState:function(){return this.$editor().queryCommandState("insertOrderedList")}}),a("quote",{iconclass:"fa fa-quote-right",tooltiptext:c.quote.tooltip,action:function(){return this.$editor().wrapSelection("formatBlock","
    ")},activeState:function(){return this.$editor().queryFormatBlockState("blockquote")}}),a("undo",{iconclass:"fa fa-undo",tooltiptext:c.undo.tooltip,action:function(){return this.$editor().wrapSelection("undo",null)}}),a("redo",{iconclass:"fa fa-repeat",tooltiptext:c.redo.tooltip,action:function(){return this.$editor().wrapSelection("redo",null)}}),a("bold",{iconclass:"fa fa-bold",tooltiptext:c.bold.tooltip,action:function(){return this.$editor().wrapSelection("bold",null)},activeState:function(){return this.$editor().queryCommandState("bold")},commandKeyCode:98}),a("justifyLeft",{iconclass:"fa fa-align-left",tooltiptext:c.justifyLeft.tooltip,action:function(){return this.$editor().wrapSelection("justifyLeft",null)},activeState:function(a){if(a&&"#document"===a.nodeName)return!1;var b=!1;return a&&(b="left"===a.css("text-align")||"left"===a.attr("align")||"right"!==a.css("text-align")&&"center"!==a.css("text-align")&&"justify"!==a.css("text-align")&&!this.$editor().queryCommandState("justifyRight")&&!this.$editor().queryCommandState("justifyCenter")&&!this.$editor().queryCommandState("justifyFull")),b=b||this.$editor().queryCommandState("justifyLeft")}}),a("justifyRight",{iconclass:"fa fa-align-right",tooltiptext:c.justifyRight.tooltip,action:function(){return this.$editor().wrapSelection("justifyRight",null)},activeState:function(a){if(a&&"#document"===a.nodeName)return!1;var b=!1;return a&&(b="right"===a.css("text-align")),b=b||this.$editor().queryCommandState("justifyRight")}}),a("justifyFull",{iconclass:"fa fa-align-justify",tooltiptext:c.justifyFull.tooltip,action:function(){return this.$editor().wrapSelection("justifyFull",null)},activeState:function(a){var b=!1;return a&&(b="justify"===a.css("text-align")),b=b||this.$editor().queryCommandState("justifyFull")}}),a("justifyCenter",{iconclass:"fa fa-align-center",tooltiptext:c.justifyCenter.tooltip,action:function(){return this.$editor().wrapSelection("justifyCenter",null)},activeState:function(a){if(a&&"#document"===a.nodeName)return!1;var b=!1;return a&&(b="center"===a.css("text-align")),b=b||this.$editor().queryCommandState("justifyCenter")}}),a("indent",{iconclass:"fa fa-indent",tooltiptext:c.indent.tooltip,action:function(){return this.$editor().wrapSelection("indent",null)},activeState:function(){return this.$editor().queryFormatBlockState("blockquote")},commandKeyCode:"TabKey"}),a("outdent",{iconclass:"fa fa-outdent",tooltiptext:c.outdent.tooltip,action:function(){return this.$editor().wrapSelection("outdent",null)},activeState:function(){return!1},commandKeyCode:"ShiftTabKey"}),a("italics",{iconclass:"fa fa-italic",tooltiptext:c.italic.tooltip,action:function(){return this.$editor().wrapSelection("italic",null)},activeState:function(){return this.$editor().queryCommandState("italic")},commandKeyCode:105}),a("underline",{iconclass:"fa fa-underline",tooltiptext:c.underline.tooltip,action:function(){return this.$editor().wrapSelection("underline",null)},activeState:function(){return this.$editor().queryCommandState("underline")},commandKeyCode:117}),a("strikeThrough",{iconclass:"fa fa-strikethrough",tooltiptext:c.strikeThrough.tooltip,action:function(){return this.$editor().wrapSelection("strikeThrough",null)},activeState:function(){return document.queryCommandState("strikeThrough")}}),a("clear",{iconclass:"fa fa-ban",tooltiptext:c.clear.tooltip,action:function(a,b){var c;this.$editor().wrapSelection("removeFormat",null);var e=angular.element(d.getSelectionElement()),f=function(a){a=angular.element(a);var b=a;angular.forEach(a.children(),function(a){var c=angular.element("

    ");c.html(angular.element(a).html()),b.after(c),b=c}),a.remove()};if(angular.forEach(e.find("ul"),f),angular.forEach(e.find("ol"),f),"li"===e[0].tagName.toLowerCase()){var g=e[0].parentNode.childNodes,h=[],i=[],j=!1;for(c=0;c

    ");if(l.html(angular.element(e[0]).html()),0===h.length||0===i.length)0===i.length?k.after(l):k[0].parentNode.insertBefore(l[0],k[0]),0===h.length&&0===i.length?k.remove():angular.element(e[0]).remove();else{var m=angular.element("<"+k[0].tagName+">"),n=angular.element("<"+k[0].tagName+">");for(c=0;c';return this.$editor().wrapSelection("insertHTML",f,!0)}},onElementSelect:{element:"img",onlyWithAttrs:["ta-insert-video"],action:e.imgOnSelectAction}}),a("insertLink",{tooltiptext:c.insertLink.tooltip,iconclass:"fa fa-link",action:function(){var a;return a=b.prompt(c.insertLink.dialogPrompt,"http://"),a&&""!==a&&"http://"!==a?this.$editor().wrapSelection("createLink",a,!0):void 0},activeState:function(a){return a?"A"===a[0].tagName:!1},onElementSelect:{element:"a",action:e.aOnSelectAction}}),a("wordcount",{display:'
    Words:
    ',disabled:!0,wordcount:0,activeState:function(){var a=this.$editor().displayElements.text,b=a[0].innerHTML||"",c=0;return""!==b.replace(/\s*<[^>]*?>\s*/g,"")&&(c=b.replace(/<\/?(b|i|em|strong|span|u|strikethrough|a|img|small|sub|sup|label)( [^>*?])?>/gi,"").replace(/(<[^>]*?>\s*<[^>]*?>)/gi," ").replace(/(<[^>]*?>)/gi,"").replace(/\s+/gi," ").match(/\S+/g).length),this.wordcount=c,this.$editor().wordcount=c,!1}}),a("charcount",{display:'
    Characters:
    ',disabled:!0,charcount:0,activeState:function(){var a=this.$editor().displayElements.text,b=a[0].innerText||a[0].textContent,c=b.replace(/(\r\n|\n|\r)/gm,"").replace(/^\s+/g," ").replace(/\s+$/g," ").length;return this.charcount=c,this.$editor().charcount=c,!1}})}]);var e={ie:function(){for(var a,b=3,c=document.createElement("div"),d=c.getElementsByTagName("i");c.innerHTML="",d[0];);return b>4?b:a}(),webkit:/AppleWebKit\/([\d.]+)/i.test(navigator.userAgent)},f=!1;e.webkit&&(document.addEventListener("mousedown",function(a){var b=a||window.event,c=b.target;if(f&&null!==c){for(var d=!1,e=c;null!==e&&"html"!==e.tagName.toLowerCase()&&!d;)d="true"===e.contentEditable,e=e.parentNode;d||(document.getElementById("textAngular-editableFix-010203040506070809").setSelectionRange(0,0),c.focus(),c.select&&c.select())}f=!1},!1),angular.element(document).ready(function(){angular.element(document.body).append(angular.element(''))}));var g=/^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video)$/i,h=/^(ul|li|ol)$/i,i=/^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video|li)$/i;String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")});var j,k,l,m,n,o;if(e.ie>8||void 0===e.ie){for(var p=document.styleSheets,q=0;q
    ");return d[0].innerHTML=c,angular.forEach(a,function(a){var c=[];a.selector&&""!==a.selector?c=d.find(a.selector):a.customAttribute&&""!==a.customAttribute&&(c=b.getByAttribute(d,a.customAttribute)),angular.forEach(c,function(b){b=angular.element(b),a.selector&&""!==a.selector&&a.customAttribute&&""!==a.customAttribute?void 0!==b.attr(a.customAttribute)&&a.renderLogic(b):a.renderLogic(b)})}),d[0].innerHTML}}]).factory("taFixChrome",function(){var a=function(a){if(!a||!angular.isString(a)||a.length<=0)return a;for(var b,c,d,e=/<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/gi,f="",g=0;b=e.exec(a);)c=b[3]||b[4],c&&c.match(/line-height: 1.[0-9]{3,12};|color: inherit; line-height: 1.1;/i)&&(c=c.replace(/( |)font-family: inherit;|( |)line-height: 1.[0-9]{3,12};|( |)color: inherit;/gi,""),d="<"+b[1].trim(),c.trim().length>0&&(d+=" style="+b[2].substring(0,1)+c+b[2].substring(0,1)),d+=b[5].trim()+">",f+=a.substring(g,b.index)+d,g=b.index+b[0].length);return f+=a.substring(g),g>0?f.replace(/(.*?)<\/span>(|)/gi,"$1"):a};return a}).factory("taSanitize",["$sanitize",function(a){function b(a,b){for(var c,d=0,e=0,f=/<[^>]*>/gi;c=f.exec(a);)if(e=c.index,"/"===c[0].substr(1,1)){if(0===d)break;d--}else d++;return b+a.substring(0,e)+angular.element(b)[0].outerHTML.substring(b.length)+a.substring(e)}function c(a){if(!a||!angular.isString(a)||a.length<=0)return a;for(var d,f,g,h,i,k,l=/<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/gi,m="",n="",o=0;f=l.exec(a);){h=f[3]||f[4];var p=new RegExp(j,"i");if(angular.isString(h)&&p.test(h)){i="";for(var q=new RegExp(j,"ig");g=q.exec(h);)for(d=0;d");k=c(a.substring(o,f.index)),n+=m.length>0?b(k,m):k,h=h.replace(new RegExp(j,"ig"),""),n+="<"+f[1].trim(),h.length>0&&(n+=' style="'+h+'"'),n+=f[5]+">",o=f.index+f[0].length,m=i}}return n+=m.length>0?b(a.substring(o),m):a.substring(o)}function d(a){if(!a||!angular.isString(a)||a.length<=0)return a;for(var b,c=/<([^>\/]+?)align=("([^"]+)"|'([^']+)')([^>]*)>/gi,d="",e=0;b=c.exec(a);){d+=a.substring(e,b.index),e=b.index+b[0].length;var f="<"+b[1]+b[5];/style=("([^"]+)"|'([^']+)')/gi.test(f)?f=f.replace(/style=("([^"]+)"|'([^']+)')/i,'style="$2$3 text-align:'+(b[3]||b[4])+';"'):f+=' style="text-align:'+(b[3]||b[4])+';"',f+=">",d+=f}return d+a.substring(e)}for(var e=[{property:"font-weight",values:["bold"],tag:"b"},{property:"font-style",values:["italic"],tag:"i"}],f=[],g=0;g0&&(h+="|"),h+=e[g].values[i];h+=");)",f.push(h)}var j="("+f.join("|")+")";return function(b,e,f){if(!f)try{b=c(b)}catch(g){}b=d(b);var h;try{h=a(b),f&&(h=b)}catch(g){h=e||""}var i,j=h.match(/(]*>.*?<\/pre[^>]*>)/gi),k=h.replace(/(&#(9|10);)*/gi,""),l=/]*>.*?<\/pre[^>]*>/gi,m=0,n=0;for(h="";null!==(i=l.exec(k))&&m=0;e--)d=angular.element("<"+c+">"+f[e].innerHTML+""),a.after(d);a.remove(),b.setSelectionToElementEnd(d[0])},f=function(a){/()$/i.test(a.innerHTML.trim())?b.setSelectionBeforeElement(angular.element(a).find("br")[0]):b.setSelectionToElementEnd(a)},i=function(a,b){var c=angular.element("<"+b+">"+a[0].innerHTML+"");a.after(c),a.remove(),f(c.find("li")[0])},j=function(a,b,d){for(var e="",g=0;g"+a[g].innerHTML+"";var h=angular.element("<"+d+">"+e+"");b.after(h),b.remove(),f(h.find("li")[0])};return function(f,k){return f=c(f),function(l,m,n,o){var p,q,r,s,t,u,v,w=angular.element("<"+f+">");try{v=b.getSelectionElement()}catch(x){}var y=angular.element(v);if(void 0!==v){var z=v.tagName.toLowerCase();if("insertorderedlist"===l.toLowerCase()||"insertunorderedlist"===l.toLowerCase()){var A=c("insertorderedlist"===l.toLowerCase()?"ol":"ul");if(z===A)return e(y,f);if("li"===z&&y.parent()[0].tagName.toLowerCase()===A&&1===y.parent().children().length)return e(y.parent(),f);if("li"===z&&y.parent()[0].tagName.toLowerCase()!==A&&1===y.parent().children().length)return i(y.parent(),A);if(z.match(g)&&!y.hasClass("ta-bind")){if("ol"===z||"ul"===z)return i(y,A);var B=!1;return angular.forEach(y.children(),function(a){a.tagName.match(g)&&(B=!0)}),B?j(y.children(),y,A):j([angular.element("
    "+v.innerHTML+"
    ")[0]],y,A)}if(z.match(g)){if(s=b.getOnlySelectedElements(),0===s.length)q=angular.element("<"+A+">
  • "+v.innerHTML+"
  • "),y.html(""),y.append(q);else{if(1===s.length&&("ol"===s[0].tagName.toLowerCase()||"ul"===s[0].tagName.toLowerCase()))return s[0].tagName.toLowerCase()===A?e(angular.element(s[0]),f):i(angular.element(s[0]),A);r="";var C=[];for(p=0;p"+D[0].innerHTML+"":D[0].childNodes[0].innerHTML,C.unshift(D)}q=angular.element("<"+A+">"+r+""),C.pop().replaceWith(q),angular.forEach(C,function(a){a.remove()})}return void b.setSelectionToElementEnd(q[0])}}else{if("formatblock"===l.toLowerCase()){for(u=n.toLowerCase().replace(/[<>]/gi,""),"default"===u.trim()&&(u=f,n="<"+f+">"),q="li"===z?y.parent():y;!q[0].tagName||!q[0].tagName.match(g)&&!q.parent().attr("contenteditable");)q=q.parent(),z=(q[0].tagName||"").toLowerCase();if(z===u){s=q.children();var E=!1;for(p=0;p=0;p--)s[p].parentNode&&s[p].parentNode.removeChild(s[p])}else for(p=0;p"),w[0].innerHTML=G[p].outerHTML,G[p]=w[0]),F.parent()[0].insertBefore(G[p],F[0]);F.remove()}return void b.setSelectionToElementEnd(q[0])}if("createlink"===l.toLowerCase()){var H='',I="",J=b.getSelection();if(J.collapsed)b.insertHtml(H+n+I,k);else if(a.getSelection().getRangeAt(0).canSurroundContents()){var K=angular.element(H+I)[0];a.getSelection().getRangeAt(0).surroundContents(K)}return}if("inserthtml"===l.toLowerCase())return void b.insertHtml(n,k)}}try{d[0].execCommand(l,m,n)}catch(x){}}}}]).service("taSelection",["$document","taDOM",function(b,c){var d=b[0],e=function(a,b){return a.tagName&&a.tagName.match(/^br$/i)&&0===b&&!a.previousSibling?{element:a.parentNode,offset:0}:{element:a,offset:b}},f={getSelection:function(){var b=a.getSelection().getRangeAt(0),c=b.commonAncestorContainer,d={start:e(b.startContainer,b.startOffset),end:e(b.endContainer,b.endOffset),collapsed:b.collapsed};return c=3===c.nodeType?c.parentNode:c,c.parentNode===d.start.element||c.parentNode===d.end.element?d.container=c.parentNode:d.container=c,d},getOnlySelectedElements:function(){var b=a.getSelection().getRangeAt(0),c=b.commonAncestorContainer;return c=3===c.nodeType?c.parentNode:c,b.getNodes([1],function(a){return a.parentNode===c})},getSelectionElement:function(){return f.getSelection().container},setSelection:function(b,c,d){var e=a.createRange();e.setStart(b,c),e.setEnd(b,d),a.getSelection().setSingleRange(e)},setSelectionBeforeElement:function(b){var c=a.createRange();c.selectNode(b),c.collapse(!0),a.getSelection().setSingleRange(c)},setSelectionAfterElement:function(b){var c=a.createRange();c.selectNode(b),c.collapse(!1),a.getSelection().setSingleRange(c)},setSelectionToElementStart:function(b){var c=a.createRange();c.selectNodeContents(b),c.collapse(!0),a.getSelection().setSingleRange(c)},setSelectionToElementEnd:function(b){var c=a.createRange();c.selectNodeContents(b),c.collapse(!1),b.childNodes&&b.childNodes[b.childNodes.length-1]&&"br"===b.childNodes[b.childNodes.length-1].nodeName&&(c.startOffset=c.endOffset=c.startOffset-1),a.getSelection().setSingleRange(c)},insertHtml:function(b,e){var h,j,k,l,m,n,o,p=angular.element("
    "+b+"
    "),q=a.getSelection().getRangeAt(0),r=d.createDocumentFragment(),s=p[0].childNodes,t=!0;if(s.length>0){for(l=[],k=0;k)$/i.test(q.startContainer.innerHTML)&&q.selectNode(q.startContainer)}else t=!0,n=r=d.createTextNode(b);if(t)q.deleteContents();else if(q.collapsed&&q.startContainer!==e)if(q.startContainer.innerHTML&&q.startContainer.innerHTML.match(/^<[^>]*>$/i))h=q.startContainer,1===q.startOffset?(q.setStartAfter(h),q.setEndAfter(h)):(q.setStartBefore(h),q.setEndBefore(h));else{if(3===q.startContainer.nodeType&&q.startContainer.parentNode!==e)for(h=q.startContainer.parentNode,j=h.cloneNode(),c.splitNodes(h.childNodes,h,j,q.startContainer,q.startOffset);!i.test(h.nodeName);){angular.element(h).after(j),h=h.parentNode;var v=j;j=h.cloneNode(),c.splitNodes(h.childNodes,h,j,v)}else h=q.startContainer,j=h.cloneNode(),c.splitNodes(h.childNodes,h,j,void 0,void 0,q.startOffset);if(angular.element(h).after(j),q.setStartAfter(h),q.setEndAfter(h),/^(|)$/i.test(h.innerHTML.trim())&&(q.setStartBefore(h),q.setEndBefore(h),angular.element(h).remove()),/^(|)$/i.test(j.innerHTML.trim())&&angular.element(j).remove(),"li"===h.nodeName.toLowerCase()){for(o=d.createDocumentFragment(),m=0;m"),c.transferChildNodes(r.childNodes[m],p[0]),c.transferNodeAttributes(r.childNodes[m],p[0]),o.appendChild(p[0]);r=o,n&&(n=r.childNodes[r.childNodes.length-1],n=n.childNodes[n.childNodes.length-1])}}else q.deleteContents();q.insertNode(r),n&&f.setSelectionToElementEnd(n)}};return f}]).service("taDOM",function(){var a={getByAttribute:function(b,c){var d=[],e=b.children();return e.length&&angular.forEach(e,function(b){d=d.concat(a.getByAttribute(angular.element(b),c))}),void 0!==b.attr(c)&&d.push(b),d},transferChildNodes:function(a,b){for(b.innerHTML="";a.childNodes.length>0;)b.appendChild(a.childNodes[0]);return b},splitNodes:function(b,c,d,e,f,g){if(!e&&isNaN(g))throw new Error("taDOM.splitNodes requires a splitNode or splitIndex");for(var h=document.createDocumentFragment(),i=document.createDocumentFragment(),j=0;b.length>0&&(isNaN(g)||g!==j)&&b[0]!==e;)h.appendChild(b[0]),j++;for(!isNaN(f)&&f>=0&&b[0]&&(h.appendChild(document.createTextNode(b[0].nodeValue.substring(0,f))),b[0].nodeValue=b[0].nodeValue.substring(f));b.length>0;)i.appendChild(b[0]);a.transferChildNodes(h,c),a.transferChildNodes(i,d)},transferNodeAttributes:function(a,b){for(var c=0;c");return b.html(a),b.text().length<=e}}}}).directive("taMinText",function(){return{restrict:"A",require:"ngModel",link:function(a,b,c,d){var e=parseInt(a.$eval(c.taMinText)); +if(isNaN(e))throw"Min text must be an integer";c.$observe("taMinText",function(a){if(e=parseInt(a),isNaN(e))throw"Min text must be an integer";d.$dirty&&d.$validate()}),d.$validators.taMinText=function(a){var b=angular.element("
    ");return b.html(a),!b.text().length||b.text().length>=e}}}}),angular.module("textAngular.taBind",["textAngular.factories","textAngular.DOM"]).service("_taBlankTest",[function(){var a=/<(a|abbr|acronym|bdi|bdo|big|cite|code|del|dfn|img|ins|kbd|label|map|mark|q|ruby|rp|rt|s|samp|time|tt|var)[^>]*(>|$)/i;return function(b){return function(c){if(!c)return!0;var d,e=/(^[^<]|>)[^<]/i.exec(c);return e?d=e.index:(c=c.toString().replace(/="[^"]*"/i,"").replace(/="[^"]*"/i,"").replace(/="[^"]*"/i,"").replace(/="[^"]*"/i,""),d=c.indexOf(">")),c=c.trim().substring(d,d+100),/^[^<>]+$/i.test(c)?!1:0===c.length||c===b||/^>(\s| )*<\/[^>]+>$/gi.test(c)?!0:/>\s*[^\s<]/i.test(c)||a.test(c)?!1:!0}}}]).directive("taButton",[function(){return{link:function(a,b,c){b.attr("unselectable","on"),b.on("mousedown",function(a,b){return b&&angular.extend(a,b),a.preventDefault(),!1})}}}]).directive("taBind",["taSanitize","$timeout","$document","taFixChrome","taBrowserTag","taSelection","taSelectableElements","taApplyCustomRenderers","taOptions","_taBlankTest","$parse","taDOM","textAngularManager",function(b,c,d,h,j,m,n,o,p,q,s,t,u){return{priority:2,require:["ngModel","?ngModelOptions"],link:function(j,v,w,x){function y(a){var b;return R.forEach(function(c){if(c.keyCode===a.keyCode){var d=(a.metaKey?O:0)+(a.ctrlKey?N:0)+(a.shiftKey?Q:0)+(a.altKey?P:0);if(c.forbiddenModifiers&d)return;c.mustHaveModifiers.every(function(a){return d&a})&&(b=c.specialKey)}}),b}var z,A,B,C,D=x[0],E=x[1]||{},F=void 0!==v.attr("contenteditable")&&v.attr("contenteditable"),G=F||"textarea"===v[0].tagName.toLowerCase()||"input"===v[0].tagName.toLowerCase(),H=!1,I=!1,J=!1,K=w.taUnsafeSanitizer||p.disableSanitizer,L=/^(9|19|20|27|33|34|35|36|37|38|39|40|45|112|113|114|115|116|117|118|119|120|121|122|123|144|145)$/i,M=/^(8|13|32|46|59|61|107|109|173|186|187|188|189|190|191|192|219|220|221|222)$/i,N=1,O=2,P=4,Q=8,R=[{specialKey:"UndoKey",forbiddenModifiers:P+Q,mustHaveModifiers:[O+N],keyCode:90},{specialKey:"RedoKey",forbiddenModifiers:P,mustHaveModifiers:[O+N,Q],keyCode:90},{specialKey:"RedoKey",forbiddenModifiers:P+Q,mustHaveModifiers:[O+N],keyCode:89},{specialKey:"TabKey",forbiddenModifiers:O+Q+P+N,mustHaveModifiers:[],keyCode:9},{specialKey:"ShiftTabKey",forbiddenModifiers:O+P+N,mustHaveModifiers:[Q],keyCode:9}];void 0===w.taDefaultWrap&&(w.taDefaultWrap="p"),""===w.taDefaultWrap?(B="",C=void 0===e.ie?"

    ":e.ie>=11?"


    ":e.ie<=8?"

     

    ":"

     

    "):(B=void 0===e.ie||e.ie>=11?"<"+w.taDefaultWrap+">
    ":e.ie<=8?"<"+w.taDefaultWrap.toUpperCase()+">":"<"+w.taDefaultWrap+">",C=void 0===e.ie||e.ie>=11?"<"+w.taDefaultWrap+">
    ":e.ie<=8?"<"+w.taDefaultWrap.toUpperCase()+"> ":"<"+w.taDefaultWrap+"> "),E.$options||(E.$options={});var S=q(C),T=function(a){if(S(a))return a;var b=angular.element("
    "+a+"
    ");if(0===b.children().length)a="<"+w.taDefaultWrap+">"+a+"";else{var c,d=b[0].childNodes,e=!1;for(c=0;c";else if("#text"===h){var i=f.textContent;a+=i.trim()?"<"+w.taDefaultWrap+">"+i+"":i}else if(h.match(g))a+=f.outerHTML;else{var j=f.outerHTML||f.nodeValue;a+=""!==j.trim()?"<"+w.taDefaultWrap+">"+j+"":j}}else a="<"+w.taDefaultWrap+">"+a+""}return a};w.taPaste&&(A=s(w.taPaste)),v.addClass("ta-bind");var U;j["$undoManager"+(w.id||"")]=D.$undoManager={_stack:[],_index:0,_max:1e3,push:function(a){return"undefined"==typeof a||null===a||"undefined"!=typeof this.current()&&null!==this.current()&&a===this.current()?a:(this._indexthis._max&&this._stack.shift(),this._index=this._stack.length-1,a)},undo:function(){return this.setToIndex(this._index-1)},redo:function(){return this.setToIndex(this._index+1)},setToIndex:function(a){return 0>a||a>this._stack.length-1?void 0:(this._index=a,this.current())},current:function(){return this._stack[this._index]}};var V,W=j["$undoTaBind"+(w.id||"")]=function(){if(!H&&F){var a=D.$undoManager.undo();"undefined"!=typeof a&&null!==a&&(ka(a),Z(a,!1),V&&c.cancel(V),V=c(function(){v[0].focus(),m.setSelectionToElementEnd(v[0])},1))}},X=j["$redoTaBind"+(w.id||"")]=function(){if(!H&&F){var a=D.$undoManager.redo();"undefined"!=typeof a&&null!==a&&(ka(a),Z(a,!1),V&&c.cancel(V),V=c(function(){v[0].focus(),m.setSelectionToElementEnd(v[0])},1))}},Y=function(){if(F)return v[0].innerHTML;if(G)return v.val();throw"textAngular Error: attempting to update non-editable taBind"},Z=function(a,b,c){J=c||!1,("undefined"==typeof b||null===b)&&(b=F),("undefined"==typeof a||null===a)&&(a=Y()),S(a)?(""!==D.$viewValue&&D.$setViewValue(""),b&&""!==D.$undoManager.current()&&D.$undoManager.push("")):(ja(),D.$viewValue!==a&&(D.$setViewValue(a),b&&D.$undoManager.push(a))),D.$render()};j["updateTaBind"+(w.id||"")]=function(){H||Z(void 0,void 0,!0)};var $=function(a){return D.$oldViewValue=b(h(a),D.$oldViewValue,K)};if(v.attr("required")&&(D.$validators.required=function(a,b){return!S(a||b)}),D.$parsers.push($),D.$parsers.unshift(T),D.$formatters.push($),D.$formatters.unshift(T),D.$formatters.unshift(function(a){return D.$undoManager.push(a||"")}),G)if(j.events={},F){var _=!1,aa=function(a){var d=a.match(/content=["']*OneNote.File/i);if(a&&a.trim().length){if(a.match(/class=["']*Mso(Normal|List)/i)||a.match(/content=["']*Word.Document/i)||a.match(/content=["']*OneNote.File/i)){var e=a.match(/([\s\S]*?)/i);e=e?e[1]:a,e=e.replace(/[\s\S]*?<\/o:p>/gi,"").replace(/class=(["']|)MsoNormal(["']|)/gi,"");var f=angular.element("
    "+e+"
    "),g=angular.element("
    "),h={element:null,lastIndent:[],lastLi:null,isUl:!1};h.lastIndent.peek=function(){var a=this.length;return a>0?this[a-1]:void 0};for(var i=function(a){h.isUl=a,h.element=angular.element(a?"
      ":"
        "),h.lastIndent=[],h.lastIndent.peek=function(){var a=this.length;return a>0?this[a-1]:void 0},h.lastLevelMatch=null},k=0;k<=f[0].childNodes.length;k++)if(f[0].childNodes[k]&&"#text"!==f[0].childNodes[k].nodeName){var l=f[0].childNodes[k].tagName.toLowerCase();if("p"===l||"h1"===l||"h2"===l||"h3"===l||"h4"===l||"h5"===l||"h6"===l){var n=angular.element(f[0].childNodes[k]),o=(n.attr("class")||"").match(/MsoList(Bullet|Number|Paragraph)(CxSp(First|Middle|Last)|)/i);if(o){if(n[0].childNodes.length<2||n[0].childNodes[1].childNodes.length<1)continue;var p="bullet"===o[1].toLowerCase()||"number"!==o[1].toLowerCase()&&!(/^[^0-9a-z<]*[0-9a-z]+[^0-9a-z<>]]":"
          "),h.lastLi.append(h.element);else if(null!=h.lastIndent.peek()&&h.lastIndent.peek()>r){for(;null!=h.lastIndent.peek()&&h.lastIndent.peek()>r;)if("li"!==h.element.parent()[0].tagName.toLowerCase()){if(!/[uo]l/i.test(h.element.parent()[0].tagName.toLowerCase()))break;h.element=h.element.parent(),h.lastIndent.pop()}else h.element=h.element.parent();h.isUl="ul"===h.element[0].tagName.toLowerCase(),p!==h.isUl&&(i(p),g.append(h.element))}h.lastLevelMatch=s,r!==h.lastIndent.peek()&&h.lastIndent.push(r),h.lastLi=angular.element("
        1. "),h.element.append(h.lastLi),h.lastLi.html(n.html().replace(/[\s\S]*?/gi,"")),n.remove()}else i(!1),g.append(n)}}var u=function(a){a=angular.element(a);for(var b=a[0].childNodes.length-1;b>=0;b--)a.after(a[0].childNodes[b]);a.remove()};angular.forEach(g.find("span"),function(a){a.removeAttribute("lang"),a.attributes.length<=0&&u(a)}),angular.forEach(g.find("font"),u),a=g.html(),d&&(a=g.html()||f.html())}else{if(a=a.replace(/<(|\/)meta[^>]*?>/gi,""),a.match(/<[^>]*?(ta-bind)[^>]*?>/)){if(a.match(/<[^>]*?(text-angular)[^>]*?>/)){var w=angular.element("
          "+a+"
          ");w.find("textarea").remove();for(var x=t.getByAttribute(w,"ta-bind"),y=0;y',"")}}else a.match(/^.<\/span>/gi)||(a=a.replace(/<(|\/)span[^>]*?>/gi,"")));a=a.replace(/
          ]*?>/gi,"").replace(/( | )<\/span>/gi," ")}//i.test(a)&&/(|).*/i.test(a)===!1&&(a=a.replace(/.*<\/li(\s.*)?>/i,"
            $&
          ")),a=a.replace(/^[ |\u00A0]+/gm,function(a){for(var b="",c=0;c").replace(/\t/g,"    "),A&&(a=A(j,{$html:a})||a),a=b(a,"",K),m.insertHtml(a,v[0]),c(function(){D.$setViewValue(Y()),_=!1,v.removeClass("processing-paste")},0)}else _=!1,v.removeClass("processing-paste")};v.on("paste",j.events.paste=function(b,e){if(e&&angular.extend(b,e),H||_)return b.stopPropagation(),b.preventDefault(),!1;_=!0,v.addClass("processing-paste");var f,g=(b.originalEvent||b).clipboardData;if(g&&g.getData&&g.types.length>0){for(var h="",i=0;i
    ');d.find("body").append(k),k[0].focus(),c(function(){a.restoreSelection(j),aa(k[0].innerHTML),v[0].focus(),k.remove()},0)}),v.on("cut",j.events.cut=function(a){H?a.preventDefault():c(function(){D.$setViewValue(Y())},0)}),v.on("keydown",j.events.keydown=function(a,b){b&&angular.extend(a,b),a.specialKey=y(a);var c;if(p.keyMappings.forEach(function(b){a.specialKey===b.commandKeyCode&&(a.specialKey=void 0),b.testForKey(a)&&(c=b.commandKeyCode),("UndoKey"===b.commandKeyCode||"RedoKey"===b.commandKeyCode)&&(b.enablePropagation||a.preventDefault())}),"undefined"!=typeof c&&(a.specialKey=c),"undefined"==typeof a.specialKey||"UndoKey"===a.specialKey&&"RedoKey"===a.specialKey||(a.preventDefault(),u.sendKeyCommand(j,a)),!H&&("UndoKey"===a.specialKey&&(W(),a.preventDefault()),"RedoKey"===a.specialKey&&(X(),a.preventDefault()),13===a.keyCode&&!a.shiftKey)){var d,e=function(a,b){for(var c=0;c$/i.test(f.innerHTML.trim())&&!f.nextSibling){d=angular.element(f);var k=d.parent();k.after(g),d.remove(),0===k.children().length&&k.remove(),m.setSelectionToElementStart(g[0]),a.preventDefault()}/^<[^>]+><\/[^>]+>$/i.test(f.innerHTML.trim())&&(d=angular.element(f),d.after(g),d.remove(),m.setSelectionToElementStart(g[0]),a.preventDefault())}}});var ba;if(v.on("keyup",j.events.keyup=function(a,b){if(b&&angular.extend(a,b),9===a.keyCode){var d=m.getSelection();return void(d.start.element===v[0]&&v.children().length&&m.setSelectionToElementStart(v.children()[0]))}if(U&&c.cancel(U),!H&&!L.test(a.keyCode)){if(""!==B&&13===a.keyCode&&!a.shiftKey){for(var e=m.getSelectionElement();!e.tagName.match(i)&&e!==v[0];)e=e.parentNode;if(e.tagName.toLowerCase()!==w.taDefaultWrap&&"li"!==e.tagName.toLowerCase()&&(""===e.innerHTML.trim()||"
    "===e.innerHTML.trim())){var f=angular.element(B);angular.element(e).replaceWith(f),m.setSelectionToElementStart(f[0])}}var g=Y();""!==B&&""===g.trim()?(ka(B),m.setSelectionToElementStart(v.children()[0])):"<"!==g.substring(0,1)&&""!==w.taDefaultWrap;var h=z!==a.keyCode&&M.test(a.keyCode);ba&&c.cancel(ba),ba=c(function(){Z(g,h,!0)},E.$options.debounce||400),h||(U=c(function(){D.$undoManager.push(g)},250)),z=a.keyCode}}),v.on("blur",j.events.blur=function(){I=!1,H?(J=!0,D.$render()):Z(void 0,void 0,!0)}),w.placeholder&&(e.ie>8||void 0===e.ie)){var ca;if(!w.id)throw"textAngular Error: An unique ID is required for placeholders to work";ca=k("#"+w.id+".placeholder-text:before",'content: "'+w.placeholder+'"'),j.$on("$destroy",function(){l(ca)})}v.on("focus",j.events.focus=function(){I=!0,v.removeClass("placeholder-text"),ja()}),v.on("mouseup",j.events.mouseup=function(){var a=m.getSelection();a.start.element===v[0]&&v.children().length&&m.setSelectionToElementStart(v.children()[0])}),v.on("mousedown",j.events.mousedown=function(a,b){b&&angular.extend(a,b),a.stopPropagation()})}else{v.on("change blur",j.events.change=j.events.blur=function(){H||D.$setViewValue(Y())}),v.on("keydown",j.events.keydown=function(a,b){if(b&&angular.extend(a,b),9===a.keyCode){var c=this.selectionStart,d=this.selectionEnd,e=v.val();if(a.shiftKey){var f=e.lastIndexOf("\n",c),g=e.lastIndexOf(" ",c);-1!==g&&g>=f&&(v.val(e.substring(0,g)+e.substring(g+1)),this.selectionStart=this.selectionEnd=c-1)}else v.val(e.substring(0,c)+" "+e.substring(d)),this.selectionStart=this.selectionEnd=c+1;a.preventDefault()}});var da=function(a,b){for(var c="",d=0;b>d;d++)c+=a;return c},ea=function(a,b,c){for(var d=0;d"):"#text"===e?void(c+=d.textContent):void(d.outerHTML&&(c+="ul"===e||"ol"===e?"\n"+fa(d,b):"\n"+da(" ",b)+d.outerHTML))}),c+="\n"+da(" ",b-1)+a.outerHTML.substring(a.outerHTML.lastIndexOf("<"))};D.$formatters.unshift(function(a){var b=angular.element("
    "+a+"
    ")[0].childNodes;return b.length>0&&(a="",ea(b,function(b,c){var d=c.nodeName.toLowerCase();return"#comment"===d?void(a+=""):"#text"===d?void(a+=c.textContent):void(c.outerHTML&&(a.length>0&&(a+="\n"),a+="ul"===d||"ol"===d?""+fa(c,0):""+c.outerHTML))})),a})}var ga,ha=function(a){return j.$emit("ta-element-select",this),a.preventDefault(),!1},ia=function(a,b){if(b&&angular.extend(a,b),!r&&!H){r=!0;var d;d=a.originalEvent?a.originalEvent.dataTransfer:a.dataTransfer,j.$emit("ta-drop-event",this,a,d),c(function(){r=!1,Z(void 0,void 0,!0)},100)}},ja=j["reApplyOnSelectorHandlers"+(w.id||"")]=function(){H||angular.forEach(n,function(a){v.find(a).off("click",ha).on("click",ha)})},ka=function(a){v[0].innerHTML=a},la=!1;D.$render=function(){if(!la){la=!0;var a=D.$viewValue||"";J||(F&&I&&(v.removeClass("placeholder-text"),ga&&c.cancel(ga),ga=c(function(){I||(v[0].focus(),m.setSelectionToElementEnd(v.children()[v.children().length-1])),ga=void 0},1)),F?(ka(w.placeholder?""===a?B:a:""===a?B:a),H?v.off("drop",ia):(ja(),v.on("drop",ia))):"textarea"!==v[0].tagName.toLowerCase()&&"input"!==v[0].tagName.toLowerCase()?ka(o(a)):v.val(a)),F&&w.placeholder&&(""===a?I?v.removeClass("placeholder-text"):v.addClass("placeholder-text"):v.removeClass("placeholder-text")),la=J=!1}},w.taReadonly&&(H=j.$eval(w.taReadonly),H?(v.addClass("ta-readonly"),("textarea"===v[0].tagName.toLowerCase()||"input"===v[0].tagName.toLowerCase())&&v.attr("disabled","disabled"),void 0!==v.attr("contenteditable")&&v.attr("contenteditable")&&v.removeAttr("contenteditable")):(v.removeClass("ta-readonly"),"textarea"===v[0].tagName.toLowerCase()||"input"===v[0].tagName.toLowerCase()?v.removeAttr("disabled"):F&&v.attr("contenteditable","true")),j.$watch(w.taReadonly,function(a,b){b!==a&&(a?(v.addClass("ta-readonly"),("textarea"===v[0].tagName.toLowerCase()||"input"===v[0].tagName.toLowerCase())&&v.attr("disabled","disabled"),void 0!==v.attr("contenteditable")&&v.attr("contenteditable")&&v.removeAttr("contenteditable"),angular.forEach(n,function(a){v.find(a).on("click",ha)}),v.off("drop",ia)):(v.removeClass("ta-readonly"),"textarea"===v[0].tagName.toLowerCase()||"input"===v[0].tagName.toLowerCase()?v.removeAttr("disabled"):F&&v.attr("contenteditable","true"),angular.forEach(n,function(a){v.find(a).off("click",ha)}),v.on("drop",ia)),H=a)})),F&&!H&&(angular.forEach(n,function(a){v.find(a).on("click",ha)}),v.on("drop",ia),v.on("blur",function(){e.webkit&&(f=!0)}))}}}]);var r=!1,s=angular.module("textAngular",["ngSanitize","textAngularSetup","textAngular.factories","textAngular.DOM","textAngular.validators","textAngular.taBind"]);return s.config([function(){angular.forEach(d,function(a,b){delete d[b]})}]),s.directive("textAngular",["$compile","$timeout","taOptions","taSelection","taExecCommand","textAngularManager","$document","$animate","$log","$q","$parse",function(b,c,d,e,f,g,h,i,j,k,l){return{require:"?ngModel",scope:{},restrict:"EA",priority:2,link:function(m,n,o,p){var q,r,s,t,u,v,w,x,y,z,A,B=o.serial?o.serial:Math.floor(1e16*Math.random());m._name=o.name?o.name:"textAngularEditor"+B;var C=function(a,b,d){c(function(){var c=function(){a.off(b,c),d.apply(this,arguments)};a.on(b,c)},100)};if(y=f(o.taDefaultWrap),angular.extend(m,angular.copy(d),{wrapSelection:function(a,b,c){"undo"===a.toLowerCase()?m["$undoTaBindtaTextElement"+B]():"redo"===a.toLowerCase()?m["$redoTaBindtaTextElement"+B]():(y(a,!1,b,m.defaultTagAttributes),c&&m["reApplyOnSelectorHandlerstaTextElement"+B](),m.displayElements.text[0].focus())},showHtml:m.$eval(o.taShowHtml)||!1}),o.taFocussedClass&&(m.classes.focussed=o.taFocussedClass),o.taTextEditorClass&&(m.classes.textEditor=o.taTextEditorClass),o.taHtmlEditorClass&&(m.classes.htmlEditor=o.taHtmlEditorClass),o.taDefaultTagAttributes)try{angular.extend(m.defaultTagAttributes,angular.fromJson(o.taDefaultTagAttributes))}catch(D){j.error(D)}o.taTextEditorSetup&&(m.setup.textEditorSetup=m.$parent.$eval(o.taTextEditorSetup)),o.taHtmlEditorSetup&&(m.setup.htmlEditorSetup=m.$parent.$eval(o.taHtmlEditorSetup)),o.taFileDrop?m.fileDropHandler=m.$parent.$eval(o.taFileDrop):m.fileDropHandler=m.defaultFileDropHandler,w=n[0].innerHTML,n[0].innerHTML="",m.displayElements={forminput:angular.element(""),html:angular.element(""),text:angular.element("
    "),scrollWindow:angular.element("
    "),popover:angular.element('
    '),popoverArrow:angular.element('
    '),popoverContainer:angular.element('
    '),resize:{overlay:angular.element('
    '),background:angular.element('
    '),anchors:[angular.element('
    '),angular.element('
    '),angular.element('
    '),angular.element('
    ')],info:angular.element('
    ')}},m.displayElements.popover.append(m.displayElements.popoverArrow),m.displayElements.popover.append(m.displayElements.popoverContainer),m.displayElements.scrollWindow.append(m.displayElements.popover),m.displayElements.popover.on("mousedown",function(a,b){return b&&angular.extend(a,b),a.preventDefault(),!1}),m.showPopover=function(a){m.displayElements.popover.css("display","block"),m.reflowPopover(a),i.addClass(m.displayElements.popover,"in"),C(h.find("body"),"click keyup",function(){m.hidePopover()})},m.reflowPopover=function(a){m.displayElements.text[0].offsetHeight-51>a[0].offsetTop?(m.displayElements.popover.css("top",a[0].offsetTop+a[0].offsetHeight+m.displayElements.scrollWindow[0].scrollTop+"px"),m.displayElements.popover.removeClass("top").addClass("bottom")):(m.displayElements.popover.css("top",a[0].offsetTop-54+m.displayElements.scrollWindow[0].scrollTop+"px"),m.displayElements.popover.removeClass("bottom").addClass("top"));var b=m.displayElements.text[0].offsetWidth-m.displayElements.popover[0].offsetWidth,c=a[0].offsetLeft+a[0].offsetWidth/2-m.displayElements.popover[0].offsetWidth/2;m.displayElements.popover.css("left",Math.max(0,Math.min(b,c))+"px"),m.displayElements.popoverArrow.css("margin-left",Math.min(c,Math.max(0,c-b))-11+"px")},m.hidePopover=function(){m.displayElements.popover.css("display",""),m.displayElements.popoverContainer.attr("style",""),m.displayElements.popoverContainer.attr("class","popover-content"),m.displayElements.popover.removeClass("in")},m.displayElements.resize.overlay.append(m.displayElements.resize.background),angular.forEach(m.displayElements.resize.anchors,function(a){m.displayElements.resize.overlay.append(a)}),m.displayElements.resize.overlay.append(m.displayElements.resize.info),m.displayElements.scrollWindow.append(m.displayElements.resize.overlay),m.reflowResizeOverlay=function(a){a=angular.element(a)[0],m.displayElements.resize.overlay.css({display:"block",left:a.offsetLeft-5+"px",top:a.offsetTop-5+"px",width:a.offsetWidth+10+"px",height:a.offsetHeight+10+"px"}),m.displayElements.resize.info.text(a.offsetWidth+" x "+a.offsetHeight)},m.showResizeOverlay=function(a){var b=h.find("body");z=function(c){var d={width:parseInt(a.attr("width")),height:parseInt(a.attr("height")),x:c.clientX,y:c.clientY};(void 0===d.width||isNaN(d.width))&&(d.width=a[0].offsetWidth),(void 0===d.height||isNaN(d.height))&&(d.height=a[0].offsetHeight),m.hidePopover();var e=d.height/d.width,f=function(b){function c(a){return Math.round(Math.max(0,a))}var f={x:Math.max(0,d.width+(b.clientX-d.x)),y:Math.max(0,d.height+(b.clientY-d.y))},g=void 0!==o.taResizeForceAspectRatio,h=o.taResizeMaintainAspectRatio,i=g||h&&!b.shiftKey;if(i){var j=f.y/f.x;f.x=e>j?f.x:f.y/e,f.y=e>j?f.x*e:f.y}var k=angular.element(a);k.css("height",c(f.y)+"px"),k.css("width",c(f.x)+"px"),m.reflowResizeOverlay(a)};b.on("mousemove",f),C(b,"mouseup",function(a){a.preventDefault(),a.stopPropagation(),b.off("mousemove",f),m.$apply(function(){m.hidePopover(),m.updateTaBindtaTextElement()},100)}),c.stopPropagation(),c.preventDefault()},m.displayElements.resize.anchors[3].off("mousedown"),m.displayElements.resize.anchors[3].on("mousedown",z),m.reflowResizeOverlay(a),C(b,"click",function(){m.hideResizeOverlay()})},m.hideResizeOverlay=function(){m.displayElements.resize.anchors[3].off("mousedown",z),m.displayElements.resize.overlay.css("display","")},m.setup.htmlEditorSetup(m.displayElements.html),m.setup.textEditorSetup(m.displayElements.text),m.displayElements.html.attr({id:"taHtmlElement"+B,"ng-show":"showHtml","ta-bind":"ta-bind","ng-model":"html","ng-model-options":n.attr("ng-model-options")}),m.displayElements.text.attr({id:"taTextElement"+B,contentEditable:"true","ta-bind":"ta-bind","ng-model":"html","ng-model-options":n.attr("ng-model-options")}),m.displayElements.scrollWindow.attr({"ng-hide":"showHtml"}),o.taDefaultWrap&&m.displayElements.text.attr("ta-default-wrap",o.taDefaultWrap),o.taUnsafeSanitizer&&(m.displayElements.text.attr("ta-unsafe-sanitizer",o.taUnsafeSanitizer),m.displayElements.html.attr("ta-unsafe-sanitizer",o.taUnsafeSanitizer)),m.displayElements.scrollWindow.append(m.displayElements.text),n.append(m.displayElements.scrollWindow),n.append(m.displayElements.html),m.displayElements.forminput.attr("name",m._name),n.append(m.displayElements.forminput),o.tabindex&&(n.removeAttr("tabindex"),m.displayElements.text.attr("tabindex",o.tabindex),m.displayElements.html.attr("tabindex",o.tabindex)),o.placeholder&&(m.displayElements.text.attr("placeholder",o.placeholder),m.displayElements.html.attr("placeholder",o.placeholder)),o.taDisabled&&(m.displayElements.text.attr("ta-readonly","disabled"),m.displayElements.html.attr("ta-readonly","disabled"),m.disabled=m.$parent.$eval(o.taDisabled),m.$parent.$watch(o.taDisabled,function(a){m.disabled=a,m.disabled?n.addClass(m.classes.disabled):n.removeClass(m.classes.disabled)})),o.taPaste&&(m._pasteHandler=function(a){return l(o.taPaste)(m.$parent,{$html:a})},m.displayElements.text.attr("ta-paste","_pasteHandler($html)")),b(m.displayElements.scrollWindow)(m),b(m.displayElements.html)(m),m.updateTaBindtaTextElement=m["updateTaBindtaTextElement"+B],m.updateTaBindtaHtmlElement=m["updateTaBindtaHtmlElement"+B],n.addClass("ta-root"),m.displayElements.scrollWindow.addClass("ta-text ta-editor "+m.classes.textEditor),m.displayElements.html.addClass("ta-html ta-editor "+m.classes.htmlEditor),m._actionRunning=!1;var E=!1;if(m.startAction=function(){return m._actionRunning=!0,E=a.saveSelection(),function(){E&&a.restoreSelection(E)}},m.endAction=function(){m._actionRunning=!1,E&&(m.showHtml?m.displayElements.html[0].focus():m.displayElements.text[0].focus(),a.removeMarkers(E)),E=!1,m.updateSelectedStyles(),m.showHtml||m["updateTaBindtaTextElement"+B]()},u=function(){m.focussed=!0,n.addClass(m.classes.focussed),x.focus(),n.triggerHandler("focus")},m.displayElements.html.on("focus",u),m.displayElements.text.on("focus",u),v=function(a){return m._actionRunning||h[0].activeElement===m.displayElements.html[0]||h[0].activeElement===m.displayElements.text[0]||(n.removeClass(m.classes.focussed),x.unfocus(),c(function(){m._bUpdateSelectedStyles=!1,n.triggerHandler("blur"),m.focussed=!1},0)),a.preventDefault(),!1},m.displayElements.html.on("blur",v),m.displayElements.text.on("blur",v),m.displayElements.text.on("paste",function(a){n.triggerHandler("paste",a)}),m.queryFormatBlockState=function(a){return!m.showHtml&&a.toLowerCase()===h[0].queryCommandValue("formatBlock").toLowerCase()},m.queryCommandState=function(a){return m.showHtml?"":h[0].queryCommandState(a)},m.switchView=function(){m.showHtml=!m.showHtml,i.enabled(!1,m.displayElements.html),i.enabled(!1,m.displayElements.text),m.showHtml?c(function(){return i.enabled(!0,m.displayElements.html),i.enabled(!0,m.displayElements.text),m.displayElements.html[0].focus()},100):c(function(){return i.enabled(!0,m.displayElements.html),i.enabled(!0,m.displayElements.text),m.displayElements.text[0].focus()},100)},o.ngModel){var F=!0;p.$render=function(){if(F){F=!1;var a=m.$parent.$eval(o.ngModel);void 0!==a&&null!==a||!w||""===w||p.$setViewValue(w)}m.displayElements.forminput.val(p.$viewValue),m.html=p.$viewValue||""},n.attr("required")&&(p.$validators.required=function(a,b){var c=a||b;return!(!c||""===c.trim())})}else m.displayElements.forminput.val(w),m.html=w;if(m.$watch("html",function(a,b){a!==b&&(o.ngModel&&p.$viewValue!==a&&p.$setViewValue(a),m.displayElements.forminput.val(a))}),o.taTargetToolbars)x=g.registerEditor(m._name,m,o.taTargetToolbars.split(","));else{var G=angular.element('
    ');o.taToolbar&&G.attr("ta-toolbar",o.taToolbar),o.taToolbarClass&&G.attr("ta-toolbar-class",o.taToolbarClass),o.taToolbarGroupClass&&G.attr("ta-toolbar-group-class",o.taToolbarGroupClass),o.taToolbarButtonClass&&G.attr("ta-toolbar-button-class",o.taToolbarButtonClass),o.taToolbarActiveButtonClass&&G.attr("ta-toolbar-active-button-class",o.taToolbarActiveButtonClass),o.taFocussedClass&&G.attr("ta-focussed-class",o.taFocussedClass),n.prepend(G),b(G)(m.$parent),x=g.registerEditor(m._name,m,["textAngularToolbar"+B])}m.$on("$destroy",function(){g.unregisterEditor(m._name),angular.element(window).off("blur")}),m.$on("ta-element-select",function(a,b){x.triggerElementSelect(a,b)&&m["reApplyOnSelectorHandlerstaTextElement"+B]()}),m.$on("ta-drop-event",function(a,b,d,e){m.displayElements.text[0].focus(),e&&e.files&&e.files.length>0?(angular.forEach(e.files,function(a){try{k.when(m.fileDropHandler(a,m.wrapSelection)||m.fileDropHandler!==m.defaultFileDropHandler&&k.when(m.defaultFileDropHandler(a,m.wrapSelection))).then(function(){m["updateTaBindtaTextElement"+B]()})}catch(b){j.error(b)}}),d.preventDefault(),d.stopPropagation()):c(function(){m["updateTaBindtaTextElement"+B]()},0)}),m._bUpdateSelectedStyles=!1,angular.element(window).on("blur",function(){m._bUpdateSelectedStyles=!1,m.focussed=!1}),m.updateSelectedStyles=function(){var a;A&&c.cancel(A),void 0!==(a=e.getSelectionElement())&&a.parentNode!==m.displayElements.text[0]?x.updateSelectedStyles(angular.element(a)):x.updateSelectedStyles(),m._bUpdateSelectedStyles&&(A=c(m.updateSelectedStyles,200))},q=function(){return m.focussed?void(m._bUpdateSelectedStyles||(m._bUpdateSelectedStyles=!0,m.$apply(function(){m.updateSelectedStyles()}))):void(m._bUpdateSelectedStyles=!1)},m.displayElements.html.on("keydown",q),m.displayElements.text.on("keydown",q),r=function(){m._bUpdateSelectedStyles=!1},m.displayElements.html.on("keyup",r),m.displayElements.text.on("keyup",r),s=function(a,b){b&&angular.extend(a,b),m.$apply(function(){return x.sendKeyCommand(a)?(m._bUpdateSelectedStyles||m.updateSelectedStyles(),a.preventDefault(),!1):void 0})},m.displayElements.html.on("keypress",s),m.displayElements.text.on("keypress",s),t=function(){m._bUpdateSelectedStyles=!1,m.$apply(function(){m.updateSelectedStyles()})},m.displayElements.html.on("mouseup",t),m.displayElements.text.on("mouseup",t)}}}]),s.service("textAngularManager",["taToolExecuteAction","taTools","taRegisterTool",function(a,b,c){var d={},e={};return{registerEditor:function(c,f,g){if(!c||""===c)throw"textAngular Error: An editor requires a name";if(!f)throw"textAngular Error: An editor requires a scope";if(e[c])throw'textAngular Error: An Editor with name "'+c+'" already exists';var h=[];return angular.forEach(g,function(a){d[a]&&h.push(d[a])}),e[c]={scope:f,toolbars:g,_registerToolbar:function(a){this.toolbars.indexOf(a.name)>=0&&h.push(a)},editorFunctions:{disable:function(){angular.forEach(h,function(a){a.disabled=!0})},enable:function(){angular.forEach(h,function(a){a.disabled=!1})},focus:function(){angular.forEach(h,function(a){a._parent=f,a.disabled=!1,a.focussed=!0,f.focussed=!0})},unfocus:function(){angular.forEach(h,function(a){a.disabled=!0,a.focussed=!1}),f.focussed=!1},updateSelectedStyles:function(a){angular.forEach(h,function(b){angular.forEach(b.tools,function(c){c.activeState&&(b._parent=f,c.active=c.activeState(a))})})},sendKeyCommand:function(c){var d=!1;return(c.ctrlKey||c.metaKey||c.specialKey)&&angular.forEach(b,function(b,e){if(b.commandKeyCode&&(b.commandKeyCode===c.which||b.commandKeyCode===c.specialKey))for(var g=0;g0)for(var k=0;k"),b&&b["class"]?d.addClass(b["class"]):d.addClass(g.classes.toolbarButton),d.attr("name",c.name),d.attr("ta-button","ta-button"),d.attr("ng-disabled","isDisabled()"),d.attr("tabindex","-1"),d.attr("ng-click","executeAction()"),d.attr("ng-class","displayActiveToolClass(active)"),b&&b.tooltiptext&&d.attr("title",b.tooltiptext),b&&!b.display&&!c._display&&(d[0].innerHTML="",b.buttontext&&(d[0].innerHTML=b.buttontext),b.iconclass)){var e=angular.element(""),f=d[0].innerHTML;e.addClass(b.iconclass),d[0].innerHTML="",d.append(e),f&&""!==f&&d.append(" "+f)}return c._lastToolDefinition=angular.copy(b),a(d)(c)};g.tools={},g._parent={disabled:!0,showHtml:!1,queryFormatBlockState:function(){return!1},queryCommandState:function(){return!1}};var k={$window:f,$editor:function(){return g._parent},isDisabled:function(){return"function"!=typeof this.$eval("disabled")&&this.$eval("disabled")||this.$eval("disabled()")||"html"!==this.name&&this.$editor().showHtml||this.$parent.disabled||this.$editor().disabled},displayActiveToolClass:function(a){return a?g.classes.toolbarButtonActive:""},executeAction:e};angular.forEach(g.toolbar,function(a){var b=angular.element("
    ");b.addClass(g.classes.toolbarGroup),angular.forEach(a,function(a){g.tools[a]=angular.extend(g.$new(!0),d[a],k,{name:a}),g.tools[a].$element=j(d[a],g.tools[a]),b.append(g.tools[a].$element)}),h.append(b)}),g.updateToolDisplay=function(a,b,c){var d=g.tools[a];if(d){if(d._lastToolDefinition&&!c&&(b=angular.extend({},d._lastToolDefinition,b)),null===b.buttontext&&null===b.iconclass&&null===b.display)throw'textAngular Error: Tool Definition for updating "'+a+'" does not have a valid display/iconclass/buttontext value';null===b.buttontext&&delete b.buttontext,null===b.iconclass&&delete b.iconclass,null===b.display&&delete b.display;var e=j(b,d);d.$element.replaceWith(e),d.$element=e}},g.addTool=function(a,b,c,e){g.tools[a]=angular.extend(g.$new(!0),d[a],k,{name:a}),g.tools[a].$element=j(d[a],g.tools[a]);var f;void 0===c&&(c=g.toolbar.length-1),f=angular.element(h.children()[c]),void 0===e?(f.append(g.tools[a].$element),g.toolbar[c][g.toolbar[c].length-1]=a):(f.children().eq(e).after(g.tools[a].$element),g.toolbar[c][e]=a)},b.registerToolbar(g),g.$on("$destroy",function(){b.unregisterToolbar(g.name)})}}}]),s.name}); From ddcfe1535a1078cf23511e579ffc9147cef2be45 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 11 May 2016 10:52:50 +1000 Subject: [PATCH 031/110] Making TagRule autocomplete available to models other than Customer Incorporate ng admin.tags module and rails TagController into existing admin.tagRules module + TagRuleController --- .../customers_controller.js.coffee | 4 +-- .../admin/customers/customers.js.coffee | 2 +- .../services/tags_resource.js.coffee | 9 ----- .../shipping_method_controller.js.coffee | 10 +++++- .../shipping_methods.js.coffee | 2 +- .../services/tag_rule_resource.js.coffee | 10 ++++++ .../admin/tag_rules/tag_rules.js.coffee | 2 +- .../javascripts/shared/ng-tags-input.min.js | 2 +- app/controllers/admin/customers_controller.rb | 10 +++--- app/controllers/admin/tag_rules_controller.rb | 33 +++++++++++++++++++ app/controllers/admin/tags_controller.rb | 28 ---------------- app/models/enterprise.rb | 14 -------- app/models/spree/ability_decorator.rb | 3 +- app/models/tag_rule.rb | 15 +++++++++ .../replace_form_fields.html.haml.deface | 2 +- .../api/admin/customer_serializer.rb | 5 ++- config/routes.rb | 4 ++- spec/javascripts/application_spec.js | 3 +- .../customers_controller_spec.js.coffee | 2 +- .../admin/customer_serializer_spec.rb | 3 +- 20 files changed, 89 insertions(+), 74 deletions(-) delete mode 100644 app/assets/javascripts/admin/customers/services/tags_resource.js.coffee create mode 100644 app/assets/javascripts/admin/tag_rules/services/tag_rule_resource.js.coffee delete mode 100644 app/controllers/admin/tags_controller.rb diff --git a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee index bfccfd3f4b..763e92b400 100644 --- a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee +++ b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerResource, TagsResource, $q, Columns, pendingChanges, shops) -> +angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerResource, TagRuleResource, $q, Columns, pendingChanges, shops) -> $scope.shop = {} $scope.shops = shops $scope.submitAll = pendingChanges.submitAll @@ -16,7 +16,7 @@ angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerR defer = $q.defer() params = enterprise_id: $scope.shop.id - TagsResource.index params, (data) => + TagRuleResource.mapByTag params, (data) => filtered = data.filter (tag) -> tag.text.toLowerCase().indexOf(query.toLowerCase()) != -1 defer.resolve filtered diff --git a/app/assets/javascripts/admin/customers/customers.js.coffee b/app/assets/javascripts/admin/customers/customers.js.coffee index 1e8ae9b988..33be58c9ac 100644 --- a/app/assets/javascripts/admin/customers/customers.js.coffee +++ b/app/assets/javascripts/admin/customers/customers.js.coffee @@ -1 +1 @@ -angular.module("admin.customers", ['ngResource', 'ngTagsInput', 'admin.indexUtils', 'admin.utils', 'admin.dropdown']) \ No newline at end of file +angular.module("admin.customers", ['ngResource', 'admin.tagRules', 'admin.indexUtils', 'admin.utils', 'admin.dropdown']) \ No newline at end of file diff --git a/app/assets/javascripts/admin/customers/services/tags_resource.js.coffee b/app/assets/javascripts/admin/customers/services/tags_resource.js.coffee deleted file mode 100644 index ad89511e32..0000000000 --- a/app/assets/javascripts/admin/customers/services/tags_resource.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -angular.module("admin.customers").factory 'TagsResource', ($resource) -> - $resource('/admin/tags.json', {}, { - 'index': - method: 'GET' - isArray: true - cache: true - params: - enterprise_id: '@enterprise_id' - }) diff --git a/app/assets/javascripts/admin/shipping_methods/controllers/shipping_method_controller.js.coffee b/app/assets/javascripts/admin/shipping_methods/controllers/shipping_method_controller.js.coffee index cc7bd4ee3e..67c53d4b66 100644 --- a/app/assets/javascripts/admin/shipping_methods/controllers/shipping_method_controller.js.coffee +++ b/app/assets/javascripts/admin/shipping_methods/controllers/shipping_method_controller.js.coffee @@ -1,2 +1,10 @@ -angular.module("admin.shippingMethods").controller "shippingMethodCtrl", ($scope, shippingMethod) -> +angular.module("admin.shippingMethods").controller "shippingMethodCtrl", ($scope, shippingMethod, TagRuleResource, $q) -> $scope.shippingMethod = shippingMethod + + $scope.findTags = (query) -> + defer = $q.defer() + TagRuleResource.mapByTag (data) => + filtered = data.filter (tag) -> + tag.text.toLowerCase().indexOf(query.toLowerCase()) != -1 + defer.resolve filtered + defer.promise diff --git a/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee b/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee index 863163a9ef..46537b303e 100644 --- a/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee +++ b/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee @@ -1 +1 @@ -angular.module("admin.shippingMethods", ["ngTagsInput", 'admin.utils', 'templates']) +angular.module("admin.shippingMethods", ["ngTagsInput", 'admin.utils', 'admin.tagRules', 'templates']) diff --git a/app/assets/javascripts/admin/tag_rules/services/tag_rule_resource.js.coffee b/app/assets/javascripts/admin/tag_rules/services/tag_rule_resource.js.coffee new file mode 100644 index 0000000000..d4a6c45ba4 --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/services/tag_rule_resource.js.coffee @@ -0,0 +1,10 @@ +angular.module("admin.tagRules").factory 'TagRuleResource', ($resource) -> + $resource('/admin/tag_rules/:action.json', {}, { + 'mapByTag': + method: 'GET' + isArray: true + cache: true + params: + action: 'map_by_tag' + enterprise_id: '@enterprise_id' + }) diff --git a/app/assets/javascripts/admin/tag_rules/tag_rules.js.coffee b/app/assets/javascripts/admin/tag_rules/tag_rules.js.coffee index 88c7734c33..d9a4b2bbfd 100644 --- a/app/assets/javascripts/admin/tag_rules/tag_rules.js.coffee +++ b/app/assets/javascripts/admin/tag_rules/tag_rules.js.coffee @@ -1 +1 @@ -angular.module("admin.tagRules", ['ngTagsInput']) +angular.module("admin.tagRules", ['ngResource', 'ngTagsInput']) diff --git a/app/assets/javascripts/shared/ng-tags-input.min.js b/app/assets/javascripts/shared/ng-tags-input.min.js index 9a1acd6e0d..cf7b0a02a0 100644 --- a/app/assets/javascripts/shared/ng-tags-input.min.js +++ b/app/assets/javascripts/shared/ng-tags-input.min.js @@ -1 +1 @@ -/*! ngTagsInput v2.3.0 License: MIT */!function(){"use strict";var a={backspace:8,tab:9,enter:13,escape:27,space:32,up:38,down:40,left:37,right:39,"delete":46,comma:188},b=9007199254740991,c=["text","email","url"],d=angular.module("ngTagsInput",[]);d.directive("tagsInput",["$timeout","$document","$window","tagsInputConfig","tiUtil",function(d,e,f,g,h){function i(a,b,c,d){var e,f,g,i={};return e=function(b){return h.safeToString(b[a.displayProperty])},f=function(b,c){b[a.displayProperty]=c},g=function(b){var d=e(b);return d&&d.length>=a.minLength&&d.length<=a.maxLength&&a.allowedTagsPattern.test(d)&&!h.findInObjectArray(i.items,b,a.keyProperty||a.displayProperty)&&c({$tag:b})},i.items=[],i.addText=function(a){var b={};return f(b,a),i.add(b)},i.add=function(c){var d=e(c);return a.replaceSpacesWithDashes&&(d=h.replaceSpacesWithDashes(d)),f(c,d),g(c)?(i.items.push(c),b.trigger("tag-added",{$tag:c})):d&&b.trigger("invalid-tag",{$tag:c}),c},i.remove=function(a){var c=i.items[a];return d({$tag:c})?(i.items.splice(a,1),i.clearSelection(),b.trigger("tag-removed",{$tag:c}),c):void 0},i.select=function(a){0>a?a=i.items.length-1:a>=i.items.length&&(a=0),i.index=a,i.selected=i.items[a]},i.selectPrior=function(){i.select(--i.index)},i.selectNext=function(){i.select(++i.index)},i.removeSelected=function(){return i.remove(i.index)},i.clearSelection=function(){i.selected=null,i.index=-1},i.clearSelection(),i}function j(a){return-1!==c.indexOf(a)}return{restrict:"E",require:"ngModel",scope:{tags:"=ngModel",onTagAdding:"&",onTagAdded:"&",onInvalidTag:"&",onTagRemoving:"&",onTagRemoved:"&"},replace:!1,transclude:!0,templateUrl:"ngTagsInput/tags-input.html",controller:["$scope","$attrs","$element",function(a,c,d){a.events=h.simplePubSub(),g.load("tagsInput",a,c,{template:[String,"ngTagsInput/tag-item.html"],type:[String,"text",j],placeholder:[String,"Add a tag"],tabindex:[Number,null],removeTagSymbol:[String,String.fromCharCode(215)],replaceSpacesWithDashes:[Boolean,!0],minLength:[Number,3],maxLength:[Number,b],addOnEnter:[Boolean,!0],addOnSpace:[Boolean,!1],addOnComma:[Boolean,!0],addOnBlur:[Boolean,!0],addOnPaste:[Boolean,!1],pasteSplitPattern:[RegExp,/,/],allowedTagsPattern:[RegExp,/.+/],enableEditingLastTag:[Boolean,!1],minTags:[Number,0],maxTags:[Number,b],displayProperty:[String,"text"],keyProperty:[String,""],allowLeftoverText:[Boolean,!1],addFromAutocompleteOnly:[Boolean,!1],spellcheck:[Boolean,!0]}),a.tagList=new i(a.options,a.events,h.handleUndefinedResult(a.onTagAdding,!0),h.handleUndefinedResult(a.onTagRemoving,!0)),this.registerAutocomplete=function(){var b=d.find("input");return{addTag:function(b){return a.tagList.add(b)},focusInput:function(){b[0].focus()},getTags:function(){return a.tags},getCurrentTagText:function(){return a.newTag.text},getOptions:function(){return a.options},on:function(b,c){return a.events.on(b,c),this}}},this.registerTagItem=function(){return{getOptions:function(){return a.options},removeTag:function(b){a.disabled||a.tagList.remove(b)}}}}],link:function(b,c,g,i){var j,k=[a.enter,a.comma,a.space,a.backspace,a["delete"],a.left,a.right],l=b.tagList,m=b.events,n=b.options,o=c.find("input"),p=["minTags","maxTags","allowLeftoverText"];j=function(){i.$setValidity("maxTags",b.tags.length<=n.maxTags),i.$setValidity("minTags",b.tags.length>=n.minTags),i.$setValidity("leftoverText",b.hasFocus||n.allowLeftoverText?!0:!b.newTag.text)},i.$isEmpty=function(a){return!a||!a.length},b.newTag={text:"",invalid:null,setText:function(a){this.text=a,m.trigger("input-change",a)}},b.track=function(a){return a[n.keyProperty||n.displayProperty]},b.$watch("tags",function(a){b.tags=h.makeObjectArray(a,n.displayProperty),l.items=b.tags}),b.$watch("tags.length",function(){j()}),g.$observe("disabled",function(a){b.disabled=a}),b.eventHandlers={input:{change:function(a){m.trigger("input-change",a)},keydown:function(a){m.trigger("input-keydown",a)},focus:function(){b.hasFocus||(b.hasFocus=!0,m.trigger("input-focus"))},blur:function(){d(function(){var a=e.prop("activeElement"),d=a===o[0],f=c[0].contains(a);(d||!f)&&(b.hasFocus=!1,m.trigger("input-blur"))})},paste:function(a){a.getTextData=function(){var b=a.clipboardData||a.originalEvent&&a.originalEvent.clipboardData;return b?b.getData("text/plain"):f.clipboardData.getData("Text")},m.trigger("input-paste",a)}},host:{click:function(){b.disabled||o[0].focus()}}},m.on("tag-added",b.onTagAdded).on("invalid-tag",b.onInvalidTag).on("tag-removed",b.onTagRemoved).on("tag-added",function(){b.newTag.setText("")}).on("tag-added tag-removed",function(){i.$setViewValue(b.tags)}).on("invalid-tag",function(){b.newTag.invalid=!0}).on("option-change",function(a){-1!==p.indexOf(a.name)&&j()}).on("input-change",function(){l.clearSelection(),b.newTag.invalid=null}).on("input-focus",function(){c.triggerHandler("focus"),i.$setValidity("leftoverText",!0)}).on("input-blur",function(){n.addOnBlur&&!n.addFromAutocompleteOnly&&l.addText(b.newTag.text),c.triggerHandler("blur"),j()}).on("input-keydown",function(c){var d,e,f,g,h=c.keyCode,i=c.shiftKey||c.altKey||c.ctrlKey||c.metaKey,j={};if(!i&&-1!==k.indexOf(h)){if(j[a.enter]=n.addOnEnter,j[a.comma]=n.addOnComma,j[a.space]=n.addOnSpace,d=!n.addFromAutocompleteOnly&&j[h],e=(h===a.backspace||h===a["delete"])&&l.selected,g=h===a.backspace&&0===b.newTag.text.length&&n.enableEditingLastTag,f=(h===a.backspace||h===a.left||h===a.right)&&0===b.newTag.text.length&&!n.enableEditingLastTag,d)l.addText(b.newTag.text);else if(g){var m;l.selectPrior(),m=l.removeSelected(),m&&b.newTag.setText(m[n.displayProperty])}else e?l.removeSelected():f&&(h===a.left||h===a.backspace?l.selectPrior():h===a.right&&l.selectNext());(d||f||e||g)&&c.preventDefault()}}).on("input-paste",function(a){if(n.addOnPaste){var b=a.getTextData(),c=b.split(n.pasteSplitPattern);c.length>1&&(c.forEach(function(a){l.addText(a)}),a.preventDefault())}})}}}]),d.directive("tiTagItem",["tiUtil",function(a){return{restrict:"E",require:"^tagsInput",template:'',scope:{data:"="},link:function(b,c,d,e){var f=e.registerTagItem(),g=f.getOptions();b.$$template=g.template,b.$$removeTagSymbol=g.removeTagSymbol,b.$getDisplayText=function(){return a.safeToString(b.data[g.displayProperty])},b.$removeTag=function(){f.removeTag(b.$index)},b.$watch("$parent.$index",function(a){b.$index=a})}}}]),d.directive("autoComplete",["$document","$timeout","$sce","$q","tagsInputConfig","tiUtil",function(b,c,d,e,f,g){function h(a,b,c){var d,f,h,i={};return h=function(){return b.tagsInput.keyProperty||b.tagsInput.displayProperty},d=function(a,c){return a.filter(function(a){return!g.findInObjectArray(c,a,h(),function(a,c){return b.tagsInput.replaceSpacesWithDashes&&(a=g.replaceSpacesWithDashes(a),c=g.replaceSpacesWithDashes(c)),g.defaultComparer(a,c)})})},i.reset=function(){f=null,i.items=[],i.visible=!1,i.index=-1,i.selected=null,i.query=null},i.show=function(){b.selectFirstMatch?i.select(0):i.selected=null,i.visible=!0},i.load=g.debounce(function(c,j){i.query=c;var k=e.when(a({$query:c}));f=k,k.then(function(a){k===f&&(a=g.makeObjectArray(a.data||a,h()),a=d(a,j),i.items=a.slice(0,b.maxResultsToShow),i.items.length>0?i.show():i.reset())})},b.debounceDelay),i.selectNext=function(){i.select(++i.index)},i.selectPrior=function(){i.select(--i.index)},i.select=function(a){0>a?a=i.items.length-1:a>=i.items.length&&(a=0),i.index=a,i.selected=i.items[a],c.trigger("suggestion-selected",a)},i.reset(),i}function i(a,b){var c=a.find("li").eq(b),d=c.parent(),e=c.prop("offsetTop"),f=c.prop("offsetHeight"),g=d.prop("clientHeight"),h=d.prop("scrollTop");h>e?d.prop("scrollTop",e):e+f>g+h&&d.prop("scrollTop",e+f-g)}return{restrict:"E",require:"^tagsInput",scope:{source:"&"},templateUrl:"ngTagsInput/auto-complete.html",controller:["$scope","$element","$attrs",function(a,b,c){a.events=g.simplePubSub(),f.load("autoComplete",a,c,{template:[String,"ngTagsInput/auto-complete-match.html"],debounceDelay:[Number,100],minLength:[Number,3],highlightMatchedText:[Boolean,!0],maxResultsToShow:[Number,10],loadOnDownArrow:[Boolean,!1],loadOnEmpty:[Boolean,!1],loadOnFocus:[Boolean,!1],selectFirstMatch:[Boolean,!0],displayProperty:[String,""]}),a.suggestionList=new h(a.source,a.options,a.events),this.registerAutocompleteMatch=function(){return{getOptions:function(){return a.options},getQuery:function(){return a.suggestionList.query}}}}],link:function(b,c,d,e){var f,g=[a.enter,a.tab,a.escape,a.up,a.down],h=b.suggestionList,j=e.registerAutocomplete(),k=b.options,l=b.events;k.tagsInput=j.getOptions(),f=function(a){return a&&a.length>=k.minLength||!a&&k.loadOnEmpty},b.addSuggestionByIndex=function(a){h.select(a),b.addSuggestion()},b.addSuggestion=function(){var a=!1;return h.selected&&(j.addTag(angular.copy(h.selected)),h.reset(),j.focusInput(),a=!0),a},b.track=function(a){return a[k.tagsInput.keyProperty||k.tagsInput.displayProperty]},j.on("tag-added invalid-tag input-blur",function(){h.reset()}).on("input-change",function(a){f(a)?h.load(a,j.getTags()):h.reset()}).on("input-focus",function(){var a=j.getCurrentTagText();k.loadOnFocus&&f(a)&&h.load(a,j.getTags())}).on("input-keydown",function(c){var d=c.keyCode,e=!1;if(-1!==g.indexOf(d))return h.visible?d===a.down?(h.selectNext(),e=!0):d===a.up?(h.selectPrior(),e=!0):d===a.escape?(h.reset(),e=!0):(d===a.enter||d===a.tab)&&(e=b.addSuggestion()):d===a.down&&b.options.loadOnDownArrow&&(h.load(j.getCurrentTagText(),j.getTags()),e=!0),e?(c.preventDefault(),c.stopImmediatePropagation(),!1):void 0}),l.on("suggestion-selected",function(a){i(c,a)})}}}]),d.directive("tiAutocompleteMatch",["$sce","tiUtil",function(a,b){return{restrict:"E",require:"^autoComplete",template:'',scope:{data:"="},link:function(c,d,e,f){var g=f.registerAutocompleteMatch(),h=g.getOptions();c.$$template=h.template,c.$index=c.$parent.$index,c.$highlight=function(c){return h.highlightMatchedText&&(c=b.safeHighlight(c,g.getQuery())),a.trustAsHtml(c)},c.$getDisplayText=function(){return b.safeToString(c.data[h.displayProperty||h.tagsInput.displayProperty])}}}}]),d.directive("tiTranscludeAppend",function(){return function(a,b,c,d,e){e(function(a){b.append(a)})}}),d.directive("tiAutosize",["tagsInputConfig",function(a){return{restrict:"A",require:"ngModel",link:function(b,c,d,e){var f,g,h=a.getTextAutosizeThreshold();f=angular.element(''),f.css("display","none").css("visibility","hidden").css("width","auto").css("white-space","pre"),c.parent().append(f),g=function(a){var b,e=a;return angular.isString(e)&&0===e.length&&(e=d.placeholder),e&&(f.text(e),f.css("display",""),b=f.prop("offsetWidth"),f.css("display","none")),c.css("width",b?b+h+"px":""),a},e.$parsers.unshift(g),e.$formatters.unshift(g),d.$observe("placeholder",function(a){e.$modelValue||g(a)})}}}]),d.directive("tiBindAttrs",function(){return function(a,b,c){a.$watch(c.tiBindAttrs,function(a){angular.forEach(a,function(a,b){c.$set(b,a)})},!0)}}),d.provider("tagsInputConfig",function(){var a={},b={},c=3;this.setDefaults=function(b,c){return a[b]=c,this},this.setActiveInterpolation=function(a,c){return b[a]=c,this},this.setTextAutosizeThreshold=function(a){return c=a,this},this.$get=["$interpolate",function(d){var e={};return e[String]=function(a){return a},e[Number]=function(a){return parseInt(a,10)},e[Boolean]=function(a){return"true"===a.toLowerCase()},e[RegExp]=function(a){return new RegExp(a)},{load:function(c,f,g,h){var i=function(){return!0};f.options={},angular.forEach(h,function(h,j){var k,l,m,n,o,p;k=h[0],l=h[1],m=h[2]||i,n=e[k],o=function(){var b=a[c]&&a[c][j];return angular.isDefined(b)?b:l},p=function(a){f.options[j]=a&&m(a)?n(a):o()},b[c]&&b[c][j]?g.$observe(j,function(a){p(a),f.events.trigger("option-change",{name:j,newValue:a})}):p(g[j]&&d(g[j])(f.$parent))})},getTextAutosizeThreshold:function(){return c}}}]}),d.factory("tiUtil",["$timeout",function(a){var b={};return b.debounce=function(b,c){var d;return function(){var e=arguments;a.cancel(d),d=a(function(){b.apply(null,e)},c)}},b.makeObjectArray=function(a,b){return a=a||[],a.length>0&&!angular.isObject(a[0])&&a.forEach(function(c,d){a[d]={},a[d][b]=c}),a},b.findInObjectArray=function(a,c,d,e){var f=null;return e=e||b.defaultComparer,a.some(function(a){return e(a[d],c[d])?(f=a,!0):void 0}),f},b.defaultComparer=function(a,c){return b.safeToString(a).toLowerCase()===b.safeToString(c).toLowerCase()},b.safeHighlight=function(a,c){function d(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}if(!c)return a;a=b.encodeHTML(a),c=b.encodeHTML(c);var e=new RegExp("&[^;]+;|"+d(c),"gi");return a.replace(e,function(a){return a.toLowerCase()===c.toLowerCase()?""+a+"":a})},b.safeToString=function(a){return angular.isUndefined(a)||null==a?"":a.toString().trim()},b.encodeHTML=function(a){return b.safeToString(a).replace(/&/g,"&").replace(//g,">")},b.handleUndefinedResult=function(a,b){return function(){var c=a.apply(null,arguments);return angular.isUndefined(c)?b:c}},b.replaceSpacesWithDashes=function(a){return b.safeToString(a).replace(/\s/g,"-")},b.simplePubSub=function(){var a={};return{on:function(b,c){return b.split(" ").forEach(function(b){a[b]||(a[b]=[]),a[b].push(c)}),this},trigger:function(c,d){var e=a[c]||[];return e.every(function(a){return b.handleUndefinedResult(a,!0)(d)}),this}}},b}]),d.run(["$templateCache",function(a){a.put("ngTagsInput/tags-input.html",'
    '),a.put("ngTagsInput/tag-item.html",' '),a.put("ngTagsInput/auto-complete.html",'
    '),a.put("ngTagsInput/auto-complete-match.html",'')}])}(); \ No newline at end of file +/*! ngTagsInput v2.3.0 License: MIT */!function(){"use strict";var a={backspace:8,tab:9,enter:13,escape:27,space:32,up:38,down:40,left:37,right:39,"delete":46,comma:188},b=9007199254740991,c=["text","email","url"],d=angular.module("ngTagsInput",[]);d.directive("tagsInput",["$timeout","$document","$window","tagsInputConfig","tiUtil",function(d,e,f,g,h){function i(a,b,c,d){var e,f,g,i={};return e=function(b){return h.safeToString(b[a.displayProperty])},f=function(b,c){b[a.displayProperty]=c},g=function(b){var d=e(b);return d&&d.length>=a.minLength&&d.length<=a.maxLength&&a.allowedTagsPattern.test(d)&&!h.findInObjectArray(i.items,b,a.keyProperty||a.displayProperty)&&c({$tag:b})},i.items=[],i.addText=function(a){var b={};return f(b,a),i.add(b)},i.add=function(c){var d=e(c);return a.replaceSpacesWithDashes&&(d=h.replaceSpacesWithDashes(d)),f(c,d),g(c)?(i.items.push(c),b.trigger("tag-added",{$tag:c})):d&&b.trigger("invalid-tag",{$tag:c}),c},i.remove=function(a){var c=i.items[a];return d({$tag:c})?(i.items.splice(a,1),i.clearSelection(),b.trigger("tag-removed",{$tag:c}),c):void 0},i.select=function(a){0>a?a=i.items.length-1:a>=i.items.length&&(a=0),i.index=a,i.selected=i.items[a]},i.selectPrior=function(){i.select(--i.index)},i.selectNext=function(){i.select(++i.index)},i.removeSelected=function(){return i.remove(i.index)},i.clearSelection=function(){i.selected=null,i.index=-1},i.clearSelection(),i}function j(a){return-1!==c.indexOf(a)}return{restrict:"E",require:"ngModel",scope:{tags:"=ngModel",onTagAdding:"&",onTagAdded:"&",onInvalidTag:"&",onTagRemoving:"&",onTagRemoved:"&"},replace:!1,transclude:!0,templateUrl:"ngTagsInput/tags-input.html",controller:["$scope","$attrs","$element",function(a,c,d){a.events=h.simplePubSub(),g.load("tagsInput",a,c,{template:[String,"ngTagsInput/tag-item.html"],type:[String,"text",j],placeholder:[String,"Add a tag"],tabindex:[Number,null],removeTagSymbol:[String,String.fromCharCode(215)],replaceSpacesWithDashes:[Boolean,!0],minLength:[Number,3],maxLength:[Number,b],addOnEnter:[Boolean,!0],addOnSpace:[Boolean,!1],addOnComma:[Boolean,!0],addOnBlur:[Boolean,!0],addOnPaste:[Boolean,!1],pasteSplitPattern:[RegExp,/,/],allowedTagsPattern:[RegExp,/.+/],enableEditingLastTag:[Boolean,!1],minTags:[Number,0],maxTags:[Number,b],displayProperty:[String,"text"],keyProperty:[String,""],allowLeftoverText:[Boolean,!1],addFromAutocompleteOnly:[Boolean,!1],spellcheck:[Boolean,!0]}),a.tagList=new i(a.options,a.events,h.handleUndefinedResult(a.onTagAdding,!0),h.handleUndefinedResult(a.onTagRemoving,!0)),this.registerAutocomplete=function(){var b=d.find("input");return{addTag:function(b){return a.tagList.add(b)},focusInput:function(){b[0].focus()},getTags:function(){return a.tags},getCurrentTagText:function(){return a.newTag.text},getOptions:function(){return a.options},on:function(b,c){return a.events.on(b,c),this}}},this.registerTagItem=function(){return{getOptions:function(){return a.options},removeTag:function(b){a.disabled||a.tagList.remove(b)}}}}],link:function(b,c,g,i){var j,k=[a.enter,a.comma,a.space,a.backspace,a["delete"],a.left,a.right],l=b.tagList,m=b.events,n=b.options,o=c.find("input"),p=["minTags","maxTags","allowLeftoverText"];j=function(){i.$setValidity("maxTags",b.tags.length<=n.maxTags),i.$setValidity("minTags",b.tags.length>=n.minTags),i.$setValidity("leftoverText",b.hasFocus||n.allowLeftoverText?!0:!b.newTag.text)},i.$isEmpty=function(a){return!a||!a.length},b.newTag={text:"",invalid:null,setText:function(a){this.text=a,m.trigger("input-change",a)}},b.track=function(a){return a[n.keyProperty||n.displayProperty]},b.$watch("tags",function(a){b.tags=h.makeObjectArray(a,n.displayProperty),l.items=b.tags}),b.$watch("tags.length",function(){j()}),g.$observe("disabled",function(a){b.disabled=a}),b.eventHandlers={input:{change:function(a){m.trigger("input-change",a)},keydown:function(a){m.trigger("input-keydown",a)},focus:function(){b.hasFocus||(b.hasFocus=!0,m.trigger("input-focus"))},blur:function(){d(function(){var a=e.prop("activeElement"),d=a===o[0],f=c[0].contains(a);(d||!f)&&(b.hasFocus=!1,m.trigger("input-blur"))})},paste:function(a){a.getTextData=function(){var b=a.clipboardData||a.originalEvent&&a.originalEvent.clipboardData;return b?b.getData("text/plain"):f.clipboardData.getData("Text")},m.trigger("input-paste",a)}},host:{click:function(){b.disabled||o[0].focus()}}},m.on("tag-added",b.onTagAdded).on("invalid-tag",b.onInvalidTag).on("tag-removed",b.onTagRemoved).on("tag-added",function(){b.newTag.setText("")}).on("tag-added tag-removed",function(){i.$setViewValue(b.tags)}).on("invalid-tag",function(){b.newTag.invalid=!0}).on("option-change",function(a){-1!==p.indexOf(a.name)&&j()}).on("input-change",function(){l.clearSelection(),b.newTag.invalid=null}).on("input-focus",function(){c.triggerHandler("focus"),i.$setValidity("leftoverText",!0)}).on("input-blur",function(){n.addOnBlur&&!n.addFromAutocompleteOnly&&l.addText(b.newTag.text),c.triggerHandler("blur"),j()}).on("input-keydown",function(c){var d,e,f,g,h=c.keyCode,i=c.shiftKey||c.altKey||c.ctrlKey||c.metaKey,j={};if(!i&&-1!==k.indexOf(h)){if(j[a.enter]=n.addOnEnter,j[a.comma]=n.addOnComma,j[a.space]=n.addOnSpace,d=!n.addFromAutocompleteOnly&&j[h],e=(h===a.backspace||h===a["delete"])&&l.selected,g=h===a.backspace&&0===b.newTag.text.length&&n.enableEditingLastTag,f=(h===a.backspace||h===a.left||h===a.right)&&0===b.newTag.text.length&&!n.enableEditingLastTag,d)l.addText(b.newTag.text);else if(g){var m;l.selectPrior(),m=l.removeSelected(),m&&b.newTag.setText(m[n.displayProperty])}else e?l.removeSelected():f&&(h===a.left||h===a.backspace?l.selectPrior():h===a.right&&l.selectNext());(d||f||e||g)&&c.preventDefault()}}).on("input-paste",function(a){if(n.addOnPaste){var b=a.getTextData(),c=b.split(n.pasteSplitPattern);c.length>1&&(c.forEach(function(a){l.addText(a)}),a.preventDefault())}})}}}]),d.directive("tiTagItem",["tiUtil",function(a){return{restrict:"E",require:"^tagsInput",template:'',scope:{data:"="},link:function(b,c,d,e){var f=e.registerTagItem(),g=f.getOptions();b.$$template=g.template,b.$$removeTagSymbol=g.removeTagSymbol,b.$getDisplayText=function(){return a.safeToString(b.data[g.displayProperty])},b.$removeTag=function(){f.removeTag(b.$index)},b.$watch("$parent.$index",function(a){b.$index=a})}}}]),d.directive("autoComplete",["$document","$timeout","$sce","$q","tagsInputConfig","tiUtil",function(b,c,d,e,f,g){function h(a,b,c){var d,f,h,i={};return h=function(){return b.tagsInput.keyProperty||b.tagsInput.displayProperty},d=function(a,c){return a.filter(function(a){return!g.findInObjectArray(c,a,h(),function(a,c){return b.tagsInput.replaceSpacesWithDashes&&(a=g.replaceSpacesWithDashes(a),c=g.replaceSpacesWithDashes(c)),g.defaultComparer(a,c)})})},i.reset=function(){f=null,i.items=[],i.visible=!1,i.index=-1,i.selected=null,i.query=null},i.show=function(){b.selectFirstMatch?i.select(0):i.selected=null,i.visible=!0},i.load=g.debounce(function(c,j){i.query=c;var k=e.when(a({$query:c}));f=k,k.then(function(a){k===f&&(a=g.makeObjectArray(a.data||a,h()),a=d(a,j),i.items=a.slice(0,b.maxResultsToShow),i.items.length>0?i.show():i.reset())})},b.debounceDelay),i.selectNext=function(){i.select(++i.index)},i.selectPrior=function(){i.select(--i.index)},i.select=function(a){0>a?a=i.items.length-1:a>=i.items.length&&(a=0),i.index=a,i.selected=i.items[a],c.trigger("suggestion-selected",a)},i.reset(),i}function i(a,b){var c=a.find("li").eq(b),d=c.parent(),e=c.prop("offsetTop"),f=c.prop("offsetHeight"),g=d.prop("clientHeight"),h=d.prop("scrollTop");h>e?d.prop("scrollTop",e):e+f>g+h&&d.prop("scrollTop",e+f-g)}return{restrict:"E",require:"^tagsInput",scope:{source:"&"},templateUrl:"ngTagsInput/auto-complete.html",controller:["$scope","$element","$attrs",function(a,b,c){a.events=g.simplePubSub(),f.load("autoComplete",a,c,{template:[String,"ngTagsInput/auto-complete-match.html"],debounceDelay:[Number,100],minLength:[Number,3],highlightMatchedText:[Boolean,!0],maxResultsToShow:[Number,10],loadOnDownArrow:[Boolean,!1],loadOnEmpty:[Boolean,!1],loadOnFocus:[Boolean,!1],selectFirstMatch:[Boolean,!0],displayProperty:[String,""]}),a.suggestionList=new h(a.source,a.options,a.events),this.registerAutocompleteMatch=function(){return{getOptions:function(){return a.options},getQuery:function(){return a.suggestionList.query}}}}],link:function(b,c,d,e){var f,g=[a.enter,a.tab,a.escape,a.up,a.down],h=b.suggestionList,j=e.registerAutocomplete(),k=b.options,l=b.events;k.tagsInput=j.getOptions(),f=function(a){return a&&a.length>=k.minLength||!a&&k.loadOnEmpty},b.addSuggestionByIndex=function(a){h.select(a),b.addSuggestion()},b.addSuggestion=function(){var a=!1;return h.selected&&(j.addTag(angular.copy(h.selected)),h.reset(),j.focusInput(),a=!0),a},b.track=function(a){return a[k.tagsInput.keyProperty||k.tagsInput.displayProperty]},j.on("tag-added invalid-tag input-blur",function(){h.reset()}).on("input-change",function(a){f(a)?h.load(a,j.getTags()):h.reset()}).on("input-focus",function(){var a=j.getCurrentTagText();k.loadOnFocus&&f(a)&&h.load(a,j.getTags())}).on("input-keydown",function(c){var d=c.keyCode,e=!1;if(-1!==g.indexOf(d))return h.visible?d===a.down?(h.selectNext(),e=!0):d===a.up?(h.selectPrior(),e=!0):d===a.escape?(h.reset(),e=!0):(d===a.enter||d===a.tab)&&(e=b.addSuggestion()):d===a.down&&b.options.loadOnDownArrow&&(h.load(j.getCurrentTagText(),j.getTags()),e=!0),e?(c.preventDefault(),c.stopImmediatePropagation(),!1):void 0}),l.on("suggestion-selected",function(a){i(c,a)})}}}]),d.directive("tiAutocompleteMatch",["$sce","tiUtil",function(a,b){return{restrict:"E",require:"^autoComplete",template:'',scope:{data:"="},link:function(c,d,e,f){var g=f.registerAutocompleteMatch(),h=g.getOptions();c.$$template=h.template,c.$index=c.$parent.$index,c.$highlight=function(c){return h.highlightMatchedText&&(c=b.safeHighlight(c,g.getQuery())),a.trustAsHtml(c)},c.$getDisplayText=function(){return b.safeToString(c.data[h.displayProperty||h.tagsInput.displayProperty])}}}}]),d.directive("tiTranscludeAppend",function(){return function(a,b,c,d,e){e(function(a){b.append(a)})}}),d.directive("tiAutosize",["tagsInputConfig",function(a){return{restrict:"A",require:"ngModel",link:function(b,c,d,e){var f,g,h=a.getTextAutosizeThreshold();f=angular.element(''),f.css("display","none").css("visibility","hidden").css("width","auto").css("white-space","pre"),c.parent().append(f),g=function(a){var b,e=a;return angular.isString(e)&&0===e.length&&(e=d.placeholder),e&&(f.text(e),f.css("display",""),b=f.prop("offsetWidth"),f.css("display","none")),c.css("width",b?b+h+"px":""),a},e.$parsers.unshift(g),e.$formatters.unshift(g),d.$observe("placeholder",function(a){e.$modelValue||g(a)})}}}]),d.directive("tiBindAttrs",function(){return function(a,b,c){a.$watch(c.tiBindAttrs,function(a){angular.forEach(a,function(a,b){c.$set(b,a)})},!0)}}),d.provider("tagsInputConfig",function(){var a={},b={},c=3;this.setDefaults=function(b,c){return a[b]=c,this},this.setActiveInterpolation=function(a,c){return b[a]=c,this},this.setTextAutosizeThreshold=function(a){return c=a,this},this.$get=["$interpolate",function(d){var e={};return e[String]=function(a){return a},e[Number]=function(a){return parseInt(a,10)},e[Boolean]=function(a){return"true"===a.toLowerCase()},e[RegExp]=function(a){return new RegExp(a)},{load:function(c,f,g,h){var i=function(){return!0};f.options={},angular.forEach(h,function(h,j){var k,l,m,n,o,p;k=h[0],l=h[1],m=h[2]||i,n=e[k],o=function(){var b=a[c]&&a[c][j];return angular.isDefined(b)?b:l},p=function(a){f.options[j]=a&&m(a)?n(a):o()},b[c]&&b[c][j]?g.$observe(j,function(a){p(a),f.events.trigger("option-change",{name:j,newValue:a})}):p(g[j]&&d(g[j])(f.$parent))})},getTextAutosizeThreshold:function(){return c}}}]}),d.factory("tiUtil",["$timeout",function(a){var b={};return b.debounce=function(b,c){var d;return function(){var e=arguments;a.cancel(d),d=a(function(){b.apply(null,e)},c)}},b.makeObjectArray=function(a,b){return a=a||[],a.length>0&&!angular.isObject(a[0])&&a.forEach(function(c,d){a[d]={},a[d][b]=c}),a},b.findInObjectArray=function(a,c,d,e){var f=null;return e=e||b.defaultComparer,a.some(function(a){return e(a[d],c[d])?(f=a,!0):void 0}),f},b.defaultComparer=function(a,c){return b.safeToString(a).toLowerCase()===b.safeToString(c).toLowerCase()},b.safeHighlight=function(a,c){function d(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}if(!c)return a;a=b.encodeHTML(a),c=b.encodeHTML(c);var e=new RegExp("&[^;]+;|"+d(c),"gi");return a.replace(e,function(a){return a.toLowerCase()===c.toLowerCase()?""+a+"":a})},b.safeToString=function(a){return angular.isUndefined(a)||null==a?"":a.toString().trim()},b.encodeHTML=function(a){return b.safeToString(a).replace(/&/g,"&").replace(//g,">")},b.handleUndefinedResult=function(a,b){return function(){var c=a.apply(null,arguments);return angular.isUndefined(c)?b:c}},b.replaceSpacesWithDashes=function(a){return b.safeToString(a).replace(/\s/g,"-")},b.simplePubSub=function(){var a={};return{on:function(b,c){return b.split(" ").forEach(function(b){a[b]||(a[b]=[]),a[b].push(c)}),this},trigger:function(c,d){var e=a[c]||[];return e.every(function(a){return b.handleUndefinedResult(a,!0)(d)}),this}}},b}]),d.run(["$templateCache",function(a){a.put("ngTagsInput/tags-input.html",'
    '),a.put("ngTagsInput/tag-item.html",' '),a.put("ngTagsInput/auto-complete.html",'
    '),a.put("ngTagsInput/auto-complete-match.html",'')}])}(); diff --git a/app/controllers/admin/customers_controller.rb b/app/controllers/admin/customers_controller.rb index 24c26661e5..9d3bb18017 100644 --- a/app/controllers/admin/customers_controller.rb +++ b/app/controllers/admin/customers_controller.rb @@ -7,11 +7,8 @@ module Admin respond_to do |format| format.html format.json do - serialised = ActiveModel::ArraySerializer.new( - @collection, - each_serializer: Api::Admin::CustomerSerializer, - spree_current_user: spree_current_user) - render json: serialised.to_json + tag_rule_mapping = TagRule.mapping_for(Enterprise.where(id: params[:enterprise_id])) + render_as_json @collection, tag_rule_mapping: tag_rule_mapping end end end @@ -20,7 +17,8 @@ module Admin @customer = Customer.new(params[:customer]) if user_can_create_customer? @customer.save - render json: Api::Admin::CustomerSerializer.new(@customer).to_json + tag_rule_mapping = TagRule.mapping_for(Enterprise.where(id: @customer.enterprise)) + render_as_json @customer, tag_rule_mapping: tag_rule_mapping else redirect_to '/unauthorized' end diff --git a/app/controllers/admin/tag_rules_controller.rb b/app/controllers/admin/tag_rules_controller.rb index 7d60cb4888..25f5b177d6 100644 --- a/app/controllers/admin/tag_rules_controller.rb +++ b/app/controllers/admin/tag_rules_controller.rb @@ -6,5 +6,38 @@ module Admin respond_override destroy: { json: { success: lambda { render nothing: true, :status => 204 } } } + + def map_by_tag + respond_to do |format| + format.json do + serialiser = ActiveModel::ArraySerializer.new(collection) + render json: serialiser.to_json + end + end + end + + + private + + def collection_actions + [:index, :map_by_tag] + end + + def collection + case action + when :map_by_tag + TagRule.mapping_for(enterprises).values + else + TagRule.for(enterprises.pluck(&:id)) + end + end + + def enterprises + if params[:enterprise_id] + Enterprise.managed_by(spree_current_user).where(id: params[:enterprise_id]) + else + Enterprise.managed_by(spree_current_user) + end + end end end diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb deleted file mode 100644 index 3ab5685ffe..0000000000 --- a/app/controllers/admin/tags_controller.rb +++ /dev/null @@ -1,28 +0,0 @@ -module Admin - class TagsController < Spree::Admin::BaseController - respond_to :json - - def index - respond_to do |format| - format.json do - serialiser = ActiveModel::ArraySerializer.new(tags_of_enterprise) - render json: serialiser.to_json - end - end - end - - private - - def enterprise - Enterprise.managed_by(spree_current_user).find_by_id(params[:enterprise_id]) - end - - def tags_of_enterprise - return [] unless enterprise - tag_rule_map = enterprise.rules_per_tag - tag_rule_map.keys.map do |tag| - { text: tag, rules: tag_rule_map[tag] } - end - end - end -end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index b7a5e3189a..9f839f9d08 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -351,20 +351,6 @@ class Enterprise < ActiveRecord::Base end end - def rules_per_tag - tag_rule_map = {} - tag_rules.each do |rule| - rule.preferred_customer_tags.split(",").each do |tag| - if tag_rule_map[tag] - tag_rule_map[tag] += 1 - else - tag_rule_map[tag] = 1 - end - end - end - tag_rule_map - end - protected def devise_mailer diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index e250d1341d..e1995b3c6f 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -72,7 +72,7 @@ class AbilityDecorator can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], ProducerProperty - can [:admin, :destroy], TagRule do |tag_rule| + can [:admin, :map_by_tag, :destroy], TagRule do |tag_rule| user.enterprises.include? tag_rule.enterprise end @@ -218,7 +218,6 @@ class AbilityDecorator can [:create], Customer can [:admin, :index, :update, :destroy], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id) - can [:admin, :index], :tag end diff --git a/app/models/tag_rule.rb b/app/models/tag_rule.rb index 6e6405a589..8d385673db 100644 --- a/app/models/tag_rule.rb +++ b/app/models/tag_rule.rb @@ -9,6 +9,8 @@ class TagRule < ActiveRecord::Base attr_accessible :enterprise, :enterprise_id, :preferred_customer_tags + scope :for, lambda { |enterprises| where(enterprise_id: enterprises) } + def set_context(subject, context) @subject = subject @context = context @@ -24,6 +26,19 @@ class TagRule < ActiveRecord::Base end end + def self.mapping_for(enterprises) + self.for(enterprises).inject({}) do |mapping, rule| + rule.preferred_customer_tags.split(",").each do |tag| + if mapping[tag] + mapping[tag][:rules] += 1 + else + mapping[tag] = { text: tag, rules: 1 } + end + end + mapping + end + end + private def relevant? diff --git a/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface b/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface index a2f9ed766a..3cffbbabcb 100644 --- a/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface +++ b/app/overrides/spree/admin/shipping_methods/_form/replace_form_fields.html.haml.deface @@ -54,7 +54,7 @@ = f.label :tags, t(:tags) .omega.eight.columns = f.hidden_field :tag_list, "ng-value" => "shippingMethod.tag_list" - %tags-with-translation#something{ object: "shippingMethod" } + %tags-with-translation#something{ object: "shippingMethod", 'find-tags' => 'findTags(query)' } .row .alpha.eleven.columns diff --git a/app/serializers/api/admin/customer_serializer.rb b/app/serializers/api/admin/customer_serializer.rb index 052f49a917..4634625c11 100644 --- a/app/serializers/api/admin/customer_serializer.rb +++ b/app/serializers/api/admin/customer_serializer.rb @@ -6,10 +6,9 @@ class Api::Admin::CustomerSerializer < ActiveModel::Serializer end def tags - tag_rule_map = object.enterprise.rules_per_tag object.tag_list.map do |tag| - { text: tag, rules: tag_rule_map[tag] } + tag_rule_map = options[:tag_rule_mapping][tag] + tag_rule_map || { text: tag, rules: nil } end end - end diff --git a/config/routes.rb b/config/routes.rb index e240f901a8..c9b81f8caf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -117,7 +117,9 @@ Openfoodnetwork::Application.routes.draw do resources :customers, only: [:index, :create, :update, :destroy] - resources :tags, only: [:index], format: :json + resources :tag_rules, only: [], format: :json do + get :map_by_tag, on: :collection + end resource :content diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js index 94f56a20c5..37c56918d4 100644 --- a/spec/javascripts/application_spec.js +++ b/spec/javascripts/application_spec.js @@ -9,8 +9,9 @@ //= require angular-flash.min.js //= require shared/ng-tags-input.min.js //= require shared/mm-foundation-tpls-0.8.0.min.js -//= require textAngular.min.js +//= require textAngular-rangy.min.js //= require textAngular-sanitize.min.js +//= require textAngular.min.js //= require moment angular.module('templates', []) diff --git a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee index 1da89f2053..b0991c7df1 100644 --- a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee @@ -56,7 +56,7 @@ describe "CustomersCtrl", -> { text: 'three' } ] beforeEach -> - http.expectGET('/admin/tags.json?enterprise_id=1').respond 200, tags + http.expectGET('/admin/tag_rules/map_by_tag.json?enterprise_id=1').respond 200, tags it "retrieves the tag list", -> promise = scope.findTags('') diff --git a/spec/serializers/admin/customer_serializer_spec.rb b/spec/serializers/admin/customer_serializer_spec.rb index d63f00d4aa..b8e43f5fcc 100644 --- a/spec/serializers/admin/customer_serializer_spec.rb +++ b/spec/serializers/admin/customer_serializer_spec.rb @@ -3,7 +3,8 @@ describe Api::Admin::CustomerSerializer do let!(:tag_rule) { create(:tag_rule, enterprise: customer.enterprise, preferred_customer_tags: "two") } it "serializes a customer" do - serializer = Api::Admin::CustomerSerializer.new customer + tag_rule_mapping = TagRule.mapping_for(Enterprise.where(id: customer.enterprise_id)) + serializer = Api::Admin::CustomerSerializer.new customer, tag_rule_mapping: tag_rule_mapping result = JSON.parse(serializer.to_json) expect(result['email']).to eq customer.email tags = result['tags'] From efaf728737bc975589e60c8e3d2c46ad8b3a50d3 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 11 May 2016 11:04:39 +1000 Subject: [PATCH 032/110] Upgrading ngTagsInput --- app/assets/javascripts/shared/ng-tags-input.min.js | 2 +- app/assets/stylesheets/shared/ng-tags-input.min.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/shared/ng-tags-input.min.js b/app/assets/javascripts/shared/ng-tags-input.min.js index cf7b0a02a0..9af98627db 100644 --- a/app/assets/javascripts/shared/ng-tags-input.min.js +++ b/app/assets/javascripts/shared/ng-tags-input.min.js @@ -1 +1 @@ -/*! ngTagsInput v2.3.0 License: MIT */!function(){"use strict";var a={backspace:8,tab:9,enter:13,escape:27,space:32,up:38,down:40,left:37,right:39,"delete":46,comma:188},b=9007199254740991,c=["text","email","url"],d=angular.module("ngTagsInput",[]);d.directive("tagsInput",["$timeout","$document","$window","tagsInputConfig","tiUtil",function(d,e,f,g,h){function i(a,b,c,d){var e,f,g,i={};return e=function(b){return h.safeToString(b[a.displayProperty])},f=function(b,c){b[a.displayProperty]=c},g=function(b){var d=e(b);return d&&d.length>=a.minLength&&d.length<=a.maxLength&&a.allowedTagsPattern.test(d)&&!h.findInObjectArray(i.items,b,a.keyProperty||a.displayProperty)&&c({$tag:b})},i.items=[],i.addText=function(a){var b={};return f(b,a),i.add(b)},i.add=function(c){var d=e(c);return a.replaceSpacesWithDashes&&(d=h.replaceSpacesWithDashes(d)),f(c,d),g(c)?(i.items.push(c),b.trigger("tag-added",{$tag:c})):d&&b.trigger("invalid-tag",{$tag:c}),c},i.remove=function(a){var c=i.items[a];return d({$tag:c})?(i.items.splice(a,1),i.clearSelection(),b.trigger("tag-removed",{$tag:c}),c):void 0},i.select=function(a){0>a?a=i.items.length-1:a>=i.items.length&&(a=0),i.index=a,i.selected=i.items[a]},i.selectPrior=function(){i.select(--i.index)},i.selectNext=function(){i.select(++i.index)},i.removeSelected=function(){return i.remove(i.index)},i.clearSelection=function(){i.selected=null,i.index=-1},i.clearSelection(),i}function j(a){return-1!==c.indexOf(a)}return{restrict:"E",require:"ngModel",scope:{tags:"=ngModel",onTagAdding:"&",onTagAdded:"&",onInvalidTag:"&",onTagRemoving:"&",onTagRemoved:"&"},replace:!1,transclude:!0,templateUrl:"ngTagsInput/tags-input.html",controller:["$scope","$attrs","$element",function(a,c,d){a.events=h.simplePubSub(),g.load("tagsInput",a,c,{template:[String,"ngTagsInput/tag-item.html"],type:[String,"text",j],placeholder:[String,"Add a tag"],tabindex:[Number,null],removeTagSymbol:[String,String.fromCharCode(215)],replaceSpacesWithDashes:[Boolean,!0],minLength:[Number,3],maxLength:[Number,b],addOnEnter:[Boolean,!0],addOnSpace:[Boolean,!1],addOnComma:[Boolean,!0],addOnBlur:[Boolean,!0],addOnPaste:[Boolean,!1],pasteSplitPattern:[RegExp,/,/],allowedTagsPattern:[RegExp,/.+/],enableEditingLastTag:[Boolean,!1],minTags:[Number,0],maxTags:[Number,b],displayProperty:[String,"text"],keyProperty:[String,""],allowLeftoverText:[Boolean,!1],addFromAutocompleteOnly:[Boolean,!1],spellcheck:[Boolean,!0]}),a.tagList=new i(a.options,a.events,h.handleUndefinedResult(a.onTagAdding,!0),h.handleUndefinedResult(a.onTagRemoving,!0)),this.registerAutocomplete=function(){var b=d.find("input");return{addTag:function(b){return a.tagList.add(b)},focusInput:function(){b[0].focus()},getTags:function(){return a.tags},getCurrentTagText:function(){return a.newTag.text},getOptions:function(){return a.options},on:function(b,c){return a.events.on(b,c),this}}},this.registerTagItem=function(){return{getOptions:function(){return a.options},removeTag:function(b){a.disabled||a.tagList.remove(b)}}}}],link:function(b,c,g,i){var j,k=[a.enter,a.comma,a.space,a.backspace,a["delete"],a.left,a.right],l=b.tagList,m=b.events,n=b.options,o=c.find("input"),p=["minTags","maxTags","allowLeftoverText"];j=function(){i.$setValidity("maxTags",b.tags.length<=n.maxTags),i.$setValidity("minTags",b.tags.length>=n.minTags),i.$setValidity("leftoverText",b.hasFocus||n.allowLeftoverText?!0:!b.newTag.text)},i.$isEmpty=function(a){return!a||!a.length},b.newTag={text:"",invalid:null,setText:function(a){this.text=a,m.trigger("input-change",a)}},b.track=function(a){return a[n.keyProperty||n.displayProperty]},b.$watch("tags",function(a){b.tags=h.makeObjectArray(a,n.displayProperty),l.items=b.tags}),b.$watch("tags.length",function(){j()}),g.$observe("disabled",function(a){b.disabled=a}),b.eventHandlers={input:{change:function(a){m.trigger("input-change",a)},keydown:function(a){m.trigger("input-keydown",a)},focus:function(){b.hasFocus||(b.hasFocus=!0,m.trigger("input-focus"))},blur:function(){d(function(){var a=e.prop("activeElement"),d=a===o[0],f=c[0].contains(a);(d||!f)&&(b.hasFocus=!1,m.trigger("input-blur"))})},paste:function(a){a.getTextData=function(){var b=a.clipboardData||a.originalEvent&&a.originalEvent.clipboardData;return b?b.getData("text/plain"):f.clipboardData.getData("Text")},m.trigger("input-paste",a)}},host:{click:function(){b.disabled||o[0].focus()}}},m.on("tag-added",b.onTagAdded).on("invalid-tag",b.onInvalidTag).on("tag-removed",b.onTagRemoved).on("tag-added",function(){b.newTag.setText("")}).on("tag-added tag-removed",function(){i.$setViewValue(b.tags)}).on("invalid-tag",function(){b.newTag.invalid=!0}).on("option-change",function(a){-1!==p.indexOf(a.name)&&j()}).on("input-change",function(){l.clearSelection(),b.newTag.invalid=null}).on("input-focus",function(){c.triggerHandler("focus"),i.$setValidity("leftoverText",!0)}).on("input-blur",function(){n.addOnBlur&&!n.addFromAutocompleteOnly&&l.addText(b.newTag.text),c.triggerHandler("blur"),j()}).on("input-keydown",function(c){var d,e,f,g,h=c.keyCode,i=c.shiftKey||c.altKey||c.ctrlKey||c.metaKey,j={};if(!i&&-1!==k.indexOf(h)){if(j[a.enter]=n.addOnEnter,j[a.comma]=n.addOnComma,j[a.space]=n.addOnSpace,d=!n.addFromAutocompleteOnly&&j[h],e=(h===a.backspace||h===a["delete"])&&l.selected,g=h===a.backspace&&0===b.newTag.text.length&&n.enableEditingLastTag,f=(h===a.backspace||h===a.left||h===a.right)&&0===b.newTag.text.length&&!n.enableEditingLastTag,d)l.addText(b.newTag.text);else if(g){var m;l.selectPrior(),m=l.removeSelected(),m&&b.newTag.setText(m[n.displayProperty])}else e?l.removeSelected():f&&(h===a.left||h===a.backspace?l.selectPrior():h===a.right&&l.selectNext());(d||f||e||g)&&c.preventDefault()}}).on("input-paste",function(a){if(n.addOnPaste){var b=a.getTextData(),c=b.split(n.pasteSplitPattern);c.length>1&&(c.forEach(function(a){l.addText(a)}),a.preventDefault())}})}}}]),d.directive("tiTagItem",["tiUtil",function(a){return{restrict:"E",require:"^tagsInput",template:'',scope:{data:"="},link:function(b,c,d,e){var f=e.registerTagItem(),g=f.getOptions();b.$$template=g.template,b.$$removeTagSymbol=g.removeTagSymbol,b.$getDisplayText=function(){return a.safeToString(b.data[g.displayProperty])},b.$removeTag=function(){f.removeTag(b.$index)},b.$watch("$parent.$index",function(a){b.$index=a})}}}]),d.directive("autoComplete",["$document","$timeout","$sce","$q","tagsInputConfig","tiUtil",function(b,c,d,e,f,g){function h(a,b,c){var d,f,h,i={};return h=function(){return b.tagsInput.keyProperty||b.tagsInput.displayProperty},d=function(a,c){return a.filter(function(a){return!g.findInObjectArray(c,a,h(),function(a,c){return b.tagsInput.replaceSpacesWithDashes&&(a=g.replaceSpacesWithDashes(a),c=g.replaceSpacesWithDashes(c)),g.defaultComparer(a,c)})})},i.reset=function(){f=null,i.items=[],i.visible=!1,i.index=-1,i.selected=null,i.query=null},i.show=function(){b.selectFirstMatch?i.select(0):i.selected=null,i.visible=!0},i.load=g.debounce(function(c,j){i.query=c;var k=e.when(a({$query:c}));f=k,k.then(function(a){k===f&&(a=g.makeObjectArray(a.data||a,h()),a=d(a,j),i.items=a.slice(0,b.maxResultsToShow),i.items.length>0?i.show():i.reset())})},b.debounceDelay),i.selectNext=function(){i.select(++i.index)},i.selectPrior=function(){i.select(--i.index)},i.select=function(a){0>a?a=i.items.length-1:a>=i.items.length&&(a=0),i.index=a,i.selected=i.items[a],c.trigger("suggestion-selected",a)},i.reset(),i}function i(a,b){var c=a.find("li").eq(b),d=c.parent(),e=c.prop("offsetTop"),f=c.prop("offsetHeight"),g=d.prop("clientHeight"),h=d.prop("scrollTop");h>e?d.prop("scrollTop",e):e+f>g+h&&d.prop("scrollTop",e+f-g)}return{restrict:"E",require:"^tagsInput",scope:{source:"&"},templateUrl:"ngTagsInput/auto-complete.html",controller:["$scope","$element","$attrs",function(a,b,c){a.events=g.simplePubSub(),f.load("autoComplete",a,c,{template:[String,"ngTagsInput/auto-complete-match.html"],debounceDelay:[Number,100],minLength:[Number,3],highlightMatchedText:[Boolean,!0],maxResultsToShow:[Number,10],loadOnDownArrow:[Boolean,!1],loadOnEmpty:[Boolean,!1],loadOnFocus:[Boolean,!1],selectFirstMatch:[Boolean,!0],displayProperty:[String,""]}),a.suggestionList=new h(a.source,a.options,a.events),this.registerAutocompleteMatch=function(){return{getOptions:function(){return a.options},getQuery:function(){return a.suggestionList.query}}}}],link:function(b,c,d,e){var f,g=[a.enter,a.tab,a.escape,a.up,a.down],h=b.suggestionList,j=e.registerAutocomplete(),k=b.options,l=b.events;k.tagsInput=j.getOptions(),f=function(a){return a&&a.length>=k.minLength||!a&&k.loadOnEmpty},b.addSuggestionByIndex=function(a){h.select(a),b.addSuggestion()},b.addSuggestion=function(){var a=!1;return h.selected&&(j.addTag(angular.copy(h.selected)),h.reset(),j.focusInput(),a=!0),a},b.track=function(a){return a[k.tagsInput.keyProperty||k.tagsInput.displayProperty]},j.on("tag-added invalid-tag input-blur",function(){h.reset()}).on("input-change",function(a){f(a)?h.load(a,j.getTags()):h.reset()}).on("input-focus",function(){var a=j.getCurrentTagText();k.loadOnFocus&&f(a)&&h.load(a,j.getTags())}).on("input-keydown",function(c){var d=c.keyCode,e=!1;if(-1!==g.indexOf(d))return h.visible?d===a.down?(h.selectNext(),e=!0):d===a.up?(h.selectPrior(),e=!0):d===a.escape?(h.reset(),e=!0):(d===a.enter||d===a.tab)&&(e=b.addSuggestion()):d===a.down&&b.options.loadOnDownArrow&&(h.load(j.getCurrentTagText(),j.getTags()),e=!0),e?(c.preventDefault(),c.stopImmediatePropagation(),!1):void 0}),l.on("suggestion-selected",function(a){i(c,a)})}}}]),d.directive("tiAutocompleteMatch",["$sce","tiUtil",function(a,b){return{restrict:"E",require:"^autoComplete",template:'',scope:{data:"="},link:function(c,d,e,f){var g=f.registerAutocompleteMatch(),h=g.getOptions();c.$$template=h.template,c.$index=c.$parent.$index,c.$highlight=function(c){return h.highlightMatchedText&&(c=b.safeHighlight(c,g.getQuery())),a.trustAsHtml(c)},c.$getDisplayText=function(){return b.safeToString(c.data[h.displayProperty||h.tagsInput.displayProperty])}}}}]),d.directive("tiTranscludeAppend",function(){return function(a,b,c,d,e){e(function(a){b.append(a)})}}),d.directive("tiAutosize",["tagsInputConfig",function(a){return{restrict:"A",require:"ngModel",link:function(b,c,d,e){var f,g,h=a.getTextAutosizeThreshold();f=angular.element(''),f.css("display","none").css("visibility","hidden").css("width","auto").css("white-space","pre"),c.parent().append(f),g=function(a){var b,e=a;return angular.isString(e)&&0===e.length&&(e=d.placeholder),e&&(f.text(e),f.css("display",""),b=f.prop("offsetWidth"),f.css("display","none")),c.css("width",b?b+h+"px":""),a},e.$parsers.unshift(g),e.$formatters.unshift(g),d.$observe("placeholder",function(a){e.$modelValue||g(a)})}}}]),d.directive("tiBindAttrs",function(){return function(a,b,c){a.$watch(c.tiBindAttrs,function(a){angular.forEach(a,function(a,b){c.$set(b,a)})},!0)}}),d.provider("tagsInputConfig",function(){var a={},b={},c=3;this.setDefaults=function(b,c){return a[b]=c,this},this.setActiveInterpolation=function(a,c){return b[a]=c,this},this.setTextAutosizeThreshold=function(a){return c=a,this},this.$get=["$interpolate",function(d){var e={};return e[String]=function(a){return a},e[Number]=function(a){return parseInt(a,10)},e[Boolean]=function(a){return"true"===a.toLowerCase()},e[RegExp]=function(a){return new RegExp(a)},{load:function(c,f,g,h){var i=function(){return!0};f.options={},angular.forEach(h,function(h,j){var k,l,m,n,o,p;k=h[0],l=h[1],m=h[2]||i,n=e[k],o=function(){var b=a[c]&&a[c][j];return angular.isDefined(b)?b:l},p=function(a){f.options[j]=a&&m(a)?n(a):o()},b[c]&&b[c][j]?g.$observe(j,function(a){p(a),f.events.trigger("option-change",{name:j,newValue:a})}):p(g[j]&&d(g[j])(f.$parent))})},getTextAutosizeThreshold:function(){return c}}}]}),d.factory("tiUtil",["$timeout",function(a){var b={};return b.debounce=function(b,c){var d;return function(){var e=arguments;a.cancel(d),d=a(function(){b.apply(null,e)},c)}},b.makeObjectArray=function(a,b){return a=a||[],a.length>0&&!angular.isObject(a[0])&&a.forEach(function(c,d){a[d]={},a[d][b]=c}),a},b.findInObjectArray=function(a,c,d,e){var f=null;return e=e||b.defaultComparer,a.some(function(a){return e(a[d],c[d])?(f=a,!0):void 0}),f},b.defaultComparer=function(a,c){return b.safeToString(a).toLowerCase()===b.safeToString(c).toLowerCase()},b.safeHighlight=function(a,c){function d(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}if(!c)return a;a=b.encodeHTML(a),c=b.encodeHTML(c);var e=new RegExp("&[^;]+;|"+d(c),"gi");return a.replace(e,function(a){return a.toLowerCase()===c.toLowerCase()?""+a+"":a})},b.safeToString=function(a){return angular.isUndefined(a)||null==a?"":a.toString().trim()},b.encodeHTML=function(a){return b.safeToString(a).replace(/&/g,"&").replace(//g,">")},b.handleUndefinedResult=function(a,b){return function(){var c=a.apply(null,arguments);return angular.isUndefined(c)?b:c}},b.replaceSpacesWithDashes=function(a){return b.safeToString(a).replace(/\s/g,"-")},b.simplePubSub=function(){var a={};return{on:function(b,c){return b.split(" ").forEach(function(b){a[b]||(a[b]=[]),a[b].push(c)}),this},trigger:function(c,d){var e=a[c]||[];return e.every(function(a){return b.handleUndefinedResult(a,!0)(d)}),this}}},b}]),d.run(["$templateCache",function(a){a.put("ngTagsInput/tags-input.html",'
    '),a.put("ngTagsInput/tag-item.html",' '),a.put("ngTagsInput/auto-complete.html",'
    '),a.put("ngTagsInput/auto-complete-match.html",'')}])}(); +/*! ngTagsInput v3.0.0 License: MIT */!function(){"use strict";var a={backspace:8,tab:9,enter:13,escape:27,space:32,up:38,down:40,left:37,right:39,"delete":46,comma:188},b=9007199254740991,c=["text","email","url"],d=angular.module("ngTagsInput",[]);d.directive("tagsInput",["$timeout","$document","$window","tagsInputConfig","tiUtil",function(d,e,f,g,h){function i(a,b,c,d){var e,f,g,i={};return e=function(b){return h.safeToString(b[a.displayProperty])},f=function(b,c){b[a.displayProperty]=c},g=function(b){var d=e(b);return d&&d.length>=a.minLength&&d.length<=a.maxLength&&a.allowedTagsPattern.test(d)&&!h.findInObjectArray(i.items,b,a.keyProperty||a.displayProperty)&&c({$tag:b})},i.items=[],i.addText=function(a){var b={};return f(b,a),i.add(b)},i.add=function(c){var d=e(c);return a.replaceSpacesWithDashes&&(d=h.replaceSpacesWithDashes(d)),f(c,d),g(c)?(i.items.push(c),b.trigger("tag-added",{$tag:c})):d&&b.trigger("invalid-tag",{$tag:c}),c},i.remove=function(a){var c=i.items[a];return d({$tag:c})?(i.items.splice(a,1),i.clearSelection(),b.trigger("tag-removed",{$tag:c}),c):void 0},i.select=function(a){0>a?a=i.items.length-1:a>=i.items.length&&(a=0),i.index=a,i.selected=i.items[a]},i.selectPrior=function(){i.select(--i.index)},i.selectNext=function(){i.select(++i.index)},i.removeSelected=function(){return i.remove(i.index)},i.clearSelection=function(){i.selected=null,i.index=-1},i.clearSelection(),i}function j(a){return-1!==c.indexOf(a)}return{restrict:"E",require:"ngModel",scope:{tags:"=ngModel",text:"=?",onTagAdding:"&",onTagAdded:"&",onInvalidTag:"&",onTagRemoving:"&",onTagRemoved:"&",onTagClicked:"&"},replace:!1,transclude:!0,templateUrl:"ngTagsInput/tags-input.html",controller:["$scope","$attrs","$element",function(a,c,d){a.events=h.simplePubSub(),g.load("tagsInput",a,c,{template:[String,"ngTagsInput/tag-item.html"],type:[String,"text",j],placeholder:[String,"Add a tag"],tabindex:[Number,null],removeTagSymbol:[String,String.fromCharCode(215)],replaceSpacesWithDashes:[Boolean,!0],minLength:[Number,3],maxLength:[Number,b],addOnEnter:[Boolean,!0],addOnSpace:[Boolean,!1],addOnComma:[Boolean,!0],addOnBlur:[Boolean,!0],addOnPaste:[Boolean,!1],pasteSplitPattern:[RegExp,/,/],allowedTagsPattern:[RegExp,/.+/],enableEditingLastTag:[Boolean,!1],minTags:[Number,0],maxTags:[Number,b],displayProperty:[String,"text"],keyProperty:[String,""],allowLeftoverText:[Boolean,!1],addFromAutocompleteOnly:[Boolean,!1],spellcheck:[Boolean,!0]}),a.tagList=new i(a.options,a.events,h.handleUndefinedResult(a.onTagAdding,!0),h.handleUndefinedResult(a.onTagRemoving,!0)),this.registerAutocomplete=function(){var b=d.find("input");return{addTag:function(b){return a.tagList.add(b)},focusInput:function(){b[0].focus()},getTags:function(){return a.tagList.items},getCurrentTagText:function(){return a.newTag.text()},getOptions:function(){return a.options},on:function(b,c){return a.events.on(b,c),this}}},this.registerTagItem=function(){return{getOptions:function(){return a.options},removeTag:function(b){a.disabled||a.tagList.remove(b)}}}}],link:function(b,c,g,i){var j,k=[a.enter,a.comma,a.space,a.backspace,a["delete"],a.left,a.right],l=b.tagList,m=b.events,n=b.options,o=c.find("input"),p=["minTags","maxTags","allowLeftoverText"];j=function(){i.$setValidity("maxTags",l.items.length<=n.maxTags),i.$setValidity("minTags",l.items.length>=n.minTags),i.$setValidity("leftoverText",b.hasFocus||n.allowLeftoverText?!0:!b.newTag.text())},i.$isEmpty=function(a){return!a||!a.length},b.newTag={text:function(a){return angular.isDefined(a)?(b.text=a,void m.trigger("input-change",a)):b.text||""},invalid:null},b.track=function(a){return a[n.keyProperty||n.displayProperty]},b.$watch("tags",function(a){a?(l.items=h.makeObjectArray(a,n.displayProperty),b.tags=l.items):l.items=[]}),b.$watch("tags.length",function(){j(),i.$validate()}),g.$observe("disabled",function(a){b.disabled=a}),b.eventHandlers={input:{keydown:function(a){m.trigger("input-keydown",a)},focus:function(){b.hasFocus||(b.hasFocus=!0,m.trigger("input-focus"))},blur:function(){d(function(){var a=e.prop("activeElement"),d=a===o[0],f=c[0].contains(a);(d||!f)&&(b.hasFocus=!1,m.trigger("input-blur"))})},paste:function(a){a.getTextData=function(){var b=a.clipboardData||a.originalEvent&&a.originalEvent.clipboardData;return b?b.getData("text/plain"):f.clipboardData.getData("Text")},m.trigger("input-paste",a)}},host:{click:function(){b.disabled||o[0].focus()}},tag:{click:function(a){m.trigger("tag-clicked",{$tag:a})}}},m.on("tag-added",b.onTagAdded).on("invalid-tag",b.onInvalidTag).on("tag-removed",b.onTagRemoved).on("tag-clicked",b.onTagClicked).on("tag-added",function(){b.newTag.text("")}).on("tag-added tag-removed",function(){b.tags=l.items,i.$setDirty()}).on("invalid-tag",function(){b.newTag.invalid=!0}).on("option-change",function(a){-1!==p.indexOf(a.name)&&j()}).on("input-change",function(){l.clearSelection(),b.newTag.invalid=null}).on("input-focus",function(){c.triggerHandler("focus"),i.$setValidity("leftoverText",!0)}).on("input-blur",function(){n.addOnBlur&&!n.addFromAutocompleteOnly&&l.addText(b.newTag.text()),c.triggerHandler("blur"),j()}).on("input-keydown",function(c){var d,e,f,g,i=c.keyCode,j={};if(!h.isModifierOn(c)&&-1!==k.indexOf(i)){if(j[a.enter]=n.addOnEnter,j[a.comma]=n.addOnComma,j[a.space]=n.addOnSpace,d=!n.addFromAutocompleteOnly&&j[i],e=(i===a.backspace||i===a["delete"])&&l.selected,g=i===a.backspace&&0===b.newTag.text().length&&n.enableEditingLastTag,f=(i===a.backspace||i===a.left||i===a.right)&&0===b.newTag.text().length&&!n.enableEditingLastTag,d)l.addText(b.newTag.text());else if(g){var m;l.selectPrior(),m=l.removeSelected(),m&&b.newTag.text(m[n.displayProperty])}else e?l.removeSelected():f&&(i===a.left||i===a.backspace?l.selectPrior():i===a.right&&l.selectNext());(d||f||e||g)&&c.preventDefault()}}).on("input-paste",function(a){if(n.addOnPaste){var b=a.getTextData(),c=b.split(n.pasteSplitPattern);c.length>1&&(c.forEach(function(a){l.addText(a)}),a.preventDefault())}})}}}]),d.directive("tiTagItem",["tiUtil",function(a){return{restrict:"E",require:"^tagsInput",template:'',scope:{data:"="},link:function(b,c,d,e){var f=e.registerTagItem(),g=f.getOptions();b.$$template=g.template,b.$$removeTagSymbol=g.removeTagSymbol,b.$getDisplayText=function(){return a.safeToString(b.data[g.displayProperty])},b.$removeTag=function(){f.removeTag(b.$index)},b.$watch("$parent.$index",function(a){b.$index=a})}}}]),d.directive("autoComplete",["$document","$timeout","$sce","$q","tagsInputConfig","tiUtil",function(b,c,d,e,f,g){function h(a,b,c){var d,f,h,i={};return h=function(){return b.tagsInput.keyProperty||b.tagsInput.displayProperty},d=function(a,c){return a.filter(function(a){return!g.findInObjectArray(c,a,h(),function(a,c){return b.tagsInput.replaceSpacesWithDashes&&(a=g.replaceSpacesWithDashes(a),c=g.replaceSpacesWithDashes(c)),g.defaultComparer(a,c)})})},i.reset=function(){f=null,i.items=[],i.visible=!1,i.index=-1,i.selected=null,i.query=null},i.show=function(){b.selectFirstMatch?i.select(0):i.selected=null,i.visible=!0},i.load=g.debounce(function(c,j){i.query=c;var k=e.when(a({$query:c}));f=k,k.then(function(a){k===f&&(a=g.makeObjectArray(a.data||a,h()),a=d(a,j),i.items=a.slice(0,b.maxResultsToShow),i.items.length>0?i.show():i.reset())})},b.debounceDelay),i.selectNext=function(){i.select(++i.index)},i.selectPrior=function(){i.select(--i.index)},i.select=function(a){0>a?a=i.items.length-1:a>=i.items.length&&(a=0),i.index=a,i.selected=i.items[a],c.trigger("suggestion-selected",a)},i.reset(),i}function i(a,b){var c=a.find("li").eq(b),d=c.parent(),e=c.prop("offsetTop"),f=c.prop("offsetHeight"),g=d.prop("clientHeight"),h=d.prop("scrollTop");h>e?d.prop("scrollTop",e):e+f>g+h&&d.prop("scrollTop",e+f-g)}return{restrict:"E",require:"^tagsInput",scope:{source:"&"},templateUrl:"ngTagsInput/auto-complete.html",controller:["$scope","$element","$attrs",function(a,b,c){a.events=g.simplePubSub(),f.load("autoComplete",a,c,{template:[String,"ngTagsInput/auto-complete-match.html"],debounceDelay:[Number,100],minLength:[Number,3],highlightMatchedText:[Boolean,!0],maxResultsToShow:[Number,10],loadOnDownArrow:[Boolean,!1],loadOnEmpty:[Boolean,!1],loadOnFocus:[Boolean,!1],selectFirstMatch:[Boolean,!0],displayProperty:[String,""]}),a.suggestionList=new h(a.source,a.options,a.events),this.registerAutocompleteMatch=function(){return{getOptions:function(){return a.options},getQuery:function(){return a.suggestionList.query}}}}],link:function(b,c,d,e){var f,h=[a.enter,a.tab,a.escape,a.up,a.down],j=b.suggestionList,k=e.registerAutocomplete(),l=b.options,m=b.events;l.tagsInput=k.getOptions(),f=function(a){return a&&a.length>=l.minLength||!a&&l.loadOnEmpty},b.addSuggestionByIndex=function(a){j.select(a),b.addSuggestion()},b.addSuggestion=function(){var a=!1;return j.selected&&(k.addTag(angular.copy(j.selected)),j.reset(),k.focusInput(),a=!0),a},b.track=function(a){return a[l.tagsInput.keyProperty||l.tagsInput.displayProperty]},k.on("tag-added tag-removed invalid-tag input-blur",function(){j.reset()}).on("input-change",function(a){f(a)?j.load(a,k.getTags()):j.reset()}).on("input-focus",function(){var a=k.getCurrentTagText();l.loadOnFocus&&f(a)&&j.load(a,k.getTags())}).on("input-keydown",function(c){var d=c.keyCode,e=!1;if(!g.isModifierOn(c)&&-1!==h.indexOf(d))return j.visible?d===a.down?(j.selectNext(),e=!0):d===a.up?(j.selectPrior(),e=!0):d===a.escape?(j.reset(),e=!0):(d===a.enter||d===a.tab)&&(e=b.addSuggestion()):d===a.down&&b.options.loadOnDownArrow&&(j.load(k.getCurrentTagText(),k.getTags()),e=!0),e?(c.preventDefault(),c.stopImmediatePropagation(),!1):void 0}),m.on("suggestion-selected",function(a){i(c,a)})}}}]),d.directive("tiAutocompleteMatch",["$sce","tiUtil",function(a,b){return{restrict:"E",require:"^autoComplete",template:'',scope:{data:"="},link:function(c,d,e,f){var g=f.registerAutocompleteMatch(),h=g.getOptions();c.$$template=h.template,c.$index=c.$parent.$index,c.$highlight=function(c){return h.highlightMatchedText&&(c=b.safeHighlight(c,g.getQuery())),a.trustAsHtml(c)},c.$getDisplayText=function(){return b.safeToString(c.data[h.displayProperty||h.tagsInput.displayProperty])}}}}]),d.directive("tiTranscludeAppend",function(){return function(a,b,c,d,e){e(function(a){b.append(a)})}}),d.directive("tiAutosize",["tagsInputConfig",function(a){return{restrict:"A",require:"ngModel",link:function(b,c,d,e){var f,g,h=a.getTextAutosizeThreshold();f=angular.element(''),f.css("display","none").css("visibility","hidden").css("width","auto").css("white-space","pre"),c.parent().append(f),g=function(a){var b,e=a;return angular.isString(e)&&0===e.length&&(e=d.placeholder),e&&(f.text(e),f.css("display",""),b=f.prop("offsetWidth"),f.css("display","none")),c.css("width",b?b+h+"px":""),a},e.$parsers.unshift(g),e.$formatters.unshift(g),d.$observe("placeholder",function(a){e.$modelValue||g(a)})}}}]),d.directive("tiBindAttrs",function(){return function(a,b,c){a.$watch(c.tiBindAttrs,function(a){angular.forEach(a,function(a,b){c.$set(b,a)})},!0)}}),d.provider("tagsInputConfig",function(){var a={},b={},c=3;this.setDefaults=function(b,c){return a[b]=c,this},this.setActiveInterpolation=function(a,c){return b[a]=c,this},this.setTextAutosizeThreshold=function(a){return c=a,this},this.$get=["$interpolate",function(d){var e={};return e[String]=function(a){return a},e[Number]=function(a){return parseInt(a,10)},e[Boolean]=function(a){return"true"===a.toLowerCase()},e[RegExp]=function(a){return new RegExp(a)},{load:function(c,f,g,h){var i=function(){return!0};f.options={},angular.forEach(h,function(h,j){var k,l,m,n,o,p;k=h[0],l=h[1],m=h[2]||i,n=e[k],o=function(){var b=a[c]&&a[c][j];return angular.isDefined(b)?b:l},p=function(a){f.options[j]=a&&m(a)?n(a):o()},b[c]&&b[c][j]?g.$observe(j,function(a){p(a),f.events.trigger("option-change",{name:j,newValue:a})}):p(g[j]&&d(g[j])(f.$parent))})},getTextAutosizeThreshold:function(){return c}}}]}),d.factory("tiUtil",["$timeout",function(a){var b={};return b.debounce=function(b,c){var d;return function(){var e=arguments;a.cancel(d),d=a(function(){b.apply(null,e)},c)}},b.makeObjectArray=function(a,b){if(!angular.isArray(a)||0===a.length||angular.isObject(a[0]))return a;var c=[];return a.forEach(function(a){var d={};d[b]=a,c.push(d)}),c},b.findInObjectArray=function(a,c,d,e){var f=null;return e=e||b.defaultComparer,a.some(function(a){return e(a[d],c[d])?(f=a,!0):void 0}),f},b.defaultComparer=function(a,c){return b.safeToString(a).toLowerCase()===b.safeToString(c).toLowerCase()},b.safeHighlight=function(a,c){function d(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}if(!c)return a;a=b.encodeHTML(a),c=b.encodeHTML(c);var e=new RegExp("&[^;]+;|"+d(c),"gi");return a.replace(e,function(a){return a.toLowerCase()===c.toLowerCase()?""+a+"":a})},b.safeToString=function(a){return angular.isUndefined(a)||null==a?"":a.toString().trim()},b.encodeHTML=function(a){return b.safeToString(a).replace(/&/g,"&").replace(//g,">")},b.handleUndefinedResult=function(a,b){return function(){var c=a.apply(null,arguments);return angular.isUndefined(c)?b:c}},b.replaceSpacesWithDashes=function(a){return b.safeToString(a).replace(/\s/g,"-")},b.isModifierOn=function(a){return a.shiftKey||a.ctrlKey||a.altKey||a.metaKey},b.simplePubSub=function(){var a={};return{on:function(b,c){return b.split(" ").forEach(function(b){a[b]||(a[b]=[]),a[b].push(c)}),this},trigger:function(c,d){var e=a[c]||[];return e.every(function(a){return b.handleUndefinedResult(a,!0)(d)}),this}}},b}]),d.run(["$templateCache",function(a){a.put("ngTagsInput/tags-input.html",'
    '),a.put("ngTagsInput/tag-item.html",' '),a.put("ngTagsInput/auto-complete.html",'
    '),a.put("ngTagsInput/auto-complete-match.html",'')}])}(); diff --git a/app/assets/stylesheets/shared/ng-tags-input.min.css b/app/assets/stylesheets/shared/ng-tags-input.min.css index ee4a4a98d5..22dcf47d66 100755 --- a/app/assets/stylesheets/shared/ng-tags-input.min.css +++ b/app/assets/stylesheets/shared/ng-tags-input.min.css @@ -1 +1 @@ -tags-input{display:block}tags-input *,tags-input :after,tags-input :before{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}tags-input .host{position:relative;margin-top:5px;margin-bottom:5px;height:100%}tags-input .host:active{outline:0}tags-input .tags{-moz-appearance:textfield;-webkit-appearance:textfield;padding:1px;overflow:hidden;word-wrap:break-word;cursor:text;background-color:#fff;border:1px solid #a9a9a9;box-shadow:1px 1px 1px 0 #d3d3d3 inset;height:100%}tags-input .tags.focused{outline:0;-webkit-box-shadow:0 0 3px 1px rgba(5,139,242,.6);-moz-box-shadow:0 0 3px 1px rgba(5,139,242,.6);box-shadow:0 0 3px 1px rgba(5,139,242,.6)}tags-input .tags .tag-list{margin:0;padding:0;list-style-type:none}tags-input .tags .tag-item{margin:2px;padding:0 5px;display:inline-block;float:left;font:14px "Helvetica Neue",Helvetica,Arial,sans-serif;height:26px;line-height:25px;border:1px solid #acacac;border-radius:3px;background:-webkit-linear-gradient(top,#f0f9ff 0,#cbebff 47%,#a1dbff 100%);background:linear-gradient(to bottom,#f0f9ff 0,#cbebff 47%,#a1dbff 100%)}tags-input .tags .tag-item.selected{background:-webkit-linear-gradient(top,#febbbb 0,#fe9090 45%,#ff5c5c 100%);background:linear-gradient(to bottom,#febbbb 0,#fe9090 45%,#ff5c5c 100%)}tags-input .tags .tag-item .remove-button{margin:0 0 0 5px;padding:0;border:none;background:0 0;cursor:pointer;vertical-align:middle;font:700 16px Arial,sans-serif;color:#585858}tags-input .tags .tag-item .remove-button:active{color:red}tags-input .tags .input{border:0;outline:0;margin:2px;padding:0;padding-left:5px;float:left;height:26px;font:14px "Helvetica Neue",Helvetica,Arial,sans-serif}tags-input .tags .input.invalid-tag{color:red}tags-input .tags .input::-ms-clear{display:none}tags-input.ng-invalid .tags{-webkit-box-shadow:0 0 3px 1px rgba(255,0,0,.6);-moz-box-shadow:0 0 3px 1px rgba(255,0,0,.6);box-shadow:0 0 3px 1px rgba(255,0,0,.6)}tags-input[disabled] .host:focus{outline:0}tags-input[disabled] .tags{background-color:#eee;cursor:default}tags-input[disabled] .tags .tag-item{opacity:.65;background:-webkit-linear-gradient(top,#f0f9ff 0,rgba(203,235,255,.75)47%,rgba(161,219,255,.62)100%);background:linear-gradient(to bottom,#f0f9ff 0,rgba(203,235,255,.75)47%,rgba(161,219,255,.62)100%)}tags-input[disabled] .tags .tag-item .remove-button{cursor:default}tags-input[disabled] .tags .tag-item .remove-button:active{color:#585858}tags-input[disabled] .tags .input{background-color:#eee;cursor:default}tags-input .autocomplete{margin-top:5px;position:absolute;padding:5px 0;z-index:999;width:100%;background-color:#fff;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}tags-input .autocomplete .suggestion-list{margin:0;padding:0;list-style-type:none;max-height:280px;overflow-y:auto;position:relative}tags-input .autocomplete .suggestion-item{padding:5px 10px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font:16px "Helvetica Neue",Helvetica,Arial,sans-serif;color:#000;background-color:#fff}tags-input .autocomplete .suggestion-item.selected,tags-input .autocomplete .suggestion-item.selected em{color:#fff;background-color:#0097cf}tags-input .autocomplete .suggestion-item em{font:normal bold 16px "Helvetica Neue",Helvetica,Arial,sans-serif;color:#000;background-color:#fff} \ No newline at end of file +tags-input{display:block}tags-input *,tags-input :after,tags-input :before{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}tags-input .host{position:relative;margin-top:5px;margin-bottom:5px;height:100%}tags-input .host:active{outline:0}tags-input .tags{-moz-appearance:textfield;-webkit-appearance:textfield;padding:1px;overflow:hidden;word-wrap:break-word;cursor:text;background-color:#fff;border:1px solid #a9a9a9;box-shadow:1px 1px 1px 0 #d3d3d3 inset;height:100%}tags-input .tags.focused{outline:0;-webkit-box-shadow:0 0 3px 1px rgba(5,139,242,.6);-moz-box-shadow:0 0 3px 1px rgba(5,139,242,.6);box-shadow:0 0 3px 1px rgba(5,139,242,.6)}tags-input .tags .tag-list{margin:0;padding:0;list-style-type:none}tags-input .tags .tag-item{margin:2px;padding:0 5px;display:inline-block;float:left;font:14px "Helvetica Neue",Helvetica,Arial,sans-serif;height:26px;line-height:25px;border:1px solid #acacac;border-radius:3px;background:-webkit-linear-gradient(top,#f0f9ff 0,#cbebff 47%,#a1dbff 100%);background:linear-gradient(to bottom,#f0f9ff 0,#cbebff 47%,#a1dbff 100%)}tags-input .tags .tag-item.selected{background:-webkit-linear-gradient(top,#febbbb 0,#fe9090 45%,#ff5c5c 100%);background:linear-gradient(to bottom,#febbbb 0,#fe9090 45%,#ff5c5c 100%)}tags-input .tags .tag-item .remove-button{margin:0 0 0 5px;padding:0;border:none;background:0 0;cursor:pointer;vertical-align:middle;font:700 16px Arial,sans-serif;color:#585858}tags-input .tags .input.invalid-tag,tags-input .tags .tag-item .remove-button:active{color:red}tags-input .tags .input{border:0;outline:0;margin:2px;padding:0 0 0 5px;float:left;height:26px;font:14px "Helvetica Neue",Helvetica,Arial,sans-serif}tags-input .tags .input::-ms-clear{display:none}tags-input.ng-invalid .tags{-webkit-box-shadow:0 0 3px 1px rgba(255,0,0,.6);-moz-box-shadow:0 0 3px 1px rgba(255,0,0,.6);box-shadow:0 0 3px 1px rgba(255,0,0,.6)}tags-input[disabled] .host:focus{outline:0}tags-input[disabled] .tags{background-color:#eee;cursor:default}tags-input[disabled] .tags .tag-item{opacity:.65;background:-webkit-linear-gradient(top,#f0f9ff 0,rgba(203,235,255,.75)47%,rgba(161,219,255,.62)100%);background:linear-gradient(to bottom,#f0f9ff 0,rgba(203,235,255,.75)47%,rgba(161,219,255,.62)100%)}tags-input[disabled] .tags .tag-item .remove-button{cursor:default}tags-input[disabled] .tags .tag-item .remove-button:active{color:#585858}tags-input[disabled] .tags .input{background-color:#eee;cursor:default}tags-input .autocomplete{margin-top:5px;position:absolute;padding:5px 0;z-index:999;width:100%;background-color:#fff;border:1px solid rgba(0,0,0,.2);-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}tags-input .autocomplete .suggestion-list{margin:0;padding:0;list-style-type:none;max-height:280px;overflow-y:auto;position:relative}tags-input .autocomplete .suggestion-item{padding:5px 10px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font:16px "Helvetica Neue",Helvetica,Arial,sans-serif;color:#000;background-color:#fff}tags-input .autocomplete .suggestion-item.selected,tags-input .autocomplete .suggestion-item.selected em{color:#fff;background-color:#0097cf}tags-input .autocomplete .suggestion-item em{font:normal bold 16px "Helvetica Neue",Helvetica,Arial,sans-serif;color:#000;background-color:#fff} From 6bfe1be04547727aa00f8187a52ca233ed61baa0 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 11 May 2016 11:39:54 +1000 Subject: [PATCH 033/110] Update spec looking for matching img src --- spec/features/consumer/shopping/shopping_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index dceecd43f8..9b03259ec5 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -29,7 +29,7 @@ feature "As a consumer I want to shop with a distributor", js: true do visit shop_path page.should have_text distributor.name find("#tab_about a").click - first("distributor img")['src'].should == distributor.logo.url(:thumb) + first("distributor img")['src'].should include distributor.logo.url(:thumb) end it "shows the producers for a distributor" do From 2c9697ff4e9723688293e73c98b1d2d79f8c36aa Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 11 May 2016 14:30:41 +1000 Subject: [PATCH 034/110] Adding -nc option to wget of PhantomJS, so that we only download it if needed --- .travis.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 04e7100f44..f914edbbb6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,15 +26,15 @@ env: - CI_NODE_INDEX=4 KARMA="true" GITHUB_DEPLOY="true" before_script: - - mkdir travis-phantomjs || true - - wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 -O $PWD/travis-phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2 - - tar -xvf $PWD/travis-phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2 -C $PWD/travis-phantomjs - - export PATH=$PWD/travis-phantomjs/phantomjs-2.1.1-linux-x86_64/bin:$PATH - - cp config/database.travis.yml config/database.yml - cp config/application.yml.example config/application.yml - RAILS_ENV=test bundle exec rake db:create db:schema:load + - mkdir -p travis-phantomjs + - wget -nc https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 -O $PWD/travis-phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2 || true + - tar -xvf $PWD/travis-phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2 -C $PWD/travis-phantomjs + - export PATH=$PWD/travis-phantomjs/phantomjs-2.1.1-linux-x86_64/bin:$PATH + - > if [ "$KARMA" = "true" ]; then npm install -g npm@'3.8.8' From 7e932f091d046affd89d6717c6f877de831657d4 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 11 May 2016 15:19:28 +1000 Subject: [PATCH 035/110] Use have_current_path rather than current_path.should == --- spec/features/admin/authentication_spec.rb | 6 +++--- spec/features/admin/cms_spec.rb | 4 ++-- spec/features/admin/orders_spec.rb | 4 ++-- spec/features/admin/payment_method_spec.rb | 2 +- spec/features/consumer/authentication_spec.rb | 2 +- spec/features/consumer/shops_spec.rb | 4 ++-- spec/support/request/web_helper.rb | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/features/admin/authentication_spec.rb b/spec/features/admin/authentication_spec.rb index 8473551c99..fe2eb156c5 100644 --- a/spec/features/admin/authentication_spec.rb +++ b/spec/features/admin/authentication_spec.rb @@ -16,14 +16,14 @@ feature "Authentication", js: true do fill_in "Email", with: user.email fill_in "Password", with: user.password click_login_button - page.should have_content "DASHBOARD" - current_path.should == spree.admin_path + expect(page).to have_content "DASHBOARD" + expect(page).to have_current_path spree.admin_path end end scenario "viewing my account" do login_to_admin_section click_link "Account" - current_path.should == spree.account_path + expect(page).to have_current_path spree.account_path end end diff --git a/spec/features/admin/cms_spec.rb b/spec/features/admin/cms_spec.rb index 8ab180906e..c200bc57e4 100644 --- a/spec/features/admin/cms_spec.rb +++ b/spec/features/admin/cms_spec.rb @@ -15,7 +15,7 @@ feature %q{ page.should have_content "ComfortableMexicanSofa" click_link 'Spree Admin' - current_path.should match(/^\/admin/) + expect(page).to have_current_path /^\/admin/ end scenario "anonymous user can't access CMS admin", js: true do @@ -29,6 +29,6 @@ feature %q{ page.should_not have_content "Login" visit cms_admin_path page.should_not have_content "ComfortableMexicanSofa" - current_path.should == root_path + expect(page).to have_current_path root_path end end diff --git a/spec/features/admin/orders_spec.rb b/spec/features/admin/orders_spec.rb index 33f91eb850..18b3466106 100644 --- a/spec/features/admin/orders_spec.rb +++ b/spec/features/admin/orders_spec.rb @@ -153,7 +153,7 @@ feature %q{ login_to_admin_section visit '/admin/orders' - current_path.should == spree.admin_orders_path + expect(page).to have_current_path spree.admin_orders_path # click the 'capture' link for the order page.find("[data-action=capture][href*=#{@order.number}]").click @@ -165,7 +165,7 @@ feature %q{ @order.payment_state.should == "paid" # we should still be on the same page - current_path.should == spree.admin_orders_path + expect(page).to have_current_path spree.admin_orders_path end diff --git a/spec/features/admin/payment_method_spec.rb b/spec/features/admin/payment_method_spec.rb index abb5feaf3e..b736155372 100644 --- a/spec/features/admin/payment_method_spec.rb +++ b/spec/features/admin/payment_method_spec.rb @@ -94,7 +94,7 @@ feature %q{ click_link "Payment Methods" end click_link 'Create One Now' - current_path.should == spree.new_admin_payment_method_path + expect(page).to have_current_path spree.new_admin_payment_method_path end it "creates payment methods" do diff --git a/spec/features/consumer/authentication_spec.rb b/spec/features/consumer/authentication_spec.rb index b01b04fe84..63f08efb71 100644 --- a/spec/features/consumer/authentication_spec.rb +++ b/spec/features/consumer/authentication_spec.rb @@ -18,7 +18,7 @@ feature "Authentication", js: true, retry: 3 do fill_in "Password", with: user.password click_login_button page.should have_content "Find local producers" - current_path.should == producers_path + expect(page).to have_current_path producers_path end end diff --git a/spec/features/consumer/shops_spec.rb b/spec/features/consumer/shops_spec.rb index 4edaa702e0..9625640fe7 100644 --- a/spec/features/consumer/shops_spec.rb +++ b/spec/features/consumer/shops_spec.rb @@ -43,12 +43,12 @@ feature 'Shops', js: true do it "should link to the hub page" do follow_active_table_node distributor.name - current_path.should == enterprise_shop_path(distributor) + expect(page).to have_current_path enterprise_shop_path(distributor) end it "should show hub producer modals" do expand_active_table_node distributor.name - page.should have_content producer.name + expect(page).to have_content producer.name open_enterprise_modal producer modal_should_be_open_for producer end diff --git a/spec/support/request/web_helper.rb b/spec/support/request/web_helper.rb index 70c803709e..23f9757c5b 100644 --- a/spec/support/request/web_helper.rb +++ b/spec/support/request/web_helper.rb @@ -29,7 +29,7 @@ module WebHelper def current_path_should_be path current_path = URI.parse(current_url).path - current_path.should == path + expect(page).to have_current_path path end def fill_in_fields(field_values) From c44f9d2537f5514e86d981f6a2d312b6c1aa715b Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 22 Apr 2016 10:34:33 +1000 Subject: [PATCH 036/110] Making text (and links) within shopfront message more legible --- .../stylesheets/darkswarm/shop.css.sass | 19 ++++++++++++++----- app/views/shop/_messages.html.haml | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index eab074369e..5cfcef46b8 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -84,16 +84,25 @@ padding-right: 0rem font-size: 0.8rem - .shopfront_message, .shopfront_closed_message, .shopfront_hidden_message + .alert-box.shopfront-message + border: 2px solid $clr-turquoise + border-radius: 5px + background-color: $clr-turquoise-light + color: $clr-turquoise + a + color: #0096ad + &:hover, &:focus, &:active + text-decoration: none + color: #4aadbd + + + .shopfront_closed_message, .shopfront_hidden_message padding: 15px border-radius: 5px - .shopfront_message, .shopfront_closed_message + .shopfront_closed_message border: 2px solid #eb4c46 - .shopfront_message - margin-top: 2em - .shopfront_closed_message margin: 2em 0em diff --git a/app/views/shop/_messages.html.haml b/app/views/shop/_messages.html.haml index ba35354bf3..9e5d3b8e82 100644 --- a/app/views/shop/_messages.html.haml +++ b/app/views/shop/_messages.html.haml @@ -24,6 +24,6 @@   .row .small-12.columns - .alert-box{ "ofn-inline-alert" => true, ng: { show: "visible" } } + .alert-box.shopfront-message{ "ofn-inline-alert" => true, ng: { show: "visible" } } = current_distributor.preferred_shopfront_message.html_safe %a.close{ ng: { click: "close()" } } × From 564c1a36503c9eb7cb90905035918c07137ddf35 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 13 May 2016 11:36:51 +1000 Subject: [PATCH 037/110] Improvements to customers page Create customer from dialog Limiting the number of products shown as once Adding SaveBar and StatusMessage --- .../customers_controller.js.coffee | 37 +++++--------- .../admin/customers/customers.js.coffee | 2 +- .../directives/new_customer_dialog.js.coffee | 46 ++++++++++++++++++ .../services/current_enterprise.js.coffee | 3 ++ .../customers/services/customers.js.coffee | 21 ++++++++ .../directives/show_more.js.coffee | 12 +++++ .../admin/index_utils/index_utils.js.coffee | 2 +- .../services/pending_changes.js.coffee | 17 +++++-- .../admin/new_customer_dialog.html.haml | 15 ++++++ .../templates/admin/show_more.html.haml | 4 ++ .../admin/components/save_bar.sass | 1 + .../admin/openfoodnetwork.css.scss | 3 ++ app/assets/stylesheets/admin/orders.css.scss | 4 -- app/controllers/admin/customers_controller.rb | 9 ++-- app/views/admin/customers/index.html.haml | 41 +++++++++------- .../variant_overrides/_show_more.html.haml | 3 +- config/locales/en.yml | 5 +- spec/features/admin/customers_spec.rb | 48 +++++++++++++++++-- .../customers_controller_spec.js.coffee | 6 +-- 19 files changed, 214 insertions(+), 65 deletions(-) create mode 100644 app/assets/javascripts/admin/customers/directives/new_customer_dialog.js.coffee create mode 100644 app/assets/javascripts/admin/customers/services/current_enterprise.js.coffee create mode 100644 app/assets/javascripts/admin/customers/services/customers.js.coffee create mode 100644 app/assets/javascripts/admin/index_utils/directives/show_more.js.coffee create mode 100644 app/assets/javascripts/templates/admin/new_customer_dialog.html.haml create mode 100644 app/assets/javascripts/templates/admin/show_more.html.haml diff --git a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee index 763e92b400..9e1c027531 100644 --- a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee +++ b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee @@ -1,43 +1,28 @@ -angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerResource, TagRuleResource, $q, Columns, pendingChanges, shops) -> - $scope.shop = {} +angular.module("admin.customers").controller "customersCtrl", ($scope, $q, Customers, TagRuleResource, CurrentShop, RequestMonitor, Columns, pendingChanges, shops) -> $scope.shops = shops + $scope.CurrentShop = CurrentShop + $scope.RequestMonitor = RequestMonitor $scope.submitAll = pendingChanges.submitAll + $scope.add = Customers.add + $scope.deleteCustomer = Customers.remove + $scope.customerLimit = 20 $scope.columns = Columns.setColumns email: { name: "Email", visible: true } code: { name: "Code", visible: true } tags: { name: "Tags", visible: true } - $scope.$watch "shop.id", -> - if $scope.shop.id? - $scope.customers = index {enterprise_id: $scope.shop.id} + $scope.$watch "CurrentShop.shop", -> + if $scope.CurrentShop.shop.id? + Customers.index({enterprise_id: $scope.CurrentShop.shop.id}).then (data) -> + $scope.customers = data $scope.findTags = (query) -> defer = $q.defer() params = - enterprise_id: $scope.shop.id + enterprise_id: $scope.CurrentShop.shop.id TagRuleResource.mapByTag params, (data) => filtered = data.filter (tag) -> tag.text.toLowerCase().indexOf(query.toLowerCase()) != -1 defer.resolve filtered defer.promise - - $scope.add = (email) -> - params = - enterprise_id: $scope.shop.id - email: email - CustomerResource.create params, (customer) => - if customer.id - $scope.customers.push customer - $scope.quickSearch = customer.email - - $scope.deleteCustomer = (customer) -> - params = id: customer.id - CustomerResource.destroy params, -> - i = $scope.customers.indexOf customer - $scope.customers.splice i, 1 unless i < 0 - - index = (params) -> - $scope.loaded = false - CustomerResource.index params, => - $scope.loaded = true diff --git a/app/assets/javascripts/admin/customers/customers.js.coffee b/app/assets/javascripts/admin/customers/customers.js.coffee index 33be58c9ac..fe8ae1de5b 100644 --- a/app/assets/javascripts/admin/customers/customers.js.coffee +++ b/app/assets/javascripts/admin/customers/customers.js.coffee @@ -1 +1 @@ -angular.module("admin.customers", ['ngResource', 'admin.tagRules', 'admin.indexUtils', 'admin.utils', 'admin.dropdown']) \ No newline at end of file +angular.module("admin.customers", ['ngResource', 'admin.tagRules', 'admin.indexUtils', 'admin.utils', 'admin.dropdown']) diff --git a/app/assets/javascripts/admin/customers/directives/new_customer_dialog.js.coffee b/app/assets/javascripts/admin/customers/directives/new_customer_dialog.js.coffee new file mode 100644 index 0000000000..02276875b9 --- /dev/null +++ b/app/assets/javascripts/admin/customers/directives/new_customer_dialog.js.coffee @@ -0,0 +1,46 @@ +angular.module("admin.customers").directive 'newCustomerDialog', ($compile, $injector, $templateCache, $window, CurrentShop, Customers) -> + restrict: 'A' + scope: true + link: (scope, element, attr) -> + scope.CurrentShop = CurrentShop + scope.submitted = null + scope.email = "" + scope.errors = [] + + scope.addCustomer = (valid) -> + scope.submitted = scope.email + scope.errors = [] + if valid + Customers.add(scope.email).$promise.then (data) -> + if data.id + scope.email = "" + scope.submitted = null + template.dialog('close') + , (response) -> + if response.data.errors + scope.errors.push(error) for error in response.data.errors + else + scope.errors.push("Sorry! Could not create '#{scope.email}'") + return + + # Compile modal template + template = $compile($templateCache.get('admin/new_customer_dialog.html'))(scope) + + # Set Dialog options + template.dialog + show: { effect: "fade", duration: 400 } + hide: { effect: "fade", duration: 300 } + autoOpen: false + resizable: false + width: $window.innerWidth * 0.4; + modal: true + open: (event, ui) -> + $('.ui-widget-overlay').bind 'click', -> + $(this).siblings('.ui-dialog').find('.ui-dialog-content').dialog('close') + + # Link opening of dialog to click event on element + element.bind 'click', (e) -> + if CurrentShop.shop.id + template.dialog('open') + else + alert('Please select a shop first') diff --git a/app/assets/javascripts/admin/customers/services/current_enterprise.js.coffee b/app/assets/javascripts/admin/customers/services/current_enterprise.js.coffee new file mode 100644 index 0000000000..9df7e09895 --- /dev/null +++ b/app/assets/javascripts/admin/customers/services/current_enterprise.js.coffee @@ -0,0 +1,3 @@ +angular.module("admin.customers").factory "CurrentShop", -> + new class CurrentShop + shop: {} diff --git a/app/assets/javascripts/admin/customers/services/customers.js.coffee b/app/assets/javascripts/admin/customers/services/customers.js.coffee new file mode 100644 index 0000000000..a9f4f0102f --- /dev/null +++ b/app/assets/javascripts/admin/customers/services/customers.js.coffee @@ -0,0 +1,21 @@ +angular.module("admin.customers").factory "Customers", ($q, RequestMonitor, CustomerResource, CurrentShop) -> + new class Customers + customers: [] + + add: (email) -> + params = + enterprise_id: CurrentShop.shop.id + email: email + CustomerResource.create params, (customer) => + @customers.unshift customer if customer.id + + remove: (customer) -> + params = id: customer.id + CustomerResource.destroy params, => + i = @customers.indexOf customer + @customers.splice i, 1 unless i < 0 + + index: (params) -> + request = CustomerResource.index(params, (data) => @customers = data) + RequestMonitor.load(request.$promise) + request.$promise diff --git a/app/assets/javascripts/admin/index_utils/directives/show_more.js.coffee b/app/assets/javascripts/admin/index_utils/directives/show_more.js.coffee new file mode 100644 index 0000000000..a6e4efa333 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/directives/show_more.js.coffee @@ -0,0 +1,12 @@ +angular.module("admin.indexUtils").component 'showMore', + templateUrl: 'admin/show_more.html' + bindings: + data: "=" + limit: "=" + increment: "=" + +# For now, this component is not being used. +# Something about binding "data" to a variable on the parent scope that is continually refreshed by +# being assigned within an ng-repeat means that we get $digest iteration errors. Seems to be solved +# by using the new "as" syntax for ng-repeat to assign and alias the outcome of the filters, but this +# has the limitation of not being able to be limited AFTER the assignment has been made, which we need diff --git a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee index 5e5b5cadf2..1ea74e614b 100644 --- a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee +++ b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee @@ -1 +1 @@ -angular.module("admin.indexUtils", ['ngResource', 'ngSanitize', 'templates']).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content"); $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"; \ No newline at end of file +angular.module("admin.indexUtils", ['ngResource', 'ngSanitize', 'templates', 'admin.utils']).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content"); $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"; \ No newline at end of file diff --git a/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee index 2f40a7faef..15626b1479 100644 --- a/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee @@ -1,10 +1,12 @@ -angular.module("admin.indexUtils").factory "pendingChanges", (resources) -> +angular.module("admin.indexUtils").factory "pendingChanges", ($q, resources, StatusMessage) -> new class pendingChanges pendingChanges: {} + errors: [] add: (id, attr, change) => @pendingChanges["#{id}"] = {} unless @pendingChanges.hasOwnProperty("#{id}") @pendingChanges["#{id}"]["#{attr}"] = change + StatusMessage.display('notice', "You have made #{@changeCount(@pendingChanges)} unsaved changes") removeAll: => @pendingChanges = {} @@ -14,11 +16,19 @@ angular.module("admin.indexUtils").factory "pendingChanges", (resources) -> delete @pendingChanges["#{id}"]["#{attr}"] delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1 - submitAll: => + submitAll: (form=null) => all = [] + @errors = [] + StatusMessage.display('progress', "Saving...") for id, objectChanges of @pendingChanges for attrName, change of objectChanges all.push @submit(change) + $q.all(all).then => + if @errors.length == 0 + StatusMessage.display('success', "All changes saved successfully") + form.$setPristine() if form? + else + StatusMessage.display('failure', "Oh no! I was unable to save your changes") all submit: (change) -> @@ -26,7 +36,8 @@ angular.module("admin.indexUtils").factory "pendingChanges", (resources) -> @remove change.object.id, change.attr change.scope.reset( data["#{change.attr}"] ) change.scope.success() - , (error) -> + , (error) => + @errors.push error change.scope.error() changeCount: (objectChanges) -> diff --git a/app/assets/javascripts/templates/admin/new_customer_dialog.html.haml b/app/assets/javascripts/templates/admin/new_customer_dialog.html.haml new file mode 100644 index 0000000000..e30cfcd607 --- /dev/null +++ b/app/assets/javascripts/templates/admin/new_customer_dialog.html.haml @@ -0,0 +1,15 @@ +#new-customer-dialog + .text-normal.margin-bottom-30.text-center + = t('admin.customers.index.add_a_new_customer_for', shop_name: "{{ CurrentShop.shop.name }}:") + + %form{ name: 'new_customer_form', novalidate: true } + + .text-center.margin-bottom-30 + %input.fullwidth{ type: 'email', name: 'email', required: true, placeholder: t('admin.customers.index.customer_placeholder'), ng: { model: "email" } } + %div{ ng: { show: "email == submitted" } } + .error{ ng: { show: "(new_customer_form.email.$error.email || new_customer_form.email.$error.required)" } } + = t('admin.customers.index.valid_email_error') + .error{ ng: { repeat: "error in errors", bind: "error" } } + + .text-center + %input.button.red.icon-plus{ type: 'submit', value: t('admin.customers.index.add_customer'), ng: { click: 'addCustomer(new_customer_form.email.$valid)' } } diff --git a/app/assets/javascripts/templates/admin/show_more.html.haml b/app/assets/javascripts/templates/admin/show_more.html.haml new file mode 100644 index 0000000000..f9a008993c --- /dev/null +++ b/app/assets/javascripts/templates/admin/show_more.html.haml @@ -0,0 +1,4 @@ +%div{ ng: { show: "data.length > limit" } } + %input{ type: 'button', value: 'Show More', ng: { click: 'limit = limit + increment' } } + or + %input{ type: 'button', value: "Show All ({{ data.length - limit }} More)", ng: { click: 'limit = data.length' } } diff --git a/app/assets/stylesheets/admin/components/save_bar.sass b/app/assets/stylesheets/admin/components/save_bar.sass index 87dcce82f9..f453c5ddfc 100644 --- a/app/assets/stylesheets/admin/components/save_bar.sass +++ b/app/assets/stylesheets/admin/components/save_bar.sass @@ -1,6 +1,7 @@ #save-bar position: fixed width: 100% + z-index: 100 bottom: 0px left: 0 padding: 8px 8px diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index 1fc95face8..293549d0f1 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -28,6 +28,9 @@ text-angular .ta-editor { left: 275px; } +span.error, div.error { + color: #DA5354; +} /* Fix conflict between Spree and elRTE's styles */ .el-rte .toolbar { diff --git a/app/assets/stylesheets/admin/orders.css.scss b/app/assets/stylesheets/admin/orders.css.scss index 761ccbc014..23cb9d8fff 100644 --- a/app/assets/stylesheets/admin/orders.css.scss +++ b/app/assets/stylesheets/admin/orders.css.scss @@ -13,10 +13,6 @@ input.show-dirty { } } -span.error { - color: #DA5354; -} - input, div { &.update-error { border: solid 1px #DA5354; diff --git a/app/controllers/admin/customers_controller.rb b/app/controllers/admin/customers_controller.rb index 9d3bb18017..fe975aa038 100644 --- a/app/controllers/admin/customers_controller.rb +++ b/app/controllers/admin/customers_controller.rb @@ -16,9 +16,12 @@ module Admin def create @customer = Customer.new(params[:customer]) if user_can_create_customer? - @customer.save - tag_rule_mapping = TagRule.mapping_for(Enterprise.where(id: @customer.enterprise)) - render_as_json @customer, tag_rule_mapping: tag_rule_mapping + if @customer.save + tag_rule_mapping = TagRule.mapping_for(Enterprise.where(id: @customer.enterprise)) + render_as_json @customer, tag_rule_mapping: tag_rule_mapping + else + render json: { errors: @customer.errors.full_messages }, status: 400 + end else redirect_to '/unauthorized' end diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index dc38304f8e..ef3ef7e7f0 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -2,35 +2,43 @@ %h1.page-title =t :customers +- content_for :app_wrapper_attrs do + = "ng-app='admin.customers'" + +- content_for :page_actions do + %li + %a.button.icon-plus#new-customer{ href: "#", "new-customer-dialog" => true } + = t('admin.customers.index.new_customer') + = admin_inject_shops -%div{ ng: { app: 'admin.customers', controller: 'customersCtrl' } } - .row{ ng: { hide: "loaded && customers.length > 0" } } +%div{ ng: { controller: 'customersCtrl' } } + .row{ ng: { hide: "!RequestMonitor.loading && customers.length > 0" } } .five.columns.alpha %h3 =t :please_select_hub .four.columns - %select.select2.fullwidth#shop_id{ 'ng-model' => 'shop.id', name: 'shop_id', 'ng-options' => 'shop.id as shop.name for shop in shops' } + %select.select2.fullwidth#shop_id{ 'ng-model' => 'CurrentShop.shop', name: 'shop_id', 'ng-options' => 'shop as shop.name for shop in shops' } .seven.columns.omega   - .row{ 'ng-hide' => '!loaded || customers.length == 0' } + .row{ 'ng-hide' => 'RequestMonitor.loading || !CurrentShop.shop.id || customers.length == 0' } .controls.sixteen.columns.alpha.omega .five.columns.alpha %input.fullwidth{ :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Quick Search' } .eight.columns   = render 'admin/shared/columns_dropdown' - .row{ 'ng-if' => 'shop.id && !loaded' } + .row{ 'ng-if' => 'CurrentShop.shop.id && RequestMonitor.loading' } .sixteen.columns.alpha#loading %img.spinner{ src: "/assets/spinning-circles.svg" } %h1 =t :loading_customers - .row{ :class => "sixteen columns alpha", 'ng-show' => 'loaded && filteredCustomers.length == 0'} + .row{ :class => "sixteen columns alpha", 'ng-show' => '!RequestMonitor.loading && filteredCustomers.length == 0'} %h1#no_results =t :no_customers_found - - .row{ ng: { show: "loaded && filteredCustomers.length > 0" } } + .row.margin-bottom-50{ ng: { show: "!RequestMonitor.loading && filteredCustomers.length > 0" } } %form{ name: "customers_form" } + %save-bar{ save: "submitAll(customers_form)", form: "customers_form" } %table.index#customers %col.email{ width: "20%"} %col.code{ width: "20%"} @@ -48,10 +56,10 @@ %th.actions Ask?  %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } - %tr.customer{ 'ng-repeat' => "customer in filteredCustomers = ( customers | filter:quickSearch | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "c_{{customer.id}}" } + %tr.customer{ 'ng-repeat' => "customer in filteredCustomers = ( customers | filter:quickSearch | orderBy:predicate:reverse ) | limitTo:customerLimit track by customer.id", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "c_{{customer.id}}" } -# %td.bulk -# %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'customer.checked' } - %td.email{ 'ng-show' => 'columns.email.visible' } {{ customer.email }} + %td.email{ 'ng-show' => 'columns.email.visible', "ng-bind" => '::customer.email' } %td.code{ 'ng-show' => 'columns.code.visible' } %input{ :type => 'text', :name => 'code', :id => 'code', 'ng-model' => 'customer.code', 'obj-for-update' => "customer", "attr-for-update" => "code" } %td.tags{ 'ng-show' => 'columns.tags.visible' } @@ -59,12 +67,9 @@ %tags_with_translation{ object: 'customer', 'find-tags' => 'findTags(query)' } %td.actions %a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" } - %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'submitAll()' } - %form{ng: {show: "loaded", submit: 'add(newCustomerEmail)'}} - %h2= t '.add_new_customer' - .row - .five.columns.alpha - %input.fullwidth{type: "text", placeholder: t('.customer_placeholder'), ng: {model: 'newCustomerEmail'}} - .eleven.columns.omega - %input{type: "submit", value: t('.add_customer')} + -# %show-more.text-center{ data: "filteredCustomers", limit: "customerLimit", increment: "20" } + %div.text-center{ ng: { show: "filteredCustomers.length > customerLimit" } } + %input{ type: 'button', value: 'Show More', ng: { click: 'customerLimit = customerLimit + 20' } } + or + %input{ type: 'button', value: "Show All ({{ filteredCustomers.length - customerLimit }} More)", ng: { click: 'customerLimit = filteredCustomers.length' } } diff --git a/app/views/admin/variant_overrides/_show_more.html.haml b/app/views/admin/variant_overrides/_show_more.html.haml index 8d60593ddc..21e927e443 100644 --- a/app/views/admin/variant_overrides/_show_more.html.haml +++ b/app/views/admin/variant_overrides/_show_more.html.haml @@ -1,4 +1,5 @@ -.text-center +-# %show-more.text-center{ data: "filteredProducts", limit: "productLimit", increment: "10" } +.text-center{ ng: { show: "filteredProducts.length > productLimit" } } %input{ type: 'button', value: 'Show More', ng: { click: 'productLimit = productLimit + 10' } } or %input{ type: 'button', value: "Show All ({{ filteredProducts.length - productLimit }} More)", ng: { click: 'productLimit = filteredProducts.length' } } diff --git a/config/locales/en.yml b/config/locales/en.yml index 79f6e471b1..02d66a5354 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -91,8 +91,11 @@ en: customers: index: - add_customer: "Add customer" + add_customer: "Add Customer" + new_customer: "New Customer" customer_placeholder: "customer@example.org" + valid_email_error: Please enter a valid email address + add_a_new_customer_for: Add a new customer for %{shop_name} inventory: title: Inventory description: Use this page to manage inventories for your enterprises. Any product details set here will override those set on the 'Products' page diff --git a/spec/features/admin/customers_spec.rb b/spec/features/admin/customers_spec.rb index 5d2ffedb4e..964608510d 100644 --- a/spec/features/admin/customers_spec.rb +++ b/spec/features/admin/customers_spec.rb @@ -9,7 +9,7 @@ feature 'Customers' do let(:managed_distributor) { create(:distributor_enterprise, owner: user) } let(:unmanaged_distributor) { create(:distributor_enterprise) } - describe "using the customers index" do + describe "using the customers index", js: true do let!(:customer1) { create(:customer, enterprise: managed_distributor) } let!(:customer2) { create(:customer, enterprise: managed_distributor) } let!(:customer3) { create(:customer, enterprise: unmanaged_distributor) } @@ -19,7 +19,7 @@ feature 'Customers' do visit admin_customers_path end - it "passes the smoke test", js: true do + it "passes the smoke test" do # Prompts for a hub for a list of my managed enterprises expect(page).to have_select2 "shop_id", with_options: [managed_distributor.name], without_options: [unmanaged_distributor.name] @@ -45,7 +45,7 @@ feature 'Customers' do expect(page).to_not have_content customer1.email end - it "allows updating of attributes", js: true do + it "allows updating of attributes" do select2_select managed_distributor.name, from: "shop_id" within "tr#c_#{customer1.id}" do @@ -56,7 +56,7 @@ feature 'Customers' do find(:css, "tags-input .tags input").set "awesome\n" expect(page).to have_css ".tag_watcher.update-pending" end - click_button "Update" + click_button "Save Changes" # Every says it updated expect(page).to have_css "input#code.update-success" @@ -66,6 +66,46 @@ feature 'Customers' do expect(customer1.reload.code).to eq "new-customer-code" expect(customer1.tag_list).to eq ["awesome"] end + + describe "creating a new customer" do + context "when no shop has been selected" do + it "asks the user to select a shop" do + accept_alert 'Please select a shop first' do + click_link('New Customer') + end + end + end + + context "when a shop is selected" do + before do + select2_select managed_distributor.name, from: "shop_id" + end + + it "creates customers when the email provided is valid" do + # When an invalid email is used + expect{ + click_link('New Customer') + fill_in 'email', with: "not_an_email" + click_button 'Add Customer' + expect(page).to have_selector "#new-customer-dialog .error", text: "Please enter a valid email address" + }.to_not change{Customer.of(managed_distributor).count} + + # When an existing email is used + expect{ + fill_in 'email', with: customer1.email + click_button 'Add Customer' + expect(page).to have_selector "#new-customer-dialog .error", text: "Email is associated with an existing customer" + }.to_not change{Customer.of(managed_distributor).count} + + # When a new valid email is used + expect{ + fill_in 'email', with: "new@email.com" + click_button 'Add Customer' + expect(page).not_to have_selector "#new-customer-dialog" + }.to change{Customer.of(managed_distributor).count}.from(2).to(3) + end + end + end end end end diff --git a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee index b0991c7df1..796dc93ebf 100644 --- a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee @@ -14,7 +14,7 @@ describe "CustomersCtrl", -> { pass: angular.equals(actual, expected) } it "has no shop pre-selected", -> - expect(scope.shop).toEqual {} + expect(scope.CurrentShop.shop).toEqual {} describe "setting the shop on scope", -> customer = { id: 5, email: 'someone@email.com'} @@ -23,7 +23,7 @@ describe "CustomersCtrl", -> beforeEach -> http.expectGET('/admin/customers.json?enterprise_id=1').respond 200, customers scope.$apply -> - scope.shop = {id: 1} + scope.CurrentShop.shop = {id: 1} http.flush() it "retrievs the list of customers", -> @@ -33,7 +33,7 @@ describe "CustomersCtrl", -> it "creates a new customer", -> email = "customer@example.org" newCustomer = {id: 6, email: email} - customers.push(newCustomer) + customers.unshift(newCustomer) http.expectPOST('/admin/customers.json?email=' + email + '&enterprise_id=1').respond 200, newCustomer scope.add(email) http.flush() From abc906186ee45b24e1c19734f0593066b7645ca9 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 13 May 2016 12:29:06 +1000 Subject: [PATCH 038/110] Using ngSrc correctly in frontend --- .../javascripts/templates/partials/enterprise_details.html.haml | 2 +- .../javascripts/templates/partials/enterprise_header.html.haml | 2 +- app/views/shop/products/_summary.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/templates/partials/enterprise_details.html.haml b/app/assets/javascripts/templates/partials/enterprise_details.html.haml index 547e233958..d1d208c5c4 100644 --- a/app/assets/javascripts/templates/partials/enterprise_details.html.haml +++ b/app/assets/javascripts/templates/partials/enterprise_details.html.haml @@ -22,7 +22,7 @@ -# / TODO: Rob - need popover, use will's directive or this? http://pineconellc.github.io/angular-foundation/ -# .about-container.pad-top - %img.enterprise-logo{"ng-src" => "::enterprise.logo", "ng-if" => "::enterprise.logo"} + %img.enterprise-logo{"ng-src" => "{{::enterprise.logo}}", "ng-if" => "::enterprise.logo"} %p.text-small{"ng-bind-html" => "::enterprise.long_description"} .small-12.large-4.columns %ng-include{src: "'partials/contact.html'"} diff --git a/app/assets/javascripts/templates/partials/enterprise_header.html.haml b/app/assets/javascripts/templates/partials/enterprise_header.html.haml index 39b967be06..066ebdd957 100644 --- a/app/assets/javascripts/templates/partials/enterprise_header.html.haml +++ b/app/assets/javascripts/templates/partials/enterprise_header.html.haml @@ -10,4 +10,4 @@ %span{"ng-bind" => "::enterprise.name"} .small-12.medium-5.large-4.columns.text-right.small-only-text-left %p{"ng-bind" => "::[enterprise.address.city, enterprise.address.state_name] | printArray"} - %img.hero-img{"ng-src" => "::enterprise.promo_image"} + %img.hero-img{"ng-src" => "{{::enterprise.promo_image}}"} diff --git a/app/views/shop/products/_summary.html.haml b/app/views/shop/products/_summary.html.haml index fcf2aa6d7b..4dd5b3f224 100644 --- a/app/views/shop/products/_summary.html.haml +++ b/app/views/shop/products/_summary.html.haml @@ -1,7 +1,7 @@ .product-thumb %a{"ng-click" => "triggerProductModal()"} %i.ofn-i_057-expand - %img{"ng-src" => "::product.primaryImageOrMissing", "ng-click" => "triggerProductModal()"} + %img{"ng-src" => "{{::product.primaryImageOrMissing}}", "ng-click" => "triggerProductModal()"} .row.summary .small-10.medium-10.large-11.columns.summary-header From 4b8146dd00239a86b3461fb5c1b203f715abb3f7 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 13 May 2016 12:35:17 +1000 Subject: [PATCH 039/110] Fixing translation error on final registration page --- .../services/enterprise_registration_service.js.coffee | 1 + .../javascripts/templates/registration/finished.html.haml | 2 +- spec/features/consumer/registration_spec.rb | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee b/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee index a7d6a657fa..1434ffa44f 100644 --- a/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee +++ b/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee @@ -2,6 +2,7 @@ Darkswarm.factory "EnterpriseRegistrationService", ($http, RegistrationService, new class EnterpriseRegistrationService enterprise: user_ids: [CurrentUser.id] + email: CurrentUser.email email_address: CurrentUser.email address: {} country: availableCountries[0] diff --git a/app/assets/javascripts/templates/registration/finished.html.haml b/app/assets/javascripts/templates/registration/finished.html.haml index 5c7b3b6ad0..76403cb1c8 100644 --- a/app/assets/javascripts/templates/registration/finished.html.haml +++ b/app/assets/javascripts/templates/registration/finished.html.haml @@ -10,6 +10,6 @@ .small-12.columns.text-center %h4{ "ng-bind" => "'registration_finished_activate' | t:{enterprise: enterprise.name}" } - %p{ "ng-bind-html" => "t('registration_finished_activate_instruction_html', {email: enterprise.email})"} + %p{ "ng-bind-html" => "'registration_finished_activate_instruction_html' | t:{email: enterprise.email}"} %a.button.primary{ type: "button", href: "/" } {{'registration_finished_action' | t}} > diff --git a/spec/features/consumer/registration_spec.rb b/spec/features/consumer/registration_spec.rb index 0e54011f1c..9b2602c1c6 100644 --- a/spec/features/consumer/registration_spec.rb +++ b/spec/features/consumer/registration_spec.rb @@ -89,6 +89,7 @@ feature "Registration", js: true do # Done expect(page).to have_content "Finished!" + expect(page).to have_content "We've sent a confirmation email to #{user.email} if it hasn't been activated before." e.reload expect(e.website).to eq "www.shop.com" expect(e.facebook).to eq "FaCeBoOk" From 92d8ee1a3637affb0cb565259b99d096da6290b0 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 13 May 2016 15:05:58 +1000 Subject: [PATCH 040/110] Making sure that hash navigation works with auth tabs --- .../controllers/authentication/forgot_controller.js.coffee | 1 - .../controllers/authentication/login_controller.js.coffee | 1 - .../controllers/authentication/signup_controller.js.coffee | 1 - .../controllers/authentication_controller.js.coffee | 5 +++++ app/assets/javascripts/templates/forgot.html.haml | 2 +- app/assets/javascripts/templates/login.html.haml | 2 +- app/assets/javascripts/templates/signup.html.haml | 2 +- 7 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/darkswarm/controllers/authentication/forgot_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication/forgot_controller.js.coffee index 85d2a699a3..85920de958 100644 --- a/app/assets/javascripts/darkswarm/controllers/authentication/forgot_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/authentication/forgot_controller.js.coffee @@ -1,6 +1,5 @@ Darkswarm.controller "ForgotCtrl", ($scope, $http, $location, AuthenticationService) -> $scope.path = "/forgot" - $scope.active = $scope.isActive($scope.path) $scope.sent = false $scope.submit = -> diff --git a/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee index 07bef90f84..f91e137ca7 100644 --- a/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee @@ -1,6 +1,5 @@ Darkswarm.controller "LoginCtrl", ($scope, $http, $window, AuthenticationService, Redirections, Loading) -> $scope.path = "/login" - $scope.active = $scope.isActive($scope.path) $scope.submit = -> Loading.message = t 'logging_in' diff --git a/app/assets/javascripts/darkswarm/controllers/authentication/signup_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication/signup_controller.js.coffee index dfa61f82b2..722414f9d4 100644 --- a/app/assets/javascripts/darkswarm/controllers/authentication/signup_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/authentication/signup_controller.js.coffee @@ -1,6 +1,5 @@ Darkswarm.controller "SignupCtrl", ($scope, $http, $window, $location, Redirections, AuthenticationService) -> $scope.path = "/signup" - $scope.active = $scope.isActive($scope.path) $scope.errors = email: null diff --git a/app/assets/javascripts/darkswarm/controllers/authentication_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication_controller.js.coffee index 7d65b853c7..135fe37b89 100644 --- a/app/assets/javascripts/darkswarm/controllers/authentication_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/authentication_controller.js.coffee @@ -5,3 +5,8 @@ Darkswarm.controller "AuthenticationCtrl", ($scope, AuthenticationService, Spree $scope.spree_user = SpreeUser.spree_user $scope.isActive = AuthenticationService.isActive $scope.select = AuthenticationService.select + + $scope.tabs = + login: { active: $scope.isActive('/login') } + signup: { active: $scope.isActive('/signup') } + forgot: { active: $scope.isActive('/forgot') } diff --git a/app/assets/javascripts/templates/forgot.html.haml b/app/assets/javascripts/templates/forgot.html.haml index 3e88350321..958f1daa39 100644 --- a/app/assets/javascripts/templates/forgot.html.haml +++ b/app/assets/javascripts/templates/forgot.html.haml @@ -1,4 +1,4 @@ -%tab#forgot{ heading: "{{'forgot_password' | t}}", active: "active", select: "select(path)"} +%tab#forgot{ heading: "{{'forgot_password' | t}}", active: "tabs.forgot.active", select: "select(path)"} %form{ ng: { controller: "ForgotCtrl", submit: "submit()" } } .row .large-12.columns diff --git a/app/assets/javascripts/templates/login.html.haml b/app/assets/javascripts/templates/login.html.haml index 8b3e64cfb1..79511d8968 100644 --- a/app/assets/javascripts/templates/login.html.haml +++ b/app/assets/javascripts/templates/login.html.haml @@ -1,4 +1,4 @@ -%tab#login-content{ heading: "{{'label_login' | t}}", active: "active", select: "select(path)"} +%tab#login-content{ heading: "{{'label_login' | t}}", active: "tabs.login.active", select: "select(path)"} %form{ ng: { controller: "LoginCtrl", submit: "submit()" } } .row .large-12.columns diff --git a/app/assets/javascripts/templates/signup.html.haml b/app/assets/javascripts/templates/signup.html.haml index 1c13985f38..23eab8e38c 100644 --- a/app/assets/javascripts/templates/signup.html.haml +++ b/app/assets/javascripts/templates/signup.html.haml @@ -1,4 +1,4 @@ -%tab#sign-up-content{ heading: "{{'label_signup' | t}}", active: 'active', select: "select(path)"} +%tab#sign-up-content{ heading: "{{'label_signup' | t}}", active: 'tabs.signup.active', select: "select(path)"} %form{ ng: { controller: "SignupCtrl", submit: "submit()" } } .row .large-12.columns From 93a4f19b40005e24bb781394cbb86487df8a8a67 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 13 May 2016 15:33:23 +1000 Subject: [PATCH 041/110] Hash Navigation works on with Group and Shopping page tabs --- .../controllers/group_page_controller.js.coffee | 3 +-- .../controllers/group_tabs_controller.js.coffee | 8 ++++++++ .../controllers/groups_controller.js.coffee | 2 +- .../shopping_tabs_controller.js.coffee | 8 ++++++++ .../controllers/tabs_controller.js.coffee | 17 ++++------------- app/views/groups/show.html.haml | 14 +++++++------- app/views/shopping_shared/_tabs.html.haml | 6 +++--- 7 files changed, 32 insertions(+), 26 deletions(-) create mode 100644 app/assets/javascripts/darkswarm/controllers/group_tabs_controller.js.coffee create mode 100644 app/assets/javascripts/darkswarm/controllers/shopping_tabs_controller.js.coffee diff --git a/app/assets/javascripts/darkswarm/controllers/group_page_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/group_page_controller.js.coffee index ed52f46bb7..5d9bb0aa02 100644 --- a/app/assets/javascripts/darkswarm/controllers/group_page_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/group_page_controller.js.coffee @@ -1,4 +1,4 @@ -Darkswarm.controller "GroupPageCtrl", ($scope, group_enterprises, Enterprises, MapConfiguration, OfnMap, visibleFilter) -> +Darkswarm.controller "GroupPageCtrl", ($scope, group_enterprises, Enterprises, MapConfiguration, OfnMap, visibleFilter, Navigation) -> $scope.Enterprises = Enterprises all_enterprises_by_id = Enterprises.enterprises_by_id @@ -19,4 +19,3 @@ Darkswarm.controller "GroupPageCtrl", ($scope, group_enterprises, Enterprises, M $scope.map = angular.copy MapConfiguration.options $scope.mapMarkers = OfnMap.enterprise_markers visible_enterprises - diff --git a/app/assets/javascripts/darkswarm/controllers/group_tabs_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/group_tabs_controller.js.coffee new file mode 100644 index 0000000000..c9a8f88f58 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/group_tabs_controller.js.coffee @@ -0,0 +1,8 @@ +Darkswarm.controller "GroupTabsCtrl", ($scope, $controller, Navigation) -> + angular.extend this, $controller('TabsCtrl', {$scope: $scope}) + + $scope.tabs = + map: { active: Navigation.isActive('/map') } + about: { active: Navigation.isActive('/about') } + producers: { active: Navigation.isActive('/producers') } + hubs: { active: Navigation.isActive('/hubs') } diff --git a/app/assets/javascripts/darkswarm/controllers/groups_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/groups_controller.js.coffee index aafb7597d6..8fd47c49f8 100644 --- a/app/assets/javascripts/darkswarm/controllers/groups_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/groups_controller.js.coffee @@ -1,3 +1,3 @@ -Darkswarm.controller "GroupsCtrl", ($scope, Groups, $anchorScroll, $rootScope) -> +Darkswarm.controller "GroupsCtrl", ($scope, Groups) -> $scope.Groups = Groups $scope.order = 'position' diff --git a/app/assets/javascripts/darkswarm/controllers/shopping_tabs_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/shopping_tabs_controller.js.coffee new file mode 100644 index 0000000000..8daac0212c --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/shopping_tabs_controller.js.coffee @@ -0,0 +1,8 @@ +Darkswarm.controller "ShoppingTabsCtrl", ($scope, $controller, Navigation) -> + angular.extend this, $controller('TabsCtrl', {$scope: $scope}) + + $scope.tabs = + about: { active: Navigation.isActive('/about') } + producers: { active: Navigation.isActive('/producers') } + contact: { active: Navigation.isActive('/contact') } + groups: { active: Navigation.isActive('/groups') } diff --git a/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee index fad13164ff..09095ebd42 100644 --- a/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee @@ -1,15 +1,6 @@ -Darkswarm.controller "TabsCtrl", ($scope, $rootScope, $location) -> - # Return active if supplied path matches url hash path. - $scope.active = (path)-> - $location.hash() == path +Darkswarm.controller "TabsCtrl", ($scope, Navigation) -> + $scope.isActive = Navigation.isActive # Select tab by setting the url hash path. - $scope.select = (path)-> - $location.hash path - - # Toggle tab selected status by setting the url hash path. - $scope.toggle = (path)-> - if $scope.active(path) - $location.hash "" - else - $location.hash path + $scope.select = (path) -> + Navigation.navigate path diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 8d5385f1f4..af497a4195 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -32,27 +32,27 @@ .small-12.columns.pad-top .row .small-12.medium-12.large-9.columns - %div{"ng-controller" => "TabsCtrl"} + %div{"ng-controller" => "GroupTabsCtrl"} %tabset %tab{heading: t(:label_map), - active: "active(\'\')", - select: "select(\'\')"} + active: "tabs.map.active", + select: "select(\'map\')"} .map-container - %map{"ng-if" => "(active(\'\') && (mapShowed = true)) || mapShowed"} + %map{"ng-if" => "(isActive(\'/map\') && (mapShowed = true)) || mapShowed"} %google-map{options: "map.additional_options", center: "map.center", zoom: "map.zoom", styles: "map.styles", draggable: "true"} %map-search %markers{models: "mapMarkers", fit: "true", coords: "'self'", icon: "'icon'", click: "'reveal'"} %tab{heading: t(:groups_about), - active: "active(\'about\')", + active: "tabs.about.active", select: "select(\'about\')"} %h1 = t :groups_about %p!= @group.long_description %tab{heading: t(:groups_producers), - active: "active(\'producers\')", + active: "tabs.producers.active", select: "select(\'producers\')"} .producers{"ng-controller" => "GroupEnterprisesCtrl"} .row @@ -78,7 +78,7 @@ = render partial: 'shared/components/enterprise_no_results' %tab{heading: t(:groups_hubs), - active: "active(\'hubs\')", + active: "tabs.hubs.active", select: "select(\'hubs\')"} .hubs{"ng-controller" => "GroupEnterprisesCtrl"} .row diff --git a/app/views/shopping_shared/_tabs.html.haml b/app/views/shopping_shared/_tabs.html.haml index 0d3cd79939..3bcd2aa37e 100644 --- a/app/views/shopping_shared/_tabs.html.haml +++ b/app/views/shopping_shared/_tabs.html.haml @@ -1,4 +1,4 @@ -#tabs{"ng-controller" => "TabsCtrl", "ng-cloak" => true} +#tabs{"ng-controller" => "ShoppingTabsCtrl", "ng-cloak" => true} .row %tabset{ 'open-on-load' => 'false' } -# Build all tabs. @@ -6,10 +6,10 @@ producers: [t(:label_producers),2], contact: [t(:shopping_tabs_contact),2], groups: [t(:label_groups),2]} - -# tabs take tab path in 'active' and 'select' functions defined in TabsCtrl. - heading, cols = heading_cols %tab.columns{heading: heading, id: "tab_#{name}", - select: "toggle(\'#{name}\')", + active: "tabs.#{name}.active", + select: "select(\'#{name}\')", class: "small-12 medium-#{cols}" } = render "shopping_shared/#{name}" From 162b11dcc5ca80e366483c733c2dee1ea93ee333 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 13 May 2016 16:30:12 +1000 Subject: [PATCH 042/110] Updating spec to new Jasmine syntax --- spec/javascripts/unit/order_cycle_spec.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index 511051d39f..ea03a38786 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -328,7 +328,7 @@ describe 'OrderCycle controllers', -> scope.submit(eventMock,'/admin/order_cycles') expect(eventMock.preventDefault).toHaveBeenCalled() expect(OrderCycle.update).toHaveBeenCalledWith('/admin/order_cycles') - expect(scope.order_cycle_form.$setPristine.calls.length).toEqual 1 + expect(scope.order_cycle_form.$setPristine.calls.count()).toEqual 1 describe 'OrderCycle services', -> From ecb9646ccb26085aa71339337d26c7f43c42a682 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 13 May 2016 16:30:39 +1000 Subject: [PATCH 043/110] Using new syntax for save-bar on customers page --- .../admin/index_utils/services/pending_changes.js.coffee | 2 +- app/views/admin/customers/index.html.haml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee index 15626b1479..3581b4921a 100644 --- a/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee @@ -16,7 +16,7 @@ angular.module("admin.indexUtils").factory "pendingChanges", ($q, resources, Sta delete @pendingChanges["#{id}"]["#{attr}"] delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1 - submitAll: (form=null) => + submitAll: (event=null, form=null) => all = [] @errors = [] StatusMessage.display('progress', "Saving...") diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index ef3ef7e7f0..f605ec3e5a 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -38,7 +38,8 @@ .row.margin-bottom-50{ ng: { show: "!RequestMonitor.loading && filteredCustomers.length > 0" } } %form{ name: "customers_form" } - %save-bar{ save: "submitAll(customers_form)", form: "customers_form" } + %save-bar{ buttons: "[{ text: 'Save Changes', action: submitAll, param: customers_form, class: 'red' }]", form: "customers_form" } + %table.index#customers %col.email{ width: "20%"} %col.code{ width: "20%"} From 533a94267a88b55246144df40d13ec68219161f6 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 13 May 2016 17:07:12 +1000 Subject: [PATCH 044/110] Refactoring save-bar --- .../admin/index_utils/services/pending_changes.js.coffee | 2 +- .../admin/order_cycles/controllers/edit.js.coffee | 3 +-- .../admin/order_cycles/controllers/simple_edit.js.coffee | 2 +- .../admin/order_cycles/services/order_cycle.js.coffee | 3 ++- .../javascripts/admin/utils/directives/save_bar.js.coffee | 5 +++-- app/assets/javascripts/templates/admin/save_bar.html.haml | 5 ++--- app/views/admin/customers/index.html.haml | 3 ++- app/views/admin/order_cycles/_form.html.haml | 2 -- app/views/admin/order_cycles/_simple_form.html.haml | 2 -- app/views/admin/order_cycles/edit.html.haml | 5 ++++- app/views/admin/variant_overrides/_products.html.haml | 3 ++- app/views/spree/admin/orders/bulk_management.html.haml | 4 +++- spec/features/admin/order_cycles_spec.rb | 5 +++-- spec/javascripts/unit/order_cycle_spec.js.coffee | 7 ++++--- 14 files changed, 28 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee index 3581b4921a..15626b1479 100644 --- a/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/pending_changes.js.coffee @@ -16,7 +16,7 @@ angular.module("admin.indexUtils").factory "pendingChanges", ($q, resources, Sta delete @pendingChanges["#{id}"]["#{attr}"] delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1 - submitAll: (event=null, form=null) => + submitAll: (form=null) => all = [] @errors = [] StatusMessage.display('progress', "Saving...") diff --git a/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee index d4c63121fe..2f3b2db5e0 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee @@ -91,8 +91,7 @@ angular.module('admin.orderCycles') $scope.submit = ($event, destination) -> $event.preventDefault() StatusMessage.display 'progress', "Saving..." - OrderCycle.update(destination) - $scope.order_cycle_form.$setPristine() + OrderCycle.update(destination, $scope.order_cycle_form) $scope.cancel = (destination) -> $window.location = destination diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee index d3eeb1c9d8..f5bae5a4d8 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee @@ -41,4 +41,4 @@ angular.module('admin.orderCycles').controller "AdminSimpleEditOrderCycleCtrl", $event.preventDefault() StatusMessage.display 'progress', "Saving..." OrderCycle.mirrorIncomingToOutgoingProducts() - OrderCycle.update(destination) + OrderCycle.update(destination, $scope.order_cycle_form) diff --git a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee index 9ca5f1028d..662201c9f5 100644 --- a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee @@ -154,11 +154,12 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, S else console.log('Failed to create order cycle') - update: (destination) -> + update: (destination, form) -> return unless @confirmNoDistributors() oc = new OrderCycleResource({order_cycle: this.dataForSubmit()}) oc.$update {order_cycle_id: this.order_cycle.id, reloading: (if destination? then 1 else 0)}, (data) => if data['success'] + form.$setPristine() if form if destination? $window.location = destination else diff --git a/app/assets/javascripts/admin/utils/directives/save_bar.js.coffee b/app/assets/javascripts/admin/utils/directives/save_bar.js.coffee index 0999679394..8bc6c98573 100644 --- a/app/assets/javascripts/admin/utils/directives/save_bar.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/save_bar.js.coffee @@ -1,8 +1,9 @@ angular.module("admin.utils").directive "saveBar", (StatusMessage) -> restrict: "E" + transclude: true scope: - form: "=" - buttons: "=" + dirty: "=" + persist: "=?" templateUrl: "admin/save_bar.html" link: (scope, element, attrs) -> scope.StatusMessage = StatusMessage diff --git a/app/assets/javascripts/templates/admin/save_bar.html.haml b/app/assets/javascripts/templates/admin/save_bar.html.haml index b710c070ca..12ccd4537c 100644 --- a/app/assets/javascripts/templates/admin/save_bar.html.haml +++ b/app/assets/javascripts/templates/admin/save_bar.html.haml @@ -1,7 +1,6 @@ -#save-bar.animate-show{ ng: { show: 'form.$dirty || StatusMessage.active()' } } +#save-bar.animate-show{ ng: { show: 'dirty || persist || StatusMessage.active()' } } .container .eight.columns.alpha %h5#status-message{ ng: { style: 'StatusMessage.statusMessage.style' } } {{ StatusMessage.statusMessage.text || " " }} - .eight.columns.omega.text-right - %input{"ng-repeat" => "button in buttons", type: "button", value: "{{button.text}}", ng: { class: "button.class", click: "button.action($event, button.param)" } } + .eight.columns.omega.text-right{ ng: { transclude: true } } diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index f605ec3e5a..4278162e30 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -38,7 +38,8 @@ .row.margin-bottom-50{ ng: { show: "!RequestMonitor.loading && filteredCustomers.length > 0" } } %form{ name: "customers_form" } - %save-bar{ buttons: "[{ text: 'Save Changes', action: submitAll, param: customers_form, class: 'red' }]", form: "customers_form" } + %save-bar{ dirty: "customers_form.$dirty", persist: "false" } + %input.red{ type: "button", value: "Save Changes", ng: { click: "submitAll(customers_form)", disabled: "!customers_form.$dirty" } } %table.index#customers %col.email{ width: "20%"} diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index f61c1ad91f..e53fd515b0 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -53,8 +53,6 @@ - if @order_cycle.new_record? = f.submit 'Create', 'ng-click' => "submit($event, '#{main_app.admin_order_cycles_path}')", 'ng-disabled' => '!loaded()' - %span{'ng-show' => 'loaded()'} - = link_to 'Cancel', main_app.admin_order_cycles_path %span{'ng-hide' => 'loaded()'} Loading... diff --git a/app/views/admin/order_cycles/_simple_form.html.haml b/app/views/admin/order_cycles/_simple_form.html.haml index 7fcf0ea80c..ba28526f7b 100644 --- a/app/views/admin/order_cycles/_simple_form.html.haml +++ b/app/views/admin/order_cycles/_simple_form.html.haml @@ -24,6 +24,4 @@ - if @order_cycle.new_record? = f.submit 'Create', 'ng-click' => "submit($event, '#{main_app.admin_order_cycles_path}')", 'ng-disabled' => '!loaded()' - %span{'ng-show' => 'loaded()'} - = link_to 'Cancel', main_app.admin_order_cycles_path %span{'ng-hide' => 'loaded()'} Loading... diff --git a/app/views/admin/order_cycles/edit.html.haml b/app/views/admin/order_cycles/edit.html.haml index 9f85261224..4712566dfd 100644 --- a/app/views/admin/order_cycles/edit.html.haml +++ b/app/views/admin/order_cycles/edit.html.haml @@ -29,7 +29,10 @@ - ng_controller = order_cycles_simple_form ? 'AdminSimpleEditOrderCycleCtrl' : 'AdminEditOrderCycleCtrl' = form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.orderCycles', 'ng-controller' => ng_controller, name: 'order_cycle_form'} do |f| - %save-bar{ buttons: "[{ text: 'Update', action: submit, param: null, class: 'red' }, { text: 'Update and Close', action: submit, param: '#{main_app.admin_order_cycles_path}', class: 'red' }, { text: 'Cancel', action: cancel, param: '#{main_app.admin_order_cycles_path}', class: '' }]", form: "order_cycle_form" } + %save-bar{ dirty: "order_cycle_form.$dirty", persist: "true" } + %input.red{ type: "button", value: "Update", ng: { click: "submit($event, null)", disabled: "!order_cycle_form.$dirty" } } + %input.red{ type: "button", value: "Update and Close", ng: { click: "submit($event, '#{main_app.admin_order_cycles_path}')", disabled: "!order_cycle_form.$dirty" } } + %input{ type: "button", ng: { value: "order_cycle_form.$dirty ? 'Cancel' : 'Close'", click: "cancel('#{main_app.admin_order_cycles_path}')" } } - if order_cycles_simple_form = render 'simple_form', f: f diff --git a/app/views/admin/variant_overrides/_products.html.haml b/app/views/admin/variant_overrides/_products.html.haml index f0b2310fdc..b54cfeba9a 100644 --- a/app/views/admin/variant_overrides/_products.html.haml +++ b/app/views/admin/variant_overrides/_products.html.haml @@ -1,5 +1,6 @@ %form{ name: 'variant_overrides_form', ng: { show: "views.inventory.visible" } } - %save-bar{ form: "variant_overrides_form", buttons: "[{ text: 'Save Changes', action: update, class: 'red' }]" } + %save-bar{ dirty: "customers_form.$dirty", persist: "false" } + %input.red{ type: "button", value: "Save Changes", ng: { click: "update()", disabled: "!variant_overrides_form.$dirty" } } %table.index.bulk#variant-overrides %col.producer{ width: "20%", ng: { show: 'columns.producer.visible' } } %col.product{ width: "20%", ng: { show: 'columns.product.visible' } } diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index d8228b5c83..213ba142d5 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -10,7 +10,9 @@ = render :partial => 'spree/admin/shared/order_sub_menu' %div{ ng: { controller: 'LineItemsCtrl' } } - %save-bar{ form: "bulk_order_form", buttons: "[{ text: 'Save Changes', action: submit, class: 'red' }]" } + %save-bar{ dirty: "bulk_order_form.$dirty", persist: "false" } + %input.red{ type: "button", value: "Save Changes", ng: { click: "submit()", disabled: "!bulk_order_form.$dirty" } } + .filters{ :class => "sixteen columns alpha" } .date_filter{ :class => "two columns alpha" } %label{ :for => 'start_date_filter' } diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 22402222fe..54d25a478a 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -464,7 +464,7 @@ feature %q{ click_link 'Order Cycles' click_link oc.name within("table.exchanges tbody tr.supplier") { page.find('td.products input').click } - page.find("#order_cycle_incoming_exchange_0_variants_#{p.master.id}", visible: true).click # uncheck + page.find("#order_cycle_incoming_exchange_0_variants_#{p.master.id}", visible: true).trigger('click') # uncheck click_button "Update" # Then the master variant should have been removed from all exchanges @@ -917,6 +917,7 @@ feature %q{ click_button 'Update' page.should have_content 'Your order cycle has been updated.' + fill_in 'order_cycle_outgoing_exchange_0_pickup_instructions', with: 'yyz' click_button 'Update and Close' # Then my order cycle should have been updated @@ -936,7 +937,7 @@ feature %q{ # And my pickup time and instructions should have been saved ex = oc.exchanges.outgoing.first ex.pickup_time.should == 'xy' - ex.pickup_instructions.should == 'zzy' + ex.pickup_instructions.should == 'yyz' end end diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index ea03a38786..bbbff7d82b 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -327,8 +327,7 @@ describe 'OrderCycle controllers', -> eventMock = {preventDefault: jasmine.createSpy()} scope.submit(eventMock,'/admin/order_cycles') expect(eventMock.preventDefault).toHaveBeenCalled() - expect(OrderCycle.update).toHaveBeenCalledWith('/admin/order_cycles') - expect(scope.order_cycle_form.$setPristine.calls.count()).toEqual 1 + expect(OrderCycle.update).toHaveBeenCalledWith('/admin/order_cycles', scope.order_cycle_form) describe 'OrderCycle services', -> @@ -844,15 +843,17 @@ describe 'OrderCycle services', -> spyOn(OrderCycle, 'confirmNoDistributors').and.returnValue true it 'redirects to the destination page on success', -> + form = jasmine.createSpyObj('order_cycle_form', ['$dirty', '$setPristine']) OrderCycle.order_cycle = 'this is the order cycle' spyOn(OrderCycle, 'dataForSubmit').and.returnValue('this is the submit data') $httpBackend.expectPUT('/admin/order_cycles.json?reloading=1', { order_cycle: 'this is the submit data' }).respond {success: true} - OrderCycle.update('/destination/page') + OrderCycle.update('/destination/page', form) $httpBackend.flush() expect($window.location).toEqual('/destination/page') + expect(form.$setPristine.calls.count()).toBe 1 it 'does not redirect on error', -> OrderCycle.order_cycle = 'this is the order cycle' From 3647b17110af67f755cf060bdd1c77aab7b7754c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 13 May 2016 17:50:49 +1000 Subject: [PATCH 045/110] Removing save_screenshot call --- spec/features/admin/order_cycles_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 54d25a478a..e362bf9946 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -363,7 +363,6 @@ feature %q{ # And I click Update expect(page).to have_selector "#save-bar" - save_screenshot('abc.png') click_button 'Update and Close' # Then my order cycle should have been updated From 1d8719b4744d1458c806bb42ac1ea21df566b4ef Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 4 May 2016 11:33:14 +1000 Subject: [PATCH 046/110] Default column visibility can be set per user using ColumnPreferences model --- .../admin/bulk_order_management.js.coffee | 26 +++---- .../admin/bulk_product_update.js.coffee | 13 +--- .../customers_controller.js.coffee | 6 +- .../columns_dropdown_controller.js.coffee | 4 + .../directives/columns_dropdown.js.coffee | 6 ++ .../enterprises_controller.js.coffee | 7 +- .../directives/toggle_column.js.coffee | 8 -- .../index_utils/services/columns.js.coffee | 17 +++- .../line_items_controller.js.coffee | 15 +--- .../variant_overrides_controller.js.coffee | 13 +--- .../admin/columns_dropdown.html.haml | 10 +++ .../admin/column_preferences_controller.rb | 38 +++++++++ app/helpers/admin/injection_helper.rb | 12 ++- app/models/column_preference.rb | 46 +++++++++++ app/models/column_preference_set.rb | 5 ++ app/models/spree/ability_decorator.rb | 4 + .../api/admin/column_preference_serializer.rb | 3 + app/views/admin/customers/index.html.haml | 3 +- .../_enterprise_user_index.html.haml | 2 +- app/views/admin/enterprises/index.html.haml | 2 + .../admin/shared/_columns_dropdown.html.haml | 7 -- .../variant_overrides/_controls.html.haml | 2 +- .../admin/variant_overrides/_data.html.haml | 1 + .../admin/orders/bulk_management.html.haml | 4 +- .../products/bulk_edit/_actions.html.haml | 2 +- .../admin/products/bulk_edit/_data.html.haml | 1 + config/routes.rb | 4 + ...0160116024333_create_column_preferences.rb | 13 ++++ db/schema.rb | 13 +++- .../column_preference_defaults.rb | 77 +++++++++++++++++++ .../column_preferences_controller_spec.rb | 44 +++++++++++ ...ariant_overrides_controller_spec.js.coffee | 1 + .../customers_controller_spec.js.coffee | 4 + .../enterprises_controller_spec.js.coffee | 4 + .../directives/panel_row_spec.js.coffee | 3 + .../services/columns_spec.js.coffee | 18 +++-- .../line_items_controller_spec.js.coffee | 3 + .../unit/bulk_product_update_spec.js.coffee | 1 + spec/models/column_preference_spec.rb | 59 ++++++++++++++ 39 files changed, 406 insertions(+), 95 deletions(-) create mode 100644 app/assets/javascripts/admin/dropdown/controllers/columns_dropdown_controller.js.coffee create mode 100644 app/assets/javascripts/admin/dropdown/directives/columns_dropdown.js.coffee delete mode 100644 app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee create mode 100644 app/assets/javascripts/templates/admin/columns_dropdown.html.haml create mode 100644 app/controllers/admin/column_preferences_controller.rb create mode 100644 app/models/column_preference.rb create mode 100644 app/models/column_preference_set.rb create mode 100644 app/serializers/api/admin/column_preference_serializer.rb delete mode 100644 app/views/admin/shared/_columns_dropdown.html.haml create mode 100644 db/migrate/20160116024333_create_column_preferences.rb create mode 100644 lib/open_food_network/column_preference_defaults.rb create mode 100644 spec/controllers/admin/column_preferences_controller_spec.rb create mode 100644 spec/models/column_preference_spec.rb diff --git a/app/assets/javascripts/admin/bulk_order_management.js.coffee b/app/assets/javascripts/admin/bulk_order_management.js.coffee index b747d3699e..2545a13915 100644 --- a/app/assets/javascripts/admin/bulk_order_management.js.coffee +++ b/app/assets/javascripts/admin/bulk_order_management.js.coffee @@ -18,19 +18,19 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [ $scope.selectedUnitsVariant = {}; $scope.sharedResource = false $scope.columns = Columns.setColumns - order_no: { name: t("bom_no"), visible: false } - full_name: { name: t("name"), visible: true } - email: { name: t("email"), visible: false } - phone: { name: t("phone"), visible: false } - order_date: { name: t("bom_date"), visible: true } - producer: { name: t("producer"), visible: true } - order_cycle: { name: t("bom_cycle"), visible: false } - hub: { name: t("bom_hub"), visible: false } - variant: { name: t("bom_variant"), visible: true } - quantity: { name: t("bom_quantity"), visible: true } - max: { name: t("bom_max"), visible: true } - final_weight_volume: { name: t("bom_final_weigth_volume"), visible: false } - price: { name: t("price"), visible: false } + order_no: { name: t("bom_no"), visible: false } + full_name: { name: t("name"), visible: true } + email: { name: t("email"), visible: false } + phone: { name: t("phone"), visible: false } + order_date: { name: t("bom_date"), visible: true } + producer: { name: t("producer"), visible: true } + order_cycle: { name: t("bom_cycle"), visible: false } + hub: { name: t("bom_hub"), visible: false } + variant: { name: t("bom_variant"), visible: true } + quantity: { name: t("bom_quantity"), visible: true } + max: { name: t("bom_max"), visible: true } + final_weight_volume: { name: t("bom_final_weigth_volume"), visible: false } + price: { name: t("price"), visible: false } $scope.initialise = -> $scope.initialiseVariables() authorise_api_reponse = "" diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 3d565f703a..751734a899 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -3,18 +3,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $scope.StatusMessage = StatusMessage - $scope.columns = Columns.setColumns - producer: {name: t("products_producer"), visible: true} - sku: {name: t("products_sku"), visible: false} - name: {name: t("products_name"), visible: true} - unit: {name: t("products_unit"), visible: true} - price: {name: t("products_price"), visible: true} - on_hand: {name: t("products_on_hand"), visible: true} - on_demand: {name: t("products_on_demand"), visible: false} - category: {name: t("products_category"), visible: false} - tax_category: {name: t("products_tax_category"), visible: false} - inherits_properties: {name: t("products_inherits_properties"), visible: false} - available_on: {name: t("products_available_on"), visible: false} + $scope.columns = Columns.columns $scope.variant_unit_options = VariantUnitManager.variantUnitOptions() diff --git a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee index 9e1c027531..23bfb37738 100644 --- a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee +++ b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee @@ -6,11 +6,7 @@ angular.module("admin.customers").controller "customersCtrl", ($scope, $q, Custo $scope.add = Customers.add $scope.deleteCustomer = Customers.remove $scope.customerLimit = 20 - - $scope.columns = Columns.setColumns - email: { name: "Email", visible: true } - code: { name: "Code", visible: true } - tags: { name: "Tags", visible: true } + $scope.columns = Columns.columns $scope.$watch "CurrentShop.shop", -> if $scope.CurrentShop.shop.id? diff --git a/app/assets/javascripts/admin/dropdown/controllers/columns_dropdown_controller.js.coffee b/app/assets/javascripts/admin/dropdown/controllers/columns_dropdown_controller.js.coffee new file mode 100644 index 0000000000..6c27980495 --- /dev/null +++ b/app/assets/javascripts/admin/dropdown/controllers/columns_dropdown_controller.js.coffee @@ -0,0 +1,4 @@ +angular.module("admin.dropdown").controller "ColumnsDropdownCtrl", ($scope, Columns) -> + $scope.columns = Columns.columns + $scope.toggle = Columns.toggleColumn + $scope.saveColumnPreferences = Columns.savePreferences diff --git a/app/assets/javascripts/admin/dropdown/directives/columns_dropdown.js.coffee b/app/assets/javascripts/admin/dropdown/directives/columns_dropdown.js.coffee new file mode 100644 index 0000000000..8248b626bf --- /dev/null +++ b/app/assets/javascripts/admin/dropdown/directives/columns_dropdown.js.coffee @@ -0,0 +1,6 @@ +angular.module("admin.dropdown").directive 'columnsDropdown', -> + restrict: 'E' + templateUrl: 'admin/columns_dropdown.html' + controller: 'ColumnsDropdownCtrl' + scope: + action: '@' diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprises_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprises_controller.js.coffee index 3d8bfa6446..d6dda4a118 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprises_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprises_controller.js.coffee @@ -5,9 +5,4 @@ angular.module("admin.enterprises").controller 'enterprisesCtrl', ($scope, $q, E $q.all(requests).then -> $scope.loaded = true - $scope.columns = Columns.setColumns - name: { name: "Name", visible: true } - producer: { name: "Producer", visible: true } - package: { name: "Package", visible: true } - status: { name: "Status", visible: true } - manage: { name: "Manage", visible: true } + $scope.columns = Columns.columns diff --git a/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee b/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee deleted file mode 100644 index 614b8d9346..0000000000 --- a/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee +++ /dev/null @@ -1,8 +0,0 @@ -angular.module("admin.indexUtils").directive "toggleColumn", (Columns) -> - link: (scope, element, attrs) -> - element.addClass "selected" if scope.column.visible - - element.click "click", -> - scope.$apply -> - Columns.toggleColumn(scope.column) - element.toggleClass "selected" diff --git a/app/assets/javascripts/admin/index_utils/services/columns.js.coffee b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee index 8bd99bf2f2..99adc85fdd 100644 --- a/app/assets/javascripts/admin/index_utils/services/columns.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee @@ -1,13 +1,14 @@ -angular.module("admin.indexUtils").factory 'Columns', ($rootScope) -> +angular.module("admin.indexUtils").factory 'Columns', ($rootScope, $http, columns) -> new class Columns columns: {} visibleCount: 0 + saving: false - setColumns: (columns) => + constructor: -> @columns = {} - @columns[name] = column for name, column of columns + for column in columns + @columns[column.column_name] = column @calculateVisibleCount() - @columns toggleColumn: (column) => column.visible = !column.visible @@ -16,3 +17,11 @@ angular.module("admin.indexUtils").factory 'Columns', ($rootScope) -> calculateVisibleCount: => @visibleCount = (column for name, column of @columns when column.visible).length $rootScope.$broadcast "columnCount:changed", @visibleCount + + savePreferences: (action_name) -> + $http + method: "PUT" + url: "/admin/column_preferences/bulk_update" + data: + action_name: action_name + column_preferences: (preference for column_name, preference of @columns) diff --git a/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee b/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee index f905d33bfe..f969d82b05 100644 --- a/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee +++ b/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee @@ -9,20 +9,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout, $scope.selectedUnitsProduct = {} $scope.selectedUnitsVariant = {} $scope.sharedResource = false - $scope.columns = Columns.setColumns - order_no: { name: t("bom_no"), visible: false } - full_name: { name: t("name"), visible: true } - email: { name: t("email"), visible: false } - phone: { name: t("phone"), visible: false } - order_date: { name: t("bom_date"), visible: true } - producer: { name: t("producer"), visible: true } - order_cycle: { name: t("bom_cycle"), visible: false } - hub: { name: t("bom_hub"), visible: false } - variant: { name: t("bom_variant"), visible: true } - quantity: { name: t("bom_quantity"), visible: true } - max: { name: t("bom_max"), visible: true } - final_weight_volume: { name: t("bom_final_weigth_volume"), visible: false } - price: { name: t("price"), visible: false } + $scope.columns = Columns.columns $scope.confirmRefresh = -> LineItems.allSaved() || confirm(t "unsaved_changes_warning") diff --git a/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee b/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee index 26d1e05250..e72be8bf71 100644 --- a/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee @@ -19,19 +19,10 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", hidden: { name: "Hidden Products", visible: false } new: { name: "New Products", visible: false } - $scope.columns = Columns.setColumns - producer: { name: "Producer", visible: true } - product: { name: "Product", visible: true } - sku: { name: "SKU", visible: false } - price: { name: "Price", visible: true } - on_hand: { name: "On Hand", visible: true } - on_demand: { name: "On Demand", visible: false } - reset: { name: "Reset Stock Level", visible: false } - inheritance: { name: "Inheritance", visible: false } - visibility: { name: "Hide", visible: false } - $scope.bulkActions = [ name: "Reset Stock Levels To Defaults", callback: 'resetStock' ] + $scope.columns = Columns.columns + $scope.resetSelectFilters = -> $scope.producerFilter = 0 $scope.query = '' diff --git a/app/assets/javascripts/templates/admin/columns_dropdown.html.haml b/app/assets/javascripts/templates/admin/columns_dropdown.html.haml new file mode 100644 index 0000000000..bf52142e30 --- /dev/null +++ b/app/assets/javascripts/templates/admin/columns_dropdown.html.haml @@ -0,0 +1,10 @@ +.ofn-drop-down.right#columns-dropdown{ ng: { controller: 'ColumnsDropdownCtrl' } } + %span{ :class => 'icon-reorder' }= "  #{t('admin.columns')}".html_safe + %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } + %div.menu{ 'ng-show' => "expanded" } + %div.menu_item{ ng: { repeat: "column in columns", click: "toggle(column)", class: "{selected: column.visible}" } } + %span.check + %span.name {{column.name }} + %hr + %div.menu_item.text-center + %input.fullwidth.red{ type: "button", value: 'Save As Default', ng: { click: "saveColumnPreferences(action)"} } diff --git a/app/controllers/admin/column_preferences_controller.rb b/app/controllers/admin/column_preferences_controller.rb new file mode 100644 index 0000000000..0a9300d71c --- /dev/null +++ b/app/controllers/admin/column_preferences_controller.rb @@ -0,0 +1,38 @@ +module Admin + class ColumnPreferencesController < ResourceController + before_filter :load_collection, only: [:bulk_update] + + respond_to :json + + def bulk_update + @cp_set.collection.each { |cp| authorize! :bulk_update, cp } + + if @cp_set.save + # Return saved VOs with IDs + render json: @cp_set.collection, each_serializer: Api::Admin::ColumnPreferenceSerializer + else + if @cp_set.errors.present? + render json: { errors: @cp_set.errors }, status: 400 + else + render nothing: true, status: 500 + end + end + end + + private + + def load_collection + collection_hash = Hash[params[:column_preferences].each_with_index.map { |cp, i| [i, cp] }] + collection_hash.reject!{ |i, cp| cp[:action_name] != params[:action_name] } + @cp_set = ColumnPreferenceSet.new @column_preferences, collection_attributes: collection_hash + end + + def collection + ColumnPreference.where(user_id: spree_current_user, action_name: params[:action_name]) + end + + def collection_actions + [:bulk_update] + end + end +end diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 0c032eaa9e..ffd2c56c02 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -47,13 +47,18 @@ module Admin admin_inject_json_ams_array opts[:module], "inventoryItems", @inventory_items, Api::Admin::InventoryItemSerializer end + def admin_inject_column_preferences(opts={module: 'ofn.admin'}) + column_preferences = ColumnPreference.for(spree_current_user, "#{controller_name}_#{action_name}") + admin_inject_json_ams_array opts[:module], "columns", column_preferences, Api::Admin::ColumnPreferenceSerializer + end + def admin_inject_enterprise_permissions permissions = {can_manage_shipping_methods: can?(:manage_shipping_methods, @enterprise), can_manage_payment_methods: can?(:manage_payment_methods, @enterprise), can_manage_enterprise_fees: can?(:manage_enterprise_fees, @enterprise)} - render partial: "admin/json/injection_ams", locals: {ngModule: "admin.enterprises", name: "enterprisePermissions", json: permissions.to_json} + admin_inject_json "admin.enterprises", "enterprisePermissions", permissions end def admin_inject_hub_permissions @@ -96,6 +101,11 @@ module Admin render partial: "admin/json/injection_ams", locals: {ngModule: 'admin.indexUtils', name: 'SpreeApiKey', json: "'#{@spree_api_key.to_s}'"} end + def admin_inject_json(ngModule, name, data) + json = data.to_json + render partial: "admin/json/injection_ams", locals: {ngModule: ngModule, name: name, json: json} + end + def admin_inject_json_ams(ngModule, name, data, serializer, opts = {}) json = serializer.new(data, scope: spree_current_user).to_json render partial: "admin/json/injection_ams", locals: {ngModule: ngModule, name: name, json: json} diff --git a/app/models/column_preference.rb b/app/models/column_preference.rb new file mode 100644 index 0000000000..09ad0e55a4 --- /dev/null +++ b/app/models/column_preference.rb @@ -0,0 +1,46 @@ +require 'open_food_network/column_preference_defaults' + +class ColumnPreference < ActiveRecord::Base + extend OpenFoodNetwork::ColumnPreferenceDefaults + + # These are the attributes used to identify a preference + attr_accessible :user_id, :action_name, :column_name + + # These are attributes that need to be mass assignable + attr_accessible :name, :visible + + # Non-persisted attributes that only have one + # setting (ie. the default) for a given column + attr_accessor :name + + belongs_to :user, class_name: "Spree::User" + + validates :action_name, presence: true, inclusion: { in: proc { known_actions } } + validates :column_name, presence: true, inclusion: { in: proc { |p| valid_columns_for(p.action_name) } } + + def self.for(user, action_name) + stored_preferences = where(user_id: user.id, action_name: action_name) + default_preferences = send("#{action_name}_columns") + default_preferences.each_with_object([]) do |(column_name, default_attributes), preferences| + stored_preference = stored_preferences.find_by_column_name(column_name) + if stored_preference + stored_preference.assign_attributes(default_attributes.select{ |k,v| stored_preference[k].nil? }) + preferences << stored_preference + else + attributes = default_attributes.merge(user_id: user.id, action_name: action_name, column_name: column_name) + preferences << ColumnPreference.new(attributes) + end + end + end + + private + + def self.valid_columns_for(action_name) + send("#{action_name}_columns").keys.map(&:to_s) + end + + def self.known_actions + OpenFoodNetwork::ColumnPreferenceDefaults.private_instance_methods + .select{|m| m.to_s.end_with?("_columns")}.map{ |m| m.to_s.sub /_columns$/, ''} + end +end diff --git a/app/models/column_preference_set.rb b/app/models/column_preference_set.rb new file mode 100644 index 0000000000..32b9b85a1b --- /dev/null +++ b/app/models/column_preference_set.rb @@ -0,0 +1,5 @@ +class ColumnPreferenceSet < ModelSet + def initialize(collection, attributes={}) + super(ColumnPreference, collection, attributes, nil, nil ) + end +end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index e1995b3c6f..2936a47d41 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -101,6 +101,10 @@ class AbilityDecorator can [:print], Spree::Order do |order| order.user == user end + + can [:admin, :bulk_update], ColumnPreference do |column_preference| + column_preference.user == user + end end def add_product_management_abilities(user) diff --git a/app/serializers/api/admin/column_preference_serializer.rb b/app/serializers/api/admin/column_preference_serializer.rb new file mode 100644 index 0000000000..8b8eeb1b3a --- /dev/null +++ b/app/serializers/api/admin/column_preference_serializer.rb @@ -0,0 +1,3 @@ +class Api::Admin::ColumnPreferenceSerializer < ActiveModel::Serializer + attributes :id, :user_id, :action_name, :column_name, :name, :visible +end diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index 4278162e30..45639b74cd 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -10,6 +10,7 @@ %a.button.icon-plus#new-customer{ href: "#", "new-customer-dialog" => true } = t('admin.customers.index.new_customer') += admin_inject_column_preferences module: 'admin.customers' = admin_inject_shops %div{ ng: { controller: 'customersCtrl' } } @@ -26,7 +27,7 @@ .five.columns.alpha %input.fullwidth{ :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Quick Search' } .eight.columns   - = render 'admin/shared/columns_dropdown' + %columns-dropdown{ action: "#{controller_name}_#{action_name}" } .row{ 'ng-if' => 'CurrentShop.shop.id && RequestMonitor.loading' } .sixteen.columns.alpha#loading %img.spinner{ src: "/assets/spinning-circles.svg" } diff --git a/app/views/admin/enterprises/_enterprise_user_index.html.haml b/app/views/admin/enterprises/_enterprise_user_index.html.haml index e53541fde2..1f17f0f82c 100644 --- a/app/views/admin/enterprises/_enterprise_user_index.html.haml +++ b/app/views/admin/enterprises/_enterprise_user_index.html.haml @@ -6,7 +6,7 @@ .six.columns   -# = render 'admin/shared/bulk_actions_dropdown' .three.columns   - = render 'admin/shared/columns_dropdown' + %columns-dropdown{ action: "#{controller_name}_#{action_name}" } .row{ 'ng-if' => '!loaded' } .sixteen.columns.alpha#loading %img.spinner{ src: "/assets/spinning-circles.svg" } diff --git a/app/views/admin/enterprises/index.html.haml b/app/views/admin/enterprises/index.html.haml index 62bc881728..14bcda7f60 100644 --- a/app/views/admin/enterprises/index.html.haml +++ b/app/views/admin/enterprises/index.html.haml @@ -9,6 +9,8 @@ = button_link_to "New Enterprise", main_app.new_admin_enterprise_path, :icon => 'icon-plus', :id => 'admin_new_enterprise_link' = admin_inject_monthly_bill_description += admin_inject_column_preferences module: 'admin.enterprises' + = render 'admin/shared/enterprises_sub_menu' = render :partial => 'spree/shared/error_messages', :locals => { :target => @enterprise_set } diff --git a/app/views/admin/shared/_columns_dropdown.html.haml b/app/views/admin/shared/_columns_dropdown.html.haml deleted file mode 100644 index 4663443321..0000000000 --- a/app/views/admin/shared/_columns_dropdown.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -.ofn-drop-down.right#columns-dropdown - %span{ :class => 'icon-reorder' }= "  #{t('admin.columns')}".html_safe - %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } - %div.menu{ 'ng-show' => "expanded" } - %div.menu_item{ ng: { repeat: "column in columns" }, toggle: { column: true } } - %span.check - %span.name {{column.name }} diff --git a/app/views/admin/variant_overrides/_controls.html.haml b/app/views/admin/variant_overrides/_controls.html.haml index 4a25677be3..6757a8c43c 100644 --- a/app/views/admin/variant_overrides/_controls.html.haml +++ b/app/views/admin/variant_overrides/_controls.html.haml @@ -12,4 +12,4 @@ %i.icon-chevron-left Back to my inventory .four.columns.omega{ng: { show: 'views.inventory.visible' } } - = render 'admin/shared/columns_dropdown' + %columns-dropdown{ action: "#{controller_name}_#{action_name}" } diff --git a/app/views/admin/variant_overrides/_data.html.haml b/app/views/admin/variant_overrides/_data.html.haml index 9d371415fe..02c52d747d 100644 --- a/app/views/admin/variant_overrides/_data.html.haml +++ b/app/views/admin/variant_overrides/_data.html.haml @@ -2,5 +2,6 @@ = admin_inject_hubs module: 'admin.variantOverrides' = admin_inject_hub_permissions = admin_inject_producers module: 'admin.variantOverrides' += admin_inject_column_preferences module: 'admin.variantOverrides' = admin_inject_variant_overrides = admin_inject_inventory_items module: 'admin.variantOverrides' diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index 213ba142d5..ce82341e03 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -9,6 +9,8 @@ = render :partial => 'spree/admin/shared/order_sub_menu' += admin_inject_column_preferences module: 'admin.lineItems' + %div{ ng: { controller: 'LineItemsCtrl' } } %save-bar{ dirty: "bulk_order_form.$dirty", persist: "false" } %input.red{ type: "button", value: "Save Changes", ng: { click: "submit()", disabled: "!bulk_order_form.$dirty" } } @@ -97,7 +99,7 @@ %input.fullwidth{ :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Quick Search' } = render 'admin/shared/bulk_actions_dropdown' %div.seven.columns   - = render 'admin/shared/columns_dropdown' + %columns-dropdown{ action: "#{controller_name}_#{action_name}" } %div.sixteen.columns.alpha#loading{ 'ng-if' => 'RequestMonitor.loading' } %img.spinner{ src: "/assets/spinning-circles.svg" } diff --git a/app/views/spree/admin/products/bulk_edit/_actions.html.haml b/app/views/spree/admin/products/bulk_edit/_actions.html.haml index 7ea29bdb4c..40f135d62e 100644 --- a/app/views/spree/admin/products/bulk_edit/_actions.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_actions.html.haml @@ -3,4 +3,4 @@ %input.four.columns.alpha{ :type => 'button', :value => 'Save Changes', 'ng-click' => 'submitProducts()'} .nine.columns = render 'spree/admin/shared/status_message' - = render 'admin/shared/columns_dropdown' + %columns-dropdown{ action: "#{controller_name}_#{action_name}" } diff --git a/app/views/spree/admin/products/bulk_edit/_data.html.haml b/app/views/spree/admin/products/bulk_edit/_data.html.haml index 3624421870..a17f725b11 100644 --- a/app/views/spree/admin/products/bulk_edit/_data.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_data.html.haml @@ -2,3 +2,4 @@ = admin_inject_taxons = admin_inject_tax_categories = admin_inject_spree_api_key += admin_inject_column_preferences diff --git a/config/routes.rb b/config/routes.rb index c9b81f8caf..f1b96de181 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -135,6 +135,10 @@ Openfoodnetwork::Application.routes.draw do resource :cache_settings resource :account, only: [:show], controller: 'account' + + resources :column_preferences, only: [], format: :json do + put :bulk_update, on: :collection + end end namespace :api do diff --git a/db/migrate/20160116024333_create_column_preferences.rb b/db/migrate/20160116024333_create_column_preferences.rb new file mode 100644 index 0000000000..e38c51d858 --- /dev/null +++ b/db/migrate/20160116024333_create_column_preferences.rb @@ -0,0 +1,13 @@ +class CreateColumnPreferences < ActiveRecord::Migration + def change + create_table :column_preferences do |t| + t.references :user, null: false, index: true + t.string :action_name, null: false, index: true + t.string :column_name, null: false + t.boolean :visible, null: false + + t.timestamps + end + add_index :column_preferences, [:user_id, :action_name, :column_name], unique: true, name: 'index_column_prefs_on_user_id_and_action_name_and_column_name' + end +end diff --git a/db/schema.rb b/db/schema.rb index dc54bd8f3d..60b72adeea 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -176,6 +176,17 @@ ActiveRecord::Schema.define(:version => 20160401043927) do add_index "cms_snippets", ["site_id", "identifier"], :name => "index_cms_snippets_on_site_id_and_identifier", :unique => true add_index "cms_snippets", ["site_id", "position"], :name => "index_cms_snippets_on_site_id_and_position" + create_table "column_preferences", :force => true do |t| + t.integer "user_id", :null => false + t.string "action_name", :null => false + t.string "column_name", :null => false + t.boolean "visible", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "column_preferences", ["user_id", "action_name", "column_name"], :name => "index_column_prefs_on_user_id_and_action_name_and_column_name", :unique => true + create_table "coordinator_fees", :force => true do |t| t.integer "order_cycle_id" t.integer "enterprise_fee_id" @@ -683,9 +694,9 @@ ActiveRecord::Schema.define(:version => 20160401043927) do t.string "email" t.text "special_instructions" t.integer "distributor_id" + t.integer "order_cycle_id" t.string "currency" t.string "last_ip_address" - t.integer "order_cycle_id" t.integer "cart_id" t.integer "customer_id" end diff --git a/lib/open_food_network/column_preference_defaults.rb b/lib/open_food_network/column_preference_defaults.rb new file mode 100644 index 0000000000..126a8a567a --- /dev/null +++ b/lib/open_food_network/column_preference_defaults.rb @@ -0,0 +1,77 @@ +module OpenFoodNetwork + module ColumnPreferenceDefaults + + private + + # NOTE: These methods define valid column names (via hash keys) + # as well as default values for column attributes (eg. visiblity) + # Default values can be overridden by storing a different value + # for a given user, action_name and column_name + + def variant_overrides_index_columns + { + producer: { name: "Producer", visible: true }, + product: { name: "Product", visible: true }, + sku: { name: "SKU", visible: false }, + price: { name: "Price", visible: true }, + on_hand: { name: "On Hand", visible: true }, + on_demand: { name: "On Demand", visible: false }, + reset: { name: "Reset Stock Level", visible: false }, + inheritance: { name: "Inheritance", visible: false }, + visibility: { name: "Hide", visible: false } + } + end + + def customers_index_columns + { + email: { name: "Email", visible: true }, + code: { name: "Code", visible: true }, + tags: { name: "Tags", visible: true } + } + end + + def orders_bulk_management_columns + { + order_no: { name: t("bom_no"), visible: false }, + full_name: { name: t("name"), visible: true }, + email: { name: t("email"), visible: false }, + phone: { name: t("phone"), visible: false }, + order_date: { name: t("bom_date"), visible: true }, + producer: { name: t("producer"), visible: true }, + order_cycle: { name: t("bom_cycle"), visible: false }, + hub: { name: t("bom_hub"), visible: false }, + variant: { name: t("bom_variant"), visible: true }, + quantity: { name: t("bom_quantity"), visible: true }, + max: { name: t("bom_max"), visible: true }, + final_weight_volume: { name: t("bom_final_weigth_volume"), visible: false }, + price: { name: t("price"), visible: false } + } + end + + def products_bulk_edit_columns + { + producer: { name: t("products_producer"), visible: true }, + sku: { name: t("products_sku"), visible: false }, + name: { name: t("products_name"), visible: true }, + unit: { name: t("products_unit"), visible: true }, + price: { name: t("products_price"), visible: true }, + on_hand: { name: t("products_on_hand"), visible: true }, + on_demand: { name: t("products_on_demand"), visible: false }, + category: { name: t("products_category"), visible: false }, + tax_category: { name: t("products_tax_category"), visible: false }, + inherits_properties: { name: t("products_inherits_properties"), visible: false }, + available_on: { name: t("products_available_on"), visible: false } + } + end + + def enterprises_index_columns + { + name: { name: "Name", visible: true }, + producer: { name: "Producer", visible: true }, + package: { name: "Package", visible: true }, + status: { name: "Status", visible: true }, + manage: { name: "Manage", visible: true } + } + end + end +end diff --git a/spec/controllers/admin/column_preferences_controller_spec.rb b/spec/controllers/admin/column_preferences_controller_spec.rb new file mode 100644 index 0000000000..19d867d9b8 --- /dev/null +++ b/spec/controllers/admin/column_preferences_controller_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe Admin::ColumnPreferencesController, type: :controller do + include AuthenticationWorkflow + + + describe "bulk_update" do + let!(:user1) { create(:user) } + let!(:user2) { create(:user) } + let!(:enterprise) { create(:enterprise, owner: user1, users: [user1, user2]) } + + context "json" do + let!(:column_preference) { ColumnPreference.create(user_id: user1.id, action_name: 'enterprises_index', column_name: "name", visible: true) } + + let(:column_preference_params) { [ + { id: column_preference.id, user_id: user1.id, action_name: "enterprises_index", column_name: 'name', visible: false }, + { id: nil, user_id: user1.id, action_name: "enterprises_index", column_name: 'producer', visible: true }, + { id: nil, user_id: user1.id, action_name: "enterprises_index", column_name: 'status', visible: true } + ] } + + context "where I don't own the preferences submitted" do + before do + allow(controller).to receive(:spree_current_user) { user2 } + end + + it "prevents me from updating the column preferences" do + spree_put :bulk_update, format: :json, action_name: "enterprises_index", column_preferences: column_preference_params + expect(ColumnPreference.count).to be 1 + end + end + + context "where I own the preferences submitted" do + before do + allow(controller).to receive(:spree_current_user) { user1 } + end + + it "allows me to update the column preferences" do + spree_put :bulk_update, format: :json, action_name: "enterprises_index", column_preferences: column_preference_params + expect(ColumnPreference.where(user_id: user1.id, action_name: 'enterprises_index').count).to be 3 + end + end + end + end +end diff --git a/spec/javascripts/unit/admin/controllers/variant_overrides_controller_spec.js.coffee b/spec/javascripts/unit/admin/controllers/variant_overrides_controller_spec.js.coffee index de3a3d5d55..450041a417 100644 --- a/spec/javascripts/unit/admin/controllers/variant_overrides_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/controllers/variant_overrides_controller_spec.js.coffee @@ -20,6 +20,7 @@ describe "VariantOverridesCtrl", -> $provide.value 'variantOverrides', variantOverrides $provide.value 'dirtyVariantOverrides', dirtyVariantOverrides $provide.value 'inventoryItems', inventoryItems + $provide.value 'columns', [] null inject ($controller, _VariantOverrides_, _DirtyVariantOverrides_, _StatusMessage_) -> diff --git a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee index 796dc93ebf..7854700dfd 100644 --- a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee @@ -4,6 +4,10 @@ describe "CustomersCtrl", -> beforeEach -> module('admin.customers') + module ($provide) -> + $provide.value 'columns', [] + null + inject ($controller, $rootScope, _CustomerResource_, $httpBackend) -> scope = $rootScope http = $httpBackend diff --git a/spec/javascripts/unit/admin/enterprises/controllers/enterprises_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/enterprises_controller_spec.js.coffee index 573ca33d9f..24c4035a3e 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/enterprises_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/enterprises_controller_spec.js.coffee @@ -5,6 +5,10 @@ describe "EnterprisesCtrl", -> beforeEach -> module('admin.enterprises') + module ($provide) -> + $provide.value 'columns', [] + null + inject ($controller, $rootScope, _Enterprises_) -> scope = $rootScope Enterprises = _Enterprises_ diff --git a/spec/javascripts/unit/admin/index_utils/directives/panel_row_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/directives/panel_row_spec.js.coffee index 2161afe7de..92eba790c3 100644 --- a/spec/javascripts/unit/admin/index_utils/directives/panel_row_spec.js.coffee +++ b/spec/javascripts/unit/admin/index_utils/directives/panel_row_spec.js.coffee @@ -5,6 +5,9 @@ describe "PanelRow directive", -> beforeEach -> module 'admin.indexUtils' + module ($provide) -> + $provide.value 'columns', [] + null beforeEach inject ($rootScope, $compile, $injector, $templateCache, _Panels_) -> Panels = _Panels_ diff --git a/spec/javascripts/unit/admin/index_utils/services/columns_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/columns_spec.js.coffee index 2bff5e5a73..66122578a7 100644 --- a/spec/javascripts/unit/admin/index_utils/services/columns_spec.js.coffee +++ b/spec/javascripts/unit/admin/index_utils/services/columns_spec.js.coffee @@ -3,19 +3,21 @@ describe "Columns service", -> beforeEach -> module 'admin.indexUtils' - + module ($provide) -> + $provide.value 'columns', [ + { column_name: 'col1', visible: true } + { column_name: 'col2', visible: false } + ] + null inject (_Columns_) -> Columns = _Columns_ - describe "setting columns", -> + describe "initialising columns", -> it "sets resets @columns and copies each column of the provided object across", -> - Columns.setColumns({ name: { visible: true } }) - expect(Columns.columns).toEqual { name: { visible: true } } + expect(Columns.columns).toEqual { col1: { column_name: 'col1', visible: true }, col2: { column_name: 'col2', visible: false } } - it "calls calculateVisibleCount", -> - spyOn(Columns, "calculateVisibleCount") - Columns.setColumns({ name: { visible: true } }) - expect(Columns.calculateVisibleCount).toHaveBeenCalled() + it "updates visibleCount", -> + expect(Columns.visibleCount).toBe 1 describe "toggling a column", -> it "switches the visibility of the given column", -> diff --git a/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee b/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee index 5b4e4730b6..a1c291f5b0 100644 --- a/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee @@ -4,6 +4,9 @@ describe "LineItemsCtrl", -> beforeEach -> module "admin.lineItems" + module ($provide) -> + $provide.value 'columns', [] + null jasmine.addMatchers toDeepEqual: (util, customEqualityTesters) -> diff --git a/spec/javascripts/unit/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/bulk_product_update_spec.js.coffee index 457010da0c..cc0b574f47 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/bulk_product_update_spec.js.coffee @@ -241,6 +241,7 @@ describe "AdminProductEditCtrl", -> $provide.value "taxons", [] $provide.value "tax_categories", [] $provide.value 'SpreeApiKey', 'API_KEY' + $provide.value 'columns', [] null beforeEach inject((_$controller_, _$timeout_, $rootScope, _$httpBackend_, _BulkProducts_, _DirtyProducts_, _DisplayProperties_) -> diff --git a/spec/models/column_preference_spec.rb b/spec/models/column_preference_spec.rb new file mode 100644 index 0000000000..4937bf1d34 --- /dev/null +++ b/spec/models/column_preference_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe ColumnPreference, type: :model do + describe "finding stored preferences for a user and action" do + before do + allow(ColumnPreference).to receive(:known_actions) { ['some_action'] } + allow(ColumnPreference).to receive(:valid_columns_for) { ['col1', 'col2', 'col3'] } + end + + let(:user) { create(:user) } + let!(:col1_pref) { ColumnPreference.create(user_id: user.id, action_name: 'some_action', column_name: 'col1', visible: true) } + let!(:col2_pref) { ColumnPreference.create(user_id: user.id, action_name: 'some_action', column_name: 'col2', visible: false) } + let(:defaults) { { + col1: { name: "col1", visible: false }, + col2: { name: "col2", visible: true }, + col3: { name: "col3", visible: false }, + } } + + context "when the user has preferences stored for the given action" do + before do + allow(ColumnPreference).to receive(:some_action_columns) { defaults } + end + + let(:preferences) { ColumnPreference.for(user, :some_action)} + + it "builds an entry for each column listed in the defaults" do + expect(preferences.count).to eq 3 + end + + it "uses values from stored preferences where present" do + expect(preferences).to include col1_pref, col2_pref + end + + it "uses defaults where no stored preference exists" do + default_pref = preferences.last + expect(default_pref).to be_a_new ColumnPreference + expect(default_pref.visible).to be false # As per default + end + end + + context "where the user does not have preferences stored for the given action" do + before do + allow(ColumnPreference).to receive(:some_action_columns) { defaults } + end + + let(:preferences) { ColumnPreference.for(create(:user), :some_action)} + + it "builds an entry for each column listed in the defaults" do + expect(preferences.count).to eq 3 + end + + it "uses defaults where no stored preference exists" do + expect(preferences.all?(&:new_record?)).to be true + expect(preferences.map(&:column_name)).to eq [:col1, :col2, :col3] + expect(preferences.map(&:visible)).to eq [false, true, false] + end + end + end +end From 55da83d0c031e5d280be30132ce2e8cbab4833d5 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Mon, 16 May 2016 10:31:07 +1000 Subject: [PATCH 047/110] Removing obsolete BOM controller (replaced by LineItemsCtrl) --- .../admin/bulk_order_management.js.coffee | 222 --------- .../unit/bulk_order_management_spec.js.coffee | 427 ------------------ 2 files changed, 649 deletions(-) delete mode 100644 app/assets/javascripts/admin/bulk_order_management.js.coffee delete mode 100644 spec/javascripts/unit/bulk_order_management_spec.js.coffee diff --git a/app/assets/javascripts/admin/bulk_order_management.js.coffee b/app/assets/javascripts/admin/bulk_order_management.js.coffee deleted file mode 100644 index 2545a13915..0000000000 --- a/app/assets/javascripts/admin/bulk_order_management.js.coffee +++ /dev/null @@ -1,222 +0,0 @@ -angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [ - "$scope", "$http", "$filter", "dataFetcher", "blankOption", "pendingChanges", "VariantUnitManager", "OptionValueNamer", "SpreeApiKey", "Columns" - ($scope, $http, $filter, dataFetcher, blankOption, pendingChanges, VariantUnitManager, OptionValueNamer, SpreeApiKey, Columns) -> - $scope.loading = true - - $scope.initialiseVariables = -> - start = daysFromToday -7 - end = daysFromToday 1 - $scope.lineItems = [] - $scope.filteredLineItems = [] - $scope.confirmDelete = true - $scope.startDate = formatDate start - $scope.endDate = formatDate end - $scope.quickSearch = "" - $scope.bulkActions = [ { name: t("bom_actions_delete"), callback: $scope.deleteLineItems } ] - $scope.selectedBulkAction = $scope.bulkActions[0] - $scope.selectedUnitsProduct = {}; - $scope.selectedUnitsVariant = {}; - $scope.sharedResource = false - $scope.columns = Columns.setColumns - order_no: { name: t("bom_no"), visible: false } - full_name: { name: t("name"), visible: true } - email: { name: t("email"), visible: false } - phone: { name: t("phone"), visible: false } - order_date: { name: t("bom_date"), visible: true } - producer: { name: t("producer"), visible: true } - order_cycle: { name: t("bom_cycle"), visible: false } - hub: { name: t("bom_hub"), visible: false } - variant: { name: t("bom_variant"), visible: true } - quantity: { name: t("bom_quantity"), visible: true } - max: { name: t("bom_max"), visible: true } - final_weight_volume: { name: t("bom_final_weigth_volume"), visible: false } - price: { name: t("price"), visible: false } - $scope.initialise = -> - $scope.initialiseVariables() - authorise_api_reponse = "" - dataFetcher("/api/users/authorise_api?token=" + SpreeApiKey).then (data) -> - authorise_api_reponse = data - $scope.spree_api_key_ok = data.hasOwnProperty("success") and data["success"] == "Use of API Authorised" - if $scope.spree_api_key_ok - $http.defaults.headers.common["X-Spree-Token"] = SpreeApiKey - dataFetcher("/api/enterprises/accessible?template=bulk_index&q[is_primary_producer_eq]=true").then (data) -> - $scope.suppliers = $filter('orderBy')(data, 'name') - $scope.suppliers.unshift blankOption() - dataFetcher("/api/enterprises/accessible?template=bulk_index&q[sells_in][]=own&q[sells_in][]=any").then (data) -> - $scope.distributors = $filter('orderBy')(data, 'name') - $scope.distributors.unshift blankOption() - ocFetcher = dataFetcher("/api/order_cycles/accessible?as=distributor&q[orders_close_at_gt]=#{formatDate(daysFromToday(-90))}").then (data) -> - $scope.orderCycles = data - $scope.orderCyclesByID = [] - $scope.orderCyclesByID[oc.id] = oc for oc in $scope.orderCycles - $scope.orderCycles.unshift blankOption() - $scope.fetchOrders() - ocFetcher.then -> - $scope.resetSelectFilters() - else if authorise_api_reponse.hasOwnProperty("error") - $scope.api_error_msg = authorise_api_reponse("error") - else - api_error_msg = "You don't have an API key yet. An attempt was made to generate one, but you are currently not authorised, please contact your site administrator for access." - - $scope.fetchOrders = -> - $scope.loading = true - dataFetcher("/admin/orders/managed?template=bulk_index;page=1;per_page=500;q[state_not_eq]=canceled;q[completed_at_not_null]=true;q[completed_at_gt]=#{$scope.startDate};q[completed_at_lt]=#{$scope.endDate}").then (data) -> - $scope.resetOrders data - $scope.loading = false - - $scope.resetOrders = (data) -> - $scope.orders = data - $scope.resetLineItems() - pendingChanges.removeAll() - - $scope.resetLineItems = -> - $scope.lineItems = $scope.orders.reduce (lineItems,order) -> - orderWithoutLineItems = $scope.lineItemOrder order - for i,line_item of order.line_items - line_item.checked = false - line_item.supplier = $scope.matchObject $scope.suppliers, line_item.supplier, null - line_item.order = orderWithoutLineItems - line_item.original_final_weight_volume = line_item.final_weight_volume - line_item.original_quantity = line_item.quantity - line_item.original_price = line_item.price - - lineItems.concat order.line_items - , [] - - $scope.lineItemOrder = (order) -> - lineItemOrder = angular.copy(order) - delete lineItemOrder.line_items - lineItemOrder.distributor = $scope.matchObject $scope.distributors, order.distributor, null - lineItemOrder.order_cycle = $scope.matchObject $scope.orderCycles, order.order_cycle, null - lineItemOrder - - $scope.matchObject = (list, testObject, noMatch) -> - for i, object of list - if angular.equals(object, testObject) - return object - return noMatch - - $scope.deleteLineItem = (lineItem) -> - if ($scope.confirmDelete && confirm("Are you sure?")) || !$scope.confirmDelete - $http( - method: "DELETE" - url: "/api/orders/" + lineItem.order.number + "/line_items/" + lineItem.id - ).success (data) -> - $scope.lineItems.splice $scope.lineItems.indexOf(lineItem), 1 - - $scope.deleteLineItems = (lineItems) -> - existingState = $scope.confirmDelete - $scope.confirmDelete = false - $scope.deleteLineItem lineItem for lineItem in lineItems when lineItem.checked - $scope.confirmDelete = existingState - - $scope.submit = -> - if $scope.bulk_order_form.$valid - pendingChanges.submitAll() - else - alert "Some errors must be resolved be before you can update orders.\nAny fields with red borders contain errors." - - $scope.allBoxesChecked = -> - checkedCount = $scope.filteredLineItems.reduce (count,lineItem) -> - count + (if lineItem.checked then 1 else 0 ) - , 0 - checkedCount == $scope.filteredLineItems.length - - $scope.toggleAllCheckboxes = -> - changeTo = !$scope.allBoxesChecked() - lineItem.checked = changeTo for lineItem in $scope.filteredLineItems - - $scope.setSelectedUnitsVariant = (unitsProduct,unitsVariant) -> - $scope.selectedUnitsProduct = unitsProduct - $scope.selectedUnitsVariant = unitsVariant - - $scope.sumUnitValues = -> - sum = $scope.filteredLineItems.reduce (sum,lineItem) -> - sum = sum + lineItem.final_weight_volume - , 0 - - $scope.sumMaxUnitValues = -> - sum = $scope.filteredLineItems.reduce (sum,lineItem) -> - sum = sum + Math.max(lineItem.max_quantity,lineItem.original_quantity) * lineItem.units_variant.unit_value - , 0 - - $scope.allFinalWeightVolumesPresent = -> - for i,lineItem of $scope.filteredLineItems - return false if !lineItem.hasOwnProperty('final_weight_volume') || !(lineItem.final_weight_volume > 0) - true - - # How is this different to OptionValueNamer#name? - # Should it be extracted to that class or VariantUnitManager? - $scope.formattedValueWithUnitName = (value, unitsProduct, unitsVariant) -> - # A Units Variant is an API object which holds unit properies of a variant - if unitsProduct.hasOwnProperty("variant_unit") && (unitsProduct.variant_unit == "weight" || unitsProduct.variant_unit == "volume") && value > 0 - scale = VariantUnitManager.getScale(value, unitsProduct.variant_unit) - Math.round(value/scale * 1000)/1000 + " " + VariantUnitManager.getUnitName(scale, unitsProduct.variant_unit) - else - '' - - $scope.fulfilled = (sumOfUnitValues) -> - # A Units Variant is an API object which holds unit properies of a variant - if $scope.selectedUnitsProduct.hasOwnProperty("group_buy_unit_size") && $scope.selectedUnitsProduct.group_buy_unit_size > 0 && - $scope.selectedUnitsProduct.hasOwnProperty("variant_unit") && - ( $scope.selectedUnitsProduct.variant_unit == "weight" || $scope.selectedUnitsProduct.variant_unit == "volume" ) - Math.round( sumOfUnitValues / $scope.selectedUnitsProduct.group_buy_unit_size * 1000)/1000 - else - '' - - $scope.unitsVariantSelected = -> - !angular.equals($scope.selectedUnitsVariant,{}) - - $scope.resetSelectFilters = -> - $scope.distributorFilter = $scope.distributors[0].id - $scope.supplierFilter = $scope.suppliers[0].id - $scope.orderCycleFilter = $scope.orderCycles[0].id - $scope.quickSearch = "" - - $scope.weightAdjustedPrice = (lineItem) -> - if lineItem.final_weight_volume > 0 - unit_value = lineItem.final_weight_volume / lineItem.quantity - original_unit_value = lineItem.original_final_weight_volume / lineItem.original_quantity - lineItem.price = lineItem.original_price * (unit_value / original_unit_value) - - $scope.unitValueLessThanZero = (lineItem) -> - if lineItem.units_variant.unit_value <= 0 - true - else - false - - $scope.updateOnQuantity = (lineItem) -> - if lineItem.quantity > 0 - lineItem.final_weight_volume = lineItem.original_final_weight_volume * lineItem.quantity / lineItem.original_quantity - $scope.weightAdjustedPrice(lineItem) - - $scope.$watch "orderCycleFilter", (newVal, oldVal) -> - unless $scope.orderCycleFilter == "0" || angular.equals(newVal, oldVal) - $scope.startDate = $scope.orderCyclesByID[$scope.orderCycleFilter].first_order - $scope.endDate = $scope.orderCyclesByID[$scope.orderCycleFilter].last_order -] - -daysFromToday = (days) -> - now = new Date - now.setHours(0) - now.setMinutes(0) - now.setSeconds(0) - now.setDate( now.getDate() + days ) - now - -formatDate = (date) -> - year = date.getFullYear() - month = twoDigitNumber date.getMonth() + 1 - day = twoDigitNumber date.getDate() - return year + "-" + month + "-" + day - -formatTime = (date) -> - hours = twoDigitNumber date.getHours() - mins = twoDigitNumber date.getMinutes() - secs = twoDigitNumber date.getSeconds() - return hours + ":" + mins + ":" + secs - -twoDigitNumber = (number) -> - twoDigits = "" + number - twoDigits = ("0" + number) if number < 10 - twoDigits diff --git a/spec/javascripts/unit/bulk_order_management_spec.js.coffee b/spec/javascripts/unit/bulk_order_management_spec.js.coffee deleted file mode 100644 index 60edc958aa..0000000000 --- a/spec/javascripts/unit/bulk_order_management_spec.js.coffee +++ /dev/null @@ -1,427 +0,0 @@ -describe "AdminOrderMgmtCtrl", -> - ctrl = scope = httpBackend = VariantUnitManager = null - - beforeEach -> - module "ofn.admin", ($provide) -> - $provide.value 'SpreeApiKey', 'API_KEY' - return - beforeEach inject(($controller, $rootScope, $httpBackend, _VariantUnitManager_) -> - scope = $rootScope.$new() - ctrl = $controller - httpBackend = $httpBackend - VariantUnitManager = _VariantUnitManager_ - spyOn(window, "formatDate").and.returnValue "SomeDate" - - ctrl "AdminOrderMgmtCtrl", {$scope: scope} - ) - - describe "loading data upon initialisation", -> - it "gets a list of suppliers, a list of distributors and a list of Order Cycles and then calls fetchOrders", -> - returnedSuppliers = ["list of suppliers"] - returnedDistributors = ["list of distributors"] - returnedOrderCycles = [ "oc1", "oc2", "oc3" ] - httpBackend.expectGET("/api/users/authorise_api?token=API_KEY").respond success: "Use of API Authorised" - httpBackend.expectGET("/api/enterprises/accessible?template=bulk_index&q[is_primary_producer_eq]=true").respond returnedSuppliers - httpBackend.expectGET("/api/enterprises/accessible?template=bulk_index&q[sells_in][]=own&q[sells_in][]=any").respond returnedDistributors - httpBackend.expectGET("/api/order_cycles/accessible?as=distributor&q[orders_close_at_gt]=SomeDate").respond returnedOrderCycles - spyOn(scope, "initialiseVariables").and.callThrough() - spyOn(scope, "fetchOrders").and.returnValue "nothing" - #spyOn(returnedSuppliers, "unshift") - #spyOn(returnedDistributors, "unshift") - #spyOn(returnedOrderCycles, "unshift") - scope.initialise() - httpBackend.flush() - - expect(scope.suppliers).toEqual [{ id : '0', name : 'All' }, 'list of suppliers'] - expect(scope.distributors).toEqual [ { id : '0', name : 'All' }, 'list of distributors' ] - expect(scope.orderCycles).toEqual [ { id : '0', name : 'All' }, 'oc1', 'oc2', 'oc3' ] - - expect(scope.initialiseVariables.calls.count()).toBe 1 - expect(scope.fetchOrders.calls.count()).toBe 1 - expect(scope.spree_api_key_ok).toBe true - - describe "fetching orders", -> - beforeEach -> - scope.initialiseVariables() - httpBackend.expectGET("/admin/orders/managed?template=bulk_index;page=1;per_page=500;q[state_not_eq]=canceled;q[completed_at_not_null]=true;q[completed_at_gt]=SomeDate;q[completed_at_lt]=SomeDate").respond "list of orders" - - it "makes a call to dataFetcher, with current start and end date parameters", -> - scope.fetchOrders() - - it "calls resetOrders after data has been received", -> - spyOn scope, "resetOrders" - scope.fetchOrders() - httpBackend.flush() - expect(scope.resetOrders).toHaveBeenCalledWith "list of orders" - - it "sets the loading property to true before fetching orders and unsets it when loading is complete", -> - spyOn scope, "resetOrders" - scope.fetchOrders() - expect(scope.loading).toEqual true - httpBackend.flush() - expect(scope.loading).toEqual false - - describe "resetting orders", -> - beforeEach -> - spyOn(scope, "matchObject").and.returnValue "nothing" - spyOn(scope, "resetLineItems").and.returnValue "nothing" - scope.resetOrders [ "order1", "order2", "order3" ] - - it "sets the value of $scope.orders to the data received", -> - expect(scope.orders).toEqual [ "order1", "order2", "order3" ] - - it "makes a call to $scope.resetLineItems", -> - expect(scope.resetLineItems).toHaveBeenCalled() - - describe "resetting line items", -> - order1 = order2 = order3 = null - - beforeEach -> - spyOn(scope, "matchObject").and.returnValue "nothing" - spyOn(scope, "lineItemOrder").and.returnValue "copied order" - order1 = { name: "order1", line_items: [ { name: "line_item1.1" }, { name: "line_item1.1" }, { name: "line_item1.1" } ] } - order2 = { name: "order2", line_items: [ { name: "line_item2.1" }, { name: "line_item2.1" }, { name: "line_item2.1" } ] } - order3 = { name: "order3", line_items: [ { name: "line_item3.1" }, { name: "line_item3.1" }, { name: "line_item3.1" } ] } - scope.orders = [ order1, order2, order3 ] - scope.resetLineItems() - - it "creates $scope.lineItems by flattening the line_items arrays in each order object", -> - expect(scope.lineItems.length).toEqual 9 - expect(scope.lineItems[0].name).toEqual "line_item1.1" - expect(scope.lineItems[3].name).toEqual "line_item2.1" - expect(scope.lineItems[6].name).toEqual "line_item3.1" - - it "adds a reference to a modified parent order object to each line item", -> - expect(scope.lineItemOrder.calls.count()).toBe scope.orders.length - expect("copied order").toEqual line_item.order for line_item in scope.lineItems - - it "calls matchObject once for each line item", -> - expect(scope.matchObject.calls.count()).toBe scope.lineItems.length - - describe "copying orders", -> - order1copy = null - - beforeEach -> - spyOn(scope, "lineItemOrder").and.callThrough() - spyOn(scope, "matchObject").and.returnValue "matched object" - order1 = { name: "order1", line_items: [ ] } - scope.orders = [ order1 ] - order1copy = scope.lineItemOrder order1 - - it "calls removes the line_items attribute of the order, in order to avoid circular referencing)", -> - expect(order1copy.hasOwnProperty("line_items")).toEqual false - - it "calls matchObject twice for each order (once for distributor and once for order cycle)", -> - expect(scope.matchObject.calls.count()).toBe scope.lineItemOrder.calls.count() * 2 - expect(order1copy.distributor).toEqual "matched object" - expect(order1copy.distributor).toEqual "matched object" - - describe "matching objects", -> - it "returns the first matching object in the list", -> - list_item1 = - id: 1 - name: "LI1" - - list_item2 = - id: 2 - name: "LI2" - - test_item = - id: 2 - name: "LI2" - - expect(list_item2 is test_item).not.toEqual true - list = [ - list_item1 - list_item2 - ] - - returned_item = scope.matchObject list, test_item, null - expect(returned_item is list_item2).toEqual true - - it "returns the default provided if no matching item is found", -> - list_item1 = - id: 1 - name: "LI1" - - list_item2 = - id: 2 - name: "LI2" - - test_item = - id: 1 - name: "LI2" - - expect(list_item2 is test_item).not.toEqual true - list = [ - list_item1 - list_item2 - ] - - returned_item = scope.matchObject list, test_item, null - expect(returned_item is null).toEqual true - - describe "deleting a line item", -> - order = line_item1 = line_item2 = null - - beforeEach -> - scope.initialiseVariables() - spyOn(window,"confirm").and.returnValue true - order = { number: "R12345678", line_items: [] } - line_item1 = { id: 1, order: order } - line_item2 = { id: 2, order: order } - order.line_items = [ line_item1, line_item2 ] - - it "sends a delete request via the API", -> - httpBackend.expectDELETE("/api/orders/#{line_item1.order.number}/line_items/#{line_item1.id}").respond "nothing" - scope.deleteLineItem line_item1 - httpBackend.flush() - - it "does not remove line_item from the line_items array when request is not successful", -> - httpBackend.expectDELETE("/api/orders/#{line_item1.order.number}/line_items/#{line_item1.id}").respond 404, "NO CONTENT" - scope.deleteLineItem line_item1 - httpBackend.flush() - expect(order.line_items).toEqual [line_item1, line_item2] - - describe "deleting 'checked' line items", -> - line_item1 = line_item2 = line_item3 = line_item4 = null - - beforeEach -> - line_item1 = { name: "line item 1", checked: false } - line_item2 = { name: "line item 2", checked: true } - line_item3 = { name: "line item 3", checked: false } - line_item4 = { name: "line item 4", checked: true } - scope.lineItems = [ line_item1, line_item2, line_item3, line_item4 ] - - it "calls deletedLineItem for each 'checked' line item", -> - spyOn(scope, "deleteLineItem") - scope.deleteLineItems(scope.lineItems) - expect(scope.deleteLineItem).toHaveBeenCalledWith(line_item2) - expect(scope.deleteLineItem).toHaveBeenCalledWith(line_item4) - expect(scope.deleteLineItem).not.toHaveBeenCalledWith(line_item1) - expect(scope.deleteLineItem).not.toHaveBeenCalledWith(line_item3) - - describe "check boxes for line items", -> - line_item1 = line_item2 = null - - beforeEach -> - line_item1 = { name: "line item 1", checked: false } - line_item2 = { name: "line item 2", checked: false } - scope.filteredLineItems = [ line_item1, line_item2 ] - - it "keeps track of whether all filtered lines items are 'checked' or not", -> - expect(scope.allBoxesChecked()).toEqual false - line_item1.checked = true - expect(scope.allBoxesChecked()).toEqual false - line_item2.checked = true - expect(scope.allBoxesChecked()).toEqual true - line_item1.checked = false - expect(scope.allBoxesChecked()).toEqual false - - it "toggles the 'checked' attribute of all line items based to the value of allBoxesChecked", -> - scope.toggleAllCheckboxes() - expect(scope.allBoxesChecked()).toEqual true - line_item1.checked = false - expect(scope.allBoxesChecked()).toEqual false - scope.toggleAllCheckboxes() - expect(scope.allBoxesChecked()).toEqual true - scope.toggleAllCheckboxes() - expect(scope.allBoxesChecked()).toEqual false - - describe "unit calculations", -> - beforeEach -> - scope.initialiseVariables() - - describe "fulfilled()", -> - it "returns '' if selectedUnitsVariant has no property 'variant_unit'", -> - expect(scope.fulfilled()).toEqual '' - - it "returns '' if selectedUnitsVariant has no property 'group_buy_unit_size' or group_buy_unit_size is 0", -> - scope.selectedUnitsProduct = { variant_unit: "weight", group_buy_unit_size: 0 } - expect(scope.fulfilled()).toEqual '' - scope.selectedUnitsProduct = { variant_unit: "weight" } - expect(scope.fulfilled()).toEqual '' - - it "returns '', and does not call Math.round if variant_unit is 'items'", -> - spyOn(Math,"round") - scope.selectedUnitsProduct = { variant_unit: "items", group_buy_unit_size: 10 } - expect(scope.fulfilled()).toEqual '' - expect(Math.round).not.toHaveBeenCalled() - - it "calls Math.round() if variant_unit is 'weight' or 'volume'", -> - spyOn(Math,"round") - scope.selectedUnitsProduct = { variant_unit: "weight", group_buy_unit_size: 10 } - scope.fulfilled() - expect(Math.round).toHaveBeenCalled() - scope.selectedUnitsProduct = { variant_unit: "volume", group_buy_unit_size: 10 } - scope.fulfilled() - expect(Math.round).toHaveBeenCalled() - - it "returns the quantity of fulfilled group buy units", -> - scope.selectedUnitsProduct = { variant_unit: "weight", group_buy_unit_size: 1000 } - expect(scope.fulfilled(1500)).toEqual 1.5 - - describe "allFinalWeightVolumesPresent()", -> - it "returns false if the unit_value of any item in filteredLineItems does not exist", -> - scope.filteredLineItems = [ - { final_weight_volume: 1000 } - { final_weight_volume: 3000 } - { final_weight_yayaya: 2000 } - ] - expect(scope.allFinalWeightVolumesPresent()).toEqual false - - it "returns false if the unit_value of any item in filteredLineItems is not a number greater than 0", -> - scope.filteredLineItems = [ - { final_weight_volume: 0 } - { final_weight_volume: 3000 } - { final_weight_volume: 2000 } - ] - expect(scope.allFinalWeightVolumesPresent()).toEqual false - scope.filteredLineItems = [ - { final_weight_volume: 'lalala' } - { final_weight_volume: 3000 } - { final_weight_volume: 2000 } - ] - expect(scope.allFinalWeightVolumesPresent()).toEqual false - - it "returns true if the unit_value of all items in filteredLineItems are numbers greater than 0", -> - scope.filteredLineItems = [ - { final_weight_volume: 1000 } - { final_weight_volume: 3000 } - { final_weight_volume: 2000 } - ] - expect(scope.allFinalWeightVolumesPresent()).toEqual true - - describe "sumUnitValues()", -> - it "returns the sum of the final_weight_volumes line_items", -> - scope.filteredLineItems = [ - { final_weight_volume: 2 } - { final_weight_volume: 7 } - { final_weight_volume: 21 } - ] - expect(scope.sumUnitValues()).toEqual 30 - - describe "sumMaxUnitValues()", -> - it "returns the sum of the product of unit_value and maxOf(max_quantity,quantity) for specified line_items", -> - scope.filteredLineItems = [ - { units_variant: { unit_value: 1 }, original_quantity: 2, max_quantity: 5 } - { units_variant: { unit_value: 2 }, original_quantity: 3, max_quantity: 1 } - { units_variant: { unit_value: 3 }, original_quantity: 7, max_quantity: 10 } - ] - sp0 = scope.filteredLineItems[0].units_variant.unit_value * Math.max(scope.filteredLineItems[0].original_quantity, scope.filteredLineItems[0].max_quantity) - sp1 = scope.filteredLineItems[1].units_variant.unit_value * Math.max(scope.filteredLineItems[1].original_quantity, scope.filteredLineItems[1].max_quantity) - sp2 = scope.filteredLineItems[2].units_variant.unit_value * Math.max(scope.filteredLineItems[2].original_quantity, scope.filteredLineItems[2].max_quantity) - expect(scope.sumMaxUnitValues()).toEqual (sp0 + sp1 + sp2) - - describe "formatting a value based upon the properties of a specified Units Variant", -> - # A Units Variant is an API object which holds unit properies of a variant - - beforeEach -> - spyOn(Math,"round").and.callThrough() - - it "returns '' if selectedUnitsVariant has no property 'variant_unit'", -> - expect(scope.formattedValueWithUnitName(1,{})).toEqual '' - - it "returns '', and does not call Math.round if variant_unit is 'items'", -> - unitsVariant = { variant_unit: "items" } - expect(scope.formattedValueWithUnitName(1,unitsVariant)).toEqual '' - expect(Math.round).not.toHaveBeenCalled() - - it "calls Math.round() if variant_unit is 'weight' or 'volume'", -> - unitsVariant = { variant_unit: "weight" } - scope.formattedValueWithUnitName(1,unitsVariant) - expect(Math.round).toHaveBeenCalled() - scope.selectedUnitsVariant = { variant_unit: "volume" } - scope.formattedValueWithUnitName(1,unitsVariant) - expect(Math.round).toHaveBeenCalled() - - it "calls Math.round with the quotient of scale and value, multiplied by 1000", -> - unitsVariant = { variant_unit: "weight" } - spyOn(VariantUnitManager, "getScale").and.returnValue 5 - scope.formattedValueWithUnitName(10, unitsVariant) - expect(Math.round).toHaveBeenCalledWith 10/5 * 1000 - - it "returns the result of Math.round divided by 1000, followed by the result of getUnitName", -> - unitsVariant = { variant_unit: "weight" } - spyOn(VariantUnitManager, "getScale").and.returnValue 1000 - spyOn(VariantUnitManager, "getUnitName").and.returnValue "kg" - expect(scope.formattedValueWithUnitName(2000,unitsVariant)).toEqual "2 kg" - - describe "updating the price upon updating the weight of a line item", -> - it "updates the price if the weight is changed", -> - scope.filteredLineItems = [ - { original_price: 2.00, price: 2.00, original_quantity: 1, quantity: 1, original_final_weight_volume: 2000, final_weight_volume: 4000 } - ] - scope.weightAdjustedPrice(scope.filteredLineItems[0]) - expect(scope.filteredLineItems[0].price).toEqual 4.00 - - it "doesn't update the price if the weight <= 0", -> - scope.filteredLineItems = [ - { original_price: 2.00, price: 2.00, original_quantity: 1, quantity: 1, original_final_weight_volume: 2000, final_weight_volume: 0 } - ] - scope.weightAdjustedPrice(scope.filteredLineItems[0]) - expect(scope.filteredLineItems[0].price).toEqual 2.00 - - it "doesn't update the price if the weight is an empty string", -> - scope.filteredLineItems = [ - { original_price: 2.00, price: 2.00, original_quantity: 1, quantity: 1, original_final_weight_volume: 2000, final_weight_volume: "" } - ] - scope.weightAdjustedPrice(scope.filteredLineItems[0]) - expect(scope.filteredLineItems[0].price).toEqual 2.00 - - describe "updating final_weight_volume upon updating the quantity for a line_item", -> - beforeEach -> - spyOn(scope, "weightAdjustedPrice") - - it "updates the weight if the quantity is changed, then calls weightAdjustedPrice()", -> - scope.filteredLineItems = [ - { original_price: 2.00, price: 2.00, original_quantity: 1, quantity: 2, original_final_weight_volume: 2000, final_weight_volume: 0 } - ] - scope.updateOnQuantity(scope.filteredLineItems[0]) - expect(scope.filteredLineItems[0].final_weight_volume).toEqual 4000 - expect(scope.weightAdjustedPrice).toHaveBeenCalled() - - it "doesn't update the weight if the quantity <= 0", -> - scope.filteredLineItems = [ - { original_price: 2.00, price: 2.00, original_quantity: 1, quantity: 0, original_final_weight_volume: 2000, final_weight_volume: 1000 } - ] - scope.updateOnQuantity(scope.filteredLineItems[0]) - expect(scope.filteredLineItems[0].final_weight_volume).toEqual 1000 - - it "doesn't update the weight if the quantity is an empty string", -> - scope.filteredLineItems = [ - { original_price: 2.00, price: 2.00, original_quantity: 1, quantity: "", original_final_weight_volume: 2000, final_weight_volume: 1000 } - ] - scope.updateOnQuantity(scope.filteredLineItems[0]) - expect(scope.filteredLineItems[0].final_weight_volume).toEqual 1000 - - -describe "Auxiliary functions", -> - describe "getting a zero filled two digit number", -> - it "returns the number as a string if its value is greater than or equal to 10", -> - expect(twoDigitNumber(10)).toEqual "10" - expect(twoDigitNumber(15)).toEqual "15" - expect(twoDigitNumber(99)).toEqual "99" - - it "returns the number formatted as a zero filled string if its value is less than 10", -> - expect(twoDigitNumber(0)).toEqual "00" - expect(twoDigitNumber(1)).toEqual "01" - expect(twoDigitNumber(9)).toEqual "09" - - describe "formatting dates and times", -> - date = null - - beforeEach -> - date = new Date - date.setYear(2010) - date.setMonth(4) # Zero indexed, so 4 is May - date.setDate(15) - date.setHours(5) - date.setMinutes(10) - date.setSeconds(30) - - it "returns a date formatted as yyyy-mm-dd", -> - expect(formatDate(date)).toEqual "2010-05-15" - - it "returns a time formatted as hh-MM:ss", -> - expect(formatTime(date)).toEqual "05:10:30" From f2de498d61cb1ab076e6cd2348384f1bab6c250f Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 15 May 2016 22:57:34 +1000 Subject: [PATCH 048/110] Show saved status of column preferences --- .../controllers/columns_dropdown_controller.js.coffee | 1 + .../admin/index_utils/services/columns.js.coffee | 11 +++++++++-- .../templates/admin/columns_dropdown.html.haml | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/admin/dropdown/controllers/columns_dropdown_controller.js.coffee b/app/assets/javascripts/admin/dropdown/controllers/columns_dropdown_controller.js.coffee index 6c27980495..4b3a5a4516 100644 --- a/app/assets/javascripts/admin/dropdown/controllers/columns_dropdown_controller.js.coffee +++ b/app/assets/javascripts/admin/dropdown/controllers/columns_dropdown_controller.js.coffee @@ -2,3 +2,4 @@ angular.module("admin.dropdown").controller "ColumnsDropdownCtrl", ($scope, Colu $scope.columns = Columns.columns $scope.toggle = Columns.toggleColumn $scope.saveColumnPreferences = Columns.savePreferences + $scope.saved = Columns.preferencesSaved diff --git a/app/assets/javascripts/admin/index_utils/services/columns.js.coffee b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee index 99adc85fdd..f2ed5463ae 100644 --- a/app/assets/javascripts/admin/index_utils/services/columns.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee @@ -1,13 +1,14 @@ angular.module("admin.indexUtils").factory 'Columns', ($rootScope, $http, columns) -> new class Columns + savedColumns: {} columns: {} visibleCount: 0 - saving: false constructor: -> @columns = {} for column in columns @columns[column.column_name] = column + @savedColumns[column.column_name] = angular.copy(column) @calculateVisibleCount() toggleColumn: (column) => @@ -18,10 +19,16 @@ angular.module("admin.indexUtils").factory 'Columns', ($rootScope, $http, column @visibleCount = (column for name, column of @columns when column.visible).length $rootScope.$broadcast "columnCount:changed", @visibleCount - savePreferences: (action_name) -> + preferencesSaved: => + angular.equals(@columns, @savedColumns) + + savePreferences: (action_name) => $http method: "PUT" url: "/admin/column_preferences/bulk_update" data: action_name: action_name column_preferences: (preference for column_name, preference of @columns) + .success (data) => + for column in data + @savedColumns[column.column_name] = column diff --git a/app/assets/javascripts/templates/admin/columns_dropdown.html.haml b/app/assets/javascripts/templates/admin/columns_dropdown.html.haml index bf52142e30..401c3df9cc 100644 --- a/app/assets/javascripts/templates/admin/columns_dropdown.html.haml +++ b/app/assets/javascripts/templates/admin/columns_dropdown.html.haml @@ -7,4 +7,4 @@ %span.name {{column.name }} %hr %div.menu_item.text-center - %input.fullwidth.red{ type: "button", value: 'Save As Default', ng: { click: "saveColumnPreferences(action)"} } + %input.fullwidth.red{ type: "button", ng: { value: "saved() ? 'Saved': 'Save As Default'", disabled: "saved()", click: "saveColumnPreferences(action)"} } From b9f6cb8800df251670de2286d902bde9632ed13d Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 15 May 2016 22:59:07 +1000 Subject: [PATCH 049/110] Adding translations for all column names --- .../line_items_controller.js.coffee | 4 +- app/helpers/admin/injection_helper.rb | 5 +- .../_enterprise_user_index.html.haml | 11 +- app/views/admin/enterprises/index.html.haml | 2 +- .../order_cycles/_advanced_settings.html.haml | 2 +- .../variant_overrides/_controls.html.haml | 6 +- .../variant_overrides/_filters.html.haml | 2 +- .../admin/variant_overrides/_header.html.haml | 6 +- .../_hidden_products.html.haml | 4 +- .../variant_overrides/_new_products.html.haml | 10 +- .../_new_products_alert.html.haml | 4 +- .../variant_overrides/_no_results.html.haml | 12 +- .../variant_overrides/_products.html.haml | 14 +- .../_products_variants.html.haml | 2 +- .../admin/orders/bulk_management.html.haml | 60 +++---- .../bulk_edit/_products_head.html.haml | 24 +-- config/locales/en.yml | 151 ++++++++++-------- .../column_preference_defaults.rb | 87 +++++----- .../admin/bulk_product_update_spec.rb | 2 +- spec/features/admin/variant_overrides_spec.rb | 4 +- 20 files changed, 215 insertions(+), 197 deletions(-) diff --git a/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee b/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee index f969d82b05..f105ebb0e5 100644 --- a/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee +++ b/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee @@ -5,14 +5,14 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout, $scope.confirmDelete = true $scope.startDate = formatDate daysFromToday -7 $scope.endDate = formatDate daysFromToday 1 - $scope.bulkActions = [ { name: t("bom_actions_delete"), callback: 'deleteLineItems' } ] + $scope.bulkActions = [ { name: t("admin.orders.bulk_management.actions_delete"), callback: 'deleteLineItems' } ] $scope.selectedUnitsProduct = {} $scope.selectedUnitsVariant = {} $scope.sharedResource = false $scope.columns = Columns.columns $scope.confirmRefresh = -> - LineItems.allSaved() || confirm(t "unsaved_changes_warning") + LineItems.allSaved() || confirm(t("unsaved_changes_warning")) $scope.resetSelectFilters = -> $scope.distributorFilter = blankOption().id diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index ffd2c56c02..079f93d41f 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -47,8 +47,9 @@ module Admin admin_inject_json_ams_array opts[:module], "inventoryItems", @inventory_items, Api::Admin::InventoryItemSerializer end - def admin_inject_column_preferences(opts={module: 'ofn.admin'}) - column_preferences = ColumnPreference.for(spree_current_user, "#{controller_name}_#{action_name}") + def admin_inject_column_preferences(opts={}) + opts.reverse_merge!(module: 'ofn.admin', action: "#{controller_name}_#{action_name}") + column_preferences = ColumnPreference.for(spree_current_user, opts[:action]) admin_inject_json_ams_array opts[:module], "columns", column_preferences, Api::Admin::ColumnPreferenceSerializer end diff --git a/app/views/admin/enterprises/_enterprise_user_index.html.haml b/app/views/admin/enterprises/_enterprise_user_index.html.haml index 1f17f0f82c..75dbf98f17 100644 --- a/app/views/admin/enterprises/_enterprise_user_index.html.haml +++ b/app/views/admin/enterprises/_enterprise_user_index.html.haml @@ -24,12 +24,11 @@ %col.manage{ width: "18%", ng: { show: 'columns.manage.visible' }} %thead %tr{ ng: { controller: "ColumnsCtrl" } } - %th.name{ ng: { show: 'columns.name.visible' } } - Name - %th.producer{ ng: { show: 'columns.producer.visible' } } Producer? - %th.package{ ng: { show: 'columns.package.visible' } } Package - %th.status{ ng: { show: 'columns.status.visible' } } Status - %th.manage{ ng: { show: 'columns.manage.visible' } } Manage + %th.name{ ng: { show: 'columns.name.visible' } }=t('admin.name') + %th.producer{ ng: { show: 'columns.producer.visible' } }=t('.producer?') + %th.package{ ng: { show: 'columns.package.visible' } }=t('.package') + %th.status{ ng: { show: 'columns.status.visible' } }=t('.status') + %th.manage{ ng: { show: 'columns.manage.visible' } }=t('.manage') %tbody{ :id => "e_{{enterprise.id}}", ng: { repeat: "enterprise in filteredEnterprises = ( allEnterprises | filter:{ name: quickSearch } )", controller: 'EnterpriseIndexRowCtrl' } } %tr.enterprise.panel-toggle-row{ object: "enterprise", ng: { class: { even: "'even'", odd: "'odd'"} } } %td.name{ ng: { show: 'columns.name.visible' } } diff --git a/app/views/admin/enterprises/index.html.haml b/app/views/admin/enterprises/index.html.haml index 14bcda7f60..b84659738c 100644 --- a/app/views/admin/enterprises/index.html.haml +++ b/app/views/admin/enterprises/index.html.haml @@ -9,7 +9,7 @@ = button_link_to "New Enterprise", main_app.new_admin_enterprise_path, :icon => 'icon-plus', :id => 'admin_new_enterprise_link' = admin_inject_monthly_bill_description -= admin_inject_column_preferences module: 'admin.enterprises' += admin_inject_column_preferences module: 'admin.enterprises', action: "enterprises_index" = render 'admin/shared/enterprises_sub_menu' diff --git a/app/views/admin/order_cycles/_advanced_settings.html.haml b/app/views/admin/order_cycles/_advanced_settings.html.haml index 27b347880b..f8f71d0b93 100644 --- a/app/views/admin/order_cycles/_advanced_settings.html.haml +++ b/app/views/admin/order_cycles/_advanced_settings.html.haml @@ -5,7 +5,7 @@ = form_for [main_app, :admin, @order_cycle] do |f| .row .six.columns.alpha - = f.label "enterprise_preferred_product_selection_from_coordinator_inventory_only", t('admin.order_cycle.choose_products_from') + = f.label "enterprise_preferred_product_selection_from_coordinator_inventory_only", t('admin.order_cycles.edit.choose_products_from') .with-tip{'data-powertip' => "You can opt to restrict all available products (both incoming and outgoing), to only those in #{@order_cycle.coordinator.name}'s inventory."} %a What's this? .four.columns diff --git a/app/views/admin/variant_overrides/_controls.html.haml b/app/views/admin/variant_overrides/_controls.html.haml index 6757a8c43c..900f46e430 100644 --- a/app/views/admin/variant_overrides/_controls.html.haml +++ b/app/views/admin/variant_overrides/_controls.html.haml @@ -3,9 +3,9 @@ .eight.columns.alpha = render 'admin/shared/bulk_actions_dropdown' = render 'admin/shared/views_dropdown' - %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.inventory.visible' } , data: { powertip: "#{t('admin.inventory.inventory_powertip')}" } } - %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.hidden.visible' } , data: { powertip: "#{t('admin.inventory.hidden_powertip')}" } } - %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.new.visible' } , data: { powertip: "#{t('admin.inventory.new_powertip')}" } } + %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.inventory.visible' } , data: { powertip: "#{t('admin.variant_overrides.index.inventory_powertip')}" } } + %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.hidden.visible' } , data: { powertip: "#{t('admin.variant_overrides.index.hidden_powertip')}" } } + %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.new.visible' } , data: { powertip: "#{t('admin.variant_overrides.index.new_powertip')}" } } .four.columns   .four.columns.omega{ ng: { show: 'views.new.visible' } } %button.fullwidth{ type: 'button', ng: { click: "selectView('inventory')" } } diff --git a/app/views/admin/variant_overrides/_filters.html.haml b/app/views/admin/variant_overrides/_filters.html.haml index c9f3a27b32..a1772ed222 100644 --- a/app/views/admin/variant_overrides/_filters.html.haml +++ b/app/views/admin/variant_overrides/_filters.html.haml @@ -5,7 +5,7 @@ %input.fullwidth{ :type => "text", :id => 'query', ng: { model: 'query', disabled: '!hub_id'} } .two.columns   .filter_select.four.columns - %label{ :for => 'hub_id', ng: { bind: "hub_id ? '#{t('admin.shop')}' : '#{t('admin.inventory.select_a_shop')}'" } } + %label{ :for => 'hub_id', ng: { bind: "hub_id ? '#{t('admin.shop')}' : '#{t('admin.variant_overrides.index.select_a_shop')}'" } } %br %select.select2.fullwidth#hub_id{ 'ng-model' => 'hub_id', name: 'hub_id', ng: { options: 'hub.id as hub.name for (id, hub) in hubs' } } .filter_select.four.columns diff --git a/app/views/admin/variant_overrides/_header.html.haml b/app/views/admin/variant_overrides/_header.html.haml index e4a8e98420..64a6bfb201 100644 --- a/app/views/admin/variant_overrides/_header.html.haml +++ b/app/views/admin/variant_overrides/_header.html.haml @@ -1,8 +1,8 @@ - content_for :html_title do - = t("admin.inventory.title") + = t("admin.variant_overrides.index.title") - content_for :page_title do - %h1.page-title= t("admin.inventory.title") - %a.with-tip{ 'data-powertip' => "#{t("admin.inventory.description")}" }=t('admin.whats_this') + %h1.page-title= t("admin.variant_overrides.index.title") + %a.with-tip{ 'data-powertip' => "#{t("admin.variant_overrides.index.description")}" }=t('admin.whats_this') = render :partial => 'spree/admin/shared/product_sub_menu' diff --git a/app/views/admin/variant_overrides/_hidden_products.html.haml b/app/views/admin/variant_overrides/_hidden_products.html.haml index 9f0d4f2d94..ed3e2555c8 100644 --- a/app/views/admin/variant_overrides/_hidden_products.html.haml +++ b/app/views/admin/variant_overrides/_hidden_products.html.haml @@ -9,7 +9,7 @@ %th.producer=t('admin.producer') %th.product=t('admin.product') %th.variant=t('(admin.variant') - %th.add=t('admin.inventory.add') + %th.add=t('admin.variant_overrides.index.add') %tbody{ ng: { repeat: 'product in filteredProducts | limitTo:productLimit' } } %tr{ id: "v_{{variant.id}}", ng: { repeat: 'variant in product.variants | inventoryVariants:hub_id:views' } } %td.producer{ ng: { bind: '::producersByID[product.producer_id].name'} } @@ -19,4 +19,4 @@ .variant-override-unit{ ng: { bind: '::variant.unit_to_display'} } %td.add %button.fullwidth.icon-plus{ ng: { click: "setVisibility(hub_id,variant.id,true)" } } - = t('admin.inventory.add') + = t('admin.variant_overrides.index.add') diff --git a/app/views/admin/variant_overrides/_new_products.html.haml b/app/views/admin/variant_overrides/_new_products.html.haml index 414fba224d..99827717f5 100644 --- a/app/views/admin/variant_overrides/_new_products.html.haml +++ b/app/views/admin/variant_overrides/_new_products.html.haml @@ -8,9 +8,9 @@ %tr %th.producer=t('admin.producer') %th.product=t('admin.product') - %th.variant=t('(admin.variant') - %th.add=t('admin.inventory.add') - %th.hide=t('admin.inventory.hide') + %th.variant=t('admin.variant') + %th.add=t('admin.variant_overrides.index.add') + %th.hide=t('admin.variant_overrides.index.hide') %tbody{ ng: { repeat: 'product in filteredProducts | limitTo:productLimit' } } %tr{ id: "v_{{variant.id}}", ng: { repeat: 'variant in product.variants | inventoryVariants:hub_id:views' } } %td.producer{ ng: { bind: '::producersByID[product.producer_id].name'} } @@ -20,7 +20,7 @@ .variant-override-unit{ ng: { bind: '::variant.unit_to_display'} } %td.add %button.fullwidth.icon-plus{ ng: { click: "setVisibility(hub_id,variant.id,true)" } } - = t('admin.inventory.add') + = t('admin.variant_overrides.index.add') %td.hide %button.fullwidth.hide.icon-remove{ ng: { click: "setVisibility(hub_id,variant.id,false)" } } - = t('admin.inventory.hide') + = t('admin.variant_overrides.index.hide') diff --git a/app/views/admin/variant_overrides/_new_products_alert.html.haml b/app/views/admin/variant_overrides/_new_products_alert.html.haml index 29ec4c9623..385616d817 100644 --- a/app/views/admin/variant_overrides/_new_products_alert.html.haml +++ b/app/views/admin/variant_overrides/_new_products_alert.html.haml @@ -1,5 +1,5 @@ %div{ ng: { show: '(newProductCount = (products | hubPermissions:hubPermissions:hub_id | newInventoryProducts:hub_id).length) > 0 && !views.new.visible && !alertDismissed' } } %hr.divider.sixteen.columns.alpha.omega - %alert-row{ message: "#{t('admin.inventory.new_products_alert_message', new_product_count: '{{ newProductCount }}')}", + %alert-row{ message: "#{t('admin.variant_overrides.index.new_products_alert_message', new_product_count: '{{ newProductCount }}')}", dismissed: "alertDismissed", - button: { text: "#{t('admin.inventory.review_now')}", action: "selectView('new')" } } + button: { text: "#{t('admin.variant_overrides.index.review_now')}", action: "selectView('new')" } } diff --git a/app/views/admin/variant_overrides/_no_results.html.haml b/app/views/admin/variant_overrides/_no_results.html.haml index cdec6ab8c5..7ec4e68f0e 100644 --- a/app/views/admin/variant_overrides/_no_results.html.haml +++ b/app/views/admin/variant_overrides/_no_results.html.haml @@ -1,7 +1,7 @@ %div.text-big.no-results{ ng: { show: 'hub_id && products.length > 0 && filteredProducts.length == 0' } } - %span{ ng: { show: 'views.inventory.visible && !filtersApplied()' } }=t('admin.inventory.currently_empty') - %span{ ng: { show: 'views.inventory.visible && filtersApplied()' } }=t('admin.inventory.no_matching_products') - %span{ ng: { show: 'views.hidden.visible && !filtersApplied()' } }=t('admin.inventory.no_hidden_products') - %span{ ng: { show: 'views.hidden.visible && filtersApplied()' } }=t('admin.inventory.no_matching_hidden_products') - %span{ ng: { show: 'views.new.visible && !filtersApplied()' } }=t('admin.inventory.no_new_products') - %span{ ng: { show: 'views.new.visible && filtersApplied()' } }=t('admin.inventory.no_matching_new_products') + %span{ ng: { show: 'views.inventory.visible && !filtersApplied()' } }=t('admin.variant_overrides.index.currently_empty') + %span{ ng: { show: 'views.inventory.visible && filtersApplied()' } }=t('admin.variant_overrides.index.no_matching_products') + %span{ ng: { show: 'views.hidden.visible && !filtersApplied()' } }=t('admin.variant_overrides.index.no_hidden_products') + %span{ ng: { show: 'views.hidden.visible && filtersApplied()' } }=t('admin.variant_overrides.index.no_matching_hidden_products') + %span{ ng: { show: 'views.new.visible && !filtersApplied()' } }=t('admin.variant_overrides.index.no_new_products') + %span{ ng: { show: 'views.new.visible && filtersApplied()' } }=t('admin.variant_overrides.index.no_matching_new_products') diff --git a/app/views/admin/variant_overrides/_products.html.haml b/app/views/admin/variant_overrides/_products.html.haml index b54cfeba9a..4330496b3e 100644 --- a/app/views/admin/variant_overrides/_products.html.haml +++ b/app/views/admin/variant_overrides/_products.html.haml @@ -16,13 +16,13 @@ %tr{ ng: { controller: "ColumnsCtrl" } } %th.producer{ ng: { show: 'columns.producer.visible' } }=t('admin.producer') %th.product{ ng: { show: 'columns.product.visible' } }=t('admin.product') - %th.sku{ ng: { show: 'columns.sku.visible' } }=t('admin.inventory.sku') - %th.price{ ng: { show: 'columns.price.visible' } }=t('admin.inventory.price') - %th.on_hand{ ng: { show: 'columns.on_hand.visible' } }=t('admin.inventory.on_hand') - %th.on_demand{ ng: { show: 'columns.on_demand.visible' } }=t('admin.inventory.on_demand') - %th.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } }=t('admin.inventory.enable_reset') - %th.inheritance{ ng: { show: 'columns.inheritance.visible' } }=t('admin.inventory.inherit') - %th.visibility{ ng: { show: 'columns.visibility.visible' } }=t('admin.inventory.hide') + %th.sku{ ng: { show: 'columns.sku.visible' } }=t('admin.sku') + %th.price{ ng: { show: 'columns.price.visible' } }=t('admin.price') + %th.on_hand{ ng: { show: 'columns.on_hand.visible' } }=t('admin.on_hand') + %th.on_demand{ ng: { show: 'columns.on_demand.visible' } }=t('admin.on_demand?') + %th.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } }=t('admin.variant_overrides.index.enable_reset?') + %th.inheritance{ ng: { show: 'columns.inheritance.visible' } }=t('admin.variant_overrides.index.inherit?') + %th.visibility{ ng: { show: 'columns.visibility.visible' } }=t('admin.variant_overrides.index.hide') %tbody{ ng: {repeat: 'product in filteredProducts = (products | hubPermissions:hubPermissions:hub_id | inventoryProducts:hub_id:views | attrFilter:{producer_id:producerFilter} | filter:query) | limitTo:productLimit' } } = render 'admin/variant_overrides/products_product' = render 'admin/variant_overrides/products_variants' diff --git a/app/views/admin/variant_overrides/_products_variants.html.haml b/app/views/admin/variant_overrides/_products_variants.html.haml index a7a94437be..c139bc7ffb 100644 --- a/app/views/admin/variant_overrides/_products_variants.html.haml +++ b/app/views/admin/variant_overrides/_products_variants.html.haml @@ -19,4 +19,4 @@ %input.field{ :type => 'checkbox', name: 'variant-overrides-{{ variant.id }}-inherit', ng: { model: 'inherit' }, 'track-inheritance' => true } %td.visibility{ ng: { show: 'columns.visibility.visible' } } %button.icon-remove.hide.fullwidth{ :type => 'button', ng: { click: "setVisibility(hub_id,variant.id,false)" } } - = t('admin.inventory.hide') + = t('admin.variant_overrides.index.hide') diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index ce82341e03..520d2376a4 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -3,9 +3,9 @@ - content_for :page_title do %h1.page-title - = t "bom_page_title" + = t("admin.orders.bulk_management.page_title") %a{ 'ofn-with-tip' => t("bom_tip") } - = t "admin.whats_this" + = t("admin.whats_this") = render :partial => 'spree/admin/shared/order_sub_menu' @@ -18,28 +18,28 @@ .filters{ :class => "sixteen columns alpha" } .date_filter{ :class => "two columns alpha" } %label{ :for => 'start_date_filter' } - = t "start_date" + = t("admin.start_date") %br %input{ :class => "two columns alpha", :type => "text", :id => 'start_date_filter', 'ng-model' => 'startDate', 'datepicker' => "startDate", 'confirm-change' => "confirmRefresh()", 'ng-change' => 'refreshData()' } .date_filter{ :class => "two columns" } %label{ :for => 'end_date_filter' } - = t "end_date" + = t("admin.end_date") %br %input{ :class => "two columns alpha", :type => "text", :id => 'end_date_filter', 'ng-model' => 'endDate', 'datepicker' => "endDate", 'confirm-change' => "confirmRefresh()", 'ng-change' => 'refreshData()' } .one.column   .filter_select{ :class => "three columns" } %label{ :for => 'supplier_filter' } - = t "producer" + = t("admin.producer") %br %select{ :class => "three columns alpha", :id => 'supplier_filter', 'select2-min-search' => 5, 'ng-model' => 'supplierFilter', 'ng-options' => 's.id as s.name for s in suppliers' } .filter_select{ :class => "three columns" } %label{ :for => 'distributor_filter' } - = t "bom_hub" + = t("admin.shop") %br %select{ :class => "three columns alpha", :id => 'distributor_filter', 'select2-min-search' => 5, 'ng-model' => 'distributorFilter', 'ng-options' => 'd.id as d.name for d in distributors'} .filter_select{ :class => "three columns" } %label{ :for => 'order_cycle_filter' } - = t "order_cycle" + = t("admin.order_cycle") %br %select{ :class => "three columns alpha", :id => 'order_cycle_filter', 'select2-min-search' => 5, 'ng-model' => 'orderCycleFilter', 'ng-options' => 'oc.id as oc.name for oc in orderCycles', 'confirm-change' => "confirmRefresh()", 'ng-change' => 'refreshData()'} .filter_clear{ :class => "two columns omega" } @@ -53,7 +53,7 @@ %div.shared_resource{ :class => "four columns alpha" } %span{ :class => 'three columns alpha' } %input{ type: 'checkbox', :id => 'shared_resource', 'ng-model' => 'sharedResource'} - = t "bom_shared" + = t("admin.orders.bulk_management.shared") %div{ :class => "eight columns" } %h6{ :class => "eight columns alpha", 'ng-show' => 'sharedResource', style: 'text-align: center;' } {{ selectedUnitsProduct.name + ": ALL" }} %h6{ :class => "eight columns alpha", 'ng-hide' => 'sharedResource', style: 'text-align: center;' } {{ selectedUnitsVariant.full_name }} @@ -65,32 +65,32 @@ .one.column.alpha   .two.columns %span.two.columns - = t "group_buy_unit_size" + = t("admin.orders.bulk_management.group_buy_unit_size") %span.two.columns {{ formattedValueWithUnitName( selectedUnitsProduct.group_buy_unit_size, selectedUnitsProduct, selectedUnitsVariant ) }} .one.column   .two.columns %span.two.columns - = t "total_qtt_ordered" + = t("admin.orders.bulk_management.total_qtt_ordered") %span.two.columns {{ formattedValueWithUnitName( sumUnitValues(), selectedUnitsProduct, selectedUnitsVariant ) }} .one.column   .two.columns %span.two.columns - = t "max_qtt_ordered" + = t("admin.orders.bulk_management.max_qtt_ordered") %span.two.columns {{ formattedValueWithUnitName( sumMaxUnitValues(), selectedUnitsProduct, selectedUnitsVariant ) }} .one.column   .two.columns %span.two.columns - = t "current_fulfilled_units" + = t("admin.orders.bulk_management.current_fulfilled_units") %span.two.columns {{ fulfilled(sumUnitValues()) }} .one.column   .two.columns %span.two.columns - = t "max_fulfilled_units" + = t("admin.orders.bulk_management.max_fulfilled_units") %span.two.columns {{ fulfilled(sumMaxUnitValues()) }} .one.column.omega   %div{ :class => "eight columns alpha", 'ng-hide' => 'allFinalWeightVolumesPresent()' } %span{ :class => "eight columns alpha", style: 'color:red' } - = t "bulk_management_warning" + = t("admin.orders.bulk_management.variants_without_unit_value") %hr.divider.sixteen.columns.alpha.omega @@ -104,11 +104,11 @@ %div.sixteen.columns.alpha#loading{ 'ng-if' => 'RequestMonitor.loading' } %img.spinner{ src: "/assets/spinning-circles.svg" } %h1 - =t "bom_loading" + = t("admin.orders.bulk_management.loading") %div{ :class => "sixteen columns alpha", 'ng-show' => '!RequestMonitor.loading && filteredLineItems.length == 0'} %h1#no_results - = t "bom_no_results" + = t("admin.orders.bulk_management.no_results") .margin-bottom-50{ 'ng-hide' => 'RequestMonitor.loading || filteredLineItems.length == 0' } %form{ name: 'bulk_order_form' } @@ -119,42 +119,42 @@ %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" } %th.order_no{ 'ng-show' => 'columns.order_no.visible' } %a{ :href => '', 'ng-click' => "predicate = 'order.number'; reverse = !reverse" } - = t "order_no" + = t("admin.orders.bulk_management.order_no") %th.full_name{ 'ng-show' => 'columns.full_name.visible' } %a{ :href => '', 'ng-click' => "predicate = 'order.full_name'; reverse = !reverse" } - = t "name" + = t("admin.name") %th.email{ 'ng-show' => 'columns.email.visible' } %a{ :href => '', 'ng-click' => "predicate = 'order.email'; reverse = !reverse" } - = t "email" + = t("admin.email") %th.phone{ 'ng-show' => 'columns.phone.visible' } %a{ :href => '', 'ng-click' => "predicate = 'order.phone'; reverse = !reverse" } - = t "phone" + = t("admin.phone") %th.date{ 'ng-show' => 'columns.order_date.visible' } %a{ :href => '', 'ng-click' => "predicate = 'order.completed_at'; reverse = !reverse" } - =t "bom_date" + = t("admin.orders.bulk_management.order_date") %th.producer{ 'ng-show' => 'columns.producer.visible' } %a{ :href => '', 'ng-click' => "predicate = 'supplier.name'; reverse = !reverse" } - = t "producer" + = t("admin.producer") %th.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } %a{ :href => '', 'ng-click' => "predicate = 'order.order_cycle.name'; reverse = !reverse" } - = t "bom_cycle" + = t("admin.order_cycle") %th.hub{ 'ng-show' => 'columns.hub.visible' } %a{ :href => '', 'ng-click' => "predicate = 'order.distributor.name'; reverse = !reverse" } - = t "bom_hub" + = t("admin.shop") %th.variant{ 'ng-show' => 'columns.variant.visible' } %a{ :href => '', 'ng-click' => "predicate = 'units_variant.full_name'; reverse = !reverse" } - = t "bom_variant" + = t("admin.orders.bulk_management.product_unit") %th.quantity{ 'ng-show' => 'columns.quantity.visible' } - = t "products_quantity" + = t("admin.quantity") %th.max{ 'ng-show' => 'columns.max.visible' } - = t "shop_variant_quantity_max" + = t("admin.orders.bulk_management.max") %th.final_weight_volume{ 'ng-show' => 'columns.final_weight_volume.visible' } - = t "weight_volume" + = t("admin.orders.bulk_management.weight_volume") %th.price{ 'ng-show' => 'columns.price.visible' } - = t "products_price" + = t("admin.price") %th.actions %th.actions - = t "ask" + = t("admin.orders.bulk_management.ask") %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } %tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" } diff --git a/app/views/spree/admin/products/bulk_edit/_products_head.html.haml b/app/views/spree/admin/products/bulk_edit/_products_head.html.haml index 7767b8de89..50789a2f82 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_head.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_head.html.haml @@ -21,18 +21,18 @@ %th.left-actions %a{ 'ng-click' => 'toggleShowAllVariants()', :style => 'color: red' } Expand All - %th.producer{ 'ng-show' => 'columns.producer.visible' } Producer - %th.sku{ 'ng-show' => 'columns.sku.visible' } SKU - %th.name{ 'ng-show' => 'columns.name.visible' } Name - %th.unit{ 'ng-show' => 'columns.unit.visible' } Unit / Value - %th.display_as{ 'ng-show' => 'columns.unit.visible' } Display As - %th.price{ 'ng-show' => 'columns.price.visible' } Price - %th.on_hand{ 'ng-show' => 'columns.on_hand.visible' } On Hand - %th.on_demand{ 'ng-show' => 'columns.on_demand.visible' } On Demand - %th.category{ 'ng-show' => 'columns.category.visible' } Category - %th.tax_category{ 'ng-show' => 'columns.tax_category.visible' } Tax Category - %th.inherits_properties{ 'ng-show' => 'columns.inherits_properties.visible' } Inherits Properties? - %th.available_on{ 'ng-show' => 'columns.available_on.visible' } Av. On + %th.producer{ 'ng-show' => 'columns.producer.visible' }=t('admin.producer') + %th.sku{ 'ng-show' => 'columns.sku.visible' }=t('admin.sku') + %th.name{ 'ng-show' => 'columns.name.visible' }=t('admin.name') + %th.unit{ 'ng-show' => 'columns.unit.visible' }=t('admin.products.bulk_edit.unit') + %th.display_as{ 'ng-show' => 'columns.unit.visible' }=t('admin.products.bulk_edit.display_as') + %th.price{ 'ng-show' => 'columns.price.visible' }=t('admin.price') + %th.on_hand{ 'ng-show' => 'columns.on_hand.visible' }=t('admin.on_hand') + %th.on_demand{ 'ng-show' => 'columns.on_demand.visible' }=t('admin.on_demand?') + %th.category{ 'ng-show' => 'columns.category.visible' }=t('admin.products.bulk_edit.category') + %th.tax_category{ 'ng-show' => 'columns.tax_category.visible' }=t('admin.products.bulk_edit.tax_category') + %th.inherits_properties{ 'ng-show' => 'columns.inherits_properties.visible' }=t('admin.products.bulk_edit.inherits_proerties?') + %th.available_on{ 'ng-show' => 'columns.available_on.visible' }=t('admin.products.bulk_edit.av_on') %th.actions %th.actions %th.actions diff --git a/config/locales/en.yml b/config/locales/en.yml index 02d66a5354..3249f00d5a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -71,13 +71,29 @@ en: admin: + # Common properties / models + date: Date + email: Email + name: Name + on_hand: On Hand + on_demand: On Demand + on_demand?: On Demand? + order_cycle: Order Cycle + phone: Phone + price: Price + producer: Producer + product: Product + quantity: Quantity + shop: Shop + sku: SKU + tags: Tags + variant: Variant + # General form elements quick_search: Quick Search clear_all: Clear All - producer: Producer - shop: Shop - product: Product - variant: Variant + start_date: "Start Date" + end_date: "End Date" columns: Columns actions: Actions @@ -96,38 +112,74 @@ en: customer_placeholder: "customer@example.org" valid_email_error: Please enter a valid email address add_a_new_customer_for: Add a new customer for %{shop_name} - inventory: - title: Inventory - description: Use this page to manage inventories for your enterprises. Any product details set here will override those set on the 'Products' page - sku: SKU - price: Price - on_hand: On Hand - on_demand: On Demand? - enable_reset: Enable Stock Level Reset? - inherit: Inherit? - add: Add - hide: Hide - select_a_shop: Select A Shop - review_now: Review Now - new_products_alert_message: There are %{new_product_count} new products available to add to your inventory. - currently_empty: Your inventory is currently empty - no_matching_products: No matching products found in your inventory - no_hidden_products: No products have been hidden from this inventory - no_matching_hidden_products: No hidden products match your search criteria - no_new_products: No new products are available to add to this inventory - no_matching_new_products: No new products match your search criteria - inventory_powertip: This is your inventory of products. To add products to your inventory, select 'New Products' from the Viewing dropdown. - hidden_powertip: These products have been hidden from your inventory and will not be available to add to your shop. You can click 'Add' to add a product to you inventory. - new_powertip: These products are available to be added to your inventory. Click 'Add' to add a product to your inventory, or 'Hide' to hide it from view. You can always change your mind later! + code: Code + products: + bulk_edit: + unit: Unit + display_as: Display As + category: Category + tax_category: Tax Category + inherits_properties?: Inherits Properties? + available_on: Available On + av_on: "Av. On" - order_cycle: - choose_products_from: "Choose Products From:" + variant_overrides: + index: + title: Inventory + description: Use this page to manage inventories for your enterprises. Any product details set here will override those set on the 'Products' page + enable_reset?: Enable Stock Reset? + inherit?: Inherit? + add: Add + hide: Hide + select_a_shop: Select A Shop + review_now: Review Now + new_products_alert_message: There are %{new_product_count} new products available to add to your inventory. + currently_empty: Your inventory is currently empty + no_matching_products: No matching products found in your inventory + no_hidden_products: No products have been hidden from this inventory + no_matching_hidden_products: No hidden products match your search criteria + no_new_products: No new products are available to add to this inventory + no_matching_new_products: No new products match your search criteria + inventory_powertip: This is your inventory of products. To add products to your inventory, select 'New Products' from the Viewing dropdown. + hidden_powertip: These products have been hidden from your inventory and will not be available to add to your shop. You can click 'Add' to add a product to you inventory. + new_powertip: These products are available to be added to your inventory. Click 'Add' to add a product to your inventory, or 'Hide' to hide it from view. You can always change your mind later! + + orders: + bulk_management: + tip: "Use this page to alter product quantities across multiple orders. Products may also be removed from orders entirely, if required." + shared: "Shared Resource?" + order_no: "Order No." + order_date: "Order Date" + max: "Max" + product_unit: "Product: Unit" + weight_volume: "Weight/Volume" + ask: "Ask?" + page_title: "Bulk Order Management" + actions_delete: "Delete Selected" + loading: "Loading orders" + no_results: "No orders found." + group_buy_unit_size: "Group Buy Unit Size" + total_qtt_ordered: "Total Quantity Ordered" + max_qtt_ordered: "Max Quantity Ordered" + current_fulfilled_units: "Current Fulfilled Units" + max_fulfilled_units: "Max Fulfilled Units" + order_error: "Some errors must be resolved before you can update orders.\nAny fields with red borders contain errors." + variants_without_unit_value: "WARNING: Some variants do not have a unit value" + + order_cycles: + edit: + choose_products_from: "Choose Products From:" enterprise: select_outgoing_oc_products_from: Select outgoing OC products from enterprises: + index: + producer?: Producer? + package: Package + status: Status + manage: Manage form: primary_details: shopfront_requires_login: "Shopfront requires login?" @@ -332,22 +384,6 @@ en: order_payment_paypal_successful: Your payment via PayPal has been processed successfully. order_hub_info: Hub Info - bom_tip: "Use this page to alter product quantities across multiple orders. Products may also be removed from orders entirely, if required." - bom_shared: "Shared Resource?" - bom_page_title: "Bulk Order Management" - bom_no: "Order no." - bom_date: "Order date" - bom_cycle: "Order cycle" - bom_max: "Max" - bom_hub: "Hub" - bom_variant: "Product: Unit" - bom_final_weigth_volume: "Weight/Volume" - bom_quantity: "Quantity" - bom_actions_delete: "Delete Selected" - bom_loading: "Loading orders" - bom_no_results: "No orders found." - bom_order_error: "Some errors must be resolved before you can update orders.\nAny fields with red borders contain errors." - unsaved_changes_warning: "Unsaved changes exist and will be lost if you continue." unsaved_changes_error: "Fields with red borders contain errors." @@ -574,20 +610,9 @@ See the %{link} to find out more about %{sitename}'s features and to start using products_description: Description products_variant: Variant products_quantity: Quantity - products_availabel: Available? + products_available: Available? products_producer: "Producer" products_price: "Price" - products_sku: "SKU" - products_name: "name" - products_unit: "unit" - products_on_hand: "on hand" - products_on_demand: "On demand?" - products_category: "Category" - products_tax_category: "tax category" - products_available_on: "Available On" - products_inherit: "Inherit?" - products_inherits_properties: "Inherits Properties?" - products_stock_level_reset: "Enable Stock Level Reset?" register_title: Register @@ -945,19 +970,7 @@ Please follow the instructions there to make your enterprise visible on the Open manage_products: "Manage products" edit_profile_details: "Edit profile details" edit_profile_details_etc: "Change your profile description, images, etc." - start_date: "Start Date" - end_date: "End Date" order_cycle: "Order Cycle" - group_buy_unit_size: "Group Buy Unit Size" - total_qtt_ordered: "Total Quantity Ordered" - max_qtt_ordered: "Max Quantity Ordered" - current_fulfilled_units: "Current Fulfilled Units" - max_fulfilled_units: "Max Fulfilled Units" - bulk_management_warning: "WARNING: Some variants do not have a unit value" - ask: "Ask?" - no_orders_found: "No orders found." - order_no: "Order No." - weight_volume: "Weight/Volume" remove_tax: "Remove tax" tax_settings: "Tax Settings" products_require_tax_category: "products require tax category" diff --git a/lib/open_food_network/column_preference_defaults.rb b/lib/open_food_network/column_preference_defaults.rb index 126a8a567a..c18ac79550 100644 --- a/lib/open_food_network/column_preference_defaults.rb +++ b/lib/open_food_network/column_preference_defaults.rb @@ -9,68 +9,73 @@ module OpenFoodNetwork # for a given user, action_name and column_name def variant_overrides_index_columns + node = 'admin.variant_overrides.index' { - producer: { name: "Producer", visible: true }, - product: { name: "Product", visible: true }, - sku: { name: "SKU", visible: false }, - price: { name: "Price", visible: true }, - on_hand: { name: "On Hand", visible: true }, - on_demand: { name: "On Demand", visible: false }, - reset: { name: "Reset Stock Level", visible: false }, - inheritance: { name: "Inheritance", visible: false }, - visibility: { name: "Hide", visible: false } + producer: { name: I18n.t("admin.producer"), visible: true }, + product: { name: I18n.t("admin.product"), visible: true }, + sku: { name: I18n.t("admin.sku"), visible: false }, + price: { name: I18n.t("admin.price"), visible: true }, + on_hand: { name: I18n.t("admin.on_hand"), visible: true }, + on_demand: { name: I18n.t("admin.on_demand?"), visible: false }, + reset: { name: I18n.t("#{node}.enable_reset?"), visible: false }, + inheritance: { name: I18n.t("#{node}.inherit?"), visible: false }, + visibility: { name: I18n.t("#{node}.hide"), visible: false } } end def customers_index_columns + node = 'admin.customers.index' { - email: { name: "Email", visible: true }, - code: { name: "Code", visible: true }, - tags: { name: "Tags", visible: true } + email: { name: I18n.t("admin.email"), visible: true }, + code: { name: I18n.t("#{node}.code"), visible: true }, + tags: { name: I18n.t("admin.tags"), visible: true } } end def orders_bulk_management_columns + node = "admin.orders.bulk_management" { - order_no: { name: t("bom_no"), visible: false }, - full_name: { name: t("name"), visible: true }, - email: { name: t("email"), visible: false }, - phone: { name: t("phone"), visible: false }, - order_date: { name: t("bom_date"), visible: true }, - producer: { name: t("producer"), visible: true }, - order_cycle: { name: t("bom_cycle"), visible: false }, - hub: { name: t("bom_hub"), visible: false }, - variant: { name: t("bom_variant"), visible: true }, - quantity: { name: t("bom_quantity"), visible: true }, - max: { name: t("bom_max"), visible: true }, - final_weight_volume: { name: t("bom_final_weigth_volume"), visible: false }, - price: { name: t("price"), visible: false } + order_no: { name: I18n.t("#{node}.order_no"), visible: false }, + full_name: { name: I18n.t("admin.name"), visible: true }, + email: { name: I18n.t("admin.email"), visible: false }, + phone: { name: I18n.t("admin.phone"), visible: false }, + order_date: { name: I18n.t("#{node}.order_date"), visible: true }, + producer: { name: I18n.t("admin.producer"), visible: true }, + order_cycle: { name: I18n.t("admin.order_cycle"), visible: false }, + hub: { name: I18n.t("admin.shop"), visible: false }, + variant: { name: I18n.t("#{node}.product_unit"), visible: true }, + quantity: { name: I18n.t("admin.quantity"), visible: true }, + max: { name: I18n.t("#{node}.max"), visible: true }, + final_weight_volume: { name: I18n.t("#{node}.weight_volume"), visible: false }, + price: { name: I18n.t("admin.price"), visible: false } } end def products_bulk_edit_columns + node = "admin.products.bulk_edit" { - producer: { name: t("products_producer"), visible: true }, - sku: { name: t("products_sku"), visible: false }, - name: { name: t("products_name"), visible: true }, - unit: { name: t("products_unit"), visible: true }, - price: { name: t("products_price"), visible: true }, - on_hand: { name: t("products_on_hand"), visible: true }, - on_demand: { name: t("products_on_demand"), visible: false }, - category: { name: t("products_category"), visible: false }, - tax_category: { name: t("products_tax_category"), visible: false }, - inherits_properties: { name: t("products_inherits_properties"), visible: false }, - available_on: { name: t("products_available_on"), visible: false } + producer: { name: I18n.t("admin.producer"), visible: true }, + sku: { name: I18n.t("admin.sku"), visible: false }, + name: { name: I18n.t("admin.name"), visible: true }, + unit: { name: I18n.t("#{node}.unit"), visible: true }, + price: { name: I18n.t("admin.price"), visible: true }, + on_hand: { name: I18n.t("admin.on_hand"), visible: true }, + on_demand: { name: I18n.t("admin.on_demand"), visible: false }, + category: { name: I18n.t("#{node}.category"), visible: false }, + tax_category: { name: I18n.t("#{node}.tax_category"), visible: false }, + inherits_properties: { name: I18n.t("#{node}.inherits_properties?"), visible: false }, + available_on: { name: I18n.t("#{node}.available_on"), visible: false } } end def enterprises_index_columns + node = "admin.enterprises.index" { - name: { name: "Name", visible: true }, - producer: { name: "Producer", visible: true }, - package: { name: "Package", visible: true }, - status: { name: "Status", visible: true }, - manage: { name: "Manage", visible: true } + name: { name: I18n.t("admin.name"), visible: true }, + producer: { name: I18n.t("#{node}.producer?"), visible: true }, + package: { name: I18n.t("#{node}.package"), visible: true }, + status: { name: I18n.t("#{node}.status"), visible: true }, + manage: { name: I18n.t("#{node}.manage"), visible: true } } end end diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 260caf207e..f7f32f6523 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -250,7 +250,7 @@ feature %q{ find("div#columns-dropdown", :text => "COLUMNS").click find("div#columns-dropdown div.menu div.menu_item", text: "Available On").click - find("div#columns-dropdown div.menu div.menu_item", text: "Category").click + find("div#columns-dropdown div.menu div.menu_item", text: /^Category?/).click find("div#columns-dropdown div.menu div.menu_item", text: "Inherits Properties?").click find("div#columns-dropdown div.menu div.menu_item", text: "SKU").click find("div#columns-dropdown", :text => "COLUMNS").click diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index a3a08c2466..0af93d19c3 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -255,12 +255,12 @@ feature %q{ it "deletes overrides when values are cleared" do first("div#columns-dropdown", :text => "COLUMNS").click first("div#columns-dropdown div.menu div.menu_item", text: "On Demand").click - first("div#columns-dropdown div.menu div.menu_item", text: "Reset Stock Level").click + first("div#columns-dropdown div.menu div.menu_item", text: "Enable Stock Reset?").click first("div#columns-dropdown", :text => "COLUMNS").click # Clearing values by 'inheriting' first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "Inheritance").click + first("div#columns-dropdown div.menu div.menu_item", text: "Inherit?").click first("div#columns-dropdown", :text => "COLUMNS").click check "variant-overrides-#{variant3.id}-inherit" From 604418d699490109faca70533d342ce11560834b Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Mon, 16 May 2016 09:23:50 +1000 Subject: [PATCH 050/110] Install phantomjs with npm --- .travis.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index f914edbbb6..c32401ac61 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,7 @@ language: ruby sudo: false cache: - bundler - - directories: - - travis-phantomjs + - $(npm bin -g)/phantomjs bundler_args: --without development rvm: - "2.1.5" @@ -30,10 +29,9 @@ before_script: - cp config/application.yml.example config/application.yml - RAILS_ENV=test bundle exec rake db:create db:schema:load - - mkdir -p travis-phantomjs - - wget -nc https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 -O $PWD/travis-phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2 || true - - tar -xvf $PWD/travis-phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2 -C $PWD/travis-phantomjs - - export PATH=$PWD/travis-phantomjs/phantomjs-2.1.1-linux-x86_64/bin:$PATH + # Only install PhantomJS if it is not already present (ie. cached) + - npm list -g phantomjs-prebuilt@~2.1.7 --depth=0 || npm install -g phantomjs-prebuilt@~2.1.7 + - export PATH=`npm bin -g`:$PATH - > if [ "$KARMA" = "true" ]; then From 924d15501274db7d293fe2fd975e8cabbfdfe0b7 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 19 May 2016 12:43:28 +1000 Subject: [PATCH 051/110] Using correct syntax for attributes on columns --- app/views/admin/customers/index.html.haml | 6 +++--- .../admin/variant_overrides/_products_product.html.haml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index 45639b74cd..c570632c64 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -43,9 +43,9 @@ %input.red{ type: "button", value: "Save Changes", ng: { click: "submitAll(customers_form)", disabled: "!customers_form.$dirty" } } %table.index#customers - %col.email{ width: "20%"} - %col.code{ width: "20%"} - %col.tags{ width: "50%"} + %col.email{ width: "20%", 'ng-show' => 'columns.email.visible' } + %col.code{ width: "20%", 'ng-show' => 'columns.code.visible' } + %col.tags{ width: "50%", 'ng-show' => 'columns.tags.visible' } %col.actions{ width: "10%"} %thead %tr{ ng: { controller: "ColumnsCtrl" } } diff --git a/app/views/admin/variant_overrides/_products_product.html.haml b/app/views/admin/variant_overrides/_products_product.html.haml index b15840dd49..119ee867df 100644 --- a/app/views/admin/variant_overrides/_products_product.html.haml +++ b/app/views/admin/variant_overrides/_products_product.html.haml @@ -1,6 +1,6 @@ %tr.product.even - %td.producer{ ng: { show: 'columns.producer.visible' }, ng: { bind: '::producersByID[product.producer_id].name'} } - %td.product{ ng: { show: 'columns.product.visible' }, ng: { bind: '::product.name'} } + %td.producer{ ng: { show: 'columns.producer.visible', bind: '::producersByID[product.producer_id].name'} } + %td.product{ ng: { show: 'columns.product.visible', bind: '::product.name'} } %td.sku{ ng: { show: 'columns.sku.visible' } } %td.price{ ng: { show: 'columns.price.visible' } } %td.on_hand{ ng: { show: 'columns.on_hand.visible' } } From b804a704695d331584428dc2f5ec23ee442dd577 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 19 May 2016 12:44:56 +1000 Subject: [PATCH 052/110] Columns service updates state of columns (especially id) when data received from server Also showing saving status --- .../controllers/columns_dropdown_controller.js.coffee | 7 ++++++- .../admin/index_utils/services/columns.js.coffee | 3 ++- .../javascripts/templates/admin/columns_dropdown.html.haml | 3 ++- app/assets/stylesheets/admin/openfoodnetwork.css.scss | 5 +++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/admin/dropdown/controllers/columns_dropdown_controller.js.coffee b/app/assets/javascripts/admin/dropdown/controllers/columns_dropdown_controller.js.coffee index 4b3a5a4516..a19b6aaed3 100644 --- a/app/assets/javascripts/admin/dropdown/controllers/columns_dropdown_controller.js.coffee +++ b/app/assets/javascripts/admin/dropdown/controllers/columns_dropdown_controller.js.coffee @@ -1,5 +1,10 @@ angular.module("admin.dropdown").controller "ColumnsDropdownCtrl", ($scope, Columns) -> $scope.columns = Columns.columns $scope.toggle = Columns.toggleColumn - $scope.saveColumnPreferences = Columns.savePreferences $scope.saved = Columns.preferencesSaved + $scope.saving = false + + $scope.saveColumnPreferences = (action_name) -> + $scope.saving = true + Columns.savePreferences(action_name).then -> + $scope.saving = false diff --git a/app/assets/javascripts/admin/index_utils/services/columns.js.coffee b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee index f2ed5463ae..45bea7c680 100644 --- a/app/assets/javascripts/admin/index_utils/services/columns.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee @@ -31,4 +31,5 @@ angular.module("admin.indexUtils").factory 'Columns', ($rootScope, $http, column column_preferences: (preference for column_name, preference of @columns) .success (data) => for column in data - @savedColumns[column.column_name] = column + angular.extend(@columns[column.column_name], column) + angular.extend(@savedColumns[column.column_name], column) diff --git a/app/assets/javascripts/templates/admin/columns_dropdown.html.haml b/app/assets/javascripts/templates/admin/columns_dropdown.html.haml index 401c3df9cc..37398b200a 100644 --- a/app/assets/javascripts/templates/admin/columns_dropdown.html.haml +++ b/app/assets/javascripts/templates/admin/columns_dropdown.html.haml @@ -7,4 +7,5 @@ %span.name {{column.name }} %hr %div.menu_item.text-center - %input.fullwidth.red{ type: "button", ng: { value: "saved() ? 'Saved': 'Save As Default'", disabled: "saved()", click: "saveColumnPreferences(action)"} } + %input.fullwidth.orange{ type: "button", ng: { value: "saved() ? 'Saved': 'Saving'", show: "saved() || saving", disabled: "saved()" } } + %input.fullwidth.red{ type: "button", value: 'Save As Default', ng: { show: "!saved() && !saving", click: "saveColumnPreferences(action)"} } diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index 293549d0f1..8d77015af7 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -43,6 +43,11 @@ input.red { margin-right: 5px; } +input.orange { + background-color: #FF9848; + margin-right: 5px; +} + input.search { margin-bottom: 1em; } From 852a12db61db7f9e50f2c7fd077c35b741176e93 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 19 May 2016 14:49:26 +1000 Subject: [PATCH 053/110] Remove caching of global npm phantomjs install --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c32401ac61..ef54e640c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: ruby sudo: false -cache: - - bundler - - $(npm bin -g)/phantomjs +cache: bundler bundler_args: --without development rvm: - "2.1.5" From 63432c2acc925584c1b88448e4bf23af26b15085 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 25 May 2016 11:45:18 +1000 Subject: [PATCH 054/110] Resize tested page to find all products --- spec/features/admin/order_cycles_spec.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index e362bf9946..874c8342b5 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -223,6 +223,9 @@ feature %q{ page.should have_field 'order_cycle_outgoing_exchange_1_pickup_time', with: 'time 1' page.should have_field 'order_cycle_outgoing_exchange_1_pickup_instructions', with: 'instructions 1' + # Make the whole page visible + page.driver.resize(1280, 3600) + # And the distributors should have products page.all('table.exchanges tbody tr.distributor').each do |row| row.find('td.products input').click From 21b84457539ef319d8d593d3347bf4ec237d5ed9 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 25 May 2016 11:58:57 +1000 Subject: [PATCH 055/110] Include job name in failure message --- spec/support/delayed_job_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/support/delayed_job_helper.rb b/spec/support/delayed_job_helper.rb index 5f9cfb6140..82fd79c95a 100644 --- a/spec/support/delayed_job_helper.rb +++ b/spec/support/delayed_job_helper.rb @@ -57,11 +57,11 @@ module OpenFoodNetwork end failure_message_for_should do |event_proc| - "expected job to be enqueued matching #{options.inspect} (#{@jobs_created.andand.count || '???'} others enqueued)" + "expected #{klass} to be enqueued matching #{options.inspect} (#{@jobs_created.andand.count || '???'} others enqueued)" end failure_message_for_should_not do |event_proc| - "expected job to not be enqueued matching #{options.inspect}" + "expected #{klass} to not be enqueued matching #{options.inspect}" end end end From 16b80a2f1b7fa40fe047777a53fce709167e34c0 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 11 May 2016 12:33:14 +1000 Subject: [PATCH 056/110] Extend PaymentMethod with calculated_adjustments --- app/models/spree/payment_method_decorator.rb | 9 +++++++++ spec/models/spree/payment_method_spec.rb | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/app/models/spree/payment_method_decorator.rb b/app/models/spree/payment_method_decorator.rb index 915eb65ae8..97ae3c6bc7 100644 --- a/app/models/spree/payment_method_decorator.rb +++ b/app/models/spree/payment_method_decorator.rb @@ -4,6 +4,10 @@ Spree::PaymentMethod.class_eval do attr_accessible :distributor_ids + calculated_adjustments + + after_initialize :init + validates :distributors, presence: { message: "^At least one hub must be selected" } # -- Scopes @@ -31,6 +35,11 @@ Spree::PaymentMethod.class_eval do where('spree_payment_methods.environment=? OR spree_payment_methods.environment=? OR spree_payment_methods.environment IS NULL', Rails.env, '') } + def init + self.class.calculated_adjustments unless reflections.keys.include? :calculator + self.calculator ||= Spree::Calculator::FlatRate.new(preferred_amount: 0) + end + def has_distributor?(distributor) self.distributors.include?(distributor) end diff --git a/spec/models/spree/payment_method_spec.rb b/spec/models/spree/payment_method_spec.rb index b61fd3bffd..0e7c61592b 100644 --- a/spec/models/spree/payment_method_spec.rb +++ b/spec/models/spree/payment_method_spec.rb @@ -25,5 +25,13 @@ module Spree # Testing else condition Spree::Gateway::BogusSimple.clean_name.should == "BogusSimple" end + + it "computes the amount of fees" do + pickup = create(:payment_method, name: 'pickup') + order = create(:order) + expect(pickup.compute_amount(order)).to eq 0 + delivery = create(:payment_method, name: 'delivery', calculator: Calculator::FlatRate.new(preferred_amount: 10)) + expect(delivery.compute_amount(order)).to eq 10 + end end end From e4f855b64a04ed7e47b8a780fff12d7d16b50b7a Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 13 May 2016 11:56:00 +1000 Subject: [PATCH 057/110] Display calculator in payment method edit form --- .../spree/admin/payment_methods_controller_decorator.rb | 1 + .../_form/replace_form_fields.html.haml.deface | 6 +++++- config/application.rb | 5 +++++ config/initializers/spree.rb | 4 +--- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/controllers/spree/admin/payment_methods_controller_decorator.rb b/app/controllers/spree/admin/payment_methods_controller_decorator.rb index 963b61fa81..fc15324d0b 100644 --- a/app/controllers/spree/admin/payment_methods_controller_decorator.rb +++ b/app/controllers/spree/admin/payment_methods_controller_decorator.rb @@ -57,6 +57,7 @@ module Spree else @providers = Gateway.providers.reject{ |p| p.name.include? "Bogus" }.sort{|p1, p2| p1.name <=> p2.name } end + @calculators = PaymentMethod.calculators.sort_by(&:name) end def load_hubs diff --git a/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface index 3b588a98c1..9e055ce538 100644 --- a/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface +++ b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface @@ -33,4 +33,8 @@ = radio_button :payment_method, :active, false   = label_tag nil, t(:say_no) - = render 'providers' \ No newline at end of file + = render 'providers' + + .row + .omega.eleven.columns + = render partial: 'spree/admin/shared/calculator_fields', :locals => { :f => f } diff --git a/config/application.rb b/config/application.rb index 1fb6c88bbf..6ffee13160 100644 --- a/config/application.rb +++ b/config/application.rb @@ -34,6 +34,11 @@ module Openfoodnetwork Spree::Calculator::PerItem, Spree::Calculator::PriceSack, OpenFoodNetwork::Calculator::Weight] + app.config.spree.calculators.payment_methods = [Spree::Calculator::FlatPercentItemTotal, + Spree::Calculator::FlatRate, + Spree::Calculator::FlexiRate, + Spree::Calculator::PerItem, + Spree::Calculator::PriceSack] end # Register Spree payment methods diff --git a/config/initializers/spree.rb b/config/initializers/spree.rb index a6a045222d..92ba3db249 100644 --- a/config/initializers/spree.rb +++ b/config/initializers/spree.rb @@ -40,9 +40,7 @@ module Spree module Core class Environment class Calculators - include EnvironmentExtension - - attr_accessor :enterprise_fees + attr_accessor :enterprise_fees, :payment_methods end end end From 0af8377844bce6decc1bdffc9526913deebabb22 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 18 May 2016 15:40:08 +1000 Subject: [PATCH 058/110] Add payment method fees to order during checkout The fee is displayed as "Transaction fee". --- .../darkswarm/services/checkout.js.coffee | 5 ++++- .../services/payment_methods.js.coffee | 1 + app/helpers/checkout_helper.rb | 9 +++++++++ app/models/spree/payment_decorator.rb | 19 +++++++++++++++++++ .../api/payment_method_serializer.rb | 7 ++++++- app/views/checkout/_payment.html.haml | 1 + app/views/checkout/_summary.html.haml | 5 +++++ config/locales/en.yml | 1 + spec/features/admin/adjustments_spec.rb | 4 ++-- 9 files changed, 48 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/checkout.js.coffee b/app/assets/javascripts/darkswarm/services/checkout.js.coffee index 34cae22fcc..0ab94f61a4 100644 --- a/app/assets/javascripts/darkswarm/services/checkout.js.coffee +++ b/app/assets/javascripts/darkswarm/services/checkout.js.coffee @@ -62,8 +62,11 @@ Darkswarm.factory 'Checkout', (CurrentOrder, ShippingMethods, PaymentMethods, $h shippingPrice: -> @shippingMethod()?.price || 0.0 + paymentPrice: -> + @paymentMethod()?.price || 0.0 + paymentMethod: -> PaymentMethods.payment_methods_by_id[@order.payment_method_id] cartTotal: -> - @shippingPrice() + @order.display_total + @order.display_total + @shippingPrice() + @paymentPrice() diff --git a/app/assets/javascripts/darkswarm/services/payment_methods.js.coffee b/app/assets/javascripts/darkswarm/services/payment_methods.js.coffee index 8b44d3e80a..423ceb57e2 100644 --- a/app/assets/javascripts/darkswarm/services/payment_methods.js.coffee +++ b/app/assets/javascripts/darkswarm/services/payment_methods.js.coffee @@ -4,5 +4,6 @@ Darkswarm.factory "PaymentMethods", (paymentMethods)-> payment_methods_by_id: {} constructor: -> for method in @payment_methods + method.price = parseFloat(method.price) @payment_methods_by_id[method.id] = method diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index 080db9c0e8..66d98d7862 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -94,4 +94,13 @@ module CheckoutHelper current_order.tokenized_permission.save! session[:access_token] = token end + + def payment_method_price(method, order) + price = method.compute_amount(order) + if price == 0 + t('checkout_method_free') + else + "{{ #{price} | localizeCurrency }}" + end + end end diff --git a/app/models/spree/payment_decorator.rb b/app/models/spree/payment_decorator.rb index 1a80f0269f..fda67fedc8 100644 --- a/app/models/spree/payment_decorator.rb +++ b/app/models/spree/payment_decorator.rb @@ -1,5 +1,24 @@ module Spree Payment.class_eval do + has_one :adjustment, as: :source, dependent: :destroy + + after_save :ensure_correct_adjustment, :update_order + + def ensure_correct_adjustment + if adjustment + adjustment.originator = payment_method + adjustment.label = adjustment_label + adjustment.save + else + payment_method.create_adjustment(adjustment_label, order, self, true) + reload + end + end + + def adjustment_label + I18n.t('payment_method_fee') + end + # Pin payments lacks void and credit methods, but it does have refund # Here we swap credit out for refund and remove void as a possible action def actions_with_pin_payment_adaptations diff --git a/app/serializers/api/payment_method_serializer.rb b/app/serializers/api/payment_method_serializer.rb index d62bc18154..a135f5d792 100644 --- a/app/serializers/api/payment_method_serializer.rb +++ b/app/serializers/api/payment_method_serializer.rb @@ -1,3 +1,8 @@ class Api::PaymentMethodSerializer < ActiveModel::Serializer - attributes :name, :description, :id, :method_type + attributes :name, :description, :id, :method_type, + :price + + def price + object.compute_amount(options[:current_order]) + end end diff --git a/app/views/checkout/_payment.html.haml b/app/views/checkout/_payment.html.haml index c11d7cd45e..c0578a8a4c 100644 --- a/app/views/checkout/_payment.html.haml +++ b/app/views/checkout/_payment.html.haml @@ -26,6 +26,7 @@ name: "order.payment_method_id", "ng-model" => "order.payment_method_id" = method.name + = "(#{payment_method_price(method, @order)})" %small.error.medium.input-text{"ng-show" => "!fieldValid('order.payment_method_id')"} = "{{ fieldErrors('order.payment_method_id') }}" diff --git a/app/views/checkout/_summary.html.haml b/app/views/checkout/_summary.html.haml index 3a4f6a4ea1..a3b61c5ded 100644 --- a/app/views/checkout/_summary.html.haml +++ b/app/views/checkout/_summary.html.haml @@ -19,6 +19,11 @@ = t :checkout_shipping_price %td.shipping.text-right {{ Checkout.shippingPrice() | localizeCurrency }} + %tr + %th + = t :payment_method_fee + %td.text-right {{ Checkout.paymentPrice() | localizeCurrency }} + %tr %th = t :checkout_total_price diff --git a/config/locales/en.yml b/config/locales/en.yml index 3249f00d5a..649b7bcf74 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1038,6 +1038,7 @@ Please follow the instructions there to make your enterprise visible on the Open properties: "Properties" shipping_methods: "Shipping Methods" payment_methods: "Payment Methods" + payment_method_fee: "Transaction fee" enterprise_fees: "Enterprise Fees" inventory_settings: "Inventory Settings" tag_rules: "Tag Rules" diff --git a/spec/features/admin/adjustments_spec.rb b/spec/features/admin/adjustments_spec.rb index b9a6ee3465..a051d28b2b 100644 --- a/spec/features/admin/adjustments_spec.rb +++ b/spec/features/admin/adjustments_spec.rb @@ -48,7 +48,7 @@ feature %q{ visit spree.admin_orders_path page.find('td.actions a.icon-edit').click click_link 'Adjustments' - page.find('td.actions a.icon-edit').click + page.find('tr', text: 'Shipping').find('a.icon-edit').click # Then I should see the uneditable included tax and our tax rate as the default page.should have_field :adjustment_included_tax, with: '10.00', disabled: true @@ -72,7 +72,7 @@ feature %q{ visit spree.admin_orders_path page.find('td.actions a.icon-edit').click click_link 'Adjustments' - page.find('td.actions a.icon-edit').click + page.find('tr', text: 'Shipping').find('a.icon-edit').click # Then I should see the uneditable included tax and 'Remove tax' as the default tax rate page.should have_field :adjustment_included_tax, with: '0.00', disabled: true From a624a57bfb1696fe92d7a3249d02f8fb2b31161c Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 25 May 2016 17:12:47 +1000 Subject: [PATCH 059/110] Pass current order to PaymentMethodSerializer --- app/helpers/injection_helper.rb | 2 +- spec/models/spree/payment_method_spec.rb | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/helpers/injection_helper.rb b/app/helpers/injection_helper.rb index c6be5dc63e..2d42d43429 100644 --- a/app/helpers/injection_helper.rb +++ b/app/helpers/injection_helper.rb @@ -24,7 +24,7 @@ module InjectionHelper def inject_available_payment_methods inject_json_ams "paymentMethods", current_order.available_payment_methods, - Api::PaymentMethodSerializer + Api::PaymentMethodSerializer, current_order: current_order end def inject_taxons diff --git a/spec/models/spree/payment_method_spec.rb b/spec/models/spree/payment_method_spec.rb index 0e7c61592b..70808c231e 100644 --- a/spec/models/spree/payment_method_spec.rb +++ b/spec/models/spree/payment_method_spec.rb @@ -27,11 +27,16 @@ module Spree end it "computes the amount of fees" do - pickup = create(:payment_method, name: 'pickup') + pickup = create(:payment_method) order = create(:order) expect(pickup.compute_amount(order)).to eq 0 - delivery = create(:payment_method, name: 'delivery', calculator: Calculator::FlatRate.new(preferred_amount: 10)) - expect(delivery.compute_amount(order)).to eq 10 + transaction = create(:payment_method, calculator: Calculator::FlatRate.new(preferred_amount: 10)) + expect(transaction.compute_amount(order)).to eq 10 + transaction = create(:payment_method, calculator: Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10)) + expect(transaction.compute_amount(order)).to eq 0 + product = create(:product) + order.add_variant(product.master) + expect(transaction.compute_amount(order)).to eq 2.0 end end end From e3b43243cc7c66d7b62d627b42d5821e323073a7 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 27 May 2016 11:30:07 +1000 Subject: [PATCH 060/110] Add line_items getter to Payment The calculator of a payment method calls `line_items` on the payment. The code got copied from Spree::Shipment. --- app/models/spree/payment_decorator.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/models/spree/payment_decorator.rb b/app/models/spree/payment_decorator.rb index fda67fedc8..d178e1de54 100644 --- a/app/models/spree/payment_decorator.rb +++ b/app/models/spree/payment_decorator.rb @@ -19,6 +19,15 @@ module Spree I18n.t('payment_method_fee') end + # This is called by the calculator of a payment method + def line_items + if order.complete? && Spree::Config[:track_inventory_levels] + order.line_items.select { |li| inventory_units.pluck(:variant_id).include?(li.variant_id) } + else + order.line_items + end + end + # Pin payments lacks void and credit methods, but it does have refund # Here we swap credit out for refund and remove void as a possible action def actions_with_pin_payment_adaptations From 8a770628881988fd52f3f4a4f274b85e1d966b90 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 27 May 2016 14:40:22 +1000 Subject: [PATCH 061/110] Include transaction fees in reports --- app/models/spree/adjustment_decorator.rb | 1 + app/models/spree/order_decorator.rb | 4 ++++ .../orders_and_fulfillments_report.rb | 10 +++++++++- lib/open_food_network/xero_invoices_report.rb | 9 +++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app/models/spree/adjustment_decorator.rb b/app/models/spree/adjustment_decorator.rb index 0d1bc941f6..c8529fc577 100644 --- a/app/models/spree/adjustment_decorator.rb +++ b/app/models/spree/adjustment_decorator.rb @@ -12,6 +12,7 @@ module Spree scope :with_tax, where('spree_adjustments.included_tax > 0') scope :without_tax, where('spree_adjustments.included_tax = 0') + scope :payment_fee, where(originator_type: 'Spree::PaymentMethod') attr_accessible :included_tax diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index a06a34bcd0..ad1a877455 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -206,6 +206,10 @@ Spree::Order.class_eval do adjustments.eligible.where("originator_type = ? AND source_type != ?", 'EnterpriseFee', 'Spree::LineItem').sum(&:amount) end + def payment_fee + adjustments.payment_fee.map(&:amount).sum + end + # Show payment methods for this distributor def available_payment_methods @available_payment_methods ||= Spree::PaymentMethod.available(:front_end).select do |pm| diff --git a/lib/open_food_network/orders_and_fulfillments_report.rb b/lib/open_food_network/orders_and_fulfillments_report.rb index 52d07771a2..089838a1b1 100644 --- a/lib/open_food_network/orders_and_fulfillments_report.rb +++ b/lib/open_food_network/orders_and_fulfillments_report.rb @@ -18,7 +18,14 @@ module OpenFoodNetwork ["Hub", "Producer", "Product", "Variant", "Amount", "Curr. Cost per Unit", "Total Cost", "Total Shipping Cost", "Shipping Method"] when "order_cycle_customer_totals" ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", - "Amount", "Item (#{currency_symbol})", "Item + Fees (#{currency_symbol})", "Admin & Handling (#{currency_symbol})", "Ship (#{currency_symbol})", "Total (#{currency_symbol})", "Paid?", + "Amount", + "Item (#{currency_symbol})", + "Item + Fees (#{currency_symbol})", + "Admin & Handling (#{currency_symbol})", + "Ship (#{currency_symbol})", + "Pay fee (#{currency_symbol})", + "Total (#{currency_symbol})", + "Paid?", "Shipping", "Delivery?", "Ship Street", "Ship Street 2", "Ship City", "Ship Postcode", "Ship State", "Comments", "SKU", @@ -118,6 +125,7 @@ module OpenFoodNetwork proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } }, proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.admin_and_handling_total } }, proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.ship_total } }, + proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.payment_fee } }, proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.total } }, proc { |line_items| line_items.all? { |li| li.order.paid? } ? "Yes" : "No" }, diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb index 331c8b204c..cab44aa55e 100644 --- a/lib/open_food_network/xero_invoices_report.rb +++ b/lib/open_food_network/xero_invoices_report.rb @@ -88,6 +88,7 @@ module OpenFoodNetwork rows += produce_summary_rows(order, invoice_number, opts) unless detail? rows += fee_summary_rows(order, invoice_number, opts) unless detail? && order.account_invoice? rows += shipping_summary_rows(order, invoice_number, opts) + rows += payment_summary_rows(order, invoice_number, opts) rows += admin_adjustment_summary_rows(order, invoice_number, opts) unless detail? rows @@ -107,6 +108,10 @@ module OpenFoodNetwork [summary_row(order, 'Delivery Shipping Cost (tax inclusive)', total_shipping(order), invoice_number, tax_on_shipping_s(order), opts)] end + def payment_summary_rows(order, invoice_number, opts) + [summary_row(order, 'Transaction Fee (no tax)', total_transaction(order), invoice_number, 'GST Free Income', opts)] + end + def admin_adjustment_summary_rows(order, invoice_number, opts) [summary_row(order, 'Total untaxable admin adjustments (no tax)', total_untaxable_admin_adjustments(order), invoice_number, 'GST Free Income', opts), summary_row(order, 'Total taxable admin adjustments (tax inclusive)', total_taxable_admin_adjustments(order), invoice_number, 'GST on Income', opts)] @@ -189,6 +194,10 @@ module OpenFoodNetwork order.adjustments.shipping.sum &:amount end + def total_transaction(order) + order.adjustments.payment_fee.sum &:amount + end + def tax_on_shipping_s(order) tax_on_shipping = order.adjustments.shipping.sum(&:included_tax) > 0 tax_on_shipping ? 'GST on Income' : 'GST Free Income' From ddc7e86e6c5b0b9bd288b9f4c0652396a6ea6f87 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 27 May 2016 15:00:54 +1000 Subject: [PATCH 062/110] Update xero report spec --- spec/lib/open_food_network/xero_invoices_report_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/lib/open_food_network/xero_invoices_report_spec.rb b/spec/lib/open_food_network/xero_invoices_report_spec.rb index 45ab77be6d..af67f8bed9 100644 --- a/spec/lib/open_food_network/xero_invoices_report_spec.rb +++ b/spec/lib/open_food_network/xero_invoices_report_spec.rb @@ -28,6 +28,7 @@ module OpenFoodNetwork report.stub(:produce_summary_rows) { ['produce'] } report.stub(:fee_summary_rows) { ['fee'] } report.stub(:shipping_summary_rows) { ['shipping'] } + report.stub(:payment_summary_rows) { ['payment'] } report.stub(:admin_adjustment_summary_rows) { ['admin'] } order.stub(:account_invoice?) { false } end From 3f8420b0e9e938e1d5c17354730f766496ff4e2c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 15 Apr 2016 11:44:19 +1000 Subject: [PATCH 063/110] Adding tags to variant overrides --- .../tags_with_translation.js.coffee | 13 +++- .../directives/track_inheritance.js.coffee | 3 +- .../directives/track_tag_list.js.coffee | 9 +++ .../track_variant_override.js.coffee | 4 +- .../dirty_variant_overrides.js.coffee | 23 +++++-- .../services/variant_overrides.js.coffee | 2 + .../variant_overrides.js.coffee | 2 +- .../templates/admin/tags_input.html.haml | 2 +- app/models/variant_override.rb | 2 + app/models/variant_override_set.rb | 18 ++++- .../api/admin/variant_override_serializer.rb | 9 +++ .../variant_overrides/_products.html.haml | 2 + .../_products_product.html.haml | 1 + .../_products_variants.html.haml | 5 +- .../column_preference_defaults.rb | 1 + spec/features/admin/variant_overrides_spec.rb | 10 ++- .../dirty_variant_overrides_spec.js.coffee | 67 ++++++++++++------- .../services/variant_overrides_spec.js.coffee | 42 ++++++------ 18 files changed, 152 insertions(+), 63 deletions(-) create mode 100644 app/assets/javascripts/admin/variant_overrides/directives/track_tag_list.js.coffee diff --git a/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee index 7a6ae9afff..a356a9e4ff 100644 --- a/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee @@ -6,11 +6,20 @@ angular.module("admin.utils").directive "tagsWithTranslation", ($timeout) -> tagsAttr: "@?" tagListAttr: "@?" findTags: "&" + form: '=?' link: (scope, element, attrs) -> $timeout -> scope.tagsAttr ||= "tags" scope.tagListAttr ||= "tag_list" - watchString = "object.#{scope.tagsAttr}" - scope.$watchCollection watchString, -> + compileTagList = -> scope.object[scope.tagListAttr] = (tag.text for tag in scope.object[scope.tagsAttr]).join(",") + + scope.tagAdded = -> + compileTagList() + + scope.tagRemoved = -> + # For some reason the tags input doesn't mark the form + # as dirty when a tag is removed, which breaks the save bar + scope.form.$setDirty(true) if typeof scope.form isnt 'undefined' + compileTagList() diff --git a/app/assets/javascripts/admin/variant_overrides/directives/track_inheritance.js.coffee b/app/assets/javascripts/admin/variant_overrides/directives/track_inheritance.js.coffee index e0a67eb7d0..ecc1cdac29 100644 --- a/app/assets/javascripts/admin/variant_overrides/directives/track_inheritance.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/directives/track_inheritance.js.coffee @@ -6,7 +6,6 @@ angular.module("admin.variantOverrides").directive "trackInheritance", (VariantO ngModel.$parsers.push (viewValue) -> if ngModel.$dirty && viewValue - variantOverride = VariantOverrides.inherit(scope.hub_id, scope.variant.id) - DirtyVariantOverrides.add variantOverride + DirtyVariantOverrides.inherit scope.hub_id, scope.variant.id, scope.variantOverrides[scope.hub_id][scope.variant.id].id scope.displayDirty() viewValue diff --git a/app/assets/javascripts/admin/variant_overrides/directives/track_tag_list.js.coffee b/app/assets/javascripts/admin/variant_overrides/directives/track_tag_list.js.coffee new file mode 100644 index 0000000000..096ffc24ff --- /dev/null +++ b/app/assets/javascripts/admin/variant_overrides/directives/track_tag_list.js.coffee @@ -0,0 +1,9 @@ +angular.module("admin.variantOverrides").directive "trackTagList", (VariantOverrides, DirtyVariantOverrides) -> + link: (scope, element, attrs) -> + watchString = "variantOverrides[#{scope.hub_id}][#{scope.variant.id}].tag_list" + scope.$watch watchString, (newValue, oldValue) -> + if typeof newValue isnt 'undefined' && newValue != oldValue + scope.inherit = false + vo_id = scope.variantOverrides[scope.hub_id][scope.variant.id].id + DirtyVariantOverrides.set scope.hub_id, scope.variant.id, vo_id, 'tag_list', newValue + scope.displayDirty() diff --git a/app/assets/javascripts/admin/variant_overrides/directives/track_variant_override.js.coffee b/app/assets/javascripts/admin/variant_overrides/directives/track_variant_override.js.coffee index 184b7af232..db4c30dacb 100644 --- a/app/assets/javascripts/admin/variant_overrides/directives/track_variant_override.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/directives/track_variant_override.js.coffee @@ -3,8 +3,8 @@ angular.module("admin.variantOverrides").directive "ofnTrackVariantOverride", (D link: (scope, element, attrs, ngModel) -> ngModel.$parsers.push (viewValue) -> if ngModel.$dirty - variantOverride = scope.variantOverrides[scope.hub_id][scope.variant.id] scope.inherit = false - DirtyVariantOverrides.add variantOverride + vo_id = scope.variantOverrides[scope.hub_id][scope.variant.id].id + DirtyVariantOverrides.set scope.hub_id, scope.variant.id, vo_id, attrs.ofnTrackVariantOverride, viewValue scope.displayDirty() viewValue diff --git a/app/assets/javascripts/admin/variant_overrides/services/dirty_variant_overrides.js.coffee b/app/assets/javascripts/admin/variant_overrides/services/dirty_variant_overrides.js.coffee index 053c6cbfa1..537fb484dc 100644 --- a/app/assets/javascripts/admin/variant_overrides/services/dirty_variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/services/dirty_variant_overrides.js.coffee @@ -1,10 +1,22 @@ -angular.module("admin.variantOverrides").factory "DirtyVariantOverrides", ($http) -> +angular.module("admin.variantOverrides").factory "DirtyVariantOverrides", ($http, VariantOverrides) -> new class DirtyVariantOverrides dirtyVariantOverrides: {} - add: (vo) -> - @dirtyVariantOverrides[vo.hub_id] ||= {} - @dirtyVariantOverrides[vo.hub_id][vo.variant_id] = vo + add: (hub_id, variant_id, vo_id) -> + @dirtyVariantOverrides[hub_id] ||= {} + @dirtyVariantOverrides[hub_id][variant_id] ||= + { id: vo_id, variant_id: variant_id, hub_id: hub_id } + + set: (hub_id, variant_id, vo_id, attr, value) -> + if attr in @requiredAttrs() + @add(hub_id, variant_id, vo_id) + @dirtyVariantOverrides[hub_id][variant_id][attr] = value + + inherit: (hub_id, variant_id, vo_id) -> + @add(hub_id, variant_id, vo_id) + blankVo = angular.copy(VariantOverrides.inherit(hub_id, variant_id)) + delete blankVo[attr] for attr, value of blankVo when attr not in @requiredAttrs() + @dirtyVariantOverrides[hub_id][variant_id] = blankVo count: -> count = 0 @@ -27,3 +39,6 @@ angular.module("admin.variantOverrides").factory "DirtyVariantOverrides", ($http url: "/admin/variant_overrides/bulk_update" data: variant_overrides: @all() + + requiredAttrs: -> + ['id','hub_id','variant_id','sku','price','count_on_hand','on_demand','default_stock','resettable','tag_list'] diff --git a/app/assets/javascripts/admin/variant_overrides/services/variant_overrides.js.coffee b/app/assets/javascripts/admin/variant_overrides/services/variant_overrides.js.coffee index 4e1128572e..94a4d093eb 100644 --- a/app/assets/javascripts/admin/variant_overrides/services/variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/services/variant_overrides.js.coffee @@ -30,6 +30,8 @@ angular.module("admin.variantOverrides").factory "VariantOverrides", (variantOve on_demand: null default_stock: null resettable: false + tag_list: '' + tags: [] updateIds: (updatedVos) -> for vo in updatedVos diff --git a/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee b/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee index 2af7ba1c16..4d875cbd33 100644 --- a/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee @@ -1 +1 @@ -angular.module("admin.variantOverrides", ["admin.indexUtils", "admin.utils", "admin.dropdown", "admin.inventoryItems"]) +angular.module("admin.variantOverrides", ["admin.indexUtils", "admin.utils", "admin.dropdown", "admin.inventoryItems", 'ngTagsInput']) diff --git a/app/assets/javascripts/templates/admin/tags_input.html.haml b/app/assets/javascripts/templates/admin/tags_input.html.haml index 2dd75ce5ac..3e02c1c332 100644 --- a/app/assets/javascripts/templates/admin/tags_input.html.haml +++ b/app/assets/javascripts/templates/admin/tags_input.html.haml @@ -1,4 +1,4 @@ -%tags-input{ template: 'admin/tag.html', ng: { model: 'object[tagsAttr]' } } +%tags-input{ template: 'admin/tag.html', ng: { model: 'object[tagsAttr]' }, on: { tag: { added: 'tagAdded()', removed:'tagRemoved()' } } } %auto-complete{source: "findTags({query: $query})", template: "admin/tag_autocomplete.html", "min-length" => "0", diff --git a/app/models/variant_override.rb b/app/models/variant_override.rb index b373900e0c..e96b56db56 100644 --- a/app/models/variant_override.rb +++ b/app/models/variant_override.rb @@ -1,4 +1,6 @@ class VariantOverride < ActiveRecord::Base + acts_as_taggable + belongs_to :hub, class_name: 'Enterprise' belongs_to :variant, class_name: 'Spree::Variant' diff --git a/app/models/variant_override_set.rb b/app/models/variant_override_set.rb index 730246cebe..2d39d452fd 100644 --- a/app/models/variant_override_set.rb +++ b/app/models/variant_override_set.rb @@ -1,16 +1,28 @@ class VariantOverrideSet < ModelSet def initialize(collection, attributes={}) - super(VariantOverride, collection, attributes, nil, proc { |attrs| deletable?(attrs) } ) + super(VariantOverride, collection, attributes, nil, proc { |attrs, tag_list| deletable?(attrs, tag_list) } ) end private - def deletable?(attrs) + def deletable?(attrs, tag_list) attrs['price'].blank? && attrs['count_on_hand'].blank? && attrs['default_stock'].blank? && attrs['resettable'].blank? && attrs['sku'].nil? && - attrs['on_demand'].nil? + attrs['on_demand'].nil? && + tag_list.empty? + end + + def collection_to_delete + # Override of ModelSet method to allow us to check presence of a tag_list (which is not an attribute) + deleted = [] + collection.delete_if { |e| deleted << e if @delete_if.andand.call(e.attributes, e.tag_list) } + deleted + end + + def collection_to_keep + collection.reject { |e| @delete_if.andand.call(e.attributes, e.tag_list) } end end diff --git a/app/serializers/api/admin/variant_override_serializer.rb b/app/serializers/api/admin/variant_override_serializer.rb index c1e6e0038c..99c99eebfa 100644 --- a/app/serializers/api/admin/variant_override_serializer.rb +++ b/app/serializers/api/admin/variant_override_serializer.rb @@ -1,3 +1,12 @@ class Api::Admin::VariantOverrideSerializer < ActiveModel::Serializer attributes :id, :hub_id, :variant_id, :sku, :price, :count_on_hand, :on_demand, :default_stock, :resettable + attributes :tag_list, :tags + + def tag_list + object.tag_list.join(",") + end + + def tags + object.tag_list.map{ |t| { text: t } } + end end diff --git a/app/views/admin/variant_overrides/_products.html.haml b/app/views/admin/variant_overrides/_products.html.haml index 4330496b3e..711c52f6cf 100644 --- a/app/views/admin/variant_overrides/_products.html.haml +++ b/app/views/admin/variant_overrides/_products.html.haml @@ -11,6 +11,7 @@ %col.reset{ width: "1%", ng: { show: 'columns.reset.visible' } } %col.reset{ width: "15%", ng: { show: 'columns.reset.visible' } } %col.inheritance{ width: "5%", ng: { show: 'columns.inheritance.visible' } } + %col.tags{ width: "30%", ng: { show: 'columns.tags.visible' } } %col.visibility{ width: "10%", ng: { show: 'columns.visibility.visible' } } %thead %tr{ ng: { controller: "ColumnsCtrl" } } @@ -22,6 +23,7 @@ %th.on_demand{ ng: { show: 'columns.on_demand.visible' } }=t('admin.on_demand?') %th.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } }=t('admin.variant_overrides.index.enable_reset?') %th.inheritance{ ng: { show: 'columns.inheritance.visible' } }=t('admin.variant_overrides.index.inherit?') + %th.tags{ ng: { show: 'columns.tags.visible' } }=t('admin.tags') %th.visibility{ ng: { show: 'columns.visibility.visible' } }=t('admin.variant_overrides.index.hide') %tbody{ ng: {repeat: 'product in filteredProducts = (products | hubPermissions:hubPermissions:hub_id | inventoryProducts:hub_id:views | attrFilter:{producer_id:producerFilter} | filter:query) | limitTo:productLimit' } } = render 'admin/variant_overrides/products_product' diff --git a/app/views/admin/variant_overrides/_products_product.html.haml b/app/views/admin/variant_overrides/_products_product.html.haml index 119ee867df..0427333790 100644 --- a/app/views/admin/variant_overrides/_products_product.html.haml +++ b/app/views/admin/variant_overrides/_products_product.html.haml @@ -7,4 +7,5 @@ %td.on_demand{ ng: { show: 'columns.on_demand.visible' } } %td.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } } %td.inheritance{ ng: { show: 'columns.inheritance.visible' } } + %td.tags{ ng: { show: 'columns.tags.visible' } } %td.visibility{ ng: { show: 'columns.visibility.visible' } } diff --git a/app/views/admin/variant_overrides/_products_variants.html.haml b/app/views/admin/variant_overrides/_products_variants.html.haml index c139bc7ffb..94fc309798 100644 --- a/app/views/admin/variant_overrides/_products_variants.html.haml +++ b/app/views/admin/variant_overrides/_products_variants.html.haml @@ -1,4 +1,4 @@ -%tr.variant{ id: "v_{{variant.id}}", ng: {repeat: 'variant in product.variants | inventoryVariants:hub_id:views'}} +%tr.variant{ id: "v_{{variant.id}}", ng: {repeat: 'variant in product.variants | inventoryVariants:hub_id:views'} } %td.producer{ ng: { show: 'columns.producer.visible' } } %td.product{ ng: { show: 'columns.product.visible' } } %span{ ng: { bind: '::variant.display_name || ""'} } @@ -17,6 +17,9 @@ %input{name: 'variant-overrides-{{ variant.id }}-default_stock', type: 'text', ng: {model: 'variantOverrides[hub_id][variant.id].default_stock'}, placeholder: '{{ variant.default_stock ? variant.default_stock : "Default stock"}}', 'ofn-track-variant-override' => 'default_stock'} %td.inheritance{ ng: { show: 'columns.inheritance.visible' } } %input.field{ :type => 'checkbox', name: 'variant-overrides-{{ variant.id }}-inherit', ng: { model: 'inherit' }, 'track-inheritance' => true } + %td.tags{ ng: { show: 'columns.tags.visible' } } + .tag_watcher{ 'track-tag-list' => true } + %tags_with_translation{ object: 'variantOverrides[hub_id][variant.id]', form: 'variant_overrides_form' } %td.visibility{ ng: { show: 'columns.visibility.visible' } } %button.icon-remove.hide.fullwidth{ :type => 'button', ng: { click: "setVisibility(hub_id,variant.id,false)" } } = t('admin.variant_overrides.index.hide') diff --git a/lib/open_food_network/column_preference_defaults.rb b/lib/open_food_network/column_preference_defaults.rb index c18ac79550..09dc197832 100644 --- a/lib/open_food_network/column_preference_defaults.rb +++ b/lib/open_food_network/column_preference_defaults.rb @@ -19,6 +19,7 @@ module OpenFoodNetwork on_demand: { name: I18n.t("admin.on_demand?"), visible: false }, reset: { name: I18n.t("#{node}.enable_reset?"), visible: false }, inheritance: { name: I18n.t("#{node}.inherit?"), visible: false }, + tags: { name: I18n.t("admin.tags"), visible: false }, visibility: { name: I18n.t("#{node}.hide"), visible: false } } end diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 0af93d19c3..3dd2306243 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -214,7 +214,7 @@ feature %q{ end context "with overrides" do - let!(:vo) { create(:variant_override, variant: variant, hub: hub, price: 77.77, count_on_hand: 11111, default_stock: 1000, resettable: true) } + let!(:vo) { create(:variant_override, variant: variant, hub: hub, price: 77.77, count_on_hand: 11111, default_stock: 1000, resettable: true, tag_list: ["tag1","tag2","tag3"]) } let!(:vo_no_auth) { create(:variant_override, variant: variant, hub: hub2, price: 1, count_on_hand: 2) } let!(:product2) { create(:simple_product, supplier: producer, variant_unit: 'weight', variant_unit_scale: 1) } let!(:variant2) { create(:variant, product: product2, unit_value: 8, price: 1.00, on_hand: 12) } @@ -256,6 +256,7 @@ feature %q{ first("div#columns-dropdown", :text => "COLUMNS").click first("div#columns-dropdown div.menu div.menu_item", text: "On Demand").click first("div#columns-dropdown div.menu div.menu_item", text: "Enable Stock Reset?").click + first("div#columns-dropdown div.menu div.menu_item", text: "Tags").click first("div#columns-dropdown", :text => "COLUMNS").click # Clearing values by 'inheriting' @@ -268,6 +269,13 @@ feature %q{ fill_in "variant-overrides-#{variant.id}-price", with: '' fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '' fill_in "variant-overrides-#{variant.id}-default_stock", with: '' + within "tr#v_#{variant.id}" do + vo.tag_list.each do |tag| + within "li.tag-item", text: "#{tag} ×" do + find("a.remove-button").trigger('click') + end + end + end page.uncheck "variant-overrides-#{variant.id}-resettable" page.should have_content "Changes to 2 overrides remain unsaved." diff --git a/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee b/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee index 553bba5713..7a1dd4fab3 100644 --- a/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee @@ -8,36 +8,53 @@ describe "maintaining a list of dirty variant overrides", -> beforeEach -> module "admin.variantOverrides" + module ($provide) -> + $provide.value "variantOverrides", { 2: { 1: variantOverride } } + null beforeEach inject (_DirtyVariantOverrides_) -> DirtyVariantOverrides = _DirtyVariantOverrides_ - it "adds new dirty variant overrides", -> - DirtyVariantOverrides.add variantOverride - expect(DirtyVariantOverrides.dirtyVariantOverrides).toEqual - 2: - 1: - variant_id: 1 - hub_id: 2 - price: 3 - count_on_hand: 4 + describe "adding a new dirty variant override", -> + it "adds new dirty variant overrides", -> + DirtyVariantOverrides.add(2,1,5) + expect(DirtyVariantOverrides.dirtyVariantOverrides).toEqual { 2: { 1: { id: 5, variant_id: 1, hub_id: 2 } } } - it "updates existing dirty variant overrides", -> - DirtyVariantOverrides.dirtyVariantOverrides = - 2: - 1: - variant_id: 5 - hub_id: 6 - price: 7 - count_on_hand: 8 - DirtyVariantOverrides.add variantOverride - expect(DirtyVariantOverrides.dirtyVariantOverrides).toEqual - 2: - 1: - variant_id: 1 - hub_id: 2 - price: 3 - count_on_hand: 4 + + describe "setting the value of an attribute", -> + beforeEach -> + spyOn(DirtyVariantOverrides, "add").andCallThrough() + + describe "when a record for the given VO does not exist", -> + beforeEach -> + DirtyVariantOverrides.dirtyVariantOverrides = {} + + it "sets the specified attribute on a new dirty VO", -> + DirtyVariantOverrides.set(2,1,5,'count_on_hand', 10) + expect(DirtyVariantOverrides.add).toHaveBeenCalledWith(2,1,5) + expect(DirtyVariantOverrides.dirtyVariantOverrides).toEqual + 2: + 1: + id: 5 + variant_id: 1 + hub_id: 2 + count_on_hand: 10 + + describe "when a record for the given VO exists", -> + beforeEach -> + DirtyVariantOverrides.dirtyVariantOverrides = { 2: { 1: { id: 5, variant_id: 1, hub_id: 2, price: 27 } } } + + it "sets the specified attribute on a new dirty VO", -> + DirtyVariantOverrides.set(2,1,5,'count_on_hand', 10) + expect(DirtyVariantOverrides.add).toHaveBeenCalledWith(2,1,5) + expect(DirtyVariantOverrides.dirtyVariantOverrides).toEqual + 2: + 1: + id: 5 + variant_id: 1 + hub_id: 2 + price: 27 + count_on_hand: 10 describe "with a number of variant overrides", -> beforeEach -> diff --git a/spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee b/spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee index 73dbdabee8..26f6d1ce93 100644 --- a/spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/variant_overrides_spec.js.coffee @@ -1,9 +1,9 @@ describe "VariantOverrides service", -> VariantOverrides = $httpBackend = null variantOverrides = [ - {id: 1, hub_id: 10, variant_id: 100, sku: "V100", price: 1, count_on_hand: 1, on_demand: null, default_stock: null, resettable: false } - {id: 2, hub_id: 10, variant_id: 200, sku: "V200", price: 2, count_on_hand: 2, on_demand: null, default_stock: null, resettable: false} - {id: 3, hub_id: 20, variant_id: 300, sku: "V300", price: 3, count_on_hand: 3, on_demand: null, default_stock: null, resettable: false} + {id: 1, hub_id: 10, variant_id: 100, sku: "V100", price: 1, count_on_hand: 1, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } + {id: 2, hub_id: 10, variant_id: 200, sku: "V200", price: 2, count_on_hand: 2, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } + {id: 3, hub_id: 20, variant_id: 300, sku: "V300", price: 3, count_on_hand: 3, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } ] beforeEach -> @@ -19,10 +19,10 @@ describe "VariantOverrides service", -> it "indexes variant overrides by hub_id -> variant_id", -> expect(VariantOverrides.variantOverrides).toEqual 10: - 100: {id: 1, hub_id: 10, variant_id: 100, sku: "V100", price: 1, count_on_hand: 1, on_demand: null, default_stock: null, resettable: false } - 200: {id: 2, hub_id: 10, variant_id: 200, sku: "V200", price: 2, count_on_hand: 2, on_demand: null, default_stock: null, resettable: false } + 100: {id: 1, hub_id: 10, variant_id: 100, sku: "V100", price: 1, count_on_hand: 1, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } + 200: {id: 2, hub_id: 10, variant_id: 200, sku: "V200", price: 2, count_on_hand: 2, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } 20: - 300: {id: 3, hub_id: 20, variant_id: 300, sku: "V300", price: 3, count_on_hand: 3, on_demand: null, default_stock: null, resettable: false } + 300: {id: 3, hub_id: 20, variant_id: 300, sku: "V300", price: 3, count_on_hand: 3, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } it "ensures blank data available for some products", -> hubs = [{id: 10}, {id: 20}, {id: 30}] @@ -34,23 +34,23 @@ describe "VariantOverrides service", -> ] VariantOverrides.ensureDataFor hubs, products expect(VariantOverrides.variantOverrides[10]).toEqual - 100: { id: 1, hub_id: 10, variant_id: 100, sku: "V100", price: 1, count_on_hand: 1, on_demand: null, default_stock: null, resettable: false } - 200: { id: 2, hub_id: 10, variant_id: 200, sku: "V200", price: 2, count_on_hand: 2, on_demand: null, default_stock: null, resettable: false } - 300: { hub_id: 10, variant_id: 300, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false } - 400: { hub_id: 10, variant_id: 400, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false } - 500: { hub_id: 10, variant_id: 500, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false } + 100: { id: 1, hub_id: 10, variant_id: 100, sku: "V100", price: 1, count_on_hand: 1, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } + 200: { id: 2, hub_id: 10, variant_id: 200, sku: "V200", price: 2, count_on_hand: 2, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } + 300: { hub_id: 10, variant_id: 300, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } + 400: { hub_id: 10, variant_id: 400, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } + 500: { hub_id: 10, variant_id: 500, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } expect(VariantOverrides.variantOverrides[20]).toEqual - 100: { hub_id: 20, variant_id: 100, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false } - 200: { hub_id: 20, variant_id: 200, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false } - 300: { id: 3, hub_id: 20, variant_id: 300, sku: "V300", price: 3, count_on_hand: 3, on_demand: null, default_stock: null, resettable: false } - 400: { hub_id: 20, variant_id: 400, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false } - 500: { hub_id: 20, variant_id: 500, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false } + 100: { hub_id: 20, variant_id: 100, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } + 200: { hub_id: 20, variant_id: 200, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } + 300: { id: 3, hub_id: 20, variant_id: 300, sku: "V300", price: 3, count_on_hand: 3, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } + 400: { hub_id: 20, variant_id: 400, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: []} + 500: { hub_id: 20, variant_id: 500, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } expect(VariantOverrides.variantOverrides[30]).toEqual - 100: { hub_id: 30, variant_id: 100, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false } - 200: { hub_id: 30, variant_id: 200, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false } - 300: { hub_id: 30, variant_id: 300, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false } - 400: { hub_id: 30, variant_id: 400, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false } - 500: { hub_id: 30, variant_id: 500, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false } + 100: { hub_id: 30, variant_id: 100, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } + 200: { hub_id: 30, variant_id: 200, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } + 300: { hub_id: 30, variant_id: 300, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: []} + 400: { hub_id: 30, variant_id: 400, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } + 500: { hub_id: 30, variant_id: 500, sku: null, price: null, count_on_hand: null, on_demand: null, default_stock: null, resettable: false, tag_list : '', tags: [] } it "updates the IDs of variant overrides", -> VariantOverrides.variantOverrides[2] = {} From 17fa0d2baf09aa3ee354134c52c153e08ee8064f Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 15 Apr 2016 13:42:18 +1000 Subject: [PATCH 064/110] Adding tag rule logic for filtering products --- app/models/tag_rule/filter_products.rb | 37 +++++++++ spec/factories.rb | 4 + spec/models/tag_rule/filter_products_spec.rb | 83 +++++++++++++++++++ .../tag_rule/filter_shipping_methods_spec.rb | 2 +- 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 app/models/tag_rule/filter_products.rb create mode 100644 spec/models/tag_rule/filter_products_spec.rb diff --git a/app/models/tag_rule/filter_products.rb b/app/models/tag_rule/filter_products.rb new file mode 100644 index 0000000000..e087d8737a --- /dev/null +++ b/app/models/tag_rule/filter_products.rb @@ -0,0 +1,37 @@ +class TagRule::FilterProducts < TagRule + preference :matched_variants_visibility, :string, default: "visible" + preference :variant_tags, :string, default: "" + + attr_accessible :preferred_matched_variants_visibility, :preferred_variant_tags + + private + + # Warning: this should only EVER be called via TagRule#apply + def apply! + unless preferred_matched_variants_visibility == "visible" + subject.reject! do |product| + product[:variants].reject!{ |v| tags_match?(v) } + product[:variants].empty? + end + end + end + + def apply_default! + if preferred_matched_variants_visibility == "visible" + subject.reject! do |product| + product[:variants].reject!{ |v| tags_match?(v) } + product[:variants].empty? + end + end + end + + def tags_match?(variant) + variant_tags = variant.andand[:tag_list] || [] + preferred_tags = preferred_variant_tags.split(",") + ( variant_tags & preferred_tags ).any? + end + + def subject_class_matches? + subject.class == HashWithIndifferentAccess + end +end diff --git a/spec/factories.rb b/spec/factories.rb index b4a3a1b573..2e29e74316 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -290,6 +290,10 @@ FactoryGirl.define do enterprise { FactoryGirl.create :distributor_enterprise } end + factory :filter_products_tag_rule, class: TagRule::FilterProducts do + enterprise { FactoryGirl.create :distributor_enterprise } + end + factory :tag_rule, class: TagRule::DiscountOrder do enterprise { FactoryGirl.create :distributor_enterprise } before(:create) do |tr| diff --git a/spec/models/tag_rule/filter_products_spec.rb b/spec/models/tag_rule/filter_products_spec.rb new file mode 100644 index 0000000000..2f1d9ec495 --- /dev/null +++ b/spec/models/tag_rule/filter_products_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe TagRule::FilterProducts, type: :model do + let!(:tag_rule) { create(:filter_products_tag_rule) } + + describe "determining whether tags match for a given variant" do + context "when the variantm is nil" do + + it "returns false" do + expect(tag_rule.send(:tags_match?, nil)).to be false + end + end + + context "when the variant is not nil" do + let(:variant_object) { { tag_list: ["member","local","volunteer"] } } + + context "when the rule has no preferred variant tags specified" do + before { allow(tag_rule).to receive(:preferred_variant_tags) { "" } } + it { expect(tag_rule.send(:tags_match?, variant_object)).to be false } + end + + context "when the rule has preferred variant tags specified that match ANY of the variant tags" do + before { allow(tag_rule).to receive(:preferred_variant_tags) { "wholesale,some_tag,member" } } + it { expect(tag_rule.send(:tags_match?, variant_object)).to be true } + end + + context "when the rule has preferred variant tags specified that match NONE of the variant tags" do + before { allow(tag_rule).to receive(:preferred_variant_tags) { "wholesale,some_tag,some_other_tag" } } + it { expect(tag_rule.send(:tags_match?, variant_object)).to be false } + end + end + end + + describe "applying the rule" do + # Assume that all validation is done by the TagRule base class + + let(:product1) { { name: "product1", variants: [{ name: "v1", tag_list: ["tag1", "something", "somethingelse"]}] } } + let(:product2) { { name: "product2", variants: [{ name: "v2", tag_list: ["tag2"]}] } } + let(:product3) { { name: "product3", variants: [{ name: "v3", tag_list: ["tag3"]}] } } + let!(:product_hash) { [product1, product2, product3] } + + before do + tag_rule.update_attribute(:preferred_variant_tags, "tag2") + tag_rule.set_context(product_hash, nil) + end + + context "apply!" do + context "when showing matching variants" do + before { tag_rule.update_attribute(:preferred_matched_variants_visibility, "visible") } + it "does nothing" do + tag_rule.send(:apply!) + expect(product_hash).to eq [product1, product2, product3] + end + end + + context "when hiding matching variants" do + before { tag_rule.update_attribute(:preferred_matched_variants_visibility, "hidden") } + it "removes matching variants from the list" do + tag_rule.send(:apply!) + expect(product_hash).to eq [product1, product3] + end + end + end + + context "apply_default!" do + context "when showing matching variants" do + before { tag_rule.update_attribute(:preferred_matched_variants_visibility, "visible") } + it "remove matching variants from the list" do + tag_rule.send(:apply_default!) + expect(product_hash).to eq [product1, product3] + end + end + + context "when hiding matching variants" do + before { tag_rule.update_attribute(:preferred_matched_variants_visibility, "hidden") } + it "does nothing" do + tag_rule.send(:apply_default!) + expect(product_hash).to eq [product1, product2, product3] + end + end + end + end +end diff --git a/spec/models/tag_rule/filter_shipping_methods_spec.rb b/spec/models/tag_rule/filter_shipping_methods_spec.rb index 539aa3c6ca..ae05478587 100644 --- a/spec/models/tag_rule/filter_shipping_methods_spec.rb +++ b/spec/models/tag_rule/filter_shipping_methods_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe TagRule::DiscountOrder, type: :model do +describe TagRule::FilterShippingMethods, type: :model do let!(:tag_rule) { create(:filter_shipping_methods_tag_rule) } describe "determining whether tags match for a given shipping method" do From 8b8da33ffcda77b5b00334b55787482c0861b9fe Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 21 Apr 2016 14:12:06 +1000 Subject: [PATCH 065/110] Further refinements to FilterProducts tag rule --- app/models/tag_rule/filter_products.rb | 12 ++++++------ spec/models/tag_rule/filter_products_spec.rb | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/models/tag_rule/filter_products.rb b/app/models/tag_rule/filter_products.rb index e087d8737a..f9cba66d62 100644 --- a/app/models/tag_rule/filter_products.rb +++ b/app/models/tag_rule/filter_products.rb @@ -10,8 +10,8 @@ class TagRule::FilterProducts < TagRule def apply! unless preferred_matched_variants_visibility == "visible" subject.reject! do |product| - product[:variants].reject!{ |v| tags_match?(v) } - product[:variants].empty? + product["variants"].reject!{ |v| tags_match?(v) } + product["variants"].empty? end end end @@ -19,19 +19,19 @@ class TagRule::FilterProducts < TagRule def apply_default! if preferred_matched_variants_visibility == "visible" subject.reject! do |product| - product[:variants].reject!{ |v| tags_match?(v) } - product[:variants].empty? + product["variants"].reject!{ |v| tags_match?(v) } + product["variants"].empty? end end end def tags_match?(variant) - variant_tags = variant.andand[:tag_list] || [] + variant_tags = variant.andand["tag_list"] || [] preferred_tags = preferred_variant_tags.split(",") ( variant_tags & preferred_tags ).any? end def subject_class_matches? - subject.class == HashWithIndifferentAccess + subject.class == Array end end diff --git a/spec/models/tag_rule/filter_products_spec.rb b/spec/models/tag_rule/filter_products_spec.rb index 2f1d9ec495..3cc47ea127 100644 --- a/spec/models/tag_rule/filter_products_spec.rb +++ b/spec/models/tag_rule/filter_products_spec.rb @@ -4,7 +4,7 @@ describe TagRule::FilterProducts, type: :model do let!(:tag_rule) { create(:filter_products_tag_rule) } describe "determining whether tags match for a given variant" do - context "when the variantm is nil" do + context "when the variant is nil" do it "returns false" do expect(tag_rule.send(:tags_match?, nil)).to be false @@ -12,7 +12,7 @@ describe TagRule::FilterProducts, type: :model do end context "when the variant is not nil" do - let(:variant_object) { { tag_list: ["member","local","volunteer"] } } + let(:variant_object) { { "tag_list" => ["member","local","volunteer"] } } context "when the rule has no preferred variant tags specified" do before { allow(tag_rule).to receive(:preferred_variant_tags) { "" } } @@ -34,14 +34,14 @@ describe TagRule::FilterProducts, type: :model do describe "applying the rule" do # Assume that all validation is done by the TagRule base class - let(:product1) { { name: "product1", variants: [{ name: "v1", tag_list: ["tag1", "something", "somethingelse"]}] } } - let(:product2) { { name: "product2", variants: [{ name: "v2", tag_list: ["tag2"]}] } } - let(:product3) { { name: "product3", variants: [{ name: "v3", tag_list: ["tag3"]}] } } + let(:product1) { { name: "product1", "variants" => [{ name: "v1", "tag_list" => ["tag1", "something", "somethingelse"]}] } } + let(:product2) { { name: "product2", "variants" => [{ name: "v2", "tag_list" => ["tag2"]}] } } + let(:product3) { { name: "product3", "variants" => [{ name: "v3", "tag_list" => ["tag3"]}] } } let!(:product_hash) { [product1, product2, product3] } before do tag_rule.update_attribute(:preferred_variant_tags, "tag2") - tag_rule.set_context(product_hash, nil) + tag_rule.set_context({subject: product_hash}) end context "apply!" do From 50738f28e9f5180eed650e521507a7c6ff0a9faf Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 21 Apr 2016 14:17:44 +1000 Subject: [PATCH 066/110] Refactoring tag rule application To allow rules to be loaded and counted before being checked for relevance --- app/helpers/enterprises_helper.rb | 6 ++- app/models/enterprise.rb | 12 +++-- app/models/spree/order_decorator.rb | 2 +- app/models/tag_rule.rb | 14 ++++-- spec/helpers/injection_helper_spec.rb | 2 +- spec/models/enterprise_spec.rb | 50 +++++++++++++++++++ spec/models/tag_rule/discount_order_spec.rb | 6 +-- .../tag_rule/filter_shipping_methods_spec.rb | 2 +- spec/models/tag_rule_spec.rb | 34 +++++++------ 9 files changed, 97 insertions(+), 31 deletions(-) diff --git a/app/helpers/enterprises_helper.rb b/app/helpers/enterprises_helper.rb index 93b7c96f95..b4375b2577 100644 --- a/app/helpers/enterprises_helper.rb +++ b/app/helpers/enterprises_helper.rb @@ -6,7 +6,11 @@ module EnterprisesHelper def available_shipping_methods shipping_methods = current_distributor.shipping_methods if current_distributor.present? - current_distributor.apply_tag_rules_to(shipping_methods, customer: current_order.customer) + current_distributor.apply_tag_rules( + type: "FilterShippingMethods", + subject: shipping_methods, + customer_tags: current_order.andand.customer.andand.tag_list + ) end shipping_methods.uniq end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 9f839f9d08..fbff30b153 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -344,9 +344,9 @@ class Enterprise < ActiveRecord::Base abn.present? end - def apply_tag_rules_to(subject, context) - tag_rules.each do |rule| - rule.set_context(subject,context) + def apply_tag_rules(context) + tag_rules_for(context).each do |rule| + rule.set_context(context) rule.apply end end @@ -459,4 +459,10 @@ class Enterprise < ActiveRecord::Base def initialize_permalink self.permalink = Enterprise.find_available_permalink(name) end + + def tag_rules_for(context) + rules = context[:rules] || tag_rules + return rules unless context[:type] + rules.merge(TagRule.of_type(context[:type])) + end end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index ad1a877455..ee485e4afd 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -180,7 +180,7 @@ Spree::Order.class_eval do end if distributor.present? && customer.present? - distributor.apply_tag_rules_to(self, customer: customer) + distributor.apply_tag_rules(type: "DiscountOrder", subject: self, customer_tags: customer.andand.tag_list) end end end diff --git a/app/models/tag_rule.rb b/app/models/tag_rule.rb index 8d385673db..ce8cc7183d 100644 --- a/app/models/tag_rule.rb +++ b/app/models/tag_rule.rb @@ -9,11 +9,15 @@ class TagRule < ActiveRecord::Base attr_accessible :enterprise, :enterprise_id, :preferred_customer_tags - scope :for, lambda { |enterprises| where(enterprise_id: enterprises) } + scope :for, ->(enterprise) { where(enterprise_id: enterprise) } + scope :of_type, ->(type) { where(type: "TagRule::#{type}") } + + def set_context(context) + raise "Context for tag rule cannot be nil" if context.nil? + raise "Subject for tag rule cannot be nil" if context[:subject].nil? - def set_context(subject, context) - @subject = subject @context = context + @subject = context[:subject] end def apply @@ -50,8 +54,8 @@ class TagRule < ActiveRecord::Base end def customer_tags_match? - context_customer_tags = context.andand[:customer].andand.tag_list || [] + context_customer_tags = context[:customer_tags] || [] preferred_tags = preferred_customer_tags.split(",") - ( context_customer_tags & preferred_tags ).any? + (context_customer_tags & preferred_tags).any? end end diff --git a/spec/helpers/injection_helper_spec.rb b/spec/helpers/injection_helper_spec.rb index 3b62fb2660..d5ec204286 100644 --- a/spec/helpers/injection_helper_spec.rb +++ b/spec/helpers/injection_helper_spec.rb @@ -30,7 +30,7 @@ describe InjectionHelper do shipping_methods = double(:shipping_methods, uniq: [sm]) current_distributor = double(:distributor, shipping_methods: shipping_methods) allow(helper).to receive(:current_distributor) { current_distributor } - allow(current_distributor).to receive(:apply_tag_rules_to).with(shipping_methods, {customer: nil} ) + allow(current_distributor).to receive(:apply_tag_rules).with({type: "FilterShippingMethods", subject: shipping_methods, customer_tags: nil}) helper.inject_available_shipping_methods.should match sm.id.to_s helper.inject_available_shipping_methods.should match sm.compute_amount(order).to_s end diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index b8360094ec..f0fd6bca4c 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -859,4 +859,54 @@ describe Enterprise do end end end + + describe "finding tag rules" do + let(:enterprise) { create(:enterprise) } + let(:tag_rule1) { create(:filter_products_tag_rule, enterprise: enterprise) } + let(:tag_rule2) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise) } + let(:tag_rule3) { create(:tag_rule, enterprise: enterprise) } + let(:context) { { subject: "something" } } + + context "when a set of rules are passed in with the context" do + let(:tag_rules) { TagRule.where(id: [tag_rule1.id, tag_rule3.id]) } + before { context[:rules] = tag_rules } + + context "when a rule type has been passed in with the context" do + before { context[:type] = "FilterProducts" } + + it "returns rules of the specified type from within the list of passed-in rules" do + rules = enterprise.send(:tag_rules_for, context) + expect(rules).to include tag_rule1 + expect(rules).to_not include tag_rule2, tag_rule3 + end + end + + context "when a rule type has not been passed in with the context" do + it "returns the list of passed-in rules" do + rules = enterprise.send(:tag_rules_for, context) + expect(rules).to include tag_rule1, tag_rule3 + expect(rules).to_not include tag_rule2 + end + end + end + + context "when a set of rules are not passed in with the context" do + context "when a rule type has been passed in with the context" do + before { context[:type] = "FilterShippingMethods" } + + it "returns rules of the specified type belonging to the enterprise" do + rules = enterprise.send(:tag_rules_for, context) + expect(rules).to include tag_rule2 + expect(rules).to_not include tag_rule1, tag_rule3 + end + end + + context "when a rule type has not been passed in with the context" do + it "returns all rules belonging to the enterprise" do + rules = enterprise.send(:tag_rules_for, context) + expect(rules).to include tag_rule1, tag_rule2, tag_rule3 + end + end + end + end end diff --git a/spec/models/tag_rule/discount_order_spec.rb b/spec/models/tag_rule/discount_order_spec.rb index dab901dfbf..efb2d6e12c 100644 --- a/spec/models/tag_rule/discount_order_spec.rb +++ b/spec/models/tag_rule/discount_order_spec.rb @@ -7,7 +7,7 @@ describe TagRule::DiscountOrder, type: :model do let(:subject) { double(:subject) } before do - tag_rule.set_context(subject,{}) + tag_rule.set_context({subject: subject}) allow(tag_rule).to receive(:customer_tags_match?) { true } allow(subject).to receive(:class) { Spree::Order } end @@ -34,7 +34,7 @@ describe TagRule::DiscountOrder, type: :model do let!(:adjustment) { order.adjustments.create({:amount => 12.34, :source => order, :originator => tag_rule, :label => 'discount' }, :without_protection => true) } before do - tag_rule.set_context(order, nil) + tag_rule.set_context({subject: order}) end context "where adjustments originating from the rule already exist" do @@ -56,7 +56,7 @@ describe TagRule::DiscountOrder, type: :model do before do order.update_distribution_charge! tag_rule.calculator.update_attribute(:preferred_flat_percent, -10.00) - tag_rule.set_context(order, nil) + tag_rule.set_context({subject: order}) end context "in a simple scenario" do diff --git a/spec/models/tag_rule/filter_shipping_methods_spec.rb b/spec/models/tag_rule/filter_shipping_methods_spec.rb index ae05478587..7783e27b18 100644 --- a/spec/models/tag_rule/filter_shipping_methods_spec.rb +++ b/spec/models/tag_rule/filter_shipping_methods_spec.rb @@ -41,7 +41,7 @@ describe TagRule::FilterShippingMethods, type: :model do before do tag_rule.update_attribute(:preferred_shipping_method_tags, "tag2") - tag_rule.set_context(shipping_methods, nil) + tag_rule.set_context({subject: shipping_methods}) end context "apply!" do diff --git a/spec/models/tag_rule_spec.rb b/spec/models/tag_rule_spec.rb index 549c2f88aa..49b3e90ee2 100644 --- a/spec/models/tag_rule_spec.rb +++ b/spec/models/tag_rule_spec.rb @@ -11,9 +11,18 @@ describe TagRule, type: :model do describe 'setting the context' do let(:subject) { double(:subject) } - let(:context) { double(:context) } + let(:context) { { subject: subject, some_other_property: "yay"} } + + it "raises an error when context is nil" do + expect{ tag_rule.set_context(nil) }.to raise_error "Context for tag rule cannot be nil" + end + + it "raises an error when subject is nil" do + expect{ tag_rule.set_context({}) }.to raise_error "Subject for tag rule cannot be nil" + end + it "stores the subject and context provided as instance variables on the model" do - tag_rule.set_context(subject, context) + tag_rule.set_context(context) expect(tag_rule.subject).to eq subject expect(tag_rule.context).to eq context expect(tag_rule.instance_variable_get(:@subject)).to eq subject @@ -32,7 +41,7 @@ describe TagRule, type: :model do let(:subject) { double(:subject) } before do - tag_rule.set_context(subject,{}) + tag_rule.set_context({subject: subject}) allow(tag_rule).to receive(:customer_tags_match?) { :customer_tags_match_result } allow(tag_rule).to receive(:subject_class) { Spree::Order} end @@ -93,27 +102,20 @@ describe TagRule, type: :model do end describe "determining whether specified customer tags match the given context" do - context "when the context is nil" do - before { tag_rule.set_context(nil, nil) } - it "returns false" do - expect(tag_rule.send(:customer_tags_match?)).to be false - end - end + context "when the context has no customer tags specified" do + let(:context) { { subject: double(:something), not_tags: double(:not_tags) } } - context "when the context has no customer specified" do - let(:context) { { something_that_is_not_a_customer: double(:something) } } - - before { tag_rule.set_context(nil, context) } + before { tag_rule.set_context(context) } it "returns false" do expect(tag_rule.send(:customer_tags_match?)).to be false end end - context "when the context has a customer specified" do - let(:context) { { customer: double(:customer, tag_list: ["member","local","volunteer"] ) } } + context "when the context has customer tags specified" do + let(:context) { { subject: double(:something), customer_tags: ["member","local","volunteer"] } } - before { tag_rule.set_context(nil, context) } + before { tag_rule.set_context(context) } context "when the rule has no preferred customer tags specified" do before do From cc6ef7b8f59eabeb713d5cfc85a689354442ebc0 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 21 Apr 2016 14:26:26 +1000 Subject: [PATCH 067/110] Load uncached products json into shop when in testing or development environment --- .../cached_products_renderer.rb | 16 ++- .../cached_products_renderer_spec.rb | 128 ++++++++++-------- 2 files changed, 83 insertions(+), 61 deletions(-) diff --git a/lib/open_food_network/cached_products_renderer.rb b/lib/open_food_network/cached_products_renderer.rb index 2174bf4094..c605f47a05 100644 --- a/lib/open_food_network/cached_products_renderer.rb +++ b/lib/open_food_network/cached_products_renderer.rb @@ -16,13 +16,17 @@ module OpenFoodNetwork def products_json raise NoProducts.new if @distributor.nil? || @order_cycle.nil? - products_json = Rails.cache.fetch("products-json-#{@distributor.id}-#{@order_cycle.id}") do - log_warning + products_json = unless Rails.env.production? || Rails.env.staging? + uncached_products_json + else + Rails.cache.fetch("products-json-#{@distributor.id}-#{@order_cycle.id}") do + log_warning - begin - uncached_products_json - rescue ProductsRenderer::NoProducts - nil + begin + uncached_products_json + rescue ProductsRenderer::NoProducts + nil + end end end diff --git a/spec/lib/open_food_network/cached_products_renderer_spec.rb b/spec/lib/open_food_network/cached_products_renderer_spec.rb index 03b0ab05d5..6e096fc6d1 100644 --- a/spec/lib/open_food_network/cached_products_renderer_spec.rb +++ b/spec/lib/open_food_network/cached_products_renderer_spec.rb @@ -8,70 +8,88 @@ module OpenFoodNetwork let(:order_cycle) { double(:order_cycle, id: 456) } let(:cpr) { CachedProductsRenderer.new(distributor, order_cycle) } - describe "when the distribution is not set" do - let(:cpr) { CachedProductsRenderer.new(nil, nil) } - - it "raises an exception and returns no products" do - expect { cpr.products_json }.to raise_error CachedProductsRenderer::NoProducts - end - end - - describe "when the products JSON is already cached" do - before do - Rails.cache.write "products-json-#{distributor.id}-#{order_cycle.id}", 'products' - end - - it "returns the cached JSON" do - expect(cpr.products_json).to eq 'products' - end - - it "raises an exception when there are no products" do - Rails.cache.write "products-json-#{distributor.id}-#{order_cycle.id}", nil - expect { cpr.products_json }.to raise_error CachedProductsRenderer::NoProducts - end - end - - describe "when the products JSON is not cached" do - let(:cached_json) { Rails.cache.read "products-json-#{distributor.id}-#{order_cycle.id}" } - let(:cache_present) { Rails.cache.exist? "products-json-#{distributor.id}-#{order_cycle.id}" } - - before do - Rails.cache.clear - cpr.stub(:uncached_products_json) { 'fresh products' } - end - - describe "when there are products" do - it "returns products as JSON" do - expect(cpr.products_json).to eq 'fresh products' + describe "fetching cached products JSON" do + context "when in testing / development" do + before do + allow(cpr).to receive(:uncached_products_json) { "uncached products" } end - it "caches the JSON" do - cpr.products_json - expect(cached_json).to eq 'fresh products' - end - - it "logs a warning" do - cpr.should_receive :log_warning - cpr.products_json + it "returns uncaches products JSON" do + expect(cpr.products_json).to eq 'uncached products' end end - describe "when there are no products" do - before { cpr.stub(:uncached_products_json).and_raise ProductsRenderer::NoProducts } - - it "raises an error" do - expect { cpr.products_json }.to raise_error CachedProductsRenderer::NoProducts + context "when in production / staging" do + before do + allow(Rails.env).to receive(:production?) { true } end - it "caches the products as nil" do - expect { cpr.products_json }.to raise_error CachedProductsRenderer::NoProducts - expect(cache_present).to be - expect(cached_json).to be_nil + describe "when the distribution is not set" do + let(:cpr) { CachedProductsRenderer.new(nil, nil) } + + it "raises an exception and returns no products" do + expect { cpr.products_json }.to raise_error CachedProductsRenderer::NoProducts + end end - it "logs a warning" do - cpr.should_receive :log_warning - expect { cpr.products_json }.to raise_error CachedProductsRenderer::NoProducts + describe "when the products JSON is already cached" do + before do + Rails.cache.write "products-json-#{distributor.id}-#{order_cycle.id}", 'products' + end + + it "returns the cached JSON" do + expect(cpr.products_json).to eq 'products' + end + + it "raises an exception when there are no products" do + Rails.cache.write "products-json-#{distributor.id}-#{order_cycle.id}", nil + expect { cpr.products_json }.to raise_error CachedProductsRenderer::NoProducts + end + end + + describe "when the products JSON is not cached" do + let(:cached_json) { Rails.cache.read "products-json-#{distributor.id}-#{order_cycle.id}" } + let(:cache_present) { Rails.cache.exist? "products-json-#{distributor.id}-#{order_cycle.id}" } + + before do + Rails.cache.clear + cpr.stub(:uncached_products_json) { 'fresh products' } + end + + describe "when there are products" do + it "returns products as JSON" do + expect(cpr.products_json).to eq 'fresh products' + end + + it "caches the JSON" do + cpr.products_json + expect(cached_json).to eq 'fresh products' + end + + it "logs a warning" do + cpr.should_receive :log_warning + cpr.products_json + end + end + + describe "when there are no products" do + before { cpr.stub(:uncached_products_json).and_raise ProductsRenderer::NoProducts } + + it "raises an error" do + expect { cpr.products_json }.to raise_error CachedProductsRenderer::NoProducts + end + + it "caches the products as nil" do + expect { cpr.products_json }.to raise_error CachedProductsRenderer::NoProducts + expect(cache_present).to be + expect(cached_json).to be_nil + end + + it "logs a warning" do + cpr.should_receive :log_warning + expect { cpr.products_json }.to raise_error CachedProductsRenderer::NoProducts + end + end end end end From 302bdfd62861c93dd17e84f82b1fbe459634466a Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 21 Apr 2016 14:40:52 +1000 Subject: [PATCH 068/110] Implementing FilterProducts rules in the frontend --- app/controllers/shop_controller.rb | 27 +++++- app/serializers/api/variant_serializer.rb | 5 +- lib/open_food_network/scope_product_to_hub.rb | 2 +- lib/open_food_network/scope_variant_to_hub.rb | 4 + spec/controllers/shop_controller_spec.rb | 88 ++++++++++++++++++- 5 files changed, 121 insertions(+), 5 deletions(-) diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index 55b6bcc02b..b0324f14e0 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -11,7 +11,11 @@ class ShopController < BaseController def products begin - products_json = OpenFoodNetwork::CachedProductsRenderer.new(current_distributor, current_order_cycle).products_json + renderer = OpenFoodNetwork::CachedProductsRenderer.new(current_distributor, current_order_cycle) + + # If we add any more filtering logic, we should probably + # move it all to a lib class like 'CachedProductsFilterer' + products_json = filtered_json(renderer.products_json) render json: products_json @@ -33,4 +37,25 @@ class ShopController < BaseController end end + private + + def filtered_json(products_json) + tag_rules = relevant_tag_rules + return apply_tag_rules(tag_rules, products_json) if tag_rules.any? + products_json + end + + def apply_tag_rules(tag_rules, products_json) + products_hash = JSON.parse(products_json) + current_distributor.apply_tag_rules( + rules: tag_rules, + subject: products_hash, + customer_tags: current_order.andand.customer.andand.tag_list || [] + ) + JSON.unparse(products_hash) + end + + def relevant_tag_rules + TagRule.for(current_distributor).of_type("FilterProducts") + end end diff --git a/app/serializers/api/variant_serializer.rb b/app/serializers/api/variant_serializer.rb index 6908eaf84f..6a38fe9b93 100644 --- a/app/serializers/api/variant_serializer.rb +++ b/app/serializers/api/variant_serializer.rb @@ -1,6 +1,7 @@ class Api::VariantSerializer < ActiveModel::Serializer - attributes :id, :is_master, :count_on_hand, :name_to_display, :unit_to_display, - :options_text, :on_demand, :price, :fees, :price_with_fees, :product_name + attributes :id, :is_master, :count_on_hand, :name_to_display, :unit_to_display + attributes :options_text, :on_demand, :price, :fees, :price_with_fees, :product_name + attributes :tag_list def price object.price diff --git a/lib/open_food_network/scope_product_to_hub.rb b/lib/open_food_network/scope_product_to_hub.rb index d3e018f295..98ffbd84ff 100644 --- a/lib/open_food_network/scope_product_to_hub.rb +++ b/lib/open_food_network/scope_product_to_hub.rb @@ -4,7 +4,7 @@ module OpenFoodNetwork class ScopeProductToHub def initialize(hub) @hub = hub - @variant_overrides = VariantOverride.indexed @hub + @variant_overrides = VariantOverride.indexed(@hub) end def scope(product) diff --git a/lib/open_food_network/scope_variant_to_hub.rb b/lib/open_food_network/scope_variant_to_hub.rb index 37a2455616..dadcfbd9a8 100644 --- a/lib/open_food_network/scope_variant_to_hub.rb +++ b/lib/open_food_network/scope_variant_to_hub.rb @@ -50,6 +50,10 @@ module OpenFoodNetwork def sku @variant_override.andand.sku || super end + + def tag_list + @variant_override.andand.tag_list || [] + end end end end diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index a4ddab3ad3..bd0dfaae78 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -8,7 +8,6 @@ describe ShopController do response.should redirect_to root_path end - describe "with a distributor in place" do before do controller.stub(:current_distributor).and_return distributor @@ -94,5 +93,92 @@ describe ShopController do end end end + + describe "determining rule relevance" do + let(:products_json) { double(:products_json) } + + before do + # allow(controller).to receive(:products_json) { products_json } + allow(controller).to receive(:relevant_tag_rules) { relevant_tag_rules } + allow(controller).to receive(:apply_tag_rules) { "some filtered json" } + end + + context "when no relevant rules exist" do + let(:relevant_tag_rules) { [] } + + before { allow(controller).to receive(:relevant_rules) { relevant_rules } } + + it "does not attempt to apply any rules" do + controller.send(:filtered_json, products_json) + expect(expect(controller).to_not have_received(:apply_tag_rules)) + end + + it "returns products as JSON" do + expect(controller.send(:filtered_json, products_json)).to eq products_json + end + end + + context "when relevant rules exist" do + let(:tag_rule) { create(:filter_products_tag_rule, preferred_customer_tags: "tag1", preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "hidden" ) } + let(:relevant_tag_rules) { [tag_rule] } + + it "attempts to apply any rules" do + controller.send(:filtered_json, products_json) + expect(controller).to have_received(:apply_tag_rules).with(relevant_tag_rules, products_json) + end + + it "returns filtered JSON" do + expect(controller.send(:filtered_json, products_json)).to eq "some filtered json" + end + end + end + + describe "applying tag rules" do + let(:product1) { { id: 1, name: 'product 1', "variants" => [{ id: 4, "tag_list" => ["tag1"] }] } } + let(:product2) { { id: 2, name: 'product 2', "variants" => [{ id: 5, "tag_list" => ["tag1"] }, {id: 9, "tag_list" => ["tag2"]}] } } + let(:product3) { { id: 3, name: 'product 3', "variants" => [{ id: 6, "tag_list" => ["tag3"] }] } } + let!(:products_array) { [product1, product2, product3] } + let!(:products_json) { JSON.unparse( products_array ) } + let(:tag_rule) { create(:filter_products_tag_rule, preferred_customer_tags: "tag1", preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "hidden" ) } + let(:relevant_tag_rules) { [tag_rule] } + + before do + allow(controller).to receive(:current_order) { order } + allow(tag_rule).to receive(:set_context) + allow(tag_rule).to receive(:apply) + allow(distributor).to receive(:apply_tag_rules).and_call_original + end + + context "when a current order with a customer does not exist" do + let(:order) { double(:order, customer: nil) } + + it "sets the context customer_tags as an empty array" do + controller.send(:apply_tag_rules, relevant_tag_rules, products_json) + expect(distributor).to have_received(:apply_tag_rules).with(rules: relevant_tag_rules, subject: JSON.parse(products_json), :customer_tags=>[]) + end + end + + context "when a customer does exist" do + let(:order) { double(:order, customer: double(:customer, tag_list: ["tag1", "tag2"])) } + + it "sets the context customer_tags" do + controller.send(:apply_tag_rules, relevant_tag_rules, products_json) + expect(distributor).to have_received(:apply_tag_rules).with(rules: relevant_tag_rules, subject: JSON.parse(products_json), :customer_tags=>["tag1", "tag2"]) + end + + context "applies the rule" do + before do + allow(tag_rule).to receive(:set_context).and_call_original + allow(tag_rule).to receive(:apply).and_call_original + end + + it "applies the rule" do + result = controller.send(:apply_tag_rules, relevant_tag_rules, products_json) + expect(tag_rule).to have_received(:apply) + expect(result).to eq JSON.unparse([{ id: 2, name: 'product 2', variants: [{id: 9, tag_list: ["tag2"]}] }, product3]) + end + end + end + end end end From 48d4c8733df15895a65a721e8d04ea8d015f6ff2 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 21 Apr 2016 14:42:34 +1000 Subject: [PATCH 069/110] When we load up incomplete orders, make sure that they have a user associated if the user is logged in Also associate customer ONLY if one already exists. This is required to prevent unauthorised access to customer-only shopfronts. --- app/controllers/enterprises_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index fe2eda8bfd..14564d1129 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -47,6 +47,11 @@ class EnterprisesController < BaseController order.distributor = distributor + if user = try_spree_current_user + order.associate_user!(user) if (order.user.blank? || order.email.blank?) + order.send(:associate_customer) if order.customer.nil? # Only associates existing customers + end + order_cycle_options = OrderCycle.active.with_distributor(distributor) order.order_cycle = order_cycle_options.first if order_cycle_options.count == 1 order.save! From 1cb51b5c71f8bd23de30212427290ab20d4651d1 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 22 Apr 2016 16:14:05 +1000 Subject: [PATCH 070/110] CodeClimate fixes for filter-products branch --- .codeclimate.yml | 3 ++ app/controllers/enterprises_controller.rb | 22 +++++--- app/models/enterprise.rb | 2 +- app/models/tag_rule.rb | 2 +- app/models/tag_rule/filter_products.rb | 52 ++++++++++--------- .../api/admin/variant_override_serializer.rb | 2 +- .../cached_products_renderer.rb | 34 ++++++------ spec/controllers/shop_controller_spec.rb | 4 +- spec/models/tag_rule/discount_order_spec.rb | 6 +-- spec/models/tag_rule/filter_products_spec.rb | 2 +- .../tag_rule/filter_shipping_methods_spec.rb | 2 +- spec/models/tag_rule_spec.rb | 12 ++--- 12 files changed, 81 insertions(+), 62 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index d737c0b3c9..dc2ebef051 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,6 +1,9 @@ engines: rubocop: enabled: true + exclude_fingerprints: + - ac41db8d4ec4cbf508c353d9b65a024f + - 8e3b6322aef5be9f38700b3fd0cd347e scss-lint: enabled: true ratings: diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 14564d1129..7f627ec73a 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -40,20 +40,30 @@ class EnterprisesController < BaseController distributor = Enterprise.is_distributor.find_by_permalink(params[:id]) || Enterprise.is_distributor.find(params[:id]) order = current_order(true) + reset_distributor(order, distributor) + + reset_user_and_customer(order) if try_spree_current_user + + reset_order_cycle(order, distributor) + + order.save! + end + + def reset_distributor(order, distributor) if order.distributor && order.distributor != distributor order.empty! order.set_order_cycle! nil end - order.distributor = distributor + end - if user = try_spree_current_user - order.associate_user!(user) if (order.user.blank? || order.email.blank?) - order.send(:associate_customer) if order.customer.nil? # Only associates existing customers - end + def reset_user_and_customer(order) + order.associate_user!(spree_current_user) if order.user.blank? || order.email.blank? + order.send(:associate_customer) if order.customer.nil? # Only associates existing customers + end + def reset_order_cycle(order, distributor) order_cycle_options = OrderCycle.active.with_distributor(distributor) order.order_cycle = order_cycle_options.first if order_cycle_options.count == 1 - order.save! end end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index fbff30b153..f7e5a4b6f3 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -346,7 +346,7 @@ class Enterprise < ActiveRecord::Base def apply_tag_rules(context) tag_rules_for(context).each do |rule| - rule.set_context(context) + rule.context = context rule.apply end end diff --git a/app/models/tag_rule.rb b/app/models/tag_rule.rb index ce8cc7183d..23659bbf0a 100644 --- a/app/models/tag_rule.rb +++ b/app/models/tag_rule.rb @@ -12,7 +12,7 @@ class TagRule < ActiveRecord::Base scope :for, ->(enterprise) { where(enterprise_id: enterprise) } scope :of_type, ->(type) { where(type: "TagRule::#{type}") } - def set_context(context) + def context=(context) raise "Context for tag rule cannot be nil" if context.nil? raise "Subject for tag rule cannot be nil" if context[:subject].nil? diff --git a/app/models/tag_rule/filter_products.rb b/app/models/tag_rule/filter_products.rb index f9cba66d62..9df35e00a0 100644 --- a/app/models/tag_rule/filter_products.rb +++ b/app/models/tag_rule/filter_products.rb @@ -1,37 +1,39 @@ -class TagRule::FilterProducts < TagRule - preference :matched_variants_visibility, :string, default: "visible" - preference :variant_tags, :string, default: "" +class TagRule + class FilterProducts < TagRule + preference :matched_variants_visibility, :string, default: "visible" + preference :variant_tags, :string, default: "" - attr_accessible :preferred_matched_variants_visibility, :preferred_variant_tags + attr_accessible :preferred_matched_variants_visibility, :preferred_variant_tags - private + private - # Warning: this should only EVER be called via TagRule#apply - def apply! - unless preferred_matched_variants_visibility == "visible" - subject.reject! do |product| - product["variants"].reject!{ |v| tags_match?(v) } - product["variants"].empty? + # Warning: this should only EVER be called via TagRule#apply + def apply! + unless preferred_matched_variants_visibility == "visible" + subject.reject! do |product| + product["variants"].reject! { |v| tags_match?(v) } + product["variants"].empty? + end end end - end - def apply_default! - if preferred_matched_variants_visibility == "visible" - subject.reject! do |product| - product["variants"].reject!{ |v| tags_match?(v) } - product["variants"].empty? + def apply_default! + if preferred_matched_variants_visibility == "visible" + subject.reject! do |product| + product["variants"].reject! { |v| tags_match?(v) } + product["variants"].empty? + end end end - end - def tags_match?(variant) - variant_tags = variant.andand["tag_list"] || [] - preferred_tags = preferred_variant_tags.split(",") - ( variant_tags & preferred_tags ).any? - end + def tags_match?(variant) + variant_tags = variant.andand["tag_list"] || [] + preferred_tags = preferred_variant_tags.split(",") + (variant_tags & preferred_tags).any? + end - def subject_class_matches? - subject.class == Array + def subject_class_matches? + subject.class == Array + end end end diff --git a/app/serializers/api/admin/variant_override_serializer.rb b/app/serializers/api/admin/variant_override_serializer.rb index 99c99eebfa..68b86818a1 100644 --- a/app/serializers/api/admin/variant_override_serializer.rb +++ b/app/serializers/api/admin/variant_override_serializer.rb @@ -7,6 +7,6 @@ class Api::Admin::VariantOverrideSerializer < ActiveModel::Serializer end def tags - object.tag_list.map{ |t| { text: t } } + object.tag_list.map { |t| { text: t } } end end diff --git a/lib/open_food_network/cached_products_renderer.rb b/lib/open_food_network/cached_products_renderer.rb index c605f47a05..eada6a7c9e 100644 --- a/lib/open_food_network/cached_products_renderer.rb +++ b/lib/open_food_network/cached_products_renderer.rb @@ -14,23 +14,11 @@ module OpenFoodNetwork end def products_json - raise NoProducts.new if @distributor.nil? || @order_cycle.nil? + raise NoProducts.new("No Products") if @distributor.nil? || @order_cycle.nil? - products_json = unless Rails.env.production? || Rails.env.staging? - uncached_products_json - else - Rails.cache.fetch("products-json-#{@distributor.id}-#{@order_cycle.id}") do - log_warning + products_json = cached_products_json - begin - uncached_products_json - rescue ProductsRenderer::NoProducts - nil - end - end - end - - raise NoProducts.new if products_json.nil? + raise NoProducts.new("No Products") if products_json.nil? products_json end @@ -44,6 +32,22 @@ module OpenFoodNetwork end end + def cached_products_json + if Rails.env.production? || Rails.env.staging? + Rails.cache.fetch("products-json-#{@distributor.id}-#{@order_cycle.id}") do + log_warning + + begin + uncached_products_json + rescue ProductsRenderer::NoProducts + nil + end + end + else + uncached_products_json + end + end + def uncached_products_json ProductsRenderer.new(@distributor, @order_cycle).products_json end diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index bd0dfaae78..669e17db5f 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -144,7 +144,7 @@ describe ShopController do before do allow(controller).to receive(:current_order) { order } - allow(tag_rule).to receive(:set_context) + allow(tag_rule).to receive(:context=) allow(tag_rule).to receive(:apply) allow(distributor).to receive(:apply_tag_rules).and_call_original end @@ -168,7 +168,7 @@ describe ShopController do context "applies the rule" do before do - allow(tag_rule).to receive(:set_context).and_call_original + allow(tag_rule).to receive(:context=).and_call_original allow(tag_rule).to receive(:apply).and_call_original end diff --git a/spec/models/tag_rule/discount_order_spec.rb b/spec/models/tag_rule/discount_order_spec.rb index efb2d6e12c..fd1a7e9251 100644 --- a/spec/models/tag_rule/discount_order_spec.rb +++ b/spec/models/tag_rule/discount_order_spec.rb @@ -7,7 +7,7 @@ describe TagRule::DiscountOrder, type: :model do let(:subject) { double(:subject) } before do - tag_rule.set_context({subject: subject}) + tag_rule.context = {subject: subject} allow(tag_rule).to receive(:customer_tags_match?) { true } allow(subject).to receive(:class) { Spree::Order } end @@ -34,7 +34,7 @@ describe TagRule::DiscountOrder, type: :model do let!(:adjustment) { order.adjustments.create({:amount => 12.34, :source => order, :originator => tag_rule, :label => 'discount' }, :without_protection => true) } before do - tag_rule.set_context({subject: order}) + tag_rule.context = {subject: order} end context "where adjustments originating from the rule already exist" do @@ -56,7 +56,7 @@ describe TagRule::DiscountOrder, type: :model do before do order.update_distribution_charge! tag_rule.calculator.update_attribute(:preferred_flat_percent, -10.00) - tag_rule.set_context({subject: order}) + tag_rule.context = {subject: order} end context "in a simple scenario" do diff --git a/spec/models/tag_rule/filter_products_spec.rb b/spec/models/tag_rule/filter_products_spec.rb index 3cc47ea127..185eb8fa47 100644 --- a/spec/models/tag_rule/filter_products_spec.rb +++ b/spec/models/tag_rule/filter_products_spec.rb @@ -41,7 +41,7 @@ describe TagRule::FilterProducts, type: :model do before do tag_rule.update_attribute(:preferred_variant_tags, "tag2") - tag_rule.set_context({subject: product_hash}) + tag_rule.context = {subject: product_hash} end context "apply!" do diff --git a/spec/models/tag_rule/filter_shipping_methods_spec.rb b/spec/models/tag_rule/filter_shipping_methods_spec.rb index 7783e27b18..598d89c798 100644 --- a/spec/models/tag_rule/filter_shipping_methods_spec.rb +++ b/spec/models/tag_rule/filter_shipping_methods_spec.rb @@ -41,7 +41,7 @@ describe TagRule::FilterShippingMethods, type: :model do before do tag_rule.update_attribute(:preferred_shipping_method_tags, "tag2") - tag_rule.set_context({subject: shipping_methods}) + tag_rule.context = {subject: shipping_methods} end context "apply!" do diff --git a/spec/models/tag_rule_spec.rb b/spec/models/tag_rule_spec.rb index 49b3e90ee2..32d2d532a4 100644 --- a/spec/models/tag_rule_spec.rb +++ b/spec/models/tag_rule_spec.rb @@ -14,15 +14,15 @@ describe TagRule, type: :model do let(:context) { { subject: subject, some_other_property: "yay"} } it "raises an error when context is nil" do - expect{ tag_rule.set_context(nil) }.to raise_error "Context for tag rule cannot be nil" + expect{ tag_rule.context = nil }.to raise_error "Context for tag rule cannot be nil" end it "raises an error when subject is nil" do - expect{ tag_rule.set_context({}) }.to raise_error "Subject for tag rule cannot be nil" + expect{ tag_rule.context = {} }.to raise_error "Subject for tag rule cannot be nil" end it "stores the subject and context provided as instance variables on the model" do - tag_rule.set_context(context) + tag_rule.context = context expect(tag_rule.subject).to eq subject expect(tag_rule.context).to eq context expect(tag_rule.instance_variable_get(:@subject)).to eq subject @@ -41,7 +41,7 @@ describe TagRule, type: :model do let(:subject) { double(:subject) } before do - tag_rule.set_context({subject: subject}) + tag_rule.context = {subject: subject} allow(tag_rule).to receive(:customer_tags_match?) { :customer_tags_match_result } allow(tag_rule).to receive(:subject_class) { Spree::Order} end @@ -105,7 +105,7 @@ describe TagRule, type: :model do context "when the context has no customer tags specified" do let(:context) { { subject: double(:something), not_tags: double(:not_tags) } } - before { tag_rule.set_context(context) } + before { tag_rule.context = context } it "returns false" do expect(tag_rule.send(:customer_tags_match?)).to be false @@ -115,7 +115,7 @@ describe TagRule, type: :model do context "when the context has customer tags specified" do let(:context) { { subject: double(:something), customer_tags: ["member","local","volunteer"] } } - before { tag_rule.set_context(context) } + before { tag_rule.context = context } context "when the rule has no preferred customer tags specified" do before do From eedb2854eca453cdc53ee4aab3b588521851aeb5 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 27 Apr 2016 15:01:52 +1000 Subject: [PATCH 071/110] Adding FilterProducts rules to tag rule UI --- .../tag_rules_controller.js.coffee | 3 +++ .../directives/new_rule_dialog.js.coffee | 5 ++-- .../tag_rules/filter_products.js.coffee | 4 +++ .../admin/tag_rules/filter_products.html.haml | 27 +++++++++++++++++++ .../api/admin/tag_rule_serializer.rb | 8 ++++++ .../enterprises/form/_tag_rules.html.haml | 1 + spec/features/admin/tag_rules_spec.rb | 25 +++++++++++++++-- 7 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_products.js.coffee create mode 100644 app/assets/javascripts/templates/admin/tag_rules/filter_products.html.haml diff --git a/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee index 7209147b66..a5d77cf7ff 100644 --- a/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee +++ b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee @@ -26,6 +26,9 @@ angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, ente when "FilterShippingMethods" newRule.peferred_shipping_method_tags = [] newRule.preferred_matched_shipping_methods_visibility = "visible" + when "FilterProducts" + newRule.peferred_variant_tags = [] + newRule.preferred_matched_variants_visibility = "visible" tagGroup.rules.push(newRule) updateRuleCounts() diff --git a/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee index 54e33006e4..c75b50bbf5 100644 --- a/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee +++ b/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee @@ -7,10 +7,11 @@ angular.module("admin.tagRules").directive 'newTagRuleDialog', ($compile, $templ scope.ruleTypes = [ # { id: "DiscountOrder", name: 'Apply a discount to orders' } - { id: "FilterShippingMethods", name: 'Show/Hide shipping methods' } + { id: "FilterProducts", name: 'Show or Hide variants in my shopfront' } + { id: "FilterShippingMethods", name: 'Show or Hide shipping methods at checkout' } ] - scope.ruleType = "DiscountOrder" + scope.ruleType = scope.ruleTypes[0].id # Set Dialog options template.dialog diff --git a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_products.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_products.js.coffee new file mode 100644 index 0000000000..58e9c47c5b --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_products.js.coffee @@ -0,0 +1,4 @@ +angular.module("admin.tagRules").directive "filterProducts", -> + restrict: "E" + replace: true + templateUrl: "admin/tag_rules/filter_products.html" diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_products.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_products.html.haml new file mode 100644 index 0000000000..0429dd7cbd --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag_rules/filter_products.html.haml @@ -0,0 +1,27 @@ +%div + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]", + ng: { value: "rule.id" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", + value: "TagRule::FilterProducts" } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", + ng: { value: "rule.preferred_customer_tags" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_variant_tags", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_variant_tags]", + ng: { value: "rule.preferred_customer_tags" } } + + %span.text-normal {{ $index + 1 }}. Variants with matching tags are + %input.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_variants_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_variants_visibility]", + ng: { model: "rule.preferred_matched_variants_visibility"}, + data: 'visibilityOptions', "min-search" => 5 } + -# %tags-with-translation{ object: "rule", "tags-attr" => "shipping_method_tags", "tag-list-attr" => "preferred_shipping_method_tags" } diff --git a/app/serializers/api/admin/tag_rule_serializer.rb b/app/serializers/api/admin/tag_rule_serializer.rb index ac333c23c6..c8fdd58c44 100644 --- a/app/serializers/api/admin/tag_rule_serializer.rb +++ b/app/serializers/api/admin/tag_rule_serializer.rb @@ -24,4 +24,12 @@ module Api::Admin::TagRule object.preferred_shipping_method_tags.split(",") end end + + class FilterProductsSerializer < BaseSerializer + attributes :preferred_matched_variants_visibility, :variant_tags + + def variant_tags + object.preferred_variant_tags.split(",") + end + end end diff --git a/app/views/admin/enterprises/form/_tag_rules.html.haml b/app/views/admin/enterprises/form/_tag_rules.html.haml index 4ed9aeb5c1..37bb699646 100644 --- a/app/views/admin/enterprises/form/_tag_rules.html.haml +++ b/app/views/admin/enterprises/form/_tag_rules.html.haml @@ -25,6 +25,7 @@ %td %discount-order{ ng: { if: "::rule.type == 'TagRule::DiscountOrder'" } } %filter-shipping-methods{ ng: { if: "::rule.type == 'TagRule::FilterShippingMethods'" } } + %filter-products{ ng: { if: "::rule.type == 'TagRule::FilterProducts'" } } %td.actions %a{ ng: { click: "deleteTagRule(tagGroup, rule)" }, :class => "delete-tag-rule icon-trash no-text" } .add_rule.text-center diff --git a/spec/features/admin/tag_rules_spec.rb b/spec/features/admin/tag_rules_spec.rb index e91215a21d..af079d3431 100644 --- a/spec/features/admin/tag_rules_spec.rb +++ b/spec/features/admin/tag_rules_spec.rb @@ -22,13 +22,19 @@ feature 'Tag Rules', js: true do find(:css, "tags-input .tags input").set "volunteer\n" # New FilterShippingMethods Rule + expect(page).to have_content 'No rules apply to this tag yet' click_button '+ Add A New Rule' - select2_select 'Show/Hide shipping methods', from: 'rule_type_selector' + select2_select 'Show or Hide shipping methods at checkout', from: 'rule_type_selector' click_button "Add Rule" select2_select "NOT VISIBLE", from: "enterprise_tag_rules_attributes_0_preferred_matched_shipping_methods_visibility" + # New FilterProducts Rule + click_button '+ Add A New Rule' + select2_select 'Show or Hide variants in my shop', from: 'rule_type_selector' + click_button "Add Rule" + select2_select "VISIBLE", from: "enterprise_tag_rules_attributes_1_preferred_matched_variants_visibility" + # New DiscountOrder Rule - # expect(page).to have_content 'No rules apply to this tag yet' # click_button '+ Add A New Rule' # select2_select 'Apply a discount to orders', from: 'rule_type_selector' # click_button "Add Rule" @@ -44,12 +50,18 @@ feature 'Tag Rules', js: true do expect(tag_rule.preferred_customer_tags).to eq "volunteer" expect(tag_rule.preferred_shipping_method_tags).to eq "volunteer" expect(tag_rule.preferred_matched_shipping_methods_visibility).to eq "hidden" + + tag_rule = TagRule::FilterProducts.last + expect(tag_rule.preferred_customer_tags).to eq "volunteer" + expect(tag_rule.preferred_variant_tags).to eq "volunteer" + expect(tag_rule.preferred_matched_variants_visibility).to eq "visible" end end context "updating" do let!(:do_tag_rule) { create(:tag_rule, enterprise: enterprise, preferred_customer_tags: "member" ) } let!(:fsm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise, preferred_matched_shipping_methods_visibility: "hidden", preferred_customer_tags: "member" ) } + let!(:fp_tag_rule) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_matched_variants_visibility: "visible", preferred_customer_tags: "member" ) } before do login_to_admin_section @@ -72,6 +84,10 @@ feature 'Tag Rules', js: true do expect(page).to have_select2 "enterprise_tag_rules_attributes_1_preferred_matched_shipping_methods_visibility", selected: 'NOT VISIBLE' select2_select 'VISIBLE', from: "enterprise_tag_rules_attributes_1_preferred_matched_shipping_methods_visibility" + # FilterProducts rule + expect(page).to have_select2 "enterprise_tag_rules_attributes_2_preferred_matched_variants_visibility", selected: 'VISIBLE' + select2_select 'NOT VISIBLE', from: "enterprise_tag_rules_attributes_2_preferred_matched_variants_visibility" + click_button 'Update' # DiscountOrder rule @@ -82,6 +98,11 @@ feature 'Tag Rules', js: true do expect(fsm_tag_rule.preferred_customer_tags).to eq "member,volunteer" expect(fsm_tag_rule.preferred_shipping_method_tags).to eq "member,volunteer" expect(fsm_tag_rule.preferred_matched_shipping_methods_visibility).to eq "visible" + + # FilterProducts rule + expect(fp_tag_rule.preferred_customer_tags).to eq "member,volunteer" + expect(fp_tag_rule.preferred_variant_tags).to eq "member,volunteer" + expect(fp_tag_rule.preferred_matched_variants_visibility).to eq "hidden" end end From c61cb1bdcdfbfa00cf41f1e8e59e7469670ea979 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 22 Apr 2016 12:31:52 +1000 Subject: [PATCH 072/110] Renaming paymentMethodCtrl to paymentMethodsCtrl --- .../controllers/payment_method_controller.js.coffee | 4 ---- .../controllers/payment_methods_controller.js.coffee | 3 +++ app/views/admin/enterprises/form/_payment_methods.html.haml | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee create mode 100644 app/assets/javascripts/admin/payment_methods/controllers/payment_methods_controller.js.coffee diff --git a/app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee b/app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee deleted file mode 100644 index c2595faa6d..0000000000 --- a/app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee +++ /dev/null @@ -1,4 +0,0 @@ -angular.module("admin.paymentMethods") - .controller "paymentMethodCtrl", ($scope, PaymentMethods) -> - $scope.findPaymentMethodByID = (id) -> - $scope.PaymentMethod = PaymentMethods.findByID(id) diff --git a/app/assets/javascripts/admin/payment_methods/controllers/payment_methods_controller.js.coffee b/app/assets/javascripts/admin/payment_methods/controllers/payment_methods_controller.js.coffee new file mode 100644 index 0000000000..ddad6cd259 --- /dev/null +++ b/app/assets/javascripts/admin/payment_methods/controllers/payment_methods_controller.js.coffee @@ -0,0 +1,3 @@ +angular.module("admin.paymentMethods").controller "paymentMethodsCtrl", ($scope, PaymentMethods) -> + $scope.findPaymentMethodByID = (id) -> + $scope.PaymentMethod = PaymentMethods.findByID(id) diff --git a/app/views/admin/enterprises/form/_payment_methods.html.haml b/app/views/admin/enterprises/form/_payment_methods.html.haml index 8857980bd9..57d9426d03 100644 --- a/app/views/admin/enterprises/form/_payment_methods.html.haml +++ b/app/views/admin/enterprises/form/_payment_methods.html.haml @@ -7,7 +7,7 @@ %th %tbody - @payment_methods.each do |payment_method| - %tr{ ng: { controller: 'paymentMethodCtrl', init: "findPaymentMethodByID(#{payment_method.id})" } } + %tr{ ng: { controller: 'paymentMethodsCtrl', init: "findPaymentMethodByID(#{payment_method.id})" } } %td= payment_method.name %td= f.check_box :payment_method_ids, { multiple: true, 'ng-model' => 'PaymentMethod.selected' }, payment_method.id, nil %td= link_to "Edit", edit_admin_payment_method_path(payment_method) @@ -30,4 +30,4 @@ .text-center %a.button{ href: "#{new_admin_payment_method_path}"} Create One Now - %i.icon-arrow-right \ No newline at end of file + %i.icon-arrow-right From 2d666caaf2527be1d51ec4e522c477ffa6718ef3 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 22 Apr 2016 12:32:36 +1000 Subject: [PATCH 073/110] Nitpicking --- .../controllers/shipping_methods_controller.js.coffee | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/admin/shipping_methods/controllers/shipping_methods_controller.js.coffee b/app/assets/javascripts/admin/shipping_methods/controllers/shipping_methods_controller.js.coffee index 91569b2256..9efd2a5fea 100644 --- a/app/assets/javascripts/admin/shipping_methods/controllers/shipping_methods_controller.js.coffee +++ b/app/assets/javascripts/admin/shipping_methods/controllers/shipping_methods_controller.js.coffee @@ -1,4 +1,3 @@ -angular.module("admin.shippingMethods") - .controller "shippingMethodsCtrl", ($scope, ShippingMethods) -> - $scope.findShippingMethodByID = (id) -> - $scope.ShippingMethod = ShippingMethods.findByID(id) +angular.module("admin.shippingMethods").controller "shippingMethodsCtrl", ($scope, ShippingMethods) -> + $scope.findShippingMethodByID = (id) -> + $scope.ShippingMethod = ShippingMethods.findByID(id) From 385fd91e70862f63c06aea20d68d1a5e9728b11d Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 22 Apr 2016 13:31:37 +1000 Subject: [PATCH 074/110] Adding angularjs payment method controller, updating payment method edit form --- .../controllers/payment_method_controller.js.coffee | 2 ++ .../controllers/providers_controller.js.coffee | 4 ++-- .../directives/provider_prefs_for.js.coffee | 4 ++-- .../_form/replace_form_fields.html.haml.deface | 3 ++- app/views/spree/admin/payment_methods/_data.html.haml | 2 ++ app/views/spree/admin/payment_methods/_providers.html.haml | 6 ++---- .../controllers/providers_controller_decorator.js.coffee | 6 +++--- 7 files changed, 15 insertions(+), 12 deletions(-) create mode 100644 app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee rename app/assets/javascripts/admin/{ => payment_methods}/controllers/providers_controller.js.coffee (61%) rename app/assets/javascripts/admin/{ => payment_methods}/directives/provider_prefs_for.js.coffee (64%) create mode 100644 app/views/spree/admin/payment_methods/_data.html.haml diff --git a/app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee b/app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee new file mode 100644 index 0000000000..6e26b39d17 --- /dev/null +++ b/app/assets/javascripts/admin/payment_methods/controllers/payment_method_controller.js.coffee @@ -0,0 +1,2 @@ +angular.module("admin.paymentMethods").controller "paymentMethodCtrl", ($scope, paymentMethod) -> + $scope.paymentMethod = paymentMethod diff --git a/app/assets/javascripts/admin/controllers/providers_controller.js.coffee b/app/assets/javascripts/admin/payment_methods/controllers/providers_controller.js.coffee similarity index 61% rename from app/assets/javascripts/admin/controllers/providers_controller.js.coffee rename to app/assets/javascripts/admin/payment_methods/controllers/providers_controller.js.coffee index 3b3378d013..6b9669c9e7 100644 --- a/app/assets/javascripts/admin/controllers/providers_controller.js.coffee +++ b/app/assets/javascripts/admin/payment_methods/controllers/providers_controller.js.coffee @@ -1,7 +1,7 @@ -angular.module("ofn.admin").controller "ProvidersCtrl", ($scope, paymentMethod) -> +angular.module("admin.paymentMethods").controller "ProvidersCtrl", ($scope, paymentMethod) -> if paymentMethod.type $scope.include_html = "/admin/payment_methods/show_provider_preferences?" + "provider_type=#{paymentMethod.type};" + "pm_id=#{paymentMethod.id};" else - $scope.include_html = "" \ No newline at end of file + $scope.include_html = "" diff --git a/app/assets/javascripts/admin/directives/provider_prefs_for.js.coffee b/app/assets/javascripts/admin/payment_methods/directives/provider_prefs_for.js.coffee similarity index 64% rename from app/assets/javascripts/admin/directives/provider_prefs_for.js.coffee rename to app/assets/javascripts/admin/payment_methods/directives/provider_prefs_for.js.coffee index 467bad4e5f..2a0fc50f6f 100644 --- a/app/assets/javascripts/admin/directives/provider_prefs_for.js.coffee +++ b/app/assets/javascripts/admin/payment_methods/directives/provider_prefs_for.js.coffee @@ -1,7 +1,7 @@ -angular.module("ofn.admin").directive "providerPrefsFor", ($http) -> +angular.module("admin.paymentMethods").directive "providerPrefsFor", ($http) -> link: (scope, element, attrs) -> element.on "change blur load", -> scope.$apply -> scope.include_html = "/admin/payment_methods/show_provider_preferences?" + "provider_type=#{element.val()};" + - "pm_id=#{attrs.providerPrefsFor};" \ No newline at end of file + "pm_id=#{attrs.providerPrefsFor};" diff --git a/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface index 9e055ce538..21fbf95723 100644 --- a/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface +++ b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface @@ -1,6 +1,7 @@ / replace "div[data-hook='admin_payment_method_form_fields']" -%div.alpha.eleven.columns += render 'data' +%div.alpha.eleven.columns{ "ng-app" => "admin.paymentMethods", "ng-controller" => "paymentMethodCtrl" } .row .alpha.three.columns = label_tag nil, t(:name) diff --git a/app/views/spree/admin/payment_methods/_data.html.haml b/app/views/spree/admin/payment_methods/_data.html.haml new file mode 100644 index 0000000000..62a36498a4 --- /dev/null +++ b/app/views/spree/admin/payment_methods/_data.html.haml @@ -0,0 +1,2 @@ +:javascript + angular.module('admin.paymentMethods').value('paymentMethod', #{ { id: @payment_method.id, type: @payment_method.type }.to_json }) diff --git a/app/views/spree/admin/payment_methods/_providers.html.haml b/app/views/spree/admin/payment_methods/_providers.html.haml index 77b11fb473..657e21029e 100644 --- a/app/views/spree/admin/payment_methods/_providers.html.haml +++ b/app/views/spree/admin/payment_methods/_providers.html.haml @@ -1,10 +1,8 @@ -:javascript - angular.module('ofn.admin').value('paymentMethod', #{ { id: @payment_method.id, type: @payment_method.type }.to_json }) -#provider-settings{ ng: { app: "ofn.admin", controller: "ProvidersCtrl" } } +#provider-settings{ ng: { controller: "ProvidersCtrl" } } .row .alpha.three.columns = label :payment_method, :type, t(:provider) .omega.eight.columns = collection_select(:payment_method, :type, @providers, :to_s, :clean_name, (!@object.persisted? ? { :selected => "Spree::PaymentMethod::Check"} : {}), { class: 'select2 fullwidth', 'provider-prefs-for' => "#{@object.id}"}) - %div{"ng-include" => "include_html" } \ No newline at end of file + %div{"ng-include" => "include_html" } diff --git a/spec/javascripts/unit/admin/controllers/providers_controller_decorator.js.coffee b/spec/javascripts/unit/admin/controllers/providers_controller_decorator.js.coffee index 2fcd776035..4cbf017244 100644 --- a/spec/javascripts/unit/admin/controllers/providers_controller_decorator.js.coffee +++ b/spec/javascripts/unit/admin/controllers/providers_controller_decorator.js.coffee @@ -5,7 +5,7 @@ describe "ProvidersCtrl", -> describe "initialising using a payment method without a type", -> beforeEach -> - module 'ofn.admin' + module 'admin.paymentMethods' scope = {} paymentMethod = type: null @@ -18,7 +18,7 @@ describe "ProvidersCtrl", -> describe "initialising using a payment method with a type", -> beforeEach -> - module 'ofn.admin' + module 'admin.paymentMethods' scope = {} paymentMethod = type: "NOT NULL" @@ -27,4 +27,4 @@ describe "ProvidersCtrl", -> ctrl = $controller 'ProvidersCtrl', {$scope: scope, paymentMethod: paymentMethod } it "sets the include_html porperty on scope to some address", -> - expect(scope.include_html).toBe "/admin/payment_methods/show_provider_preferences?provider_type=NOT NULL;pm_id=#{paymentMethod.id};" \ No newline at end of file + expect(scope.include_html).toBe "/admin/payment_methods/show_provider_preferences?provider_type=NOT NULL;pm_id=#{paymentMethod.id};" From 4d83bf2135653a2b6c5069285096dbcdbd665f60 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 27 Apr 2016 13:36:49 +1000 Subject: [PATCH 075/110] Payment methods can be tagged --- .../payment_methods/payment_methods.js.coffee | 2 +- app/helpers/admin/injection_helper.rb | 4 ++ .../gateway/pay_pal_express_decorator.rb | 7 ++ app/models/spree/gateway_decorator.rb | 3 + app/models/spree/payment_method_decorator.rb | 4 +- .../replace_form_fields.html.haml.deface | 10 ++- .../api/admin/payment_method_serializer.rb | 11 +++ .../admin/payment_methods/_data.html.haml | 2 - spec/features/admin/payment_method_spec.rb | 71 +++++++++++-------- 9 files changed, 78 insertions(+), 36 deletions(-) create mode 100644 app/models/spree/gateway/pay_pal_express_decorator.rb create mode 100644 app/serializers/api/admin/payment_method_serializer.rb delete mode 100644 app/views/spree/admin/payment_methods/_data.html.haml diff --git a/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee b/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee index 01553647d4..c74a1c7054 100644 --- a/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee +++ b/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee @@ -1 +1 @@ -angular.module("admin.paymentMethods", []) +angular.module("admin.paymentMethods", ['ngTagsInput', 'admin.utils']) diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 079f93d41f..67105b6b34 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -23,6 +23,10 @@ module Admin admin_inject_json_ams_array "admin.paymentMethods", "paymentMethods", @payment_methods, Api::Admin::IdNameSerializer end + def admin_inject_payment_method + admin_inject_json_ams "admin.paymentMethods", "paymentMethod", @payment_method, Api::Admin::PaymentMethodSerializer + end + def admin_inject_shipping_methods admin_inject_json_ams_array "admin.shippingMethods", "shippingMethods", @shipping_methods, Api::Admin::IdNameSerializer end diff --git a/app/models/spree/gateway/pay_pal_express_decorator.rb b/app/models/spree/gateway/pay_pal_express_decorator.rb new file mode 100644 index 0000000000..dc8239a107 --- /dev/null +++ b/app/models/spree/gateway/pay_pal_express_decorator.rb @@ -0,0 +1,7 @@ +module Spree + class Gateway::PayPalExpress < Gateway + # Something odd is happening with class inheritance here, this class (defined in spree_paypal_express gem) + # doesn't seem to pick up attr_accessible from the Gateway class, so we redefine the attrs we need here + attr_accessible :tag_list + end +end diff --git a/app/models/spree/gateway_decorator.rb b/app/models/spree/gateway_decorator.rb index 3c6bb2638d..9125ef1d2d 100644 --- a/app/models/spree/gateway_decorator.rb +++ b/app/models/spree/gateway_decorator.rb @@ -1,4 +1,5 @@ Spree::Gateway.class_eval do + acts_as_taggable # Due to class load order, when config.cache_classes is enabled (ie. staging and production # environments), this association isn't inherited from PaymentMethod. As a result, creating @@ -12,4 +13,6 @@ Spree::Gateway.class_eval do # Default to live preference :server, :string, :default => 'live' preference :test_mode, :boolean, :default => false + + attr_accessible :tag_list end diff --git a/app/models/spree/payment_method_decorator.rb b/app/models/spree/payment_method_decorator.rb index 97ae3c6bc7..77e22c591f 100644 --- a/app/models/spree/payment_method_decorator.rb +++ b/app/models/spree/payment_method_decorator.rb @@ -1,8 +1,10 @@ Spree::PaymentMethod.class_eval do + acts_as_taggable + # See gateway_decorator.rb when modifying this association has_and_belongs_to_many :distributors, join_table: 'distributors_payment_methods', :class_name => 'Enterprise', association_foreign_key: 'distributor_id' - attr_accessible :distributor_ids + attr_accessible :distributor_ids, :tag_list calculated_adjustments diff --git a/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface index 21fbf95723..aa9ead7a0f 100644 --- a/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface +++ b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface @@ -1,6 +1,6 @@ / replace "div[data-hook='admin_payment_method_form_fields']" -= render 'data' += admin_inject_payment_method %div.alpha.eleven.columns{ "ng-app" => "admin.paymentMethods", "ng-controller" => "paymentMethodCtrl" } .row .alpha.three.columns @@ -34,6 +34,14 @@ = radio_button :payment_method, :active, false   = label_tag nil, t(:say_no) + + .row + .alpha.three.columns + = label(:payment_method, :tags, t(:tags)) + .omega.eight.columns + = hidden_field(:payment_method, :tag_list, "ng-value" => "paymentMethod.tag_list") + %tags-with-translation#something{ object: "paymentMethod" } + = render 'providers' .row diff --git a/app/serializers/api/admin/payment_method_serializer.rb b/app/serializers/api/admin/payment_method_serializer.rb new file mode 100644 index 0000000000..42c11a6953 --- /dev/null +++ b/app/serializers/api/admin/payment_method_serializer.rb @@ -0,0 +1,11 @@ +class Api::Admin::PaymentMethodSerializer < ActiveModel::Serializer + attributes :id, :name, :type, :tag_list, :tags + + def tag_list + object.tag_list.join(",") + end + + def tags + object.tag_list.map{ |t| { text: t } } + end +end diff --git a/app/views/spree/admin/payment_methods/_data.html.haml b/app/views/spree/admin/payment_methods/_data.html.haml deleted file mode 100644 index 62a36498a4..0000000000 --- a/app/views/spree/admin/payment_methods/_data.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -:javascript - angular.module('admin.paymentMethods').value('paymentMethod', #{ { id: @payment_method.id, type: @payment_method.type }.to_json }) diff --git a/spec/features/admin/payment_method_spec.rb b/spec/features/admin/payment_method_spec.rb index b736155372..87f22d969f 100644 --- a/spec/features/admin/payment_method_spec.rb +++ b/spec/features/admin/payment_method_spec.rb @@ -29,47 +29,53 @@ feature %q{ payment_method = Spree::PaymentMethod.find_by_name('Cheque payment method') payment_method.distributors.should == [@distributors[0]] end + end - scenario "updating a payment method" do - pm = create(:payment_method, distributors: [@distributors[0]]) - login_to_admin_section + scenario "updating a payment method", js: true do + pm = create(:payment_method, distributors: [@distributors[0]]) + login_to_admin_section - visit spree.edit_admin_payment_method_path pm + visit spree.edit_admin_payment_method_path pm - fill_in 'payment_method_name', :with => 'New PM Name' + fill_in 'payment_method_name', :with => 'New PM Name' + find(:css, "tags-input .tags input").set "member\n" - uncheck "payment_method_distributor_ids_#{@distributors[0].id}" - check "payment_method_distributor_ids_#{@distributors[1].id}" - check "payment_method_distributor_ids_#{@distributors[2].id}" - select2_select "PayPal Express", from: "payment_method_type" - expect(page).to have_field 'Login' - fill_in 'payment_method_preferred_login', with: 'testlogin' - fill_in 'payment_method_preferred_password', with: 'secret' - fill_in 'payment_method_preferred_signature', with: 'sig' + uncheck "payment_method_distributor_ids_#{@distributors[0].id}" + check "payment_method_distributor_ids_#{@distributors[1].id}" + check "payment_method_distributor_ids_#{@distributors[2].id}" + select2_select "PayPal Express", from: "payment_method_type" + expect(page).to have_field 'Login' + fill_in 'payment_method_preferred_login', with: 'testlogin' + fill_in 'payment_method_preferred_password', with: 'secret' + fill_in 'payment_method_preferred_signature', with: 'sig' - click_button 'Update' + click_button 'Update' - expect(flash_message).to eq 'Payment Method has been successfully updated!' + expect(flash_message).to eq 'Payment Method has been successfully updated!' + save_screenshot '/Users/rob/Desktop/ss.png' - payment_method = Spree::PaymentMethod.find_by_name('New PM Name') - expect(payment_method.distributors).to include @distributors[1], @distributors[2] - expect(payment_method.distributors).not_to include @distributors[0] - expect(payment_method.type).to eq "Spree::Gateway::PayPalExpress" - expect(payment_method.preferences[:login]).to eq 'testlogin' - expect(payment_method.preferences[:password]).to eq 'secret' - expect(payment_method.preferences[:signature]).to eq 'sig' + expect(first('tags-input .tag-list ti-tag-item')).to have_content "member" - fill_in 'payment_method_preferred_login', with: 'otherlogin' - click_button 'Update' + payment_method = Spree::PaymentMethod.find_by_name('New PM Name') + expect(payment_method.distributors).to include @distributors[1], @distributors[2] + expect(payment_method.distributors).not_to include @distributors[0] + expect(payment_method.type).to eq "Spree::Gateway::PayPalExpress" + expect(payment_method.preferences[:login]).to eq 'testlogin' + expect(payment_method.preferences[:password]).to eq 'secret' + expect(payment_method.preferences[:signature]).to eq 'sig' - expect(flash_message).to eq 'Payment Method has been successfully updated!' - expect(page).to have_field 'Password', with: '' + fill_in 'payment_method_preferred_login', with: 'otherlogin' + click_button 'Update' - payment_method = Spree::PaymentMethod.find_by_name('New PM Name') - expect(payment_method.preferences[:login]).to eq 'otherlogin' - expect(payment_method.preferences[:password]).to eq 'secret' - expect(payment_method.preferences[:signature]).to eq 'sig' - end + expect(flash_message).to eq 'Payment Method has been successfully updated!' + expect(page).to have_field 'Password', with: '' + expect(first('tags-input .tag-list ti-tag-item')).to have_content "member" + + payment_method = Spree::PaymentMethod.find_by_name('New PM Name') + expect(payment_method.tag_list).to eq ["member"] + expect(payment_method.preferences[:login]).to eq 'otherlogin' + expect(payment_method.preferences[:password]).to eq 'secret' + expect(payment_method.preferences[:signature]).to eq 'sig' end context "as an enterprise user", js: true do @@ -102,12 +108,15 @@ feature %q{ fill_in 'payment_method_name', :with => 'Cheque payment method' check "payment_method_distributor_ids_#{distributor1.id}" + find(:css, "tags-input .tags input").set "local\n" click_button 'Create' flash_message.should == 'Payment Method has been successfully created!' + expect(first('tags-input .tag-list ti-tag-item')).to have_content "local" payment_method = Spree::PaymentMethod.find_by_name('Cheque payment method') payment_method.distributors.should == [distributor1] + payment_method.tag_list.should == ["local"] end it "shows me only payment methods I have access to" do From e553b8c5903f773ce38e755ffae62cbfe65db570 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 27 Apr 2016 13:40:29 +1000 Subject: [PATCH 076/110] Adding filter payment method tag rule logic --- app/models/tag_rule/filter_payment_methods.rb | 32 +++++++ spec/factories.rb | 4 + .../tag_rule/filter_payment_methods_spec.rb | 83 +++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 app/models/tag_rule/filter_payment_methods.rb create mode 100644 spec/models/tag_rule/filter_payment_methods_spec.rb diff --git a/app/models/tag_rule/filter_payment_methods.rb b/app/models/tag_rule/filter_payment_methods.rb new file mode 100644 index 0000000000..da29426298 --- /dev/null +++ b/app/models/tag_rule/filter_payment_methods.rb @@ -0,0 +1,32 @@ +class TagRule::FilterPaymentMethods < TagRule + preference :matched_payment_methods_visibility, :string, default: "visible" + preference :payment_method_tags, :string, default: "" + + attr_accessible :preferred_matched_payment_methods_visibility, :preferred_payment_method_tags + + private + + # Warning: this should only EVER be called via TagRule#apply + def apply! + unless preferred_matched_payment_methods_visibility == "visible" + subject.reject!{ |pm| tags_match?(pm) } + end + end + + def apply_default! + if preferred_matched_payment_methods_visibility == "visible" + subject.reject!{ |pm| tags_match?(pm) } + end + end + + def tags_match?(payment_method) + payment_method_tags = payment_method.andand.tag_list || [] + preferred_tags = preferred_payment_method_tags.split(",") + ( payment_method_tags & preferred_tags ).any? + end + + def subject_class_matches? + subject.class == Array && + subject.all? { |i| i.class < Spree::PaymentMethod } + end +end diff --git a/spec/factories.rb b/spec/factories.rb index 2e29e74316..d0d7755cc0 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -294,6 +294,10 @@ FactoryGirl.define do enterprise { FactoryGirl.create :distributor_enterprise } end + factory :filter_payment_methods_tag_rule, class: TagRule::FilterPaymentMethods do + enterprise { FactoryGirl.create :distributor_enterprise } + end + factory :tag_rule, class: TagRule::DiscountOrder do enterprise { FactoryGirl.create :distributor_enterprise } before(:create) do |tr| diff --git a/spec/models/tag_rule/filter_payment_methods_spec.rb b/spec/models/tag_rule/filter_payment_methods_spec.rb new file mode 100644 index 0000000000..8eec06f773 --- /dev/null +++ b/spec/models/tag_rule/filter_payment_methods_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe TagRule::FilterPaymentMethods, type: :model do + let!(:tag_rule) { create(:filter_payment_methods_tag_rule) } + + describe "determining whether tags match for a given payment method" do + context "when the payment method is nil" do + + it "returns false" do + expect(tag_rule.send(:tags_match?, nil)).to be false + end + end + + context "when the payment method is not nil" do + let(:payment_method) { create(:payment_method, tag_list: ["member","local","volunteer"]) } + + context "when the rule has no preferred payment method tags specified" do + before { allow(tag_rule).to receive(:preferred_payment_method_tags) { "" } } + it { expect(tag_rule.send(:tags_match?, payment_method)).to be false } + end + + context "when the rule has preferred customer tags specified that match ANY of the customer tags" do + before { allow(tag_rule).to receive(:preferred_payment_method_tags) { "wholesale,some_tag,member" } } + it { expect(tag_rule.send(:tags_match?, payment_method)).to be true } + end + + context "when the rule has preferred customer tags specified that match NONE of the customer tags" do + before { allow(tag_rule).to receive(:preferred_payment_method_tags) { "wholesale,some_tag,some_other_tag" } } + it { expect(tag_rule.send(:tags_match?, payment_method)).to be false } + end + end + end + + describe "applying the rule" do + # Assume that all validation is done by the TagRule base class + + let(:sm1) { create(:payment_method, tag_list: ["tag1", "something", "somethingelse"]) } + let(:sm2) { create(:payment_method, tag_list: ["tag2"]) } + let(:sm3) { create(:payment_method, tag_list: ["tag3"]) } + let!(:payment_methods) { [sm1, sm2, sm3] } + + before do + tag_rule.update_attribute(:preferred_payment_method_tags, "tag2") + tag_rule.context = {subject: payment_methods} + end + + context "apply!" do + context "when showing matching payment methods" do + before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, "visible") } + it "does nothing" do + tag_rule.send(:apply!) + expect(payment_methods).to eq [sm1, sm2, sm3] + end + end + + context "when hiding matching payment methods" do + before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, "hidden") } + it "removes matching payment methods from the list" do + tag_rule.send(:apply!) + expect(payment_methods).to eq [sm1, sm3] + end + end + end + + context "apply_default!" do + context "when showing matching payment methods" do + before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, "visible") } + it "remove matching payment methods from the list" do + tag_rule.send(:apply_default!) + expect(payment_methods).to eq [sm1, sm3] + end + end + + context "when hiding matching payment methods" do + before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, "hidden") } + it "does nothing" do + tag_rule.send(:apply_default!) + expect(payment_methods).to eq [sm1, sm2, sm3] + end + end + end + end +end From e21735b0374d46c3045e294263bbd641fff18a22 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 27 Apr 2016 14:39:09 +1000 Subject: [PATCH 077/110] Removing naughty save_screenshot call from payment method spec --- spec/features/admin/payment_method_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/features/admin/payment_method_spec.rb b/spec/features/admin/payment_method_spec.rb index 87f22d969f..fba8e69401 100644 --- a/spec/features/admin/payment_method_spec.rb +++ b/spec/features/admin/payment_method_spec.rb @@ -52,7 +52,6 @@ feature %q{ click_button 'Update' expect(flash_message).to eq 'Payment Method has been successfully updated!' - save_screenshot '/Users/rob/Desktop/ss.png' expect(first('tags-input .tag-list ti-tag-item')).to have_content "member" From 3fb6fba0f06fd23870b37a801d009ab97460b753 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 27 Apr 2016 14:39:49 +1000 Subject: [PATCH 078/110] Adding payment method tag rule logic to Spree::Order.available_payment_methods --- app/models/spree/order_decorator.rb | 7 ++- spec/models/spree/order_spec.rb | 95 ++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 11 deletions(-) diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index ee485e4afd..411e7e71a2 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -212,9 +212,10 @@ Spree::Order.class_eval do # Show payment methods for this distributor def available_payment_methods - @available_payment_methods ||= Spree::PaymentMethod.available(:front_end).select do |pm| - (self.distributor && (pm.distributors.include? self.distributor)) - end + return [] unless distributor.present? + payment_methods = distributor.payment_methods.available(:front_end).all + distributor.apply_tag_rules( type: "FilterPaymentMethods", subject: payment_methods, customer_tags: customer.andand.tag_list) + payment_methods end # Does this order have shipments that can be shipped? diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 4db0541d6c..89ffd66e00 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -22,18 +22,97 @@ describe Spree::Order do end describe "Payment methods" do - let(:order_distributor) { create(:distributor_enterprise) } + let(:distributor) { create(:distributor_enterprise) } let(:some_other_distributor) { create(:distributor_enterprise) } - let(:order) { build(:order, distributor: order_distributor) } - let(:pm1) { create(:payment_method, distributors: [order_distributor])} - let(:pm2) { create(:payment_method, distributors: [some_other_distributor])} + let(:order) { create(:order, distributor: distributor) } + let!(:pm1) { create(:payment_method, distributors: [distributor])} + let!(:pm2) { create(:payment_method, distributors: [some_other_distributor])} - it "finds the correct payment methods" do - Spree::PaymentMethod.stub(:available).and_return [pm1, pm2] - order.available_payment_methods.include?(pm2).should == false - order.available_payment_methods.include?(pm1).should == true + context "when the order has no distributor" do + let(:order_without_distributor) { create(:order, distributor: nil) } + + it "returns an empty array" do + expect(order_without_distributor.available_payment_methods).to eq [] + end end + context "when no tag rules are in effect" do + it "finds the payment methods for the current distributor" do + order.available_payment_methods.include?(pm2).should == false + order.available_payment_methods.include?(pm1).should == true + end + end + + context "when a FilterPaymentMethods tag rule is in effect, with preferred visibility of 'visible'" do + let!(:allowed_customer) { create(:customer, enterprise: distributor, tag_list: "trusted") } + let!(:disallowed_customer) { create(:customer, enterprise: distributor, tag_list: "") } + let!(:tag_rule) { create(:filter_payment_methods_tag_rule, + enterprise: distributor, + preferred_customer_tags: "trusted", + preferred_payment_method_tags: "trusted") } + let(:tagged_pm) { pm1 } + let(:untagged_pm) { pm2 } + + before do + tagged_pm.update_attribute(:tag_list, 'trusted') + distributor.payment_methods = [tagged_pm, untagged_pm] + end + + context "with a preferred visiblity of 'visible" do + before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, 'visible') } + + context "when the customer is nil" do + it "applies default action (hide)" do + expect(order.available_payment_methods).to include untagged_pm + expect(order.available_payment_methods).to_not include tagged_pm + end + end + + context "when the customer's tags match" do + before { order.update_attribute(:customer_id, allowed_customer.id) } + + it "applies the action (show)" do + expect(order.available_payment_methods).to include tagged_pm, untagged_pm + end + end + + context "when the customer's tags don't match" do + before { order.update_attribute(:customer_id, disallowed_customer.id) } + + it "applies the default action (hide)" do + expect(order.available_payment_methods).to include untagged_pm + expect(order.available_payment_methods).to_not include tagged_pm + end + end + end + + context "with a preferred visiblity of 'hidden" do + before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, 'hidden') } + + context "when the customer is nil" do + it "applies default action (show)" do + expect(order.available_payment_methods).to include tagged_pm, untagged_pm + end + end + + context "when the customer's tags match" do + before { order.update_attribute(:customer_id, allowed_customer.id) } + + it "applies the action (hide)" do + expect(order.available_payment_methods).to include untagged_pm + expect(order.available_payment_methods).to_not include tagged_pm + end + end + + context "when the customer's tags don't match" do + before { order.update_attribute(:customer_id, disallowed_customer.id) } + + it "applies the default action (show)" do + expect(order.available_payment_methods).to include tagged_pm, untagged_pm + end + end + end + end end describe "updating the distribution charge" do From e999b5715aeeaf82a50f8a9bd3b598cc19fd9f75 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 27 Apr 2016 15:33:45 +1000 Subject: [PATCH 079/110] Adding FilterPaymentMethods rules to tag rule UI --- .../tag_rules_controller.js.coffee | 3 +++ .../directives/new_rule_dialog.js.coffee | 1 + .../filter_payment_methods.js.coffee | 4 +++ .../filter_payment_methods.html.haml | 27 +++++++++++++++++++ .../api/admin/tag_rule_serializer.rb | 8 ++++++ .../enterprises/form/_tag_rules.html.haml | 1 + spec/features/admin/tag_rules_spec.rb | 21 +++++++++++++++ 7 files changed, 65 insertions(+) create mode 100644 app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_payment_methods.js.coffee create mode 100644 app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods.html.haml diff --git a/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee index a5d77cf7ff..00d379085e 100644 --- a/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee +++ b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee @@ -26,6 +26,9 @@ angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, ente when "FilterShippingMethods" newRule.peferred_shipping_method_tags = [] newRule.preferred_matched_shipping_methods_visibility = "visible" + when "FilterPaymentMethods" + newRule.peferred_payment_method_tags = [] + newRule.preferred_matched_payment_methods_visibility = "visible" when "FilterProducts" newRule.peferred_variant_tags = [] newRule.preferred_matched_variants_visibility = "visible" diff --git a/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee index c75b50bbf5..1887853315 100644 --- a/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee +++ b/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee @@ -9,6 +9,7 @@ angular.module("admin.tagRules").directive 'newTagRuleDialog', ($compile, $templ # { id: "DiscountOrder", name: 'Apply a discount to orders' } { id: "FilterProducts", name: 'Show or Hide variants in my shopfront' } { id: "FilterShippingMethods", name: 'Show or Hide shipping methods at checkout' } + { id: "FilterPaymentMethods", name: 'Show or Hide payment methods at checkout' } ] scope.ruleType = scope.ruleTypes[0].id diff --git a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_payment_methods.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_payment_methods.js.coffee new file mode 100644 index 0000000000..c9f6926fc5 --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_payment_methods.js.coffee @@ -0,0 +1,4 @@ +angular.module("admin.tagRules").directive "filterPaymentMethods", -> + restrict: "E" + replace: true + templateUrl: "admin/tag_rules/filter_payment_methods.html" diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods.html.haml new file mode 100644 index 0000000000..5bd06a6cd6 --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods.html.haml @@ -0,0 +1,27 @@ +%div + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]", + ng: { value: "rule.id" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", + value: "TagRule::FilterPaymentMethods" } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", + ng: { value: "rule.preferred_customer_tags" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_payment_method_tags", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_payment_method_tags]", + ng: { value: "rule.preferred_customer_tags" } } + + %span.text-normal {{ $index + 1 }}. Payment methods with matching tags are + %input.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_payment_methods_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_payment_methods_visibility]", + ng: { model: "rule.preferred_matched_payment_methods_visibility"}, + data: 'visibilityOptions', "min-search" => 5 } + -# %tags-with-translation{ object: "rule", "tags-attr" => "payment_method_tags", "tag-list-attr" => "preferred_payment_method_tags" } diff --git a/app/serializers/api/admin/tag_rule_serializer.rb b/app/serializers/api/admin/tag_rule_serializer.rb index c8fdd58c44..5d4497cdf3 100644 --- a/app/serializers/api/admin/tag_rule_serializer.rb +++ b/app/serializers/api/admin/tag_rule_serializer.rb @@ -25,6 +25,14 @@ module Api::Admin::TagRule end end + class FilterPaymentMethodsSerializer < BaseSerializer + attributes :preferred_matched_payment_methods_visibility, :payment_method_tags + + def payment_method_tags + object.preferred_payment_method_tags.split(",") + end + end + class FilterProductsSerializer < BaseSerializer attributes :preferred_matched_variants_visibility, :variant_tags diff --git a/app/views/admin/enterprises/form/_tag_rules.html.haml b/app/views/admin/enterprises/form/_tag_rules.html.haml index 37bb699646..2742859220 100644 --- a/app/views/admin/enterprises/form/_tag_rules.html.haml +++ b/app/views/admin/enterprises/form/_tag_rules.html.haml @@ -26,6 +26,7 @@ %discount-order{ ng: { if: "::rule.type == 'TagRule::DiscountOrder'" } } %filter-shipping-methods{ ng: { if: "::rule.type == 'TagRule::FilterShippingMethods'" } } %filter-products{ ng: { if: "::rule.type == 'TagRule::FilterProducts'" } } + %filter-payment-methods{ ng: { if: "::rule.type == 'TagRule::FilterPaymentMethods'" } } %td.actions %a{ ng: { click: "deleteTagRule(tagGroup, rule)" }, :class => "delete-tag-rule icon-trash no-text" } .add_rule.text-center diff --git a/spec/features/admin/tag_rules_spec.rb b/spec/features/admin/tag_rules_spec.rb index af079d3431..17163b0a58 100644 --- a/spec/features/admin/tag_rules_spec.rb +++ b/spec/features/admin/tag_rules_spec.rb @@ -34,6 +34,12 @@ feature 'Tag Rules', js: true do click_button "Add Rule" select2_select "VISIBLE", from: "enterprise_tag_rules_attributes_1_preferred_matched_variants_visibility" + # New FilterPaymentMethods Rule + click_button '+ Add A New Rule' + select2_select 'Show or Hide payment methods at checkout', from: 'rule_type_selector' + click_button "Add Rule" + select2_select "VISIBLE", from: "enterprise_tag_rules_attributes_2_preferred_matched_payment_methods_visibility" + # New DiscountOrder Rule # click_button '+ Add A New Rule' # select2_select 'Apply a discount to orders', from: 'rule_type_selector' @@ -55,6 +61,11 @@ feature 'Tag Rules', js: true do expect(tag_rule.preferred_customer_tags).to eq "volunteer" expect(tag_rule.preferred_variant_tags).to eq "volunteer" expect(tag_rule.preferred_matched_variants_visibility).to eq "visible" + + tag_rule = TagRule::FilterPaymentMethods.last + expect(tag_rule.preferred_customer_tags).to eq "volunteer" + expect(tag_rule.preferred_payment_method_tags).to eq "volunteer" + expect(tag_rule.preferred_matched_payment_methods_visibility).to eq "visible" end end @@ -62,6 +73,7 @@ feature 'Tag Rules', js: true do let!(:do_tag_rule) { create(:tag_rule, enterprise: enterprise, preferred_customer_tags: "member" ) } let!(:fsm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise, preferred_matched_shipping_methods_visibility: "hidden", preferred_customer_tags: "member" ) } let!(:fp_tag_rule) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_matched_variants_visibility: "visible", preferred_customer_tags: "member" ) } + let!(:fpm_tag_rule) { create(:filter_payment_methods_tag_rule, enterprise: enterprise, preferred_matched_payment_methods_visibility: "hidden", preferred_customer_tags: "member" ) } before do login_to_admin_section @@ -88,6 +100,10 @@ feature 'Tag Rules', js: true do expect(page).to have_select2 "enterprise_tag_rules_attributes_2_preferred_matched_variants_visibility", selected: 'VISIBLE' select2_select 'NOT VISIBLE', from: "enterprise_tag_rules_attributes_2_preferred_matched_variants_visibility" + # FilterPaymentMethods rule + expect(page).to have_select2 "enterprise_tag_rules_attributes_3_preferred_matched_payment_methods_visibility", selected: 'NOT VISIBLE' + select2_select 'VISIBLE', from: "enterprise_tag_rules_attributes_3_preferred_matched_payment_methods_visibility" + click_button 'Update' # DiscountOrder rule @@ -103,6 +119,11 @@ feature 'Tag Rules', js: true do expect(fp_tag_rule.preferred_customer_tags).to eq "member,volunteer" expect(fp_tag_rule.preferred_variant_tags).to eq "member,volunteer" expect(fp_tag_rule.preferred_matched_variants_visibility).to eq "hidden" + + # FilterPaymentMethods rule + expect(fpm_tag_rule.preferred_customer_tags).to eq "member,volunteer" + expect(fpm_tag_rule.preferred_payment_method_tags).to eq "member,volunteer" + expect(fpm_tag_rule.preferred_matched_payment_methods_visibility).to eq "visible" end end From d201e4a039643f851f4a876cbad7aa104abce5f8 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 27 Apr 2016 15:34:07 +1000 Subject: [PATCH 080/110] Tweaking styling for JQuery Dialog box in admin section --- .../admin/components/jquery_dialog.scss | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/admin/components/jquery_dialog.scss b/app/assets/stylesheets/admin/components/jquery_dialog.scss index 2e36db6e33..f3fe08da60 100644 --- a/app/assets/stylesheets/admin/components/jquery_dialog.scss +++ b/app/assets/stylesheets/admin/components/jquery_dialog.scss @@ -4,8 +4,8 @@ dark: #545454 light: #ccc */ .ui-dialog { - border: 2px solid #4a4a4a; - border-radius:3px; + border: 0px solid #4a4a4a; + border-radius:5px; padding:0px; -moz-box-shadow: 3px 3px 4px #797979; -webkit-box-shadow: 3px 3px 4px #797979; @@ -18,10 +18,6 @@ light: #ccc filter: progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#545454'); } -.ui-dialog .ui-dialog-titlebar{ - border-radius: 3px; -} - .ui-dialog .ui-state-hover { &.ui-dialog-titlebar-close{ @@ -34,17 +30,17 @@ light: #ccc background-image: none; background-color: #ffffff; border:0px; - border-radius: 3px; + border-radius: 8px; padding: 0px 5px 0px 5px; } .ui-dialog .ui-widget-content{ border: none; - border-radius: 3px; + border-radius: 8px; padding: 0px 50px 30px 50px; } .ui-dialog .ui-corner-all{ - border-radius:0px; + border-radius: 8px; } .ui-dialog { .ui-state-hover, .ui-state-focus{ @@ -83,6 +79,6 @@ light: #ccc } .ui-widget-overlay { - background: #e9e9e9; - opacity: 0.6; + background: #000000; + opacity: 0.5; } From 8035b3f24a2c8ab82976feef40c0d1dbf30872d1 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 1 May 2016 09:38:41 +1000 Subject: [PATCH 081/110] Refactoring admin table panels infrastructure Updating enterprise index --- .../enterprise_index_row_controller.js.coffee | 49 -------- .../enterprises_controller.js.coffee | 48 +++++++ .../index_panel_controller.js.coffee | 2 +- .../services/enterprise_resource.js.coffee | 3 + .../services/enterprises.js.coffee | 5 +- .../directives/panel_row.js.coffee | 39 ++---- .../directives/panel_toggle.js.coffee | 10 +- .../directives/panel_toggle_row.js.coffee | 32 ++--- .../index_utils/services/columns.js.coffee | 10 +- .../index_utils/services/panels.js.coffee | 33 ++--- .../templates/admin/panel.html.haml | 2 +- .../stylesheets/admin/index_panels.css.scss | 18 +-- .../_enterprise_user_index.html.haml | 14 +-- .../index_panel_controller_spec.js.coffee | 2 +- .../directives/panel_row_spec.js.coffee | 18 +-- .../services/panels_spec.js.coffee | 119 ++++++++++++++---- 16 files changed, 231 insertions(+), 173 deletions(-) delete mode 100644 app/assets/javascripts/admin/enterprises/controllers/enterprise_index_row_controller.js.coffee diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_index_row_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_index_row_controller.js.coffee deleted file mode 100644 index 63b8daf07c..0000000000 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_index_row_controller.js.coffee +++ /dev/null @@ -1,49 +0,0 @@ -angular.module("admin.enterprises").controller "EnterpriseIndexRowCtrl", ($scope) -> - $scope.status = -> - if $scope.enterprise.issues.length > 0 - "issue" - else if $scope.enterprise.warnings.length > 0 - "warning" - else - "ok" - - - $scope.producerText = -> - switch $scope.enterprise.is_primary_producer - when true - "Producer" - else - "Non-Producer" - - $scope.packageText = -> - switch $scope.enterprise.is_primary_producer - when true - switch $scope.enterprise.sells - when "none" - "Profile" - when "own" - "Shop" - when "any" - "Hub" - else - "Choose" - else - switch $scope.enterprise.sells - when "none" - "Profile" - when "any" - "Hub" - else - "Choose" - - $scope.updateRowText = -> - $scope.producer = $scope.producerText() - $scope.package = $scope.packageText() - $scope.producerError = ($scope.producer == "Choose") - $scope.packageError = ($scope.package == "Choose") - - - $scope.updateRowText() - - $scope.$on "enterprise:updated", -> - $scope.updateRowText() diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprises_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprises_controller.js.coffee index d6dda4a118..02553a822b 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprises_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprises_controller.js.coffee @@ -3,6 +3,54 @@ angular.module("admin.enterprises").controller 'enterprisesCtrl', ($scope, $q, E requests.push ($scope.allEnterprises = Enterprises.index(ams_prefix: "index")).$promise $q.all(requests).then -> + $scope.updateStaticFieldsFor(enterprise) for enterprise in $scope.allEnterprises $scope.loaded = true $scope.columns = Columns.columns + + $scope.updateStaticFieldsFor = (enterprise) -> + enterprise.producer = $scope.producerTextFor(enterprise) + enterprise.package = $scope.packageTextFor(enterprise) + enterprise.producerError = (enterprise.producer == "Choose") + enterprise.packageError = (enterprise.package == "Choose") + enterprise.status = $scope.statusFor(enterprise) + + $scope.$on "enterprise:updated", (event, enterprise) -> + $scope.updateStaticFieldsFor(enterprise) + + $scope.statusFor = (enterprise) -> + if enterprise.issues.length > 0 + "issue" + else if enterprise.warnings.length > 0 + "warning" + else + "ok" + + + $scope.producerTextFor = (enterprise) -> + switch enterprise.is_primary_producer + when true + "Producer" + else + "Non-Producer" + + $scope.packageTextFor = (enterprise) -> + switch enterprise.is_primary_producer + when true + switch enterprise.sells + when "none" + "Profile" + when "own" + "Shop" + when "any" + "Hub" + else + "Choose" + else + switch enterprise.sells + when "none" + "Profile" + when "any" + "Hub" + else + "Choose" diff --git a/app/assets/javascripts/admin/enterprises/controllers/index_panel_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/index_panel_controller.js.coffee index 6f568ca5ea..1e207884d3 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/index_panel_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/index_panel_controller.js.coffee @@ -9,7 +9,7 @@ angular.module("admin.enterprises").controller 'indexPanelCtrl', ($scope, Enterp unless $scope.saved() $scope.saving = true Enterprises.save($scope.enterprise).then (data) -> - $scope.$emit("enterprise:updated") + $scope.$emit("enterprise:updated", $scope.enterprise) $scope.saving = false , (response) -> $scope.saving = false diff --git a/app/assets/javascripts/admin/enterprises/services/enterprise_resource.js.coffee b/app/assets/javascripts/admin/enterprises/services/enterprise_resource.js.coffee index 023f33d86c..b522f5be3b 100644 --- a/app/assets/javascripts/admin/enterprises/services/enterprise_resource.js.coffee +++ b/app/assets/javascripts/admin/enterprises/services/enterprise_resource.js.coffee @@ -1,4 +1,7 @@ angular.module("admin.enterprises").factory 'EnterpriseResource', ($resource) -> + ignoredAttrs = -> + ["$$hashKey", "producer", "package", "producerError", "packageError", "status"] + $resource('/admin/enterprises/:id/:action.json', {}, { 'index': method: 'GET' diff --git a/app/assets/javascripts/admin/enterprises/services/enterprises.js.coffee b/app/assets/javascripts/admin/enterprises/services/enterprises.js.coffee index b159816709..80db943689 100644 --- a/app/assets/javascripts/admin/enterprises/services/enterprises.js.coffee +++ b/app/assets/javascripts/admin/enterprises/services/enterprises.js.coffee @@ -33,8 +33,11 @@ angular.module("admin.enterprises").factory 'Enterprises', ($q, EnterpriseResour diff: (enterprise) -> changed = [] for attr, value of enterprise when not angular.equals(value, @pristineByID[enterprise.id][attr]) - changed.push attr unless attr is "$$hashKey" + changed.push attr unless attr in @ignoredAttrs() changed + ignoredAttrs: -> + ["$$hashKey", "producer", "package", "producerError", "packageError", "status"] + resetAttribute: (enterprise, attribute) -> enterprise[attribute] = @pristineByID[enterprise.id][attribute] diff --git a/app/assets/javascripts/admin/index_utils/directives/panel_row.js.coffee b/app/assets/javascripts/admin/index_utils/directives/panel_row.js.coffee index eb5a4171f4..474ec5664d 100644 --- a/app/assets/javascripts/admin/index_utils/directives/panel_row.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/panel_row.js.coffee @@ -1,37 +1,24 @@ angular.module("admin.indexUtils").directive "panelRow", (Panels, Columns) -> restrict: "C" + require: "^^panelToggleRow" templateUrl: "admin/panel.html" scope: object: "=" panels: "=" - link: (scope, element, attrs) -> - scope.template = "" - selected = null - scope.columnCount = Columns.visibleCount + colspan: "=?" + locals: '@?' + link: (scope, element, attrs, ctrl) -> + scope.template = null + scope.columnCount = (scope.colspan || Columns.visibleCount) + + if scope.locals + scope[local] = scope.$parent.$eval(local.trim()) for local in scope.locals.split(',') scope.$on "columnCount:changed", (event, count) -> scope.columnCount = count - setTemplate = -> - if selected? - scope.template = 'admin/panels/' + scope.panels[selected] + '.html' + ctrl.registerSelectionListener (selection) -> + if selection? + scope.template = "admin/panels/#{scope.panels[selection]}.html" else - scope.template = "" - - scope.getSelected = -> - selected - - scope.setSelected = (name) -> - scope.$apply -> - selected = name - setTemplate() - - scope.open = (name) -> - element.show 0, -> - scope.setSelected name - - scope.close = -> - element.hide 0, -> - scope.setSelected null - - Panels.register(scope.object.id, scope) + scope.template = null diff --git a/app/assets/javascripts/admin/index_utils/directives/panel_toggle.js.coffee b/app/assets/javascripts/admin/index_utils/directives/panel_toggle.js.coffee index df81328905..56d80d88b9 100644 --- a/app/assets/javascripts/admin/index_utils/directives/panel_toggle.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/panel_toggle.js.coffee @@ -2,11 +2,13 @@ angular.module("admin.indexUtils").directive "panelToggle", -> restrict: "C" transclude: true template: '
    ' - require: "^panelToggleRow" + require: "^^panelToggleRow" scope: name: "@" link: (scope, element, attrs, ctrl) -> - scope.selected = ctrl.register(scope.name, element) - element.on "click", -> - scope.selected = ctrl.select(scope.name) + scope.$apply -> + ctrl.toggle(scope.name) + + ctrl.registerSelectionListener (selection) -> + element.toggleClass('selected', selection == scope.name) diff --git a/app/assets/javascripts/admin/index_utils/directives/panel_toggle_row.js.coffee b/app/assets/javascripts/admin/index_utils/directives/panel_toggle_row.js.coffee index d2d9c90ff8..ec6cb6d5ed 100644 --- a/app/assets/javascripts/admin/index_utils/directives/panel_toggle_row.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/panel_toggle_row.js.coffee @@ -3,27 +3,19 @@ angular.module("admin.indexUtils").directive "panelToggleRow", (Panels) -> scope: object: "=" selected: "@?" - controller: ($scope) -> - panelToggles = {} + controller: ($scope, $element) -> + this.toggle = (name) -> + Panels.toggle($scope.object, name) - this.register = (name, element) -> - panelToggles[name] = element - panelToggles[name].addClass("selected") if $scope.selected == name - $scope.selected == name + this.select = (selection) -> + $scope.$broadcast("selection:changed", selection) + $element.toggleClass("expanded", selection?) - this.select = (name) -> - panelToggle.removeClass("selected") for panelName, panelToggle of panelToggles - - switch $scope.selected = Panels.toggle($scope.object.id, name) - when null - panelToggles[name].parent(".panel-toggle-row").removeClass("expanded") - else - panelToggles[$scope.selected].addClass("selected") - panelToggles[$scope.selected].parent(".panel-toggle-row").addClass("expanded") - - $scope.selected == name + this.registerSelectionListener = (callback) -> + $scope.$on "selection:changed", (event, selection) -> + callback(selection) this - # - # link: (scope, element, attrs) -> - # Panels.registerInitialSelection(scope.object.id, scope.selected) + + link: (scope, element, attrs, ctrl) -> + Panels.register(ctrl, scope.object, scope.selected) diff --git a/app/assets/javascripts/admin/index_utils/services/columns.js.coffee b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee index 45bea7c680..fbc5149a3f 100644 --- a/app/assets/javascripts/admin/index_utils/services/columns.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.indexUtils").factory 'Columns', ($rootScope, $http, columns) -> +angular.module("admin.indexUtils").factory 'Columns', ($rootScope, $http, $injector) -> new class Columns savedColumns: {} columns: {} @@ -6,11 +6,17 @@ angular.module("admin.indexUtils").factory 'Columns', ($rootScope, $http, column constructor: -> @columns = {} - for column in columns + for column in @injectColumns() @columns[column.column_name] = column @savedColumns[column.column_name] = angular.copy(column) @calculateVisibleCount() + injectColumns: -> + if $injector.has('columns') + $injector.get('columns') + else + [] + toggleColumn: (column) => column.visible = !column.visible @calculateVisibleCount() diff --git a/app/assets/javascripts/admin/index_utils/services/panels.js.coffee b/app/assets/javascripts/admin/index_utils/services/panels.js.coffee index 27852bed12..d020b88264 100644 --- a/app/assets/javascripts/admin/index_utils/services/panels.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/panels.js.coffee @@ -1,19 +1,22 @@ angular.module("admin.indexUtils").factory 'Panels', -> new class Panels - panels: {} + panels: [] - register: (id, scope) -> - if id? && scope? - @panels[id] = scope + register: (ctrl, object, selected=null) -> + if ctrl? && object? + @panels.push { ctrl: ctrl, object: object, selected: selected } + ctrl.select(selected) if selected? - toggle: (id, name) -> - scope = @panels[id] - selected = scope.getSelected() - switch selected - when name - scope.close() - when null - scope.open(name) - else - scope.setSelected(name) - scope.getSelected() + toggle: (object, name, state=null) -> + panel = @findPanelByObject(object) + if panel.selected == name + @select(panel, null) unless state == "open" + else + @select(panel, name) unless state == "closed" + + select: (panel, name) -> + panel.selected = name + panel.ctrl.select(name) + + findPanelByObject: (object) -> + (panel for panel in @panels when panel.object == object)[0] diff --git a/app/assets/javascripts/templates/admin/panel.html.haml b/app/assets/javascripts/templates/admin/panel.html.haml index be0c98109f..4d3780aed0 100644 --- a/app/assets/javascripts/templates/admin/panel.html.haml +++ b/app/assets/javascripts/templates/admin/panel.html.haml @@ -1,2 +1,2 @@ -%td{ colspan: "{{columnCount}}" } +%td{ colspan: "{{columnCount}}", ng: { if: "template" } } .panel{ ng: { include: "template" } } diff --git a/app/assets/stylesheets/admin/index_panels.css.scss b/app/assets/stylesheets/admin/index_panels.css.scss index a910e41d14..ca8464ab26 100644 --- a/app/assets/stylesheets/admin/index_panels.css.scss +++ b/app/assets/stylesheets/admin/index_panels.css.scss @@ -1,4 +1,4 @@ -tr.panel-toggle-row { +tbody.panel-toggle-row { td.panel-toggle{ -webkit-touch-callout: none; -webkit-user-select: none; @@ -66,13 +66,13 @@ tr.panel-toggle-row { &.expanded{ td { - border-bottom: 2px solid #444444; + border-bottom: 1px solid #6788a2; &.selected { background-color: #ffffff; - border-left: 2px solid #444444; - border-right: 2px solid #444444; - border-top: 2px solid #444444; + border-left: 1px solid #6788a2; + border-right: 1px solid #6788a2; + border-top: 1px solid #6788a2; border-bottom: none; &:hover { @@ -96,8 +96,6 @@ tr.panel-toggle-row { } tr.panel-row { - display: none; - &:hover { td { background-color: #ffffff; @@ -105,13 +103,9 @@ tr.panel-row { } >td { - border-color: #444444; + border-color: #6788a2; padding: 0; .panel { - border-left: 1px solid #444444; - border-right: 1px solid #444444; - border-bottom: 1px solid #444444; - .row{ margin: 0px -4px; diff --git a/app/views/admin/enterprises/_enterprise_user_index.html.haml b/app/views/admin/enterprises/_enterprise_user_index.html.haml index 75dbf98f17..3c976185d5 100644 --- a/app/views/admin/enterprises/_enterprise_user_index.html.haml +++ b/app/views/admin/enterprises/_enterprise_user_index.html.haml @@ -29,16 +29,16 @@ %th.package{ ng: { show: 'columns.package.visible' } }=t('.package') %th.status{ ng: { show: 'columns.status.visible' } }=t('.status') %th.manage{ ng: { show: 'columns.manage.visible' } }=t('.manage') - %tbody{ :id => "e_{{enterprise.id}}", ng: { repeat: "enterprise in filteredEnterprises = ( allEnterprises | filter:{ name: quickSearch } )", controller: 'EnterpriseIndexRowCtrl' } } - %tr.enterprise.panel-toggle-row{ object: "enterprise", ng: { class: { even: "'even'", odd: "'odd'"} } } + %tbody.panel-toggle-row{ :id => "e_{{enterprise.id}}", object: "enterprise", ng: { repeat: "enterprise in filteredEnterprises = ( allEnterprises | filter:{ name: quickSearch } )" } } + %tr.enterprise{ ng: { class: { even: "'even'", odd: "'odd'"} } } %td.name{ ng: { show: 'columns.name.visible' } } %span{ ng: { bind: "::enterprise.name" } } - %td.producer.panel-toggle.text-center{ ng: { show: 'columns.producer.visible', class: "{error: producerError}" }, name: "producer" } - %h5{ ng: { bind: "producer" } } - %td.package.panel-toggle.text-center{ ng: { show: 'columns.package.visible', class: "{error: packageError}" }, name: "package" } - %h5{ ng: { bind: "package" } } + %td.producer.panel-toggle.text-center{ ng: { show: 'columns.producer.visible', class: "{error: enterprise.producerError}" }, name: "producer" } + %h5{ ng: { bind: "enterprise.producer" } } + %td.package.panel-toggle.text-center{ ng: { show: 'columns.package.visible', class: "{error: enterprise.packageError}" }, name: "package" } + %h5{ ng: { bind: "enterprise.package" } } %td.status.panel-toggle.text-center{ ng: { show: 'columns.status.visible' }, name: "status" } - %i.icon-status{ ng: { class: "::status()" } } + %i.icon-status{ ng: { class: "enterprise.status" } } %td.manage{ ng: { show: 'columns.manage.visible' } } %a.button.fullwidth{ ng: { href: '{{::enterprise.edit_path}}' } } Manage diff --git a/spec/javascripts/unit/admin/enterprises/controllers/index_panel_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/index_panel_controller_spec.js.coffee index 8fca1c6ab5..1ade26f19e 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/index_panel_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/index_panel_controller_spec.js.coffee @@ -38,7 +38,7 @@ describe "indexPanelCtrl", -> expect(scope.saving).toBe false it "emits an 'enterprise:updated' event", -> - expect(scope.$emit).toHaveBeenCalledWith("enterprise:updated") + expect(scope.$emit).toHaveBeenCalledWith("enterprise:updated", scope.enterprise) describe "when the save is unsuccessful", -> beforeEach inject ($rootScope) -> diff --git a/spec/javascripts/unit/admin/index_utils/directives/panel_row_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/directives/panel_row_spec.js.coffee index 92eba790c3..f4f0ac4ce3 100644 --- a/spec/javascripts/unit/admin/index_utils/directives/panel_row_spec.js.coffee +++ b/spec/javascripts/unit/admin/index_utils/directives/panel_row_spec.js.coffee @@ -1,7 +1,8 @@ describe "PanelRow directive", -> Panels = null element = null - directiveScope = null + panelScope = null + rowScope = null beforeEach -> module 'admin.indexUtils' @@ -13,24 +14,27 @@ describe "PanelRow directive", -> Panels = _Panels_ $templateCache.put 'admin/panel.html', '{{ template }}' # Declare the directive HTML. - element = angular.element('
    ') + element = angular.element('
    ') # Define the root scope. scope = $rootScope # Compile and digest the directive. $compile(element) scope scope.$digest() - directiveScope = element.find('span').scope() + rowScope = element.find('tbody').isolateScope() + panelScope = element.find('tr').isolateScope() return describe "initialisation", -> - it "registers the scope with the panels service", -> - expect(Panels.panels[12]).toEqual directiveScope + it "registers a listener on the row scope", -> + expect(rowScope.$$listeners["selection:changed"].length).toEqual 1 - describe "setting the selected panel", -> + describe "when a select event is triggered on the row scope", -> beforeEach -> - directiveScope.setSelected('panel1') + rowScope.$broadcast('selection:changed', 'panel1') it 'updates the active template on the scope', -> + panelScope.$digest() + expect(panelScope.template).toEqual "admin/panels/template.html" expect(element.find('span').html()).toEqual "admin/panels/template.html" return diff --git a/spec/javascripts/unit/admin/index_utils/services/panels_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/panels_spec.js.coffee index 7ee69cf88e..8a659df984 100644 --- a/spec/javascripts/unit/admin/index_utils/services/panels_spec.js.coffee +++ b/spec/javascripts/unit/admin/index_utils/services/panels_spec.js.coffee @@ -8,45 +8,110 @@ describe "Panels service", -> Panels = _Panels_ describe "registering panels", -> - it "adds the panel provided scope to @panelsm indexed by the provided id", -> - Panels.register(23, { some: 'scope'} ) - expect(Panels.panels[23]).toEqual { some: 'scope' } + ctrl1 = ctrl2 = null + beforeEach -> + ctrl1 = jasmine.createSpyObj('ctrl', ['select']) + ctrl2 = jasmine.createSpyObj('ctrl', ['select']) - it "ignores the input if id or scope are null", -> - Panels.register(null, { some: 'scope'} ) - Panels.register(23, null) - expect(Panels.panels).toEqual { } + it "adds the panels controller, object and selection to @panels", -> + Panels.register(ctrl1, { name: "obj1"}, "panel1") + Panels.register(ctrl2, { name: "obj2"}) + expect(Panels.panels).toEqual [ + { ctrl: ctrl1, object: { name: "obj1"}, selected: "panel1" }, + { ctrl: ctrl2, object: { name: "obj2"}, selected: null } + ] + + it "call select on the controller if a selection is provided", -> + Panels.register(ctrl1, { name: "obj1"}, "panel1") + Panels.register(ctrl2, { name: "obj2"}) + expect(ctrl1.select.calls.count()).toEqual 1 + expect(ctrl2.select.calls.count()).toEqual 0 + + it "ignores the input if object or ctrl are null", -> + Panels.register(ctrl1, null) + Panels.register(null, { name: "obj2"}) + expect(Panels.panels).toEqual [] describe "toggling a panel", -> - scopeMock = null + panelMock = ctrlMock = objMock = null beforeEach -> - scopeMock = - open: jasmine.createSpy('open') - close: jasmine.createSpy('close') - setSelected: jasmine.createSpy('setSelected') - Panels.panels = { '12': scopeMock } + ctrlMock = jasmine.createSpyObj('ctrl', ['select']) + panelMock = { ctrl: ctrlMock } + spyOn(Panels, "findPanelByObject").and.returnValue(panelMock) describe "when no panel is currently selected", -> beforeEach -> - scopeMock.getSelected = jasmine.createSpy('getSelected').and.returnValue(null) - Panels.toggle(12, 'panel_name') + panelMock.selected = null - it "calls #open on the scope", -> - expect(scopeMock.open).toHaveBeenCalledWith('panel_name') + describe "when no state is provided", -> + beforeEach -> Panels.toggle({some: "object"}, 'panel_name') - describe "when #toggle is called for the currently selected panel", -> + it "selects the named panel", -> + expect(panelMock.selected).toEqual 'panel_name' + expect(ctrlMock.select).toHaveBeenCalledWith('panel_name') + + describe "when the state given is 'open'", -> + beforeEach -> Panels.toggle({some: "object"}, 'panel_name', "open") + + it "selects the named panel", -> + expect(panelMock.selected).toEqual 'panel_name' + expect(ctrlMock.select).toHaveBeenCalledWith('panel_name') + + describe "when the state given is 'closed'", -> + beforeEach -> Panels.toggle({some: "object"}, 'panel_name', "closed") + + it "does not select the named panel", -> + expect(panelMock.selected).toEqual null + expect(ctrlMock.select).not.toHaveBeenCalledWith('panel_name') + + describe "when the currently selected panel matches the named panel", -> beforeEach -> - scopeMock.getSelected = jasmine.createSpy('getSelected').and.returnValue('panel_name') - Panels.toggle(12, 'panel_name') + panelMock.selected = 'panel_name' - it "calls #close on the scope", -> - expect(scopeMock.close).toHaveBeenCalled() + describe "when no state is provided", -> + beforeEach -> Panels.toggle({some: "object"}, 'panel_name') - describe "when #toggle is called for a different panel", -> + it "de-selects the named panel", -> + expect(panelMock.selected).toEqual null + expect(ctrlMock.select).toHaveBeenCalledWith(null) + + describe "when the state given is 'open'", -> + beforeEach -> Panels.toggle({some: "object"}, 'panel_name', "open") + + it "keeps the the named panel selected, but does not call select on the controller", -> + expect(panelMock.selected).toEqual 'panel_name' + expect(ctrlMock.select).not.toHaveBeenCalledWith('panel_name') + + describe "when the state given is 'closed'", -> + beforeEach -> Panels.toggle({some: "object"}, 'panel_name', "closed") + + it "de-selects the named panel", -> + expect(panelMock.selected).toEqual null + expect(ctrlMock.select).not.toHaveBeenCalledWith('panel_name') + + describe "when the currently selected panel does not match the requested panel", -> beforeEach -> - scopeMock.getSelected = jasmine.createSpy('getSelected').and.returnValue('some_other_panel_name') - Panels.toggle(12, 'panel_name') + panelMock.selected = 'some_other_panel' - it "calls #setSelected on the scope", -> - expect(scopeMock.setSelected).toHaveBeenCalledWith('panel_name') + describe "when no state is provided", -> + beforeEach -> Panels.toggle({some: "object"}, 'panel_name') + + it "selects the named panel", -> + expect(panelMock.selected).toEqual 'panel_name' + expect(ctrlMock.select).toHaveBeenCalledWith('panel_name') + + describe "when the state given is 'open'", -> + beforeEach -> Panels.toggle({some: "object"}, 'panel_name', "open") + + it "selects the named panel", -> + expect(panelMock.selected).toEqual 'panel_name' + expect(ctrlMock.select).toHaveBeenCalledWith('panel_name') + + describe "when the state given is 'closed'", -> + beforeEach -> Panels.toggle({some: "object"}, 'panel_name', "closed") + + it "keeps the currently selected panel selected, ie. does nothing", -> + expect(panelMock.selected).toEqual "some_other_panel" + expect(ctrlMock.select).not.toHaveBeenCalledWith('panel_name') + expect(ctrlMock.select).not.toHaveBeenCalledWith('some_other_panel') From e81858c0dc880dd20b3ad500572940260dc30259 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 1 May 2016 12:13:17 +1000 Subject: [PATCH 082/110] Renaming PanelToggleRow to PanelCtrl --- ...panel_toggle_row.js.coffee => panel_ctrl.js.coffee} | 2 +- .../admin/index_utils/directives/panel_row.js.coffee | 2 +- .../index_utils/directives/panel_toggle.js.coffee | 2 +- app/assets/stylesheets/admin/index_panels.css.scss | 2 +- .../admin/enterprises/_enterprise_user_index.html.haml | 2 +- .../index_utils/directives/panel_row_spec.js.coffee | 10 +++++----- 6 files changed, 10 insertions(+), 10 deletions(-) rename app/assets/javascripts/admin/index_utils/directives/{panel_toggle_row.js.coffee => panel_ctrl.js.coffee} (88%) diff --git a/app/assets/javascripts/admin/index_utils/directives/panel_toggle_row.js.coffee b/app/assets/javascripts/admin/index_utils/directives/panel_ctrl.js.coffee similarity index 88% rename from app/assets/javascripts/admin/index_utils/directives/panel_toggle_row.js.coffee rename to app/assets/javascripts/admin/index_utils/directives/panel_ctrl.js.coffee index ec6cb6d5ed..4259ce63e2 100644 --- a/app/assets/javascripts/admin/index_utils/directives/panel_toggle_row.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/panel_ctrl.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.indexUtils").directive "panelToggleRow", (Panels) -> +angular.module("admin.indexUtils").directive "panelCtrl", (Panels) -> restrict: "C" scope: object: "=" diff --git a/app/assets/javascripts/admin/index_utils/directives/panel_row.js.coffee b/app/assets/javascripts/admin/index_utils/directives/panel_row.js.coffee index 474ec5664d..8b1e8d4b24 100644 --- a/app/assets/javascripts/admin/index_utils/directives/panel_row.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/panel_row.js.coffee @@ -1,6 +1,6 @@ angular.module("admin.indexUtils").directive "panelRow", (Panels, Columns) -> restrict: "C" - require: "^^panelToggleRow" + require: "^^panelCtrl" templateUrl: "admin/panel.html" scope: object: "=" diff --git a/app/assets/javascripts/admin/index_utils/directives/panel_toggle.js.coffee b/app/assets/javascripts/admin/index_utils/directives/panel_toggle.js.coffee index 56d80d88b9..cdd15d1d62 100644 --- a/app/assets/javascripts/admin/index_utils/directives/panel_toggle.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/panel_toggle.js.coffee @@ -2,7 +2,7 @@ angular.module("admin.indexUtils").directive "panelToggle", -> restrict: "C" transclude: true template: '
    ' - require: "^^panelToggleRow" + require: "^^panelCtrl" scope: name: "@" link: (scope, element, attrs, ctrl) -> diff --git a/app/assets/stylesheets/admin/index_panels.css.scss b/app/assets/stylesheets/admin/index_panels.css.scss index ca8464ab26..d2861eb661 100644 --- a/app/assets/stylesheets/admin/index_panels.css.scss +++ b/app/assets/stylesheets/admin/index_panels.css.scss @@ -1,4 +1,4 @@ -tbody.panel-toggle-row { +tbody.panel-ctrl { td.panel-toggle{ -webkit-touch-callout: none; -webkit-user-select: none; diff --git a/app/views/admin/enterprises/_enterprise_user_index.html.haml b/app/views/admin/enterprises/_enterprise_user_index.html.haml index 3c976185d5..d89970946d 100644 --- a/app/views/admin/enterprises/_enterprise_user_index.html.haml +++ b/app/views/admin/enterprises/_enterprise_user_index.html.haml @@ -29,7 +29,7 @@ %th.package{ ng: { show: 'columns.package.visible' } }=t('.package') %th.status{ ng: { show: 'columns.status.visible' } }=t('.status') %th.manage{ ng: { show: 'columns.manage.visible' } }=t('.manage') - %tbody.panel-toggle-row{ :id => "e_{{enterprise.id}}", object: "enterprise", ng: { repeat: "enterprise in filteredEnterprises = ( allEnterprises | filter:{ name: quickSearch } )" } } + %tbody.panel-ctrl{ :id => "e_{{enterprise.id}}", object: "enterprise", ng: { repeat: "enterprise in filteredEnterprises = ( allEnterprises | filter:{ name: quickSearch } )" } } %tr.enterprise{ ng: { class: { even: "'even'", odd: "'odd'"} } } %td.name{ ng: { show: 'columns.name.visible' } } %span{ ng: { bind: "::enterprise.name" } } diff --git a/spec/javascripts/unit/admin/index_utils/directives/panel_row_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/directives/panel_row_spec.js.coffee index f4f0ac4ce3..1b075a94be 100644 --- a/spec/javascripts/unit/admin/index_utils/directives/panel_row_spec.js.coffee +++ b/spec/javascripts/unit/admin/index_utils/directives/panel_row_spec.js.coffee @@ -1,8 +1,8 @@ describe "PanelRow directive", -> Panels = null element = null + ctrlScope = null panelScope = null - rowScope = null beforeEach -> module 'admin.indexUtils' @@ -14,24 +14,24 @@ describe "PanelRow directive", -> Panels = _Panels_ $templateCache.put 'admin/panel.html', '{{ template }}' # Declare the directive HTML. - element = angular.element('
    ') + element = angular.element('
    ') # Define the root scope. scope = $rootScope # Compile and digest the directive. $compile(element) scope scope.$digest() - rowScope = element.find('tbody').isolateScope() + ctrlScope = element.find('tbody').isolateScope() panelScope = element.find('tr').isolateScope() return describe "initialisation", -> it "registers a listener on the row scope", -> - expect(rowScope.$$listeners["selection:changed"].length).toEqual 1 + expect(ctrlScope.$$listeners["selection:changed"].length).toEqual 1 describe "when a select event is triggered on the row scope", -> beforeEach -> - rowScope.$broadcast('selection:changed', 'panel1') + ctrlScope.$broadcast('selection:changed', 'panel1') it 'updates the active template on the scope', -> panelScope.$digest() From 9dc1294ec527e239ef0c9b75dfb05a2f00f856aa Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sun, 1 May 2016 14:08:55 +1000 Subject: [PATCH 083/110] Outgoing exchanges can be tagged Using panels infrastructure to display tags and products interfaces --- .../order_cycles/controllers/create.js.coffee | 4 - .../order_cycles/controllers/edit.js.coffee | 4 - .../admin/order_cycles/order_cycles.js.coffee | 2 +- .../services/order_cycle.js.coffee | 16 ++-- .../exchange_distributed_products.html.haml | 31 ++++++++ .../exchange_supplied_products.html.haml | 49 ++++++++++++ .../admin/panels/exchange_tags.html.haml | 5 ++ .../admin/openfoodnetwork.css.scss | 4 - app/models/exchange.rb | 3 + .../api/admin/exchange_serializer.rb | 9 +++ ...change_distributed_products_form.html.haml | 23 ------ .../order_cycles/_exchange_form.html.haml | 65 +++++++++------ app/views/admin/order_cycles/_form.html.haml | 16 ++-- .../admin/order_cycles/_simple_form.html.haml | 2 +- .../order_cycle_form_applicator.rb | 7 +- spec/features/admin/order_cycles_spec.rb | 79 ++++++++++++++----- .../unit/order_cycle_spec.js.coffee | 32 +------- .../order_cycle_form_applicator_spec.rb | 23 +++--- spec/models/exchange_spec.rb | 2 + 19 files changed, 232 insertions(+), 144 deletions(-) create mode 100644 app/assets/javascripts/templates/admin/panels/exchange_distributed_products.html.haml create mode 100644 app/assets/javascripts/templates/admin/panels/exchange_supplied_products.html.haml create mode 100644 app/assets/javascripts/templates/admin/panels/exchange_tags.html.haml delete mode 100644 app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml diff --git a/app/assets/javascripts/admin/order_cycles/controllers/create.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/create.js.coffee index 3077aa8a11..3f28b27f9f 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/create.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/create.js.coffee @@ -41,10 +41,6 @@ angular.module('admin.orderCycles') $scope.enterprisesWithFees = -> $scope.enterprises[id] for id in OrderCycle.participatingEnterpriseIds() when $scope.enterpriseFeesForEnterprise(id).length > 0 - $scope.toggleProducts = ($event, exchange) -> - $event.preventDefault() - OrderCycle.toggleProducts(exchange) - $scope.enterpriseFeesForEnterprise = (enterprise_id) -> EnterpriseFee.forEnterprise(parseInt(enterprise_id)) diff --git a/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee index 2f3b2db5e0..c0818acafa 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/edit.js.coffee @@ -45,10 +45,6 @@ angular.module('admin.orderCycles') $scope.enterprisesWithFees = -> $scope.enterprises[id] for id in OrderCycle.participatingEnterpriseIds() when $scope.enterpriseFeesForEnterprise(id).length > 0 - $scope.toggleProducts = ($event, exchange) -> - $event.preventDefault() - OrderCycle.toggleProducts(exchange) - $scope.enterpriseFeesForEnterprise = (enterprise_id) -> EnterpriseFee.forEnterprise(parseInt(enterprise_id)) diff --git a/app/assets/javascripts/admin/order_cycles/order_cycles.js.coffee b/app/assets/javascripts/admin/order_cycles/order_cycles.js.coffee index a75fdad58c..6ea3f76984 100644 --- a/app/assets/javascripts/admin/order_cycles/order_cycles.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/order_cycles.js.coffee @@ -1,4 +1,4 @@ -angular.module('admin.orderCycles', ['ngResource', 'admin.utils', 'admin.indexUtils']) +angular.module('admin.orderCycles', ['ngResource', 'admin.utils', 'admin.indexUtils', 'ngTagsInput']) .config ($httpProvider) -> $httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content') diff --git a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee index 662201c9f5..5ab14dfdcd 100644 --- a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee @@ -1,4 +1,4 @@ -angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, StatusMessage) -> +angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, StatusMessage, Panels) -> OrderCycleResource = $resource '/admin/order_cycles/:action_name/:order_cycle_id.json', {}, { 'index': { method: 'GET', isArray: true} 'new' : { method: 'GET', params: { action_name: "new" } } @@ -30,23 +30,25 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, S exchangeDirection: (exchange) -> if this.order_cycle.incoming_exchanges.indexOf(exchange) == -1 then 'outgoing' else 'incoming' - toggleProducts: (exchange) -> - exchange.showProducts = !exchange.showProducts - toggleAllProducts: (direction) -> this.showProducts[direction] = !this.showProducts[direction] - exchange.showProducts = this.showProducts[direction] for exchange in this.exchangesByDirection(direction) + state = if this.showProducts[direction] then "open" else "closed" + exchanges = this.exchangesByDirection(direction) + Panels.toggle(exchange,'products',state) for exchange in exchanges setExchangeVariants: (exchange, variants, selected) -> direction = if exchange.incoming then "incoming" else "outgoing" editable = @order_cycle["editable_variants_for_#{direction}_exchanges"][exchange.enterprise_id] || [] - exchange.variants[variant] = selected for variant in variants when variant in editable + for variant in variants when variant in editable + exchange.variants[variant] = selected + @removeDistributionOfVariant(variant.id) if exchange.incoming + addSupplier: (new_supplier_id) -> this.order_cycle.incoming_exchanges.push({enterprise_id: new_supplier_id, incoming: true, active: true, variants: {}, enterprise_fees: []}) addDistributor: (new_distributor_id) -> - this.order_cycle.outgoing_exchanges.push({enterprise_id: new_distributor_id, incoming: false, active: true, variants: {}, enterprise_fees: []}) + this.order_cycle.outgoing_exchanges.push({enterprise_id: new_distributor_id, incoming: false, active: true, variants: {}, enterprise_fees: [], tags: [], tag_list: ""}) removeExchange: (exchange) -> if exchange.incoming diff --git a/app/assets/javascripts/templates/admin/panels/exchange_distributed_products.html.haml b/app/assets/javascripts/templates/admin/panels/exchange_distributed_products.html.haml new file mode 100644 index 0000000000..4c6a86e926 --- /dev/null +++ b/app/assets/javascripts/templates/admin/panels/exchange_distributed_products.html.haml @@ -0,0 +1,31 @@ +.row.exchange-distributed-products + .sixteen.columns.alpha.omega + .exchange-select-all-variants + %label + %input{ type: 'checkbox', name: 'order_cycle_outgoing_exchange_{{ $parent.$index }}_select_all_variants', + value: 1, + 'ng-model' => 'exchange.select_all_variants', + 'ng-change' => 'setExchangeVariants(exchange, incomingExchangeVariantsFor(exchange.enterprise_id), exchange.select_all_variants)', + 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$index }}_select_all_variants' } + Select all + + .exchange-products + -# Scope product list based on permissions the current user has to view variants in this exchange + .exchange-product{'ng-repeat' => 'product in supplied_products | filter:productSuppliedToOrderCycle | visibleProducts:exchange:order_cycle.visible_variants_for_outgoing_exchanges | orderBy:"name"' } + .exchange-product-details + %label + -# MASTER_VARIANTS: No longer required + -# = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', 1, 1, 'ng-hide' => 'product.variants.length > 0', 'ng-model' => 'exchange.variants[product.master_id]', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', + -# 'ng-disabled' => 'product.variants.length > 0 || !order_cycle.editable_variants_for_outgoing_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_outgoing_exchanges[exchange.enterprise_id].indexOf(product.master_id) < 0' + %img{'ng-src' => '{{ product.image_url }}'} + .name {{ product.name }} + .supplier {{ product.supplier_name }} + + .exchange-product-variant{'ng-repeat' => 'variant in product.variants | visibleVariants:exchange:order_cycle.visible_variants_for_outgoing_exchanges | filter:variantSuppliedToOrderCycle'} + %label + %input{ type: 'checkbox', name: 'order_cycle_outgoing_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}', + value: 1, + 'ng-model' => 'exchange.variants[variant.id]', + 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}', + 'ng-disabled' => '!order_cycle.editable_variants_for_outgoing_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_outgoing_exchanges[exchange.enterprise_id].indexOf(variant.id) < 0' } + {{ variant.label }} diff --git a/app/assets/javascripts/templates/admin/panels/exchange_supplied_products.html.haml b/app/assets/javascripts/templates/admin/panels/exchange_supplied_products.html.haml new file mode 100644 index 0000000000..04e191fcf0 --- /dev/null +++ b/app/assets/javascripts/templates/admin/panels/exchange_supplied_products.html.haml @@ -0,0 +1,49 @@ +.row.exchange-supplied-products + .sixteen.columns.alpha.omega + .exchange-select-all-variants + %label + %input{ type: 'checkbox', name: 'order_cycle_incoming_exchange_{{ $index }}_select_all_variants', + value: 1, + 'ng-model' => 'exchange.select_all_variants', + 'ng-change' => 'setExchangeVariants(exchange, suppliedVariants(exchange.enterprise_id), exchange.select_all_variants)', + 'id' => 'order_cycle_incoming_exchange_{{ $index }}_select_all_variants' } + Select all + + .exchange-products + -# No need to scope product list based on permissions, because if an incoming exchange is visible, + -# then all of the variants within it should be visible. May change in the future? + .exchange-product{'ng-repeat' => 'product in enterprises[exchange.enterprise_id].supplied_products'} + + .exchange-product-details + %label + %input{ type: 'checkbox', name: 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', + value: 1, + 'ng-hide' => 'product.variants.length > 0', + 'ng-model' => 'exchange.variants[product.master_id]', + 'ofn-sync-distributions' => '{{ product.master_id }}', + 'id' => 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', + 'ng-disabled' => 'product.variants.length > 0 || !order_cycle.editable_variants_for_incoming_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_incoming_exchanges[exchange.enterprise_id].indexOf(product.master_id) < 0' } + %img{'ng-src' => '{{ product.image_url }}'} + {{ product.name }} + + -# When the master variant is in the order cycle but the product has variants, we want to + -# be able to remove the master variant, since it serves no purpose. Display a checkbox to do so. + .exchange-product-variant{'ng-show' => 'exchange.variants[product.master_id] && product.variants.length > 0'} + %label + %input{ type: 'checkbox', name: 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', + value: 1, + 'ng-model' => 'exchange.variants[product.master_id]', + 'ofn-sync-distributions' => '{{ product.master_id }}', + 'id' => 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', + 'ng-disabled' => '!order_cycle.editable_variants_for_incoming_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_incoming_exchanges[exchange.enterprise_id].indexOf(product.master_id) < 0' } + Obsolete master + + .exchange-product-variant{'ng-repeat' => 'variant in product.variants'} + %label + %input{ type: 'checkbox', name: 'order_cycle_incoming_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}', + value: 1, + 'ng-model' => 'exchange.variants[variant.id]', + 'ofn-sync-distributions' => '{{ variant.id }}', + 'id' => 'order_cycle_incoming_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}', + 'ng-disabled' => '!order_cycle.editable_variants_for_incoming_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_incoming_exchanges[exchange.enterprise_id].indexOf(variant.id) < 0' } + {{ variant.label }} diff --git a/app/assets/javascripts/templates/admin/panels/exchange_tags.html.haml b/app/assets/javascripts/templates/admin/panels/exchange_tags.html.haml new file mode 100644 index 0000000000..0963c480c0 --- /dev/null +++ b/app/assets/javascripts/templates/admin/panels/exchange_tags.html.haml @@ -0,0 +1,5 @@ +.row.exchange-tags + .sixteen.columns.alpha.omega + %span.text-normal Tags + %br + %tags-with-translation.fullwidth{ object: 'object' } diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index 8d77015af7..1532ea8520 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -79,10 +79,6 @@ form.order_cycle { width: 20px; } - tr.supplier td { - border-bottom: 2px solid #C3D9FF; - } - .exchange-select-all-variants { clear: both; margin: 5px; diff --git a/app/models/exchange.rb b/app/models/exchange.rb index ad290f6c26..3d0e6417e6 100644 --- a/app/models/exchange.rb +++ b/app/models/exchange.rb @@ -1,4 +1,6 @@ class Exchange < ActiveRecord::Base + acts_as_taggable + belongs_to :order_cycle belongs_to :sender, :class_name => 'Enterprise' belongs_to :receiver, :class_name => 'Enterprise' @@ -58,6 +60,7 @@ class Exchange < ActiveRecord::Base exchange.order_cycle = new_order_cycle exchange.enterprise_fee_ids = self.enterprise_fee_ids exchange.variant_ids = self.variant_ids + exchange.tag_ids = self.tag_ids exchange.save! exchange end diff --git a/app/serializers/api/admin/exchange_serializer.rb b/app/serializers/api/admin/exchange_serializer.rb index 615d49f695..e1c15c5210 100644 --- a/app/serializers/api/admin/exchange_serializer.rb +++ b/app/serializers/api/admin/exchange_serializer.rb @@ -1,5 +1,6 @@ class Api::Admin::ExchangeSerializer < ActiveModel::Serializer attributes :id, :sender_id, :receiver_id, :incoming, :variants, :receival_instructions, :pickup_time, :pickup_instructions + attributes :tags, :tag_list has_many :enterprise_fees, serializer: Api::Admin::BasicEnterpriseFeeSerializer @@ -35,4 +36,12 @@ class Api::Admin::ExchangeSerializer < ActiveModel::Serializer OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle) .visible_variants_for_outgoing_exchanges_to(object.receiver) end + + def tag_list + object.tag_list.join(",") + end + + def tags + object.tag_list.map{ |t| { text: t } } + end end diff --git a/app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml b/app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml deleted file mode 100644 index e2218599ce..0000000000 --- a/app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -%td{:colspan => 4} - .exchange-select-all-variants - %label - = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$index }}_select_all_variants', 1, 1, 'ng-model' => 'exchange.select_all_variants', 'ng-change' => 'setExchangeVariants(exchange, incomingExchangeVariantsFor(exchange.enterprise_id), exchange.select_all_variants)', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$index }}_select_all_variants' - Select all - - .exchange-products - -# Scope product list based on permissions the current user has to view variants in this exchange - .exchange-product{'ng-repeat' => 'product in supplied_products | filter:productSuppliedToOrderCycle | visibleProducts:exchange:order_cycle.visible_variants_for_outgoing_exchanges | orderBy:"name"' } - .exchange-product-details - %label - -# MASTER_VARIANTS: No longer required - -# = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', 1, 1, 'ng-hide' => 'product.variants.length > 0', 'ng-model' => 'exchange.variants[product.master_id]', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', - -# 'ng-disabled' => 'product.variants.length > 0 || !order_cycle.editable_variants_for_outgoing_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_outgoing_exchanges[exchange.enterprise_id].indexOf(product.master_id) < 0' - %img{'ng-src' => '{{ product.image_url }}'} - .name {{ product.name }} - .supplier {{ product.supplier_name }} - - .exchange-product-variant{'ng-repeat' => 'variant in product.variants | visibleVariants:exchange:order_cycle.visible_variants_for_outgoing_exchanges | filter:variantSuppliedToOrderCycle'} - %label - = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}', 1, 1, 'ng-model' => 'exchange.variants[variant.id]', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}', - 'ng-disabled' => '!order_cycle.editable_variants_for_outgoing_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_outgoing_exchanges[exchange.enterprise_id].indexOf(variant.id) < 0' - {{ variant.label }} diff --git a/app/views/admin/order_cycles/_exchange_form.html.haml b/app/views/admin/order_cycles/_exchange_form.html.haml index ed3e43fd6b..0e3dbb4340 100644 --- a/app/views/admin/order_cycles/_exchange_form.html.haml +++ b/app/views/admin/order_cycles/_exchange_form.html.haml @@ -1,29 +1,42 @@ -%td{:class => "#{type}_name"} {{ enterprises[exchange.enterprise_id].name }} -%td.products - = f.submit 'Products', 'ng-click' => 'toggleProducts($event, exchange)' - {{ exchangeSelectedVariants(exchange) }} / +%tr{ ng: { class: "'#{type} #{type}-{{ exchange.enterprise_id }}'" } } + %td{:class => "#{type}_name"} {{ enterprises[exchange.enterprise_id].name }} + %td.products.panel-toggle.text-center{ name: "products" } + {{ exchangeSelectedVariants(exchange) }} / + - if type == 'supplier' + {{ enterpriseTotalVariants(enterprises[exchange.enterprise_id]) }} + - else + {{ (incomingExchangeVariantsFor(exchange.enterprise_id)).length }} + selected - if type == 'supplier' - {{ enterpriseTotalVariants(enterprises[exchange.enterprise_id]) }} - - else - {{ (incomingExchangeVariantsFor(exchange.enterprise_id)).length }} - selected + %td.receival-details + = text_field_tag 'order_cycle_incoming_exchange_{{ $index }}_receival_instructions', '', 'id' => 'order_cycle_incoming_exchange_{{ $index }}_receival_instructions', 'placeholder' => 'Receival instructions', 'ng-model' => 'exchange.receival_instructions' + - if type == 'distributor' + %td.tags.panel-toggle.text-center{ name: "tags", ng: { if: 'enterprises[exchange.enterprise_id].managed || order_cycle.viewing_as_coordinator' } } + {{ exchange.tags.length }} + %td.collection-details + = text_field_tag 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', '', 'id' => 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', 'placeholder' => 'Ready for (ie. Date / Time)', 'ng-model' => 'exchange.pickup_time', 'ng-disabled' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator' + %br/ + = text_field_tag 'order_cycle_outgoing_exchange_{{ $index }}_pickup_instructions', '', 'id' => 'order_cycle_outgoing_exchange_{{ $index }}_pickup_instructions', 'placeholder' => 'Pick-up instructions', 'ng-model' => 'exchange.pickup_instructions', 'ng-disabled' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator' + %td.fees + %ol{ ng: { show: 'enterprises[exchange.enterprise_id].managed || order_cycle.viewing_as_coordinator' } } + %li{'ng-repeat' => 'enterprise_fee in exchange.enterprise_fees'} + = select_tag 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_enterprise_id', nil, {'id' => 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_enterprise_id', 'ng-model' => 'enterprise_fee.enterprise_id', 'ng-options' => 'enterprise.id as enterprise.name for enterprise in enterprisesWithFees()'} + + = select_tag 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_enterprise_fee_id', nil, {'id' => 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_enterprise_fee_id', 'ng-model' => 'enterprise_fee.id', 'ng-options' => 'enterprise_fee.id as enterprise_fee.name for enterprise_fee in enterpriseFeesForEnterprise(enterprise_fee.enterprise_id)'} + + = link_to 'Remove', '#', {'id' => 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_remove', 'ng-click' => 'removeExchangeFee($event, exchange, $index)'} + + = f.submit 'Add fee', 'ng-click' => 'addExchangeFee($event, exchange)', 'ng-hide' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator' + %td.actions + %a{'ng-click' => 'removeExchange($event, exchange)', :class => "icon-trash no-text remove-exchange"} + - if type == 'supplier' - %td.receival-details - = text_field_tag 'order_cycle_incoming_exchange_{{ $index }}_receival_instructions', '', 'id' => 'order_cycle_incoming_exchange_{{ $index }}_receival_instructions', 'placeholder' => 'Receival instructions', 'ng-model' => 'exchange.receival_instructions' + %tr.panel-row{ object: "exchange", + panels: "{products: 'exchange_supplied_products'}", + locals: "$index,order_cycle,exchange,enterprises,setExchangeVariants,suppliedVariants,removeDistributionOfVariant", + colspan: 4 } - if type == 'distributor' - %td.collection-details - = text_field_tag 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', '', 'id' => 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', 'placeholder' => 'Ready for (ie. Date / Time)', 'ng-model' => 'exchange.pickup_time', 'ng-disabled' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator' - %br/ - = text_field_tag 'order_cycle_outgoing_exchange_{{ $index }}_pickup_instructions', '', 'id' => 'order_cycle_outgoing_exchange_{{ $index }}_pickup_instructions', 'placeholder' => 'Pick-up instructions', 'ng-model' => 'exchange.pickup_instructions', 'ng-disabled' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator' -%td.fees - %ol{ ng: { show: 'enterprises[exchange.enterprise_id].managed || order_cycle.viewing_as_coordinator' } } - %li{'ng-repeat' => 'enterprise_fee in exchange.enterprise_fees'} - = select_tag 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_enterprise_id', nil, {'id' => 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_enterprise_id', 'ng-model' => 'enterprise_fee.enterprise_id', 'ng-options' => 'enterprise.id as enterprise.name for enterprise in enterprisesWithFees()'} - - = select_tag 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_enterprise_fee_id', nil, {'id' => 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_enterprise_fee_id', 'ng-model' => 'enterprise_fee.id', 'ng-options' => 'enterprise_fee.id as enterprise_fee.name for enterprise_fee in enterpriseFeesForEnterprise(enterprise_fee.enterprise_id)'} - - = link_to 'Remove', '#', {'id' => 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_remove', 'ng-click' => 'removeExchangeFee($event, exchange, $index)'} - - = f.submit 'Add fee', 'ng-click' => 'addExchangeFee($event, exchange)', 'ng-hide' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator' -%td.actions - %a{'ng-click' => 'removeExchange($event, exchange)', :class => "icon-trash no-text remove-exchange"} + %tr.panel-row{ object: "exchange", + panels: "{products: 'exchange_distributed_products', tags: 'exchange_tags'}", + locals: "$index,order_cycle,exchange,supplied_products,setExchangeVariants,incomingExchangeVariantsFor,productSuppliedToOrderCycle,variantSuppliedToOrderCycle", + colspan: 5 } diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index e53fd515b0..a0f0a178c8 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -17,11 +17,8 @@ %th Receival details %th Fees %th.actions - %tbody{'ng-repeat' => 'exchange in order_cycle.incoming_exchanges'} - %tr{'class' => "supplier supplier-{{ exchange.enterprise_id }}"} - = render 'exchange_form', :f => f, :type => 'supplier' - %tr.products{'ng-show' => 'exchange.showProducts'} - = render 'exchange_supplied_products_form' + %tbody.panel-ctrl{ object: 'exchange', 'ng-repeat' => 'exchange in order_cycle.incoming_exchanges'} + = render 'exchange_form', :f => f, :type => 'supplier' - if Enterprise.managed_by(spree_current_user).include? @order_cycle.coordinator = render 'add_exchange_form', f: f, type: 'supplier' @@ -37,14 +34,13 @@ %a{href: '#', 'ng-click' => "OrderCycle.toggleAllProducts('outgoing')"} %span{'ng-show' => "OrderCycle.showProducts['outgoing']"} Collapse all %span{'ng-hide' => "OrderCycle.showProducts['outgoing']"} Expand all + %th{ ng: { if: 'enterprises[exchange.enterprise_id].managed || order_cycle.viewing_as_coordinator' } } Tags %th Pickup / Delivery details %th Fees %th.actions - %tbody{'ng-repeat' => 'exchange in order_cycle.outgoing_exchanges'} - %tr{'class' => "distributor distributor-{{ exchange.enterprise_id }}"} - = render 'exchange_form', :f => f, :type => 'distributor' - %tr.products{'ng-show' => 'exchange.showProducts'} - = render 'exchange_distributed_products_form' + %tbody.panel-ctrl{ object: 'exchange', 'ng-repeat' => 'exchange in order_cycle.outgoing_exchanges'} + = render 'exchange_form', :f => f, :type => 'distributor' + - if Enterprise.managed_by(spree_current_user).include? @order_cycle.coordinator = render 'add_exchange_form', f: f, type: 'distributor' diff --git a/app/views/admin/order_cycles/_simple_form.html.haml b/app/views/admin/order_cycles/_simple_form.html.haml index ba28526f7b..6bb0034c73 100644 --- a/app/views/admin/order_cycles/_simple_form.html.haml +++ b/app/views/admin/order_cycles/_simple_form.html.haml @@ -14,7 +14,7 @@ %table.exchanges %tbody{ng: {repeat: "exchange in order_cycle.incoming_exchanges"}} %tr.products - = render 'exchange_supplied_products_form' + %td{ ng: { include: "'admin/panels/exchange_supplied_products.html'" } } %br/ = label_tag 'Fees' diff --git a/lib/open_food_network/order_cycle_form_applicator.rb b/lib/open_food_network/order_cycle_form_applicator.rb index 6d2e3e3e53..2cccc4a4fa 100644 --- a/lib/open_food_network/order_cycle_form_applicator.rb +++ b/lib/open_food_network/order_cycle_form_applicator.rb @@ -42,13 +42,15 @@ module OpenFoodNetwork {variant_ids: variant_ids, enterprise_fee_ids: enterprise_fee_ids, pickup_time: exchange[:pickup_time], - pickup_instructions: exchange[:pickup_instructions]}) + pickup_instructions: exchange[:pickup_instructions], + tag_list: exchange[:tag_list]}) else add_exchange(@order_cycle.coordinator_id, exchange[:enterprise_id], false, {variant_ids: variant_ids, enterprise_fee_ids: enterprise_fee_ids, pickup_time: exchange[:pickup_time], - pickup_instructions: exchange[:pickup_instructions]}) + pickup_instructions: exchange[:pickup_instructions], + tag_list: exchange[:tag_list]}) end end @@ -81,6 +83,7 @@ module OpenFoodNetwork attrs.delete :enterprise_fee_ids attrs.delete :pickup_time attrs.delete :pickup_instructions + attrs.delete :tag_list end if permission_for exchange diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 874c8342b5..e17ace2580 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -103,7 +103,7 @@ feature %q{ select 'My supplier', from: 'new_supplier_id' click_button 'Add supplier' fill_in 'order_cycle_incoming_exchange_0_receival_instructions', with: 'receival instructions' - page.find('table.exchanges tr.supplier td.products input').click + page.find('table.exchanges tr.supplier td.products').click check "order_cycle_incoming_exchange_0_variants_#{v1.id}" check "order_cycle_incoming_exchange_0_variants_#{v2.id}" @@ -124,10 +124,15 @@ feature %q{ fill_in 'order_cycle_outgoing_exchange_0_pickup_time', with: 'pickup time' fill_in 'order_cycle_outgoing_exchange_0_pickup_instructions', with: 'pickup instructions' - page.find('table.exchanges tr.distributor td.products input').click + page.find('table.exchanges tr.distributor td.products').click check "order_cycle_outgoing_exchange_0_variants_#{v1.id}" check "order_cycle_outgoing_exchange_0_variants_#{v2.id}" + page.find('table.exchanges tr.distributor td.tags').click + within ".exchange-tags" do + find(:css, "tags-input .tags input").set "wholesale\n" + end + # And I add a distributor fee within("tr.distributor-#{distributor.id}") { click_button 'Add fee' } select 'My distributor', from: 'order_cycle_outgoing_exchange_0_enterprise_fees_0_enterprise_id' @@ -165,6 +170,7 @@ feature %q{ exchange = oc.exchanges.outgoing.first exchange.pickup_time.should == 'pickup time' exchange.pickup_instructions.should == 'pickup instructions' + exchange.tag_list.should == ['wholesale'] end @@ -196,13 +202,13 @@ feature %q{ page.should have_field 'order_cycle_incoming_exchange_1_receival_instructions', with: 'instructions 1' # And the suppliers should have products - page.all('table.exchanges tbody tr.supplier').each do |row| - row.find('td.products input').click + page.all('table.exchanges tbody tr.supplier').each_with_index do |row, i| + row.find('td.products').click - products_row = page.all('table.exchanges tr.products').select { |r| r.visible? }.first - products_row.should have_selector "input[type='checkbox'][checked='checked']" + products_panel = page.all('table.exchanges tr.panel-row .exchange-supplied-products').select { |r| r.visible? }.first + products_panel.should have_selector "input[name='order_cycle_incoming_exchange_#{i}_select_all_variants']" - row.find('td.products input').click + row.find('td.products').click end # And the suppliers should have fees @@ -227,13 +233,13 @@ feature %q{ page.driver.resize(1280, 3600) # And the distributors should have products - page.all('table.exchanges tbody tr.distributor').each do |row| - row.find('td.products input').click + page.all('table.exchanges tbody tr.distributor').each_with_index do |row, i| + row.find('td.products').click - products_row = page.all('table.exchanges tr.products').select { |r| r.visible? }.first - products_row.should have_selector "input[type='checkbox'][checked='checked']" + products_panel = page.all('table.exchanges tr.panel-row .exchange-distributed-products').select { |r| r.visible? }.first + products_panel.should have_selector "input[name='order_cycle_outgoing_exchange_#{i}_select_all_variants']" - row.find('td.products input').click + row.find('td.products').click end # And the distributors should have fees @@ -323,7 +329,7 @@ feature %q{ # And I add a supplier and some products select 'My supplier', from: 'new_supplier_id' click_button 'Add supplier' - page.all("table.exchanges tr.supplier td.products input").each { |e| e.click } + page.all("table.exchanges tr.supplier td.products").each { |e| e.click } page.should have_selector "#order_cycle_incoming_exchange_1_variants_#{initial_variants.last.id}", visible: true page.find("#order_cycle_incoming_exchange_1_variants_#{initial_variants.last.id}", visible: true).click # uncheck (with visible:true filter) @@ -349,7 +355,12 @@ feature %q{ fill_in 'order_cycle_outgoing_exchange_1_pickup_time', with: 'New time 1' fill_in 'order_cycle_outgoing_exchange_1_pickup_instructions', with: 'New instructions 1' - page.all("table.exchanges tr.distributor td.products input").each { |e| e.click } + page.find("table.exchanges tr.distributor-#{distributor.id} td.tags").click + within ".exchange-tags" do + find(:css, "tags-input .tags input").set "wholesale\n" + end + + page.all("table.exchanges tr.distributor td.products").each { |e| e.click } uncheck "order_cycle_outgoing_exchange_2_variants_#{v1.id}" check "order_cycle_outgoing_exchange_2_variants_#{v2.id}" @@ -389,6 +400,9 @@ feature %q{ # And my distributor fees should have been configured OrderCycle.last.exchanges.outgoing.last.enterprise_fee_ids.should == [distributor_fee2.id] + # And my tags should have been save + OrderCycle.last.exchanges.outgoing.last.tag_list.should == ['wholesale'] + # And it should have some variants selected selected_initial_variants = initial_variants.take initial_variants.size - 1 OrderCycle.last.variants.map(&:id).should match_array (selected_initial_variants.map(&:id) + [v1.id, v2.id]) @@ -465,7 +479,7 @@ feature %q{ login_to_admin_section click_link 'Order Cycles' click_link oc.name - within("table.exchanges tbody tr.supplier") { page.find('td.products input').click } + within("table.exchanges tbody tr.supplier") { page.find('td.products').click } page.find("#order_cycle_incoming_exchange_0_variants_#{p.master.id}", visible: true).trigger('click') # uncheck click_button "Update" @@ -591,6 +605,11 @@ feature %q{ page.should_not have_select 'order_cycle_coordinator_id', with_options: [enterprise_name] end + page.find("table.exchanges tr.distributor-#{distributor_managed.id} td.tags").click + within ".exchange-tags" do + find(:css, "tags-input .tags input").set "wholesale\n" + end + click_button 'Create' flash_message.should == "Your order cycle has been created." @@ -598,6 +617,8 @@ feature %q{ order_cycle.suppliers.should match_array [supplier_managed, supplier_permitted] order_cycle.coordinator.should == distributor_managed order_cycle.distributors.should match_array [distributor_managed, distributor_permitted] + exchange = order_cycle.exchanges.outgoing.to_enterprise(distributor_managed).first + exchange.tag_list.should == ["wholesale"] end scenario "editing an order cycle we can see (and for now, edit) all exchanges in the order cycle" do @@ -614,12 +635,17 @@ feature %q{ expect(page).to have_selector "tr.supplier-#{supplier_managed.id}" expect(page).to have_selector "tr.supplier-#{supplier_permitted.id}" expect(page).to have_selector "tr.supplier-#{supplier_unmanaged.id}" - expect(page.all('tr.supplier').count).to be 3 + expect(page).to have_selector 'tr.supplier', count: 3 expect(page).to have_selector "tr.distributor-#{distributor_managed.id}" expect(page).to have_selector "tr.distributor-#{distributor_permitted.id}" expect(page).to have_selector "tr.distributor-#{distributor_unmanaged.id}" - expect(page.all('tr.distributor').count).to be 3 + expect(page).to have_selector 'tr.distributor', count: 3 + + + # When I save, then those exchanges should remain + click_button 'Update' + page.should have_content "Your order cycle has been updated." oc.reload oc.suppliers.should match_array [supplier_managed, supplier_permitted, supplier_unmanaged] @@ -640,7 +666,6 @@ feature %q{ page.find("tr.supplier-#{supplier_permitted.id} a.remove-exchange").click page.find("tr.distributor-#{distributor_managed.id} a.remove-exchange").click page.find("tr.distributor-#{distributor_permitted.id} a.remove-exchange").click - click_button 'Update' # Then the exchanges should be removed @@ -707,7 +732,7 @@ feature %q{ # Open the products list for managed_supplier's incoming exchange within "tr.distributor-#{distributor_managed.id}" do - page.find("td.products input").click + page.find("td.products").click end # I should be able to see and toggle v1 @@ -716,6 +741,12 @@ feature %q{ # I should be able to see but not toggle v2, because I don't have permission expect(page).to have_field "order_cycle_outgoing_exchange_0_variants_#{v2.id}", disabled: true + page.should_not have_selector "table.exchanges tr.distributor-#{distributor_managed.id} td.tags" + + # When I save, any exchanges that I can't manage remain + click_button 'Update' + page.should have_content "Your order cycle has been updated." + oc.reload oc.suppliers.should match_array [supplier_managed, supplier_permitted, supplier_unmanaged] oc.coordinator.should == distributor_managed @@ -759,7 +790,7 @@ feature %q{ # Open the products list for managed_supplier's incoming exchange within "tr.supplier-#{supplier_managed.id}" do - page.find("td.products input").click + page.find("td.products").click end # I should be able to see and toggle v1 @@ -768,6 +799,12 @@ feature %q{ # I should be able to see but not toggle v2, because I don't have permission expect(page).to have_field "order_cycle_incoming_exchange_0_variants_#{v2.id}", disabled: true + page.should have_selector "table.exchanges tr.distributor-#{my_distributor.id} td.tags" + + # When I save, any exchange that I can't manage remains + click_button 'Update' + page.should have_content "Your order cycle has been updated." + oc.reload oc.suppliers.should match_array [supplier_managed, supplier_permitted, supplier_unmanaged] oc.coordinator.should == distributor_managed @@ -956,7 +993,7 @@ feature %q{ private def select_incoming_variant(supplier, exchange_no, variant) - page.find("table.exchanges tr.supplier-#{supplier.id} td.products input").click + page.find("table.exchanges tr.supplier-#{supplier.id} td.products").click check "order_cycle_incoming_exchange_#{exchange_no}_variants_#{variant.id}" end end diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index bbbff7d82b..df227c7960 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -106,11 +106,6 @@ describe 'OrderCycle controllers', -> {id: 2, name: 'Pepper Tree Place'} ]) - it 'Delegates toggleProducts to OrderCycle', -> - scope.toggleProducts(event, 'exchange') - expect(event.preventDefault).toHaveBeenCalled() - expect(OrderCycle.toggleProducts).toHaveBeenCalledWith('exchange') - it 'Delegates enterpriseFeesForEnterprise to EnterpriseFee', -> scope.enterpriseFeesForEnterprise('123') expect(EnterpriseFee.forEnterprise).toHaveBeenCalledWith(123) @@ -272,11 +267,6 @@ describe 'OrderCycle controllers', -> {id: 2, name: 'Pepper Tree Place'} ]) - it 'Delegates toggleProducts to OrderCycle', -> - scope.toggleProducts(event, 'exchange') - expect(event.preventDefault).toHaveBeenCalled() - expect(OrderCycle.toggleProducts).toHaveBeenCalledWith('exchange') - it 'Delegates enterpriseFeesForEnterprise to EnterpriseFee', -> scope.enterpriseFeesForEnterprise('123') expect(EnterpriseFee.forEnterprise).toHaveBeenCalledWith(123) @@ -534,26 +524,6 @@ describe 'OrderCycle services', -> OrderCycle.order_cycle.outgoing_exchanges = [exchange] expect(OrderCycle.exchangeDirection(exchange)).toEqual 'outgoing' - describe 'toggling products', -> - exchange = null - - beforeEach -> - exchange = {} - - it 'sets a blank value to true', -> - OrderCycle.toggleProducts(exchange) - expect(exchange.showProducts).toEqual(true) - - it 'sets a true value to false', -> - exchange.showProducts = true - OrderCycle.toggleProducts(exchange) - expect(exchange.showProducts).toEqual(false) - - it 'sets a false value to true', -> - exchange.showProducts = false - OrderCycle.toggleProducts(exchange) - expect(exchange.showProducts).toEqual(true) - describe "setting exchange variants", -> describe "when I have permissions to edit the variants", -> beforeEach -> @@ -598,7 +568,7 @@ describe 'OrderCycle services', -> it 'adds the distributor to outgoing exchanges', -> OrderCycle.addDistributor('123') expect(OrderCycle.order_cycle.outgoing_exchanges).toEqual [ - {enterprise_id: '123', incoming: false, active: true, variants: {}, enterprise_fees: []} + {enterprise_id: '123', incoming: false, active: true, variants: {}, enterprise_fees: [], tags: [], tag_list: ""} ] describe 'removing exchanges', -> diff --git a/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb b/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb index 89dcb364b8..fe3155c5e5 100644 --- a/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb +++ b/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb @@ -29,7 +29,7 @@ module OpenFoodNetwork coordinator_id = 123 distributor_id = 456 - outgoing_exchange = {:enterprise_id => distributor_id, :incoming => false, :variants => {'1' => true, '2' => false, '3' => true}, :enterprise_fee_ids => [1, 2], :pickup_time => 'pickup time', :pickup_instructions => 'pickup instructions'} + outgoing_exchange = {:enterprise_id => distributor_id, :incoming => false, :variants => {'1' => true, '2' => false, '3' => true}, :enterprise_fee_ids => [1, 2], :pickup_time => 'pickup time', :pickup_instructions => 'pickup instructions', tag_list: 'wholesale'} oc = double(:order_cycle, :coordinator_id => coordinator_id, :exchanges => [], :incoming_exchanges => [], :outgoing_exchanges => [outgoing_exchange]) @@ -37,7 +37,7 @@ module OpenFoodNetwork applicator.should_receive(:outgoing_exchange_variant_ids).with(outgoing_exchange).and_return([1, 3]) applicator.should_receive(:exchange_exists?).with(coordinator_id, distributor_id, false).and_return(false) - applicator.should_receive(:add_exchange).with(coordinator_id, distributor_id, false, {:variant_ids => [1, 3], :enterprise_fee_ids => [1, 2], :pickup_time => 'pickup time', :pickup_instructions => 'pickup instructions'}) + applicator.should_receive(:add_exchange).with(coordinator_id, distributor_id, false, {:variant_ids => [1, 3], :enterprise_fee_ids => [1, 2], :pickup_time => 'pickup time', :pickup_instructions => 'pickup instructions', tag_list: 'wholesale'}) applicator.should_receive(:destroy_untouched_exchanges) applicator.go! @@ -69,7 +69,7 @@ module OpenFoodNetwork coordinator_id = 123 distributor_id = 456 - outgoing_exchange = {:enterprise_id => distributor_id, :incoming => false, :variants => {'1' => true, '2' => false, '3' => true}, :enterprise_fee_ids => [1, 2], :pickup_time => 'pickup time', :pickup_instructions => 'pickup instructions'} + outgoing_exchange = {:enterprise_id => distributor_id, :incoming => false, :variants => {'1' => true, '2' => false, '3' => true}, :enterprise_fee_ids => [1, 2], :pickup_time => 'pickup time', :pickup_instructions => 'pickup instructions', tag_list: 'wholesale'} oc = double(:order_cycle, :coordinator_id => coordinator_id, @@ -81,7 +81,7 @@ module OpenFoodNetwork applicator.should_receive(:outgoing_exchange_variant_ids).with(outgoing_exchange).and_return([1, 3]) applicator.should_receive(:exchange_exists?).with(coordinator_id, distributor_id, false).and_return(true) - applicator.should_receive(:update_exchange).with(coordinator_id, distributor_id, false, {:variant_ids => [1, 3], :enterprise_fee_ids => [1, 2], :pickup_time => 'pickup time', :pickup_instructions => 'pickup instructions'}) + applicator.should_receive(:update_exchange).with(coordinator_id, distributor_id, false, {:variant_ids => [1, 3], :enterprise_fee_ids => [1, 2], :pickup_time => 'pickup time', :pickup_instructions => 'pickup instructions', tag_list: 'wholesale'}) applicator.should_receive(:destroy_untouched_exchanges) applicator.go! @@ -375,15 +375,16 @@ module OpenFoodNetwork allow(applicator).to receive(:manager_for) { false } allow(applicator).to receive(:permission_for) { true } applicator.send(:touched_exchanges=, []) - applicator.send(:update_exchange, sender.id, receiver.id, incoming, {:variant_ids => [variant1.id, variant3.id], :enterprise_fee_ids => [enterprise_fee2.id, enterprise_fee3.id], :pickup_time => 'New Pickup Time', :pickup_instructions => 'New Pickup Instructions'}) + applicator.send(:update_exchange, sender.id, receiver.id, incoming, {:variant_ids => [variant1.id, variant3.id], :enterprise_fee_ids => [enterprise_fee2.id, enterprise_fee3.id], :pickup_time => 'New Pickup Time', :pickup_instructions => 'New Pickup Instructions', tag_list: 'wholesale'}) end - it "updates the variants, enterprise fees and pickup information of the exchange" do + it "updates the variants, enterprise fees tags, and pickup information of the exchange" do exchange.reload expect(exchange.variants).to match_array [variant1, variant3] expect(exchange.enterprise_fees).to match_array [enterprise_fee2, enterprise_fee3] expect(exchange.pickup_time).to eq 'New Pickup Time' expect(exchange.pickup_instructions).to eq 'New Pickup Instructions' + expect(exchange.tag_list).to eq ['wholesale'] expect(applicator.send(:touched_exchanges)).to eq [exchange] end end @@ -394,15 +395,16 @@ module OpenFoodNetwork allow(applicator).to receive(:manager_for) { true } allow(applicator).to receive(:permission_for) { true } applicator.send(:touched_exchanges=, []) - applicator.send(:update_exchange, sender.id, receiver.id, incoming, {:variant_ids => [variant1.id, variant3.id], :enterprise_fee_ids => [enterprise_fee2.id, enterprise_fee3.id], :pickup_time => 'New Pickup Time', :pickup_instructions => 'New Pickup Instructions'}) + applicator.send(:update_exchange, sender.id, receiver.id, incoming, {:variant_ids => [variant1.id, variant3.id], :enterprise_fee_ids => [enterprise_fee2.id, enterprise_fee3.id], :pickup_time => 'New Pickup Time', :pickup_instructions => 'New Pickup Instructions', tag_list: 'wholesale'}) end - it "updates the variants, enterprise fees and pickup information of the exchange" do + it "updates the variants, enterprise fees, tags and pickup information of the exchange" do exchange.reload expect(exchange.variants).to match_array [variant1, variant3] expect(exchange.enterprise_fees).to match_array [enterprise_fee2, enterprise_fee3] expect(exchange.pickup_time).to eq 'New Pickup Time' expect(exchange.pickup_instructions).to eq 'New Pickup Instructions' + expect(exchange.tag_list).to eq ['wholesale'] expect(applicator.send(:touched_exchanges)).to eq [exchange] end end @@ -413,15 +415,16 @@ module OpenFoodNetwork allow(applicator).to receive(:manager_for) { false } allow(applicator).to receive(:permission_for) { true } applicator.send(:touched_exchanges=, []) - applicator.send(:update_exchange, sender.id, receiver.id, incoming, {:variant_ids => [variant1.id, variant3.id], :enterprise_fee_ids => [enterprise_fee2.id, enterprise_fee3.id], :pickup_time => 'New Pickup Time', :pickup_instructions => 'New Pickup Instructions'}) + applicator.send(:update_exchange, sender.id, receiver.id, incoming, {:variant_ids => [variant1.id, variant3.id], :enterprise_fee_ids => [enterprise_fee2.id, enterprise_fee3.id], :pickup_time => 'New Pickup Time', :pickup_instructions => 'New Pickup Instructions', tag_list: 'wholesale'}) end - it "updates the variants in the exchange, but not the fees or pickup information" do + it "updates the variants in the exchange, but not the fees, tags or pickup information" do exchange.reload expect(exchange.variants).to match_array [variant1, variant3] expect(exchange.enterprise_fees).to match_array [enterprise_fee1, enterprise_fee2] expect(exchange.pickup_time).to_not eq 'New Pickup Time' expect(exchange.pickup_instructions).to_not eq 'New Pickup Instructions' + expect(exchange.tag_list).to eq [] expect(applicator.send(:touched_exchanges)).to eq [exchange] end end diff --git a/spec/models/exchange_spec.rb b/spec/models/exchange_spec.rb index 4b8f0ed1a3..1a4a4f7e3f 100644 --- a/spec/models/exchange_spec.rb +++ b/spec/models/exchange_spec.rb @@ -282,9 +282,11 @@ describe Exchange do new_oc = create(:simple_order_cycle) ex1 = oc.exchanges.last + ex1.update_attribute(:tag_list, "wholesale") ex2 = ex1.clone! new_oc ex1.eql?(ex2).should be_true + expect(ex2.reload.tag_list).to eq ["wholesale"] end describe "converting to hash" do From 70e6428c1f0928d74671962b8e775338e328dbf5 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 18 May 2016 15:52:58 +1000 Subject: [PATCH 084/110] Adding FilterOrderCycle tag rule --- app/models/tag_rule/filter_order_cycles.rb | 36 ++++++++ spec/factories.rb | 4 + .../tag_rule/filter_order_cycles_spec.rb | 91 +++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 app/models/tag_rule/filter_order_cycles.rb create mode 100644 spec/models/tag_rule/filter_order_cycles_spec.rb diff --git a/app/models/tag_rule/filter_order_cycles.rb b/app/models/tag_rule/filter_order_cycles.rb new file mode 100644 index 0000000000..2ed99320a5 --- /dev/null +++ b/app/models/tag_rule/filter_order_cycles.rb @@ -0,0 +1,36 @@ +class TagRule::FilterOrderCycles < TagRule + preference :matched_order_cycles_visibility, :string, default: "visible" + preference :exchange_tags, :string, default: "" + + attr_accessible :preferred_matched_order_cycles_visibility, :preferred_exchange_tags + + private + + # Warning: this should only EVER be called via TagRule#apply + def apply! + unless preferred_matched_order_cycles_visibility == "visible" + subject.reject!{ |oc| tags_match?(oc) } + end + end + + def apply_default! + if preferred_matched_order_cycles_visibility == "visible" + subject.reject!{ |oc| tags_match?(oc) } + end + end + + def tags_match?(order_cycle) + exchange_tags = exchange_for(order_cycle).andand.tag_list || [] + preferred_tags = preferred_exchange_tags.split(",") + ( exchange_tags & preferred_tags ).any? + end + + def exchange_for(order_cycle) + order_cycle.exchanges.outgoing.to_enterprise(context[:shop]).first + end + + def subject_class_matches? + subject.class == ActiveRecord::Relation && + subject.klass == OrderCycle + end +end diff --git a/spec/factories.rb b/spec/factories.rb index d0d7755cc0..6d931ee2d0 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -286,6 +286,10 @@ FactoryGirl.define do month { 1 + rand(12) } end + factory :filter_order_cycles_tag_rule, class: TagRule::FilterOrderCycles do + enterprise { FactoryGirl.create :distributor_enterprise } + end + factory :filter_shipping_methods_tag_rule, class: TagRule::FilterShippingMethods do enterprise { FactoryGirl.create :distributor_enterprise } end diff --git a/spec/models/tag_rule/filter_order_cycles_spec.rb b/spec/models/tag_rule/filter_order_cycles_spec.rb new file mode 100644 index 0000000000..53619a776d --- /dev/null +++ b/spec/models/tag_rule/filter_order_cycles_spec.rb @@ -0,0 +1,91 @@ +require 'spec_helper' + +describe TagRule::FilterOrderCycles, type: :model do + let!(:tag_rule) { create(:filter_order_cycles_tag_rule) } + + describe "determining whether tags match for a given exchange" do + context "when the exchange is nil" do + before do + allow(tag_rule).to receive(:exchange_for) { nil } + end + + it "returns false" do + expect(tag_rule.send(:tags_match?, nil)).to be false + end + end + + context "when the exchange is not nil" do + let(:exchange_object) { double(:exchange, tag_list: ["member","local","volunteer"]) } + + before do + allow(tag_rule).to receive(:exchange_for) { exchange_object } + end + + context "when the rule has no preferred exchange tags specified" do + before { allow(tag_rule).to receive(:preferred_exchange_tags) { "" } } + it { expect(tag_rule.send(:tags_match?, exchange_object)).to be false } + end + + context "when the rule has preferred exchange tags specified that match ANY of the exchange tags" do + before { allow(tag_rule).to receive(:preferred_exchange_tags) { "wholesale,some_tag,member" } } + it { expect(tag_rule.send(:tags_match?, exchange_object)).to be true } + end + + context "when the rule has preferred exchange tags specified that match NONE of the exchange tags" do + before { allow(tag_rule).to receive(:preferred_exchange_tags) { "wholesale,some_tag,some_other_tag" } } + it { expect(tag_rule.send(:tags_match?, exchange_object)).to be false } + end + end + end + + describe "applying the rule" do + # Assume that all validation is done by the TagRule base class + + let(:enterprise) { create(:distributor_enterprise) } + let(:order_cycle1) { create(:simple_order_cycle, name: "order_cycle1", exchanges: [ create(:exchange, incoming: false, receiver: enterprise, tag_list: ["tag1", "something", "somethingelse"])]) } + let(:order_cycle2) { create(:simple_order_cycle, name: "order_cycle2", exchanges: [ create(:exchange, incoming: false, receiver: enterprise, tag_list: ["tag2"])]) } + let(:order_cycle3) { create(:simple_order_cycle, name: "order_cycle3", exchanges: [ create(:exchange, incoming: false, receiver: enterprise, tag_list: ["tag3"])]) } + let!(:order_cycle_hash) { [order_cycle1, order_cycle2, order_cycle3] } + + before do + tag_rule.update_attribute(:preferred_exchange_tags, "tag2") + tag_rule.context = {subject: order_cycle_hash, shop: enterprise} + end + + context "apply!" do + context "when showing matching exchanges" do + before { tag_rule.update_attribute(:preferred_matched_order_cycles_visibility, "visible") } + it "does nothing" do + tag_rule.send(:apply!) + expect(order_cycle_hash).to eq [order_cycle1, order_cycle2, order_cycle3] + end + end + + context "when hiding matching exchanges" do + before { tag_rule.update_attribute(:preferred_matched_order_cycles_visibility, "hidden") } + it "removes matching exchanges from the list" do + tag_rule.send(:apply!) + expect(order_cycle_hash).to eq [order_cycle1, order_cycle3] + end + end + end + + context "apply_default!" do + context "when showing matching exchanges" do + before { tag_rule.update_attribute(:preferred_matched_order_cycles_visibility, "visible") } + it "remove matching exchanges from the list" do + tag_rule.send(:apply_default!) + expect(order_cycle_hash).to eq [order_cycle1, order_cycle3] + end + end + + context "when hiding matching exchanges" do + before { tag_rule.update_attribute(:preferred_matched_order_cycles_visibility, "hidden") } + it "does nothing" do + tag_rule.send(:apply_default!) + expect(order_cycle_hash).to eq [order_cycle1, order_cycle2, order_cycle3] + end + end + end + end +end From e32e3ddf70b21bbc825fc30402989d2fcfd413f7 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 18 May 2016 17:07:09 +1000 Subject: [PATCH 085/110] Applying FilterOrderCycles rules in the frontend --- app/controllers/base_controller.rb | 7 ++ .../enterprises_controller_spec.rb | 87 +++++++++++++------ 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/app/controllers/base_controller.rb b/app/controllers/base_controller.rb index 9d2c3cff85..124dd7f38a 100644 --- a/app/controllers/base_controller.rb +++ b/app/controllers/base_controller.rb @@ -19,6 +19,13 @@ class BaseController < ApplicationController @order_cycles = OrderCycle.with_distributor(@distributor).active .order(@distributor.preferred_shopfront_order_cycle_order) + @distributor.apply_tag_rules( + type: "FilterOrderCycles", + subject: @order_cycles, + customer_tags: current_order.andand.customer.andand.tag_list || [], + shop: @distributor + ) + # And default to the only order cycle if there's only the one if @order_cycles.count == 1 current_order(true).set_order_cycle! @order_cycles.first diff --git a/spec/controllers/enterprises_controller_spec.rb b/spec/controllers/enterprises_controller_spec.rb index 0727488841..1ed68958f9 100644 --- a/spec/controllers/enterprises_controller_spec.rb +++ b/spec/controllers/enterprises_controller_spec.rb @@ -4,51 +4,86 @@ describe EnterprisesController do describe "shopping for a distributor" do let(:order) { controller.current_order(true) } - before(:each) do - @current_distributor = create(:distributor_enterprise, with_payment_and_shipping: true) - @distributor = create(:distributor_enterprise, with_payment_and_shipping: true) - @order_cycle1 = create(:simple_order_cycle, distributors: [@distributor], orders_open_at: 2.days.ago, orders_close_at: 3.days.from_now ) - @order_cycle2 = create(:simple_order_cycle, distributors: [@distributor], orders_open_at: 3.days.ago, orders_close_at: 4.days.from_now ) - order.set_distributor! @current_distributor + + let!(:current_distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) } + let!(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) } + let!(:order_cycle1) { create(:simple_order_cycle, distributors: [distributor], orders_open_at: 2.days.ago, orders_close_at: 3.days.from_now ) } + let!(:order_cycle2) { create(:simple_order_cycle, distributors: [distributor], orders_open_at: 3.days.ago, orders_close_at: 4.days.from_now ) } + + before do + order.set_distributor! current_distributor end it "sets the shop as the distributor on the order when shopping for the distributor" do - spree_get :shop, {id: @distributor} + spree_get :shop, {id: distributor} - controller.current_order.distributor.should == @distributor + controller.current_order.distributor.should == distributor controller.current_order.order_cycle.should be_nil end it "sorts order cycles by the distributor's preferred ordering attr" do - @distributor.update_attribute(:preferred_shopfront_order_cycle_order, 'orders_close_at') - spree_get :shop, {id: @distributor} - assigns(:order_cycles).should == [@order_cycle1, @order_cycle2].sort_by(&:orders_close_at) + distributor.update_attribute(:preferred_shopfront_order_cycle_order, 'orders_close_at') + spree_get :shop, {id: distributor} + assigns(:order_cycles).should == [order_cycle1, order_cycle2].sort_by(&:orders_close_at) - @distributor.update_attribute(:preferred_shopfront_order_cycle_order, 'orders_open_at') - spree_get :shop, {id: @distributor} - assigns(:order_cycles).should == [@order_cycle1, @order_cycle2].sort_by(&:orders_open_at) + distributor.update_attribute(:preferred_shopfront_order_cycle_order, 'orders_open_at') + spree_get :shop, {id: distributor} + assigns(:order_cycles).should == [order_cycle1, order_cycle2].sort_by(&:orders_open_at) + end + + context "using FilterOrderCycles tag rules" do + let!(:order_cycle3) { create(:simple_order_cycle, distributors: [distributor], orders_open_at: 3.days.ago, orders_close_at: 4.days.from_now) } + let!(:oc3_exchange) { order_cycle3.exchanges.outgoing.to_enterprise(distributor).first } + let!(:customer) { create(:customer, enterprise: distributor) } + + before do + order.update_attribute(:customer_id, customer.id) + end + + it "shows order cycles allowed by the rule" do + create(:filter_order_cycles_tag_rule, + enterprise: distributor, + preferred_customer_tags: "wholesale", + preferred_exchange_tags: "wholesale", + preferred_matched_order_cycles_visibility: 'visible') + + spree_get :shop, {id: distributor} + expect(assigns(:order_cycles)).to include order_cycle1, order_cycle2, order_cycle3 + + oc3_exchange.update_attribute(:tag_list, "wholesale") + + spree_get :shop, {id: distributor} + expect(assigns(:order_cycles)).to include order_cycle1, order_cycle2 + expect(assigns(:order_cycles)).not_to include order_cycle3 + + customer.update_attribute(:tag_list, ["wholesale"]) + order.reload + + spree_get :shop, {id: distributor} + expect(assigns(:order_cycles)).to include order_cycle1, order_cycle2, order_cycle3 + end end it "empties an order that was set for a previous distributor, when shopping at a new distributor" do line_item = create(:line_item) controller.current_order.line_items << line_item - spree_get :shop, {id: @distributor} + spree_get :shop, {id: distributor} - controller.current_order.distributor.should == @distributor + controller.current_order.distributor.should == distributor controller.current_order.order_cycle.should be_nil controller.current_order.line_items.size.should == 0 end it "should not empty an order if returning to the same distributor" do product = create(:product) - create(:product_distribution, product: product, distributor: @current_distributor) + create(:product_distribution, product: product, distributor: current_distributor) line_item = create(:line_item, variant: product.master) controller.current_order.line_items << line_item - spree_get :shop, {id: @current_distributor} + spree_get :shop, {id: current_distributor} - controller.current_order.distributor.should == @current_distributor + controller.current_order.distributor.should == current_distributor controller.current_order.order_cycle.should be_nil controller.current_order.line_items.size.should == 1 end @@ -56,10 +91,10 @@ describe EnterprisesController do describe "when an out of stock item is in the cart" do let(:variant) { create(:variant, on_demand: false, on_hand: 10) } let(:line_item) { create(:line_item, variant: variant) } - let(:order_cycle) { create(:simple_order_cycle, distributors: [@distributor], variants: [variant]) } + let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], variants: [variant]) } before do - order.set_distribution! @current_distributor, order_cycle + order.set_distribution! current_distributor, order_cycle order.line_items << line_item Spree::Config.set allow_backorders: false @@ -68,19 +103,19 @@ describe EnterprisesController do end it "redirects to the cart" do - spree_get :shop, {id: @current_distributor} + spree_get :shop, {id: current_distributor} response.should redirect_to spree.cart_path end end it "sets order cycle if only one is available at the chosen distributor" do - @order_cycle2.destroy + order_cycle2.destroy - spree_get :shop, {id: @distributor} + spree_get :shop, {id: distributor} - controller.current_order.distributor.should == @distributor - controller.current_order.order_cycle.should == @order_cycle1 + controller.current_order.distributor.should == distributor + controller.current_order.order_cycle.should == order_cycle1 end end From 10434f5ee39e41e6e7a51e6ad0c03cd7a7f6869c Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 18 May 2016 17:07:28 +1000 Subject: [PATCH 086/110] Renaming class --- app/views/admin/enterprises/form/_tag_rules.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/enterprises/form/_tag_rules.html.haml b/app/views/admin/enterprises/form/_tag_rules.html.haml index 2742859220..5bd461a5c3 100644 --- a/app/views/admin/enterprises/form/_tag_rules.html.haml +++ b/app/views/admin/enterprises/form/_tag_rules.html.haml @@ -31,5 +31,5 @@ %a{ ng: { click: "deleteTagRule(tagGroup, rule)" }, :class => "delete-tag-rule icon-trash no-text" } .add_rule.text-center %input.button.icon-plus{ type: 'button', value: "+ Add A New Rule", "new-tag-rule-dialog" => true } - .add_tage + .add_tag %input.button.red.icon-plus{ type: 'button', value: "+ Add A New Tag", ng: { click: 'addNewTag()' } } From 5624fa0c5eb916841c82aca38be881bb580eb618 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 18 May 2016 17:59:46 +1000 Subject: [PATCH 087/110] Adding FilterOrderCycles tag rules to UI --- .../tag_rules_controller.js.coffee | 3 +++ .../directives/new_rule_dialog.js.coffee | 1 + .../tag_rules/filter_order_cycles.js.coffee | 4 +++ .../tag_rules/filter_order_cycles.html.haml | 27 +++++++++++++++++++ .../api/admin/tag_rule_serializer.rb | 8 ++++++ .../enterprises/form/_tag_rules.html.haml | 1 + spec/features/admin/tag_rules_spec.rb | 21 +++++++++++++++ 7 files changed, 65 insertions(+) create mode 100644 app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_order_cycles.js.coffee create mode 100644 app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles.html.haml diff --git a/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee index 00d379085e..3522bb56bf 100644 --- a/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee +++ b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee @@ -32,6 +32,9 @@ angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, ente when "FilterProducts" newRule.peferred_variant_tags = [] newRule.preferred_matched_variants_visibility = "visible" + when "FilterOrderCycles" + newRule.peferred_exchange_tags = [] + newRule.preferred_matched_order_cycles_visibility = "visible" tagGroup.rules.push(newRule) updateRuleCounts() diff --git a/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee index 1887853315..6662d5be35 100644 --- a/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee +++ b/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee @@ -10,6 +10,7 @@ angular.module("admin.tagRules").directive 'newTagRuleDialog', ($compile, $templ { id: "FilterProducts", name: 'Show or Hide variants in my shopfront' } { id: "FilterShippingMethods", name: 'Show or Hide shipping methods at checkout' } { id: "FilterPaymentMethods", name: 'Show or Hide payment methods at checkout' } + { id: "FilterOrderCycles", name: 'Show or Hide order cycles in my shopfront' } ] scope.ruleType = scope.ruleTypes[0].id diff --git a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_order_cycles.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_order_cycles.js.coffee new file mode 100644 index 0000000000..6829063a97 --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_order_cycles.js.coffee @@ -0,0 +1,4 @@ +angular.module("admin.tagRules").directive "filterOrderCycles", -> + restrict: "E" + replace: true + templateUrl: "admin/tag_rules/filter_order_cycles.html" diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles.html.haml new file mode 100644 index 0000000000..c0f5300809 --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles.html.haml @@ -0,0 +1,27 @@ +%div + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]", + ng: { value: "rule.id" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", + value: "TagRule::FilterOrderCycles" } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", + ng: { value: "rule.preferred_customer_tags" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_exchange_tags", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_exchange_tags]", + ng: { value: "rule.preferred_customer_tags" } } + + %span.text-normal {{ $index + 1 }}. Order Cycles with matching tags are + %input.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_order_cycles_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_order_cycles_visibility]", + ng: { model: "rule.preferred_matched_order_cycles_visibility"}, + data: 'visibilityOptions', "min-search" => 5 } + -# %tags-with-translation{ object: "rule", "tags-attr" => "shipping_method_tags", "tag-list-attr" => "preferred_shipping_method_tags" } diff --git a/app/serializers/api/admin/tag_rule_serializer.rb b/app/serializers/api/admin/tag_rule_serializer.rb index 5d4497cdf3..6be7ec1835 100644 --- a/app/serializers/api/admin/tag_rule_serializer.rb +++ b/app/serializers/api/admin/tag_rule_serializer.rb @@ -40,4 +40,12 @@ module Api::Admin::TagRule object.preferred_variant_tags.split(",") end end + + class FilterOrderCyclesSerializer < BaseSerializer + attributes :preferred_matched_order_cycles_visibility, :exchange_tags + + def exchange_tags + object.preferred_exchange_tags.split(",") + end + end end diff --git a/app/views/admin/enterprises/form/_tag_rules.html.haml b/app/views/admin/enterprises/form/_tag_rules.html.haml index 5bd461a5c3..6ad28ee431 100644 --- a/app/views/admin/enterprises/form/_tag_rules.html.haml +++ b/app/views/admin/enterprises/form/_tag_rules.html.haml @@ -27,6 +27,7 @@ %filter-shipping-methods{ ng: { if: "::rule.type == 'TagRule::FilterShippingMethods'" } } %filter-products{ ng: { if: "::rule.type == 'TagRule::FilterProducts'" } } %filter-payment-methods{ ng: { if: "::rule.type == 'TagRule::FilterPaymentMethods'" } } + %filter-order_cycles{ ng: { if: "::rule.type == 'TagRule::FilterOrderCycles'" } } %td.actions %a{ ng: { click: "deleteTagRule(tagGroup, rule)" }, :class => "delete-tag-rule icon-trash no-text" } .add_rule.text-center diff --git a/spec/features/admin/tag_rules_spec.rb b/spec/features/admin/tag_rules_spec.rb index 17163b0a58..c2503407ca 100644 --- a/spec/features/admin/tag_rules_spec.rb +++ b/spec/features/admin/tag_rules_spec.rb @@ -40,6 +40,12 @@ feature 'Tag Rules', js: true do click_button "Add Rule" select2_select "VISIBLE", from: "enterprise_tag_rules_attributes_2_preferred_matched_payment_methods_visibility" + # New FilterPaymentMethods Rule + click_button '+ Add A New Rule' + select2_select 'Show or Hide order cycles in my shopfront', from: 'rule_type_selector' + click_button "Add Rule" + select2_select "NOT VISIBLE", from: "enterprise_tag_rules_attributes_3_preferred_matched_order_cycles_visibility" + # New DiscountOrder Rule # click_button '+ Add A New Rule' # select2_select 'Apply a discount to orders', from: 'rule_type_selector' @@ -66,6 +72,11 @@ feature 'Tag Rules', js: true do expect(tag_rule.preferred_customer_tags).to eq "volunteer" expect(tag_rule.preferred_payment_method_tags).to eq "volunteer" expect(tag_rule.preferred_matched_payment_methods_visibility).to eq "visible" + + tag_rule = TagRule::FilterOrderCycles.last + expect(tag_rule.preferred_customer_tags).to eq "volunteer" + expect(tag_rule.preferred_exchange_tags).to eq "volunteer" + expect(tag_rule.preferred_matched_order_cycles_visibility).to eq "hidden" end end @@ -74,6 +85,7 @@ feature 'Tag Rules', js: true do let!(:fsm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise, preferred_matched_shipping_methods_visibility: "hidden", preferred_customer_tags: "member" ) } let!(:fp_tag_rule) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_matched_variants_visibility: "visible", preferred_customer_tags: "member" ) } let!(:fpm_tag_rule) { create(:filter_payment_methods_tag_rule, enterprise: enterprise, preferred_matched_payment_methods_visibility: "hidden", preferred_customer_tags: "member" ) } + let!(:foc_tag_rule) { create(:filter_order_cycles_tag_rule, enterprise: enterprise, preferred_matched_order_cycles_visibility: "visible", preferred_customer_tags: "member" ) } before do login_to_admin_section @@ -104,6 +116,10 @@ feature 'Tag Rules', js: true do expect(page).to have_select2 "enterprise_tag_rules_attributes_3_preferred_matched_payment_methods_visibility", selected: 'NOT VISIBLE' select2_select 'VISIBLE', from: "enterprise_tag_rules_attributes_3_preferred_matched_payment_methods_visibility" + # FilterPaymentMethods rule + expect(page).to have_select2 "enterprise_tag_rules_attributes_4_preferred_matched_order_cycles_visibility", selected: 'VISIBLE' + select2_select 'NOT VISIBLE', from: "enterprise_tag_rules_attributes_4_preferred_matched_order_cycles_visibility" + click_button 'Update' # DiscountOrder rule @@ -124,6 +140,11 @@ feature 'Tag Rules', js: true do expect(fpm_tag_rule.preferred_customer_tags).to eq "member,volunteer" expect(fpm_tag_rule.preferred_payment_method_tags).to eq "member,volunteer" expect(fpm_tag_rule.preferred_matched_payment_methods_visibility).to eq "visible" + + # FilterPaymentMethods rule + expect(foc_tag_rule.preferred_customer_tags).to eq "member,volunteer" + expect(foc_tag_rule.preferred_exchange_tags).to eq "member,volunteer" + expect(foc_tag_rule.preferred_matched_order_cycles_visibility).to eq "hidden" end end From 84817b01249438a3f683b6471b6489a3d8ef231d Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 5 May 2016 13:50:58 +1000 Subject: [PATCH 088/110] Fixing specs broken by faster PhantomJS --- spec/features/admin/order_cycles_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index e17ace2580..0011681cbd 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -642,7 +642,6 @@ feature %q{ expect(page).to have_selector "tr.distributor-#{distributor_unmanaged.id}" expect(page).to have_selector 'tr.distributor', count: 3 - # When I save, then those exchanges should remain click_button 'Update' page.should have_content "Your order cycle has been updated." From 9b12b86053afc60fc352d7a57b3a5bd1922c193b Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 19 May 2016 10:05:40 +1000 Subject: [PATCH 089/110] Simplifying supplier and distributor row counts --- spec/features/admin/order_cycles_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 0011681cbd..f39c724d6d 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -723,11 +723,11 @@ feature %q{ # distributor_managed and distributor_permitted (who I have given permission to) AND # and distributor_unmanaged (who distributes my products) expect(page).to have_selector "tr.supplier-#{supplier_managed.id}" - expect(page.all('tr.supplier').count).to be 1 + expect(page).to have_selector 'tr.supplier', count: 1 expect(page).to have_selector "tr.distributor-#{distributor_managed.id}" expect(page).to have_selector "tr.distributor-#{distributor_permitted.id}" - expect(page.all('tr.distributor').count).to be 2 + expect(page).to have_selector 'tr.distributor', count: 2 # Open the products list for managed_supplier's incoming exchange within "tr.distributor-#{distributor_managed.id}" do @@ -782,10 +782,10 @@ feature %q{ # I should see exchanges for my_distributor, and the incoming exchanges supplying the variants in it expect(page).to have_selector "tr.supplier-#{supplier_managed.id}" - expect(page.all('tr.supplier').count).to be 1 + expect(page).to have_selector 'tr.supplier', count: 1 expect(page).to have_selector "tr.distributor-#{my_distributor.id}" - expect(page.all('tr.distributor').count).to be 1 + expect(page).to have_selector 'tr.distributor', count: 1 # Open the products list for managed_supplier's incoming exchange within "tr.supplier-#{supplier_managed.id}" do From 3d7786e185cbbcf587680cae78ecf309bf861c12 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 19 May 2016 11:10:20 +1000 Subject: [PATCH 090/110] Optional autocomplete for tag rules directive --- .../admin/utils/directives/tags_with_translation.js.coffee | 2 ++ app/assets/javascripts/templates/admin/tags_input.html.haml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee index a356a9e4ff..221ee03c76 100644 --- a/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee @@ -8,6 +8,8 @@ angular.module("admin.utils").directive "tagsWithTranslation", ($timeout) -> findTags: "&" form: '=?' link: (scope, element, attrs) -> + scope.findTags = undefined unless attrs.hasOwnProperty("findTags") + $timeout -> scope.tagsAttr ||= "tags" scope.tagListAttr ||= "tag_list" diff --git a/app/assets/javascripts/templates/admin/tags_input.html.haml b/app/assets/javascripts/templates/admin/tags_input.html.haml index 3e02c1c332..15a69b650c 100644 --- a/app/assets/javascripts/templates/admin/tags_input.html.haml +++ b/app/assets/javascripts/templates/admin/tags_input.html.haml @@ -1,5 +1,5 @@ %tags-input{ template: 'admin/tag.html', ng: { model: 'object[tagsAttr]' }, on: { tag: { added: 'tagAdded()', removed:'tagRemoved()' } } } - %auto-complete{source: "findTags({query: $query})", + %auto-complete{ ng: { if: "findTags" }, source: "findTags({query: $query})", template: "admin/tag_autocomplete.html", "min-length" => "0", "load-on-focus" => "true", From c568ac4d64bc279eda355bd995c73cc9c5d83004 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 19 May 2016 11:11:50 +1000 Subject: [PATCH 091/110] Re-adding button clicks removed in 885d489bc3431f09ee8677ff381fd162ad62e4a9 --- spec/features/admin/order_cycles_spec.rb | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index f39c724d6d..2733aeaeaf 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -631,6 +631,8 @@ feature %q{ visit edit_admin_order_cycle_path(oc) + fill_in 'order_cycle_name', with: 'Coordinated' + # I should be able to see but not edit exchanges for supplier_unmanaged or distributor_unmanaged expect(page).to have_selector "tr.supplier-#{supplier_managed.id}" expect(page).to have_selector "tr.supplier-#{supplier_permitted.id}" @@ -735,10 +737,11 @@ feature %q{ end # I should be able to see and toggle v1 - expect(page).to have_field "order_cycle_outgoing_exchange_0_variants_#{v1.id}", disabled: false + expect(page).to have_checked_field "order_cycle_outgoing_exchange_0_variants_#{v1.id}", disabled: false + uncheck "order_cycle_outgoing_exchange_0_variants_#{v1.id}" # I should be able to see but not toggle v2, because I don't have permission - expect(page).to have_field "order_cycle_outgoing_exchange_0_variants_#{v2.id}", disabled: true + expect(page).to have_checked_field "order_cycle_outgoing_exchange_0_variants_#{v2.id}", disabled: true page.should_not have_selector "table.exchanges tr.distributor-#{distributor_managed.id} td.tags" @@ -768,8 +771,14 @@ feature %q{ oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: distributor_managed, distributors: [my_distributor, distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) v1 = create(:variant, product: create(:product, supplier: supplier_managed) ) v2 = create(:variant, product: create(:product, supplier: supplier_managed) ) - ex = oc.exchanges.where(sender_id: distributor_managed, receiver_id: my_distributor, incoming: false).first - ex.update_attributes(variant_ids: [v1.id, v2.id]) + + # Incoming exchange + ex_in = oc.exchanges.where(sender_id: supplier_managed, receiver_id: distributor_managed, incoming: true).first + ex_in.update_attributes(variant_ids: [v1.id, v2.id]) + + # Outgoing exchange + ex_out = oc.exchanges.where(sender_id: distributor_managed, receiver_id: my_distributor, incoming: false).first + ex_out.update_attributes(variant_ids: [v1.id, v2.id]) # Stub editable_variants_for_incoming_exchanges method so we can test permissions serializer = Api::Admin::OrderCycleSerializer.new(oc, current_user: new_user) @@ -793,10 +802,11 @@ feature %q{ end # I should be able to see and toggle v1 - expect(page).to have_field "order_cycle_incoming_exchange_0_variants_#{v1.id}", disabled: false + expect(page).to have_checked_field "order_cycle_incoming_exchange_0_variants_#{v1.id}", disabled: false + uncheck "order_cycle_incoming_exchange_0_variants_#{v1.id}" # I should be able to see but not toggle v2, because I don't have permission - expect(page).to have_field "order_cycle_incoming_exchange_0_variants_#{v2.id}", disabled: true + expect(page).to have_checked_field "order_cycle_incoming_exchange_0_variants_#{v2.id}", disabled: true page.should have_selector "table.exchanges tr.distributor-#{my_distributor.id} td.tags" From 339074a66ea04c34c07546bd71ed902ba2d37a34 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 19 May 2016 11:28:24 +1000 Subject: [PATCH 092/110] Making templates available to paymentMethods module --- .../javascripts/admin/payment_methods/payment_methods.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee b/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee index c74a1c7054..fb4fec1ff9 100644 --- a/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee +++ b/app/assets/javascripts/admin/payment_methods/payment_methods.js.coffee @@ -1 +1 @@ -angular.module("admin.paymentMethods", ['ngTagsInput', 'admin.utils']) +angular.module("admin.paymentMethods", ['ngTagsInput', 'admin.utils', 'templates']) From bf0db2287b13dba0dd45b428a2c22c3ef03ca2dd Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 20 May 2016 15:15:26 +1000 Subject: [PATCH 093/110] More structured layout for Tag Rules UI --- .../tag_rules/discount_order.js.coffee | 4 - .../tag_rules/filter_order_cycles.js.coffee | 4 - .../filter_payment_methods.js.coffee | 4 - .../tag_rules/filter_products.js.coffee | 4 - .../filter_shipping_methods.js.coffee | 4 - .../directives/tag_rules/tag_rule.js.coffee | 41 ++++++ .../admin/tag_rules/discount_order.html.haml | 37 ------ .../tag_rules/discount_order_input.html.haml | 7 + .../tag_rules/filter_order_cycles.html.haml | 27 ---- .../filter_order_cycles_input.html.haml | 4 + .../filter_payment_methods.html.haml | 27 ---- .../filter_payment_methods_input.html.haml | 4 + .../admin/tag_rules/filter_products.html.haml | 27 ---- .../tag_rules/filter_products_input.html.haml | 4 + .../filter_shipping_methods.html.haml | 27 ---- .../filter_shipping_methods_input.html.haml | 4 + .../admin/tag_rules/tag_rule.html.haml | 40 ++++++ .../stylesheets/admin/tag_rules.css.scss | 30 +++-- .../api/admin/tag_rule_serializer.rb | 8 +- .../enterprises/form/_tag_rules.html.haml | 11 +- spec/features/admin/tag_rules_spec.rb | 121 +++++++++++------- 21 files changed, 205 insertions(+), 234 deletions(-) delete mode 100644 app/assets/javascripts/admin/tag_rules/directives/tag_rules/discount_order.js.coffee delete mode 100644 app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_order_cycles.js.coffee delete mode 100644 app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_payment_methods.js.coffee delete mode 100644 app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_products.js.coffee delete mode 100644 app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_shipping_methods.js.coffee create mode 100644 app/assets/javascripts/admin/tag_rules/directives/tag_rules/tag_rule.js.coffee delete mode 100644 app/assets/javascripts/templates/admin/tag_rules/discount_order.html.haml create mode 100644 app/assets/javascripts/templates/admin/tag_rules/discount_order_input.html.haml delete mode 100644 app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles.html.haml create mode 100644 app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles_input.html.haml delete mode 100644 app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods.html.haml create mode 100644 app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods_input.html.haml delete mode 100644 app/assets/javascripts/templates/admin/tag_rules/filter_products.html.haml create mode 100644 app/assets/javascripts/templates/admin/tag_rules/filter_products_input.html.haml delete mode 100644 app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods.html.haml create mode 100644 app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods_input.html.haml create mode 100644 app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml diff --git a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/discount_order.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/discount_order.js.coffee deleted file mode 100644 index b374f88782..0000000000 --- a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/discount_order.js.coffee +++ /dev/null @@ -1,4 +0,0 @@ -angular.module("admin.tagRules").directive "discountOrder", -> - restrict: "E" - replace: true - templateUrl: "admin/tag_rules/discount_order.html" diff --git a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_order_cycles.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_order_cycles.js.coffee deleted file mode 100644 index 6829063a97..0000000000 --- a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_order_cycles.js.coffee +++ /dev/null @@ -1,4 +0,0 @@ -angular.module("admin.tagRules").directive "filterOrderCycles", -> - restrict: "E" - replace: true - templateUrl: "admin/tag_rules/filter_order_cycles.html" diff --git a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_payment_methods.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_payment_methods.js.coffee deleted file mode 100644 index c9f6926fc5..0000000000 --- a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_payment_methods.js.coffee +++ /dev/null @@ -1,4 +0,0 @@ -angular.module("admin.tagRules").directive "filterPaymentMethods", -> - restrict: "E" - replace: true - templateUrl: "admin/tag_rules/filter_payment_methods.html" diff --git a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_products.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_products.js.coffee deleted file mode 100644 index 58e9c47c5b..0000000000 --- a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_products.js.coffee +++ /dev/null @@ -1,4 +0,0 @@ -angular.module("admin.tagRules").directive "filterProducts", -> - restrict: "E" - replace: true - templateUrl: "admin/tag_rules/filter_products.html" diff --git a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_shipping_methods.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_shipping_methods.js.coffee deleted file mode 100644 index 1a75cf8ff2..0000000000 --- a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/filter_shipping_methods.js.coffee +++ /dev/null @@ -1,4 +0,0 @@ -angular.module("admin.tagRules").directive "filterShippingMethods", -> - restrict: "E" - replace: true - templateUrl: "admin/tag_rules/filter_shipping_methods.html" diff --git a/app/assets/javascripts/admin/tag_rules/directives/tag_rules/tag_rule.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/tag_rule.js.coffee new file mode 100644 index 0000000000..11c7ed014b --- /dev/null +++ b/app/assets/javascripts/admin/tag_rules/directives/tag_rules/tag_rule.js.coffee @@ -0,0 +1,41 @@ +angular.module("admin.tagRules").directive "tagRule", -> + restrict: "C" + templateUrl: "admin/tag_rules/tag_rule.html" + link: (scope, element, attrs) -> + scope.opt = + "TagRule::FilterShippingMethods": + textTop: "Shipping methods tagged" + textBottom: "are:" + taggable: "shipping_method" + tagsAttr: "shipping_method_tags" + tagListAttr: "preferred_shipping_method_tags" + inputTemplate: "admin/tag_rules/filter_shipping_methods_input.html" + tagListFor: (rule) -> + rule.preferred_shipping_method_tags + "TagRule::FilterPaymentMethods": + textTop: "Payment methods tagged" + textBottom: "are:" + taggable: "payment_method" + tagsAttr: "payment_method_tags" + tagListAttr: "preferred_payment_method_tags" + inputTemplate: "admin/tag_rules/filter_payment_methods_input.html" + tagListFor: (rule) -> + rule.preferred_payment_method_tags + "TagRule::FilterOrderCycles": + textTop: "Order Cycles tagged" + textBottom: "are:" + taggable: "exchange" + tagsAttr: "exchange_tags" + tagListAttr: "preferred_exchange_tags" + inputTemplate: "admin/tag_rules/filter_order_cycles_input.html" + tagListFor: (rule) -> + rule.preferred_exchange_tags + "TagRule::FilterProducts": + textTop: "Inventory variants tagged" + textBottom: "are:" + taggable: "variant" + tagsAttr: "variant_tags" + tagListAttr: "preferred_variant_tags" + inputTemplate: "admin/tag_rules/filter_products_input.html" + tagListFor: (rule) -> + rule.preferred_variant_tags diff --git a/app/assets/javascripts/templates/admin/tag_rules/discount_order.html.haml b/app/assets/javascripts/templates/admin/tag_rules/discount_order.html.haml deleted file mode 100644 index 358d9ce1a6..0000000000 --- a/app/assets/javascripts/templates/admin/tag_rules/discount_order.html.haml +++ /dev/null @@ -1,37 +0,0 @@ -%div - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]", - ng: { value: "rule.id" } } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", - value: "TagRule::DiscountOrder" } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", - ng: { value: "rule.preferred_customer_tags" } } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_type", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][calculator_type]", - value: "Spree::Calculator::FlatPercentItemTotal" } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_attributes_id", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][calculator_attributes][id]", - ng: { value: "rule.calculator.id" } } - - %input{ type: "hidden", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][calculator_attributes][preferred_flat_percent]", - ng: { value: "rule.calculator.preferred_flat_percent" } } - - %span.text-normal {{ $index + 1 }}. Orders are discounted by - %input{ type: "number", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_attributes_preferred_flat_percent", - min: -100, - max: 100, - ng: { model: "rule.calculator.preferred_flat_percent" }, 'invert-number' => true } - %span.text-normal % diff --git a/app/assets/javascripts/templates/admin/tag_rules/discount_order_input.html.haml b/app/assets/javascripts/templates/admin/tag_rules/discount_order_input.html.haml new file mode 100644 index 0000000000..abad41ea62 --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag_rules/discount_order_input.html.haml @@ -0,0 +1,7 @@ +%div + %input{ type: "number", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_attributes_preferred_flat_percent", + min: -100, + max: 100, + ng: { model: "rule.calculator.preferred_flat_percent" }, 'invert-number' => true } + %span.text-normal % diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles.html.haml deleted file mode 100644 index c0f5300809..0000000000 --- a/app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -%div - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]", - ng: { value: "rule.id" } } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", - value: "TagRule::FilterOrderCycles" } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", - ng: { value: "rule.preferred_customer_tags" } } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_exchange_tags", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_exchange_tags]", - ng: { value: "rule.preferred_customer_tags" } } - - %span.text-normal {{ $index + 1 }}. Order Cycles with matching tags are - %input.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_order_cycles_visibility", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_order_cycles_visibility]", - ng: { model: "rule.preferred_matched_order_cycles_visibility"}, - data: 'visibilityOptions', "min-search" => 5 } - -# %tags-with-translation{ object: "rule", "tags-attr" => "shipping_method_tags", "tag-list-attr" => "preferred_shipping_method_tags" } diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles_input.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles_input.html.haml new file mode 100644 index 0000000000..5cd8a4400d --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles_input.html.haml @@ -0,0 +1,4 @@ +%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_order_cycles_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_order_cycles_visibility]", + ng: { model: "rule.preferred_matched_order_cycles_visibility"}, + data: 'visibilityOptions', "min-search" => 5 } diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods.html.haml deleted file mode 100644 index 5bd06a6cd6..0000000000 --- a/app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -%div - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]", - ng: { value: "rule.id" } } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", - value: "TagRule::FilterPaymentMethods" } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", - ng: { value: "rule.preferred_customer_tags" } } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_payment_method_tags", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_payment_method_tags]", - ng: { value: "rule.preferred_customer_tags" } } - - %span.text-normal {{ $index + 1 }}. Payment methods with matching tags are - %input.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_payment_methods_visibility", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_payment_methods_visibility]", - ng: { model: "rule.preferred_matched_payment_methods_visibility"}, - data: 'visibilityOptions', "min-search" => 5 } - -# %tags-with-translation{ object: "rule", "tags-attr" => "payment_method_tags", "tag-list-attr" => "preferred_payment_method_tags" } diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods_input.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods_input.html.haml new file mode 100644 index 0000000000..1446c6bcff --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods_input.html.haml @@ -0,0 +1,4 @@ +%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_payment_methods_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_payment_methods_visibility]", + ng: { model: "rule.preferred_matched_payment_methods_visibility"}, + data: 'visibilityOptions', "min-search" => 5 } diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_products.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_products.html.haml deleted file mode 100644 index 0429dd7cbd..0000000000 --- a/app/assets/javascripts/templates/admin/tag_rules/filter_products.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -%div - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]", - ng: { value: "rule.id" } } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", - value: "TagRule::FilterProducts" } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", - ng: { value: "rule.preferred_customer_tags" } } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_variant_tags", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_variant_tags]", - ng: { value: "rule.preferred_customer_tags" } } - - %span.text-normal {{ $index + 1 }}. Variants with matching tags are - %input.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_variants_visibility", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_variants_visibility]", - ng: { model: "rule.preferred_matched_variants_visibility"}, - data: 'visibilityOptions', "min-search" => 5 } - -# %tags-with-translation{ object: "rule", "tags-attr" => "shipping_method_tags", "tag-list-attr" => "preferred_shipping_method_tags" } diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_products_input.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_products_input.html.haml new file mode 100644 index 0000000000..9326af7d5a --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag_rules/filter_products_input.html.haml @@ -0,0 +1,4 @@ +%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_variants_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_variants_visibility]", + ng: { model: "rule.preferred_matched_variants_visibility"}, + data: 'visibilityOptions', "min-search" => 5 } diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods.html.haml deleted file mode 100644 index 6552d834b5..0000000000 --- a/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -%div - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]", - ng: { value: "rule.id" } } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", - value: "TagRule::FilterShippingMethods" } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", - ng: { value: "rule.preferred_customer_tags" } } - - %input{ type: "hidden", - id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_shipping_method_tags", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_shipping_method_tags]", - ng: { value: "rule.preferred_customer_tags" } } - - %span.text-normal {{ $index + 1 }}. Shipping methods with matching tags are - %input.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_shipping_methods_visibility", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_shipping_methods_visibility]", - ng: { model: "rule.preferred_matched_shipping_methods_visibility"}, - data: 'visibilityOptions', "min-search" => 5 } - -# %tags-with-translation{ object: "rule", "tags-attr" => "shipping_method_tags", "tag-list-attr" => "preferred_shipping_method_tags" } diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods_input.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods_input.html.haml new file mode 100644 index 0000000000..af31e93cfa --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods_input.html.haml @@ -0,0 +1,4 @@ +%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_shipping_methods_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_shipping_methods_visibility]", + ng: { model: "rule.preferred_matched_shipping_methods_visibility"}, + data: 'visibilityOptions', "min-search" => 5 } diff --git a/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml b/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml new file mode 100644 index 0000000000..086c3f59cd --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml @@ -0,0 +1,40 @@ +%div{ id: "tr_{{tagGroup.startIndex + $index}}" } + %table + %colgroup + %col.text{ width: "35%" } + %col.inputs{ width: "55%" } + %col.actions{ width: "10%" } + %tr + %td + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]", + ng: { value: "rule.id" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", + ng: { value: "rule.type" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", + ng: { value: "rule.preferred_customer_tags" } } + + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_{{opt[rule.type].taggable}}_tags", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_{{opt[rule.type].taggable}}_tags]", + ng: { value: "opt[rule.type].tagListFor(rule)" } } + + %span.text-normal {{ opt[rule.type].textTop }} + %td + %tags-with-translation{ object: "rule", "tags-attr" => "{{opt[rule.type].tagsAttr}}", "tag-list-attr" => "{{opt[rule.type].tagListAttr}}" } + %td.actions{ rowspan: 2 } + %a{ ng: { click: "deleteTagRule(tagGroup, rule)" }, :class => "delete-tag-rule icon-trash no-text" } + %tr + %td + %span.text-normal {{ opt[rule.type].textBottom }} + %td + %div{ ng: { include: "opt[rule.type].inputTemplate"} } + + %hr diff --git a/app/assets/stylesheets/admin/tag_rules.css.scss b/app/assets/stylesheets/admin/tag_rules.css.scss index 028ad6ce1c..142af801a7 100644 --- a/app/assets/stylesheets/admin/tag_rules.css.scss +++ b/app/assets/stylesheets/admin/tag_rules.css.scss @@ -33,22 +33,30 @@ font-weight: bold; } - table { - padding: 0px; - margin: 0px 0px 10px 0px; - - tr.tag_rule { - border: none; + .tag_rule { + table { padding: 0px; - margin: 0px; + margin: 0px 0px 10px 0px; - td { + &:hover { + td { + background-color: #ebf3fb; + } + } + + tr { border: none; - padding: 4px 10px 10px 10px; + padding: 0px; margin: 0px; - input { - width: auto; + td { + border: none; + padding: 2px 10px 2px 10px; + margin: 0px; + + input { + width: auto; + } } } } diff --git a/app/serializers/api/admin/tag_rule_serializer.rb b/app/serializers/api/admin/tag_rule_serializer.rb index 6be7ec1835..4f5be5cee7 100644 --- a/app/serializers/api/admin/tag_rule_serializer.rb +++ b/app/serializers/api/admin/tag_rule_serializer.rb @@ -18,7 +18,7 @@ module Api::Admin::TagRule end class FilterShippingMethodsSerializer < BaseSerializer - attributes :preferred_matched_shipping_methods_visibility, :shipping_method_tags + attributes :preferred_matched_shipping_methods_visibility, :preferred_shipping_method_tags, :shipping_method_tags def shipping_method_tags object.preferred_shipping_method_tags.split(",") @@ -26,7 +26,7 @@ module Api::Admin::TagRule end class FilterPaymentMethodsSerializer < BaseSerializer - attributes :preferred_matched_payment_methods_visibility, :payment_method_tags + attributes :preferred_matched_payment_methods_visibility, :preferred_payment_method_tags, :payment_method_tags def payment_method_tags object.preferred_payment_method_tags.split(",") @@ -34,7 +34,7 @@ module Api::Admin::TagRule end class FilterProductsSerializer < BaseSerializer - attributes :preferred_matched_variants_visibility, :variant_tags + attributes :preferred_matched_variants_visibility, :preferred_variant_tags, :variant_tags def variant_tags object.preferred_variant_tags.split(",") @@ -42,7 +42,7 @@ module Api::Admin::TagRule end class FilterOrderCyclesSerializer < BaseSerializer - attributes :preferred_matched_order_cycles_visibility, :exchange_tags + attributes :preferred_matched_order_cycles_visibility, :preferred_exchange_tags, :exchange_tags def exchange_tags object.preferred_exchange_tags.split(",") diff --git a/app/views/admin/enterprises/form/_tag_rules.html.haml b/app/views/admin/enterprises/form/_tag_rules.html.haml index 6ad28ee431..76d971f963 100644 --- a/app/views/admin/enterprises/form/_tag_rules.html.haml +++ b/app/views/admin/enterprises/form/_tag_rules.html.haml @@ -20,16 +20,7 @@ .no_rules{ ng: { show: "tagGroup.rules.length == 0" } } No rules apply to this tag yet - %table - %tr.tag_rule{ id: "tr_{{rule.id}}", ng: { repeat: "rule in tagGroup.rules" } } - %td - %discount-order{ ng: { if: "::rule.type == 'TagRule::DiscountOrder'" } } - %filter-shipping-methods{ ng: { if: "::rule.type == 'TagRule::FilterShippingMethods'" } } - %filter-products{ ng: { if: "::rule.type == 'TagRule::FilterProducts'" } } - %filter-payment-methods{ ng: { if: "::rule.type == 'TagRule::FilterPaymentMethods'" } } - %filter-order_cycles{ ng: { if: "::rule.type == 'TagRule::FilterOrderCycles'" } } - %td.actions - %a{ ng: { click: "deleteTagRule(tagGroup, rule)" }, :class => "delete-tag-rule icon-trash no-text" } + .tag_rule{ ng: { repeat: "rule in tagGroup.rules" } } .add_rule.text-center %input.button.icon-plus{ type: 'button', value: "+ Add A New Rule", "new-tag-rule-dialog" => true } .add_tag diff --git a/spec/features/admin/tag_rules_spec.rb b/spec/features/admin/tag_rules_spec.rb index c2503407ca..1e31039523 100644 --- a/spec/features/admin/tag_rules_spec.rb +++ b/spec/features/admin/tag_rules_spec.rb @@ -26,25 +26,37 @@ feature 'Tag Rules', js: true do click_button '+ Add A New Rule' select2_select 'Show or Hide shipping methods at checkout', from: 'rule_type_selector' click_button "Add Rule" - select2_select "NOT VISIBLE", from: "enterprise_tag_rules_attributes_0_preferred_matched_shipping_methods_visibility" + within("#tr_0") do + find(:css, "tags-input .tags input").set "volunteers-only\n" + select2_select "NOT VISIBLE", from: "enterprise_tag_rules_attributes_0_preferred_matched_shipping_methods_visibility" + end # New FilterProducts Rule click_button '+ Add A New Rule' select2_select 'Show or Hide variants in my shop', from: 'rule_type_selector' click_button "Add Rule" - select2_select "VISIBLE", from: "enterprise_tag_rules_attributes_1_preferred_matched_variants_visibility" + within("#tr_1") do + find(:css, "tags-input .tags input").set "volunteers-only1\n" + select2_select "VISIBLE", from: "enterprise_tag_rules_attributes_1_preferred_matched_variants_visibility" + end # New FilterPaymentMethods Rule click_button '+ Add A New Rule' select2_select 'Show or Hide payment methods at checkout', from: 'rule_type_selector' click_button "Add Rule" - select2_select "VISIBLE", from: "enterprise_tag_rules_attributes_2_preferred_matched_payment_methods_visibility" + within("#tr_2") do + find(:css, "tags-input .tags input").set "volunteers-only2\n" + select2_select "VISIBLE", from: "enterprise_tag_rules_attributes_2_preferred_matched_payment_methods_visibility" + end # New FilterPaymentMethods Rule click_button '+ Add A New Rule' select2_select 'Show or Hide order cycles in my shopfront', from: 'rule_type_selector' click_button "Add Rule" - select2_select "NOT VISIBLE", from: "enterprise_tag_rules_attributes_3_preferred_matched_order_cycles_visibility" + within("#tr_3") do + find(:css, "tags-input .tags input").set "volunteers-only3\n" + select2_select "NOT VISIBLE", from: "enterprise_tag_rules_attributes_3_preferred_matched_order_cycles_visibility" + end # New DiscountOrder Rule # click_button '+ Add A New Rule' @@ -60,32 +72,32 @@ feature 'Tag Rules', js: true do tag_rule = TagRule::FilterShippingMethods.last expect(tag_rule.preferred_customer_tags).to eq "volunteer" - expect(tag_rule.preferred_shipping_method_tags).to eq "volunteer" + expect(tag_rule.preferred_shipping_method_tags).to eq "volunteers-only" expect(tag_rule.preferred_matched_shipping_methods_visibility).to eq "hidden" tag_rule = TagRule::FilterProducts.last expect(tag_rule.preferred_customer_tags).to eq "volunteer" - expect(tag_rule.preferred_variant_tags).to eq "volunteer" + expect(tag_rule.preferred_variant_tags).to eq "volunteers-only1" expect(tag_rule.preferred_matched_variants_visibility).to eq "visible" tag_rule = TagRule::FilterPaymentMethods.last expect(tag_rule.preferred_customer_tags).to eq "volunteer" - expect(tag_rule.preferred_payment_method_tags).to eq "volunteer" + expect(tag_rule.preferred_payment_method_tags).to eq "volunteers-only2" expect(tag_rule.preferred_matched_payment_methods_visibility).to eq "visible" tag_rule = TagRule::FilterOrderCycles.last expect(tag_rule.preferred_customer_tags).to eq "volunteer" - expect(tag_rule.preferred_exchange_tags).to eq "volunteer" + expect(tag_rule.preferred_exchange_tags).to eq "volunteers-only3" expect(tag_rule.preferred_matched_order_cycles_visibility).to eq "hidden" end end context "updating" do - let!(:do_tag_rule) { create(:tag_rule, enterprise: enterprise, preferred_customer_tags: "member" ) } - let!(:fsm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise, preferred_matched_shipping_methods_visibility: "hidden", preferred_customer_tags: "member" ) } - let!(:fp_tag_rule) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_matched_variants_visibility: "visible", preferred_customer_tags: "member" ) } - let!(:fpm_tag_rule) { create(:filter_payment_methods_tag_rule, enterprise: enterprise, preferred_matched_payment_methods_visibility: "hidden", preferred_customer_tags: "member" ) } - let!(:foc_tag_rule) { create(:filter_order_cycles_tag_rule, enterprise: enterprise, preferred_matched_order_cycles_visibility: "visible", preferred_customer_tags: "member" ) } + let!(:fsm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise, preferred_matched_shipping_methods_visibility: "hidden", preferred_customer_tags: "local", preferred_shipping_method_tags: "local" ) } + let!(:fp_tag_rule) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_matched_variants_visibility: "visible", preferred_customer_tags: "member", preferred_variant_tags: "member" ) } + let!(:fpm_tag_rule) { create(:filter_payment_methods_tag_rule, enterprise: enterprise, preferred_matched_payment_methods_visibility: "hidden", preferred_customer_tags: "trusted", preferred_payment_method_tags: "trusted" ) } + let!(:foc_tag_rule) { create(:filter_order_cycles_tag_rule, enterprise: enterprise, preferred_matched_order_cycles_visibility: "visible", preferred_customer_tags: "wholesale", preferred_exchange_tags: "wholesale" ) } + # let!(:do_tag_rule) { create(:tag_rule, enterprise: enterprise, preferred_customer_tags: "member" ) } before do login_to_admin_section @@ -95,56 +107,77 @@ feature 'Tag Rules', js: true do it "saves changes to rules of each type" do click_link "Tag Rules" - # Tag group exists - expect(first('.customer_tag .header')).to have_content "For customers tagged:" - expect(first('tags-input .tag-list ti-tag-item')).to have_content "member" - find(:css, "tags-input .tags input").set "volunteer\n" - - # DiscountOrder rule - expect(page).to have_field "enterprise_tag_rules_attributes_0_calculator_attributes_preferred_flat_percent", with: '0' - fill_in "enterprise_tag_rules_attributes_0_calculator_attributes_preferred_flat_percent", with: 45 + # Tag groups exist + expect(page).to have_selector '.customer_tag .header', text: "For customers tagged:", count: 4 + expect(page).to have_selector '.customer_tag .header tags-input .tag-list ti-tag-item', text: "member", count: 1 + expect(page).to have_selector '.customer_tag .header tags-input .tag-list ti-tag-item', text: "local", count: 1 + expect(page).to have_selector '.customer_tag .header tags-input .tag-list ti-tag-item', text: "wholesale", count: 1 + expect(page).to have_selector '.customer_tag .header tags-input .tag-list ti-tag-item', text: "trusted", count: 1 + all(:css, ".customer_tag .header tags-input .tags input").each { |node| node.set "volunteer\n" } # FilterShippingMethods rule - expect(page).to have_select2 "enterprise_tag_rules_attributes_1_preferred_matched_shipping_methods_visibility", selected: 'NOT VISIBLE' - select2_select 'VISIBLE', from: "enterprise_tag_rules_attributes_1_preferred_matched_shipping_methods_visibility" + within "#tr_0" do + expect(first('tags-input .tag-list ti-tag-item')).to have_content "local" + find(:css, "tags-input .tags input").set "volunteers-only\n" + expect(page).to have_select2 "enterprise_tag_rules_attributes_0_preferred_matched_shipping_methods_visibility", selected: 'NOT VISIBLE' + select2_select 'VISIBLE', from: "enterprise_tag_rules_attributes_0_preferred_matched_shipping_methods_visibility" + end # FilterProducts rule - expect(page).to have_select2 "enterprise_tag_rules_attributes_2_preferred_matched_variants_visibility", selected: 'VISIBLE' - select2_select 'NOT VISIBLE', from: "enterprise_tag_rules_attributes_2_preferred_matched_variants_visibility" + within "#tr_1" do + expect(first('tags-input .tag-list ti-tag-item')).to have_content "member" + find(:css, "tags-input .tags input").set "volunteers-only1\n" + expect(page).to have_select2 "enterprise_tag_rules_attributes_1_preferred_matched_variants_visibility", selected: 'VISIBLE' + select2_select 'NOT VISIBLE', from: "enterprise_tag_rules_attributes_1_preferred_matched_variants_visibility" + end # FilterPaymentMethods rule - expect(page).to have_select2 "enterprise_tag_rules_attributes_3_preferred_matched_payment_methods_visibility", selected: 'NOT VISIBLE' - select2_select 'VISIBLE', from: "enterprise_tag_rules_attributes_3_preferred_matched_payment_methods_visibility" + within "#tr_2" do + expect(first('tags-input .tag-list ti-tag-item')).to have_content "trusted" + find(:css, "tags-input .tags input").set "volunteers-only2\n" + expect(page).to have_select2 "enterprise_tag_rules_attributes_2_preferred_matched_payment_methods_visibility", selected: 'NOT VISIBLE' + select2_select 'VISIBLE', from: "enterprise_tag_rules_attributes_2_preferred_matched_payment_methods_visibility" + end - # FilterPaymentMethods rule - expect(page).to have_select2 "enterprise_tag_rules_attributes_4_preferred_matched_order_cycles_visibility", selected: 'VISIBLE' - select2_select 'NOT VISIBLE', from: "enterprise_tag_rules_attributes_4_preferred_matched_order_cycles_visibility" + # FilterOrderCycles rule + within "#tr_3" do + expect(first('tags-input .tag-list ti-tag-item')).to have_content "wholesale" + find(:css, "tags-input .tags input").set "volunteers-only3\n" + expect(page).to have_select2 "enterprise_tag_rules_attributes_3_preferred_matched_order_cycles_visibility", selected: 'VISIBLE' + select2_select 'NOT VISIBLE', from: "enterprise_tag_rules_attributes_3_preferred_matched_order_cycles_visibility" + end + + # # DiscountOrder rule + # within "#tr_2" do + # expect(page).to have_field "enterprise_tag_rules_attributes_2_calculator_attributes_preferred_flat_percent", with: '0' + # fill_in "enterprise_tag_rules_attributes_2_calculator_attributes_preferred_flat_percent", with: 45 + # end click_button 'Update' - # DiscountOrder rule - expect(do_tag_rule.preferred_customer_tags).to eq "member,volunteer" - expect(do_tag_rule.calculator.preferred_flat_percent).to eq -45 - # FilterShippingMethods rule - expect(fsm_tag_rule.preferred_customer_tags).to eq "member,volunteer" - expect(fsm_tag_rule.preferred_shipping_method_tags).to eq "member,volunteer" + expect(fsm_tag_rule.preferred_customer_tags).to eq "local,volunteer" + expect(fsm_tag_rule.preferred_shipping_method_tags).to eq "local,volunteers-only" expect(fsm_tag_rule.preferred_matched_shipping_methods_visibility).to eq "visible" # FilterProducts rule expect(fp_tag_rule.preferred_customer_tags).to eq "member,volunteer" - expect(fp_tag_rule.preferred_variant_tags).to eq "member,volunteer" + expect(fp_tag_rule.preferred_variant_tags).to eq "member,volunteers-only1" expect(fp_tag_rule.preferred_matched_variants_visibility).to eq "hidden" # FilterPaymentMethods rule - expect(fpm_tag_rule.preferred_customer_tags).to eq "member,volunteer" - expect(fpm_tag_rule.preferred_payment_method_tags).to eq "member,volunteer" + expect(fpm_tag_rule.preferred_customer_tags).to eq "trusted,volunteer" + expect(fpm_tag_rule.preferred_payment_method_tags).to eq "trusted,volunteers-only2" expect(fpm_tag_rule.preferred_matched_payment_methods_visibility).to eq "visible" # FilterPaymentMethods rule - expect(foc_tag_rule.preferred_customer_tags).to eq "member,volunteer" - expect(foc_tag_rule.preferred_exchange_tags).to eq "member,volunteer" + expect(foc_tag_rule.preferred_customer_tags).to eq "wholesale,volunteer" + expect(foc_tag_rule.preferred_exchange_tags).to eq "wholesale,volunteers-only3" expect(foc_tag_rule.preferred_matched_order_cycles_visibility).to eq "hidden" + + # DiscountOrder rule + # expect(do_tag_rule.preferred_customer_tags).to eq "member,volunteer" + # expect(do_tag_rule.calculator.preferred_flat_percent).to eq -45 end end @@ -159,13 +192,13 @@ feature 'Tag Rules', js: true do it "deletes rules from the database" do click_link "Tag Rules" - expect(page).to have_selector "#tr_#{tag_rule.id}" + expect(page).to have_selector "#tr_0" expect{ - within "#tr_#{tag_rule.id}" do + within "#tr_0" do first("a.delete-tag-rule").click end - expect(page).to_not have_selector "#tr_#{tag_rule.id}" + expect(page).to_not have_selector "#tr_0" }.to change{TagRule.count}.by(-1) end end From fa3b43a9702f70023e2714906a088d9b2e1745e4 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Mon, 23 May 2016 10:19:21 +1000 Subject: [PATCH 094/110] Adding default tag rules --- .../tag_rules_controller.js.coffee | 4 +- .../directives/new_rule_dialog.js.coffee | 4 +- .../admin/tag_rules/tag_rule.html.haml | 5 + .../stylesheets/admin/index_panels.css.scss | 96 ++++++++++--------- .../stylesheets/admin/tag_rules.css.scss | 2 +- app/models/tag_rule.rb | 2 +- .../api/admin/enterprise_serializer.rb | 12 ++- .../api/admin/tag_rule_serializer.rb | 2 +- .../enterprises/form/_tag_rules.html.haml | 4 +- .../form/tag_rules/_default_rules.html.haml | 14 +++ ...160520065217_add_is_default_to_tag_rule.rb | 5 + db/schema.rb | 11 ++- spec/features/admin/tag_rules_spec.rb | 58 ++++++++--- .../tag_rules_controller_spec.js.coffee | 17 ++-- 14 files changed, 151 insertions(+), 85 deletions(-) create mode 100644 app/views/admin/enterprises/form/tag_rules/_default_rules.html.haml create mode 100644 db/migrate/20160520065217_add_is_default_to_tag_rule.rb diff --git a/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee index 3522bb56bf..38e12182a0 100644 --- a/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee +++ b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee @@ -1,10 +1,11 @@ angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, enterprise) -> $scope.tagGroups = enterprise.tag_groups + $scope.defaultTagGroup = enterprise.default_tag_group $scope.visibilityOptions = [ { id: "visible", name: "VISIBLE" }, { id: "hidden", name: "NOT VISIBLE" } ] updateRuleCounts = -> - index = 0 + index = $scope.defaultTagGroup.rules.length for tagGroup in $scope.tagGroups tagGroup.startIndex = index index = index + tagGroup.rules.length @@ -18,6 +19,7 @@ angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, ente $scope.addNewRuleTo = (tagGroup, ruleType) -> newRule = id: null + is_default: tagGroup == $scope.defaultTagGroup preferred_customer_tags: (tag.text for tag in tagGroup.tags).join(",") type: "TagRule::#{ruleType}" switch ruleType diff --git a/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee index 6662d5be35..df09bfd74b 100644 --- a/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee +++ b/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee @@ -1,6 +1,8 @@ angular.module("admin.tagRules").directive 'newTagRuleDialog', ($compile, $templateCache, $window) -> restrict: 'A' - scope: true + scope: + tagGroup: '=' + addNewRuleTo: '=' link: (scope, element, attr) -> # Compile modal template template = $compile($templateCache.get('admin/new_tag_rule_dialog.html'))(scope) diff --git a/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml b/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml index 086c3f59cd..0a752ae17b 100644 --- a/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml +++ b/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml @@ -16,6 +16,11 @@ name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", ng: { value: "rule.type" } } + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_is_default", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][is_default]", + ng: { value: "rule.is_default" } } + %input{ type: "hidden", id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]", diff --git a/app/assets/stylesheets/admin/index_panels.css.scss b/app/assets/stylesheets/admin/index_panels.css.scss index d2861eb661..b622212446 100644 --- a/app/assets/stylesheets/admin/index_panels.css.scss +++ b/app/assets/stylesheets/admin/index_panels.css.scss @@ -65,58 +65,60 @@ tbody.panel-ctrl { } &.expanded{ - td { - border-bottom: 1px solid #6788a2; + >tr:not(.panel-row) { + >td { + border-bottom: 1px solid #6788a2; - &.selected { - background-color: #ffffff; - border-left: 1px solid #6788a2; - border-right: 1px solid #6788a2; - border-top: 1px solid #6788a2; - border-bottom: none; - - &:hover { + &.selected { background-color: #ffffff; - } + border-left: 1px solid #6788a2; + border-right: 1px solid #6788a2; + border-top: 1px solid #6788a2; + border-bottom: none; - * { - color: #1b3c56; - } + &:hover { + background-color: #ffffff; + } - i.icon-status::before { - opacity: 1.0; - } + * { + color: #1b3c56; + } - i.icon-chevron::before { - content: "\f077"; - } - } - } - } -} - -tr.panel-row { - &:hover { - td { - background-color: #ffffff; - } - } - - >td { - border-color: #6788a2; - padding: 0; - .panel { - .row{ - margin: 0px -4px; - - padding: 20px 0px; - - .column.alpha, .columns.alpha { - padding-left: 20px; - } - - .column.omega, .columns.omega { - padding-right: 20px; + i.icon-status::before { + opacity: 1.0; + } + + i.icon-chevron::before { + content: "\f077"; + } + } + } + } + } + + tr.panel-row { + &:hover { + td { + background-color: #ffffff; + } + } + + >td { + border-color: #6788a2; + padding: 0; + .panel { + .row{ + margin: 0px -4px; + + padding: 20px 0px; + + .column.alpha, .columns.alpha { + padding-left: 20px; + } + + .column.omega, .columns.omega { + padding-right: 20px; + } } } } diff --git a/app/assets/stylesheets/admin/tag_rules.css.scss b/app/assets/stylesheets/admin/tag_rules.css.scss index 142af801a7..0540dfa89f 100644 --- a/app/assets/stylesheets/admin/tag_rules.css.scss +++ b/app/assets/stylesheets/admin/tag_rules.css.scss @@ -5,7 +5,7 @@ font-weight: bold; } -.customer_tag { +.customer_tag, .default_rules { border: 1px solid #cee1f4; margin-bottom: 40px; diff --git a/app/models/tag_rule.rb b/app/models/tag_rule.rb index 23659bbf0a..1c578d2848 100644 --- a/app/models/tag_rule.rb +++ b/app/models/tag_rule.rb @@ -7,7 +7,7 @@ class TagRule < ActiveRecord::Base validates :enterprise, presence: true - attr_accessible :enterprise, :enterprise_id, :preferred_customer_tags + attr_accessible :enterprise, :enterprise_id, :is_default, :preferred_customer_tags scope :for, ->(enterprise) { where(enterprise_id: enterprise) } scope :of_type, ->(type) { where(type: "TagRule::#{type}") } diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index 8f70da1a0d..86b40c90ad 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -3,19 +3,23 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer attributes :producer_profile_only, :email, :long_description, :permalink attributes :preferred_shopfront_message, :preferred_shopfront_closed_message, :preferred_shopfront_taxon_order, :preferred_shopfront_order_cycle_order attributes :preferred_product_selection_from_inventory_only - attributes :owner, :users, :tag_groups + attributes :owner, :users, :tag_groups, :default_tag_group has_one :owner, serializer: Api::Admin::UserSerializer has_many :users, serializer: Api::Admin::UserSerializer def tag_groups - tag_groups = [] - object.tag_rules.each do |tag_rule| + object.tag_rules.reject(&:is_default).each_with_object([]) do |tag_rule, tag_groups| tag_group = find_match(tag_groups, tag_rule.preferred_customer_tags.split(",").map{ |t| { text: t } }) tag_groups << tag_group if tag_group[:rules].empty? tag_group[:rules] << Api::Admin::TagRuleSerializer.new(tag_rule).serializable_hash end - tag_groups + end + + def default_tag_group + default_rules = object.tag_rules.select(&:is_default) + serialized_rules = ActiveModel::ArraySerializer.new(default_rules, each_serializer: Api::Admin::TagRuleSerializer) + { tags: [], rules: serialized_rules } end def find_match(tag_groups, tags) diff --git a/app/serializers/api/admin/tag_rule_serializer.rb b/app/serializers/api/admin/tag_rule_serializer.rb index 4f5be5cee7..ca24a02638 100644 --- a/app/serializers/api/admin/tag_rule_serializer.rb +++ b/app/serializers/api/admin/tag_rule_serializer.rb @@ -10,7 +10,7 @@ end module Api::Admin::TagRule class BaseSerializer < ActiveModel::Serializer - attributes :id, :enterprise_id, :type, :preferred_customer_tags + attributes :id, :enterprise_id, :type, :is_default, :preferred_customer_tags end class DiscountOrderSerializer < BaseSerializer diff --git a/app/views/admin/enterprises/form/_tag_rules.html.haml b/app/views/admin/enterprises/form/_tag_rules.html.haml index 76d971f963..5752b25b06 100644 --- a/app/views/admin/enterprises/form/_tag_rules.html.haml +++ b/app/views/admin/enterprises/form/_tag_rules.html.haml @@ -3,6 +3,8 @@ .eleven.columns.alpha.omega .no_tags{ ng: { show: "tagGroups.length == 0" } } No tags apply to this enterprise yet + = render 'admin/enterprises/form/tag_rules/default_rules' + -# = render 'customer_tags' .customer_tag{ ng: { repeat: "tagGroup in tagGroups" } } .header %table @@ -22,6 +24,6 @@ No rules apply to this tag yet .tag_rule{ ng: { repeat: "rule in tagGroup.rules" } } .add_rule.text-center - %input.button.icon-plus{ type: 'button', value: "+ Add A New Rule", "new-tag-rule-dialog" => true } + %input.button.icon-plus{ type: 'button', value: "+ Add A New Rule", "add-new-rule-to" => "addNewRuleTo", "tag-group" => "tagGroup", "new-tag-rule-dialog" => true } .add_tag %input.button.red.icon-plus{ type: 'button', value: "+ Add A New Tag", ng: { click: 'addNewTag()' } } diff --git a/app/views/admin/enterprises/form/tag_rules/_default_rules.html.haml b/app/views/admin/enterprises/form/tag_rules/_default_rules.html.haml new file mode 100644 index 0000000000..c8fdcc2a8a --- /dev/null +++ b/app/views/admin/enterprises/form/tag_rules/_default_rules.html.haml @@ -0,0 +1,14 @@ +.default_rules + .header + %table + %colgroup + %col{width: '100%'} + %tr + %td + %h5 + By Default: + .no_rules{ ng: { show: "defaultTagGroup.rules.length == 0" } } + No default rules apply yet + .tag_rule{ ng: { repeat: "rule in defaultTagGroup.rules" } } + .add_rule.text-center + %input.button.icon-plus{ type: 'button', value: "+ Add A New Default Rule", "add-new-rule-to" => "addNewRuleTo", "tag-group" => "defaultTagGroup", "new-tag-rule-dialog" => true } diff --git a/db/migrate/20160520065217_add_is_default_to_tag_rule.rb b/db/migrate/20160520065217_add_is_default_to_tag_rule.rb new file mode 100644 index 0000000000..209520b693 --- /dev/null +++ b/db/migrate/20160520065217_add_is_default_to_tag_rule.rb @@ -0,0 +1,5 @@ +class AddIsDefaultToTagRule < ActiveRecord::Migration + def change + add_column :tag_rules, :is_default, :boolean, default: false, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 60b72adeea..f68f6c6a0a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20160401043927) do +ActiveRecord::Schema.define(:version => 20160520065217) do create_table "account_invoices", :force => true do |t| t.integer "user_id", :null => false @@ -1159,10 +1159,11 @@ ActiveRecord::Schema.define(:version => 20160401043927) do end create_table "tag_rules", :force => true do |t| - t.integer "enterprise_id", :null => false - t.string "type", :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.integer "enterprise_id", :null => false + t.string "type", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.boolean "is_default", :default => false, :null => false end create_table "taggings", :force => true do |t| diff --git a/spec/features/admin/tag_rules_spec.rb b/spec/features/admin/tag_rules_spec.rb index 1e31039523..45b28f603c 100644 --- a/spec/features/admin/tag_rules_spec.rb +++ b/spec/features/admin/tag_rules_spec.rb @@ -26,7 +26,7 @@ feature 'Tag Rules', js: true do click_button '+ Add A New Rule' select2_select 'Show or Hide shipping methods at checkout', from: 'rule_type_selector' click_button "Add Rule" - within("#tr_0") do + within(".customer_tag #tr_0") do find(:css, "tags-input .tags input").set "volunteers-only\n" select2_select "NOT VISIBLE", from: "enterprise_tag_rules_attributes_0_preferred_matched_shipping_methods_visibility" end @@ -35,7 +35,7 @@ feature 'Tag Rules', js: true do click_button '+ Add A New Rule' select2_select 'Show or Hide variants in my shop', from: 'rule_type_selector' click_button "Add Rule" - within("#tr_1") do + within(".customer_tag #tr_1") do find(:css, "tags-input .tags input").set "volunteers-only1\n" select2_select "VISIBLE", from: "enterprise_tag_rules_attributes_1_preferred_matched_variants_visibility" end @@ -44,20 +44,29 @@ feature 'Tag Rules', js: true do click_button '+ Add A New Rule' select2_select 'Show or Hide payment methods at checkout', from: 'rule_type_selector' click_button "Add Rule" - within("#tr_2") do + within(".customer_tag #tr_2") do find(:css, "tags-input .tags input").set "volunteers-only2\n" select2_select "VISIBLE", from: "enterprise_tag_rules_attributes_2_preferred_matched_payment_methods_visibility" end - # New FilterPaymentMethods Rule + # New FilterOrderCycles Rule click_button '+ Add A New Rule' select2_select 'Show or Hide order cycles in my shopfront', from: 'rule_type_selector' click_button "Add Rule" - within("#tr_3") do + within(".customer_tag #tr_3") do find(:css, "tags-input .tags input").set "volunteers-only3\n" select2_select "NOT VISIBLE", from: "enterprise_tag_rules_attributes_3_preferred_matched_order_cycles_visibility" end + # New DEFAULT FilterOrderCycles Rule + click_button '+ Add A New Default Rule' + select2_select 'Show or Hide order cycles in my shopfront', from: 'rule_type_selector' + click_button "Add Rule" + within(".default_rules #tr_0") do + find(:css, "tags-input .tags input").set "wholesale\n" + select2_select "NOT VISIBLE", from: "enterprise_tag_rules_attributes_0_preferred_matched_order_cycles_visibility" + end + # New DiscountOrder Rule # click_button '+ Add A New Rule' # select2_select 'Apply a discount to orders', from: 'rule_type_selector' @@ -85,18 +94,24 @@ feature 'Tag Rules', js: true do expect(tag_rule.preferred_payment_method_tags).to eq "volunteers-only2" expect(tag_rule.preferred_matched_payment_methods_visibility).to eq "visible" - tag_rule = TagRule::FilterOrderCycles.last + tag_rule = TagRule::FilterOrderCycles.all.reject(&:is_default).last expect(tag_rule.preferred_customer_tags).to eq "volunteer" expect(tag_rule.preferred_exchange_tags).to eq "volunteers-only3" expect(tag_rule.preferred_matched_order_cycles_visibility).to eq "hidden" + + tag_rule = TagRule::FilterOrderCycles.all.select(&:is_default).last + expect(tag_rule.preferred_customer_tags).to eq "" + expect(tag_rule.preferred_exchange_tags).to eq "wholesale" + expect(tag_rule.preferred_matched_order_cycles_visibility).to eq "hidden" end end context "updating" do - let!(:fsm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise, preferred_matched_shipping_methods_visibility: "hidden", preferred_customer_tags: "local", preferred_shipping_method_tags: "local" ) } + let!(:default_fsm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise, preferred_matched_shipping_methods_visibility: "visible", is_default: true, preferred_shipping_method_tags: "local" ) } let!(:fp_tag_rule) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_matched_variants_visibility: "visible", preferred_customer_tags: "member", preferred_variant_tags: "member" ) } let!(:fpm_tag_rule) { create(:filter_payment_methods_tag_rule, enterprise: enterprise, preferred_matched_payment_methods_visibility: "hidden", preferred_customer_tags: "trusted", preferred_payment_method_tags: "trusted" ) } let!(:foc_tag_rule) { create(:filter_order_cycles_tag_rule, enterprise: enterprise, preferred_matched_order_cycles_visibility: "visible", preferred_customer_tags: "wholesale", preferred_exchange_tags: "wholesale" ) } + let!(:fsm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise, preferred_matched_shipping_methods_visibility: "hidden", preferred_customer_tags: "local", preferred_shipping_method_tags: "local" ) } # let!(:do_tag_rule) { create(:tag_rule, enterprise: enterprise, preferred_customer_tags: "member" ) } before do @@ -115,16 +130,16 @@ feature 'Tag Rules', js: true do expect(page).to have_selector '.customer_tag .header tags-input .tag-list ti-tag-item', text: "trusted", count: 1 all(:css, ".customer_tag .header tags-input .tags input").each { |node| node.set "volunteer\n" } - # FilterShippingMethods rule - within "#tr_0" do + # DEFAULT FilterShippingMethods rule + within ".default_rules #tr_0" do expect(first('tags-input .tag-list ti-tag-item')).to have_content "local" find(:css, "tags-input .tags input").set "volunteers-only\n" - expect(page).to have_select2 "enterprise_tag_rules_attributes_0_preferred_matched_shipping_methods_visibility", selected: 'NOT VISIBLE' - select2_select 'VISIBLE', from: "enterprise_tag_rules_attributes_0_preferred_matched_shipping_methods_visibility" + expect(page).to have_select2 "enterprise_tag_rules_attributes_0_preferred_matched_shipping_methods_visibility", selected: 'VISIBLE' + select2_select 'NOT VISIBLE', from: "enterprise_tag_rules_attributes_0_preferred_matched_shipping_methods_visibility" end # FilterProducts rule - within "#tr_1" do + within ".customer_tag #tr_1" do expect(first('tags-input .tag-list ti-tag-item')).to have_content "member" find(:css, "tags-input .tags input").set "volunteers-only1\n" expect(page).to have_select2 "enterprise_tag_rules_attributes_1_preferred_matched_variants_visibility", selected: 'VISIBLE' @@ -132,7 +147,7 @@ feature 'Tag Rules', js: true do end # FilterPaymentMethods rule - within "#tr_2" do + within ".customer_tag #tr_2" do expect(first('tags-input .tag-list ti-tag-item')).to have_content "trusted" find(:css, "tags-input .tags input").set "volunteers-only2\n" expect(page).to have_select2 "enterprise_tag_rules_attributes_2_preferred_matched_payment_methods_visibility", selected: 'NOT VISIBLE' @@ -140,13 +155,21 @@ feature 'Tag Rules', js: true do end # FilterOrderCycles rule - within "#tr_3" do + within ".customer_tag #tr_3" do expect(first('tags-input .tag-list ti-tag-item')).to have_content "wholesale" find(:css, "tags-input .tags input").set "volunteers-only3\n" expect(page).to have_select2 "enterprise_tag_rules_attributes_3_preferred_matched_order_cycles_visibility", selected: 'VISIBLE' select2_select 'NOT VISIBLE', from: "enterprise_tag_rules_attributes_3_preferred_matched_order_cycles_visibility" end + # FilterShippingMethods rule + within ".customer_tag #tr_4" do + expect(first('tags-input .tag-list ti-tag-item')).to have_content "local" + find(:css, "tags-input .tags input").set "volunteers-only4\n" + expect(page).to have_select2 "enterprise_tag_rules_attributes_4_preferred_matched_shipping_methods_visibility", selected: 'NOT VISIBLE' + select2_select 'VISIBLE', from: "enterprise_tag_rules_attributes_4_preferred_matched_shipping_methods_visibility" + end + # # DiscountOrder rule # within "#tr_2" do # expect(page).to have_field "enterprise_tag_rules_attributes_2_calculator_attributes_preferred_flat_percent", with: '0' @@ -155,9 +178,14 @@ feature 'Tag Rules', js: true do click_button 'Update' + # FilterShippingMethods rule + expect(default_fsm_tag_rule.preferred_customer_tags).to eq "" + expect(default_fsm_tag_rule.preferred_shipping_method_tags).to eq "local,volunteers-only" + expect(default_fsm_tag_rule.preferred_matched_shipping_methods_visibility).to eq "hidden" + # FilterShippingMethods rule expect(fsm_tag_rule.preferred_customer_tags).to eq "local,volunteer" - expect(fsm_tag_rule.preferred_shipping_method_tags).to eq "local,volunteers-only" + expect(fsm_tag_rule.preferred_shipping_method_tags).to eq "local,volunteers-only4" expect(fsm_tag_rule.preferred_matched_shipping_methods_visibility).to eq "visible" # FilterProducts rule diff --git a/spec/javascripts/unit/admin/tag_rules/controllers/tag_rules_controller_spec.js.coffee b/spec/javascripts/unit/admin/tag_rules/controllers/tag_rules_controller_spec.js.coffee index 4a0c304ee8..2526016d3d 100644 --- a/spec/javascripts/unit/admin/tag_rules/controllers/tag_rules_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/tag_rules/controllers/tag_rules_controller_spec.js.coffee @@ -7,6 +7,7 @@ describe "TagRulesCtrl", -> module('admin.tagRules') enterprise = id: 45 + default_tag_group: { tags: "", rules: [{ id: 7, preferred_customer_tags: "trusted" }] } tag_groups: [ { tags: "member", rules: [{ id: 1, preferred_customer_tags: "member" }, { id: 2, preferred_customer_tags: "member" }] }, { tags: "volunteer", rules: [{ id: 3, preferred_customer_tags: "local" }] } @@ -18,8 +19,8 @@ describe "TagRulesCtrl", -> describe "tagGroup start indices", -> it "updates on initialization", -> - expect(scope.tagGroups[0].startIndex).toEqual 0 - expect(scope.tagGroups[1].startIndex).toEqual 2 + expect(scope.tagGroups[0].startIndex).toEqual 1 + expect(scope.tagGroups[1].startIndex).toEqual 3 describe "adding a new tag group", -> beforeEach -> @@ -30,8 +31,8 @@ describe "TagRulesCtrl", -> expect(scope.tagGroups[0].rules[2].type).toEqual "TagRule::DiscountOrder" it "updates tagGroup start indices", -> - expect(scope.tagGroups[0].startIndex).toEqual 0 - expect(scope.tagGroups[1].startIndex).toEqual 3 + expect(scope.tagGroups[0].startIndex).toEqual 1 + expect(scope.tagGroups[1].startIndex).toEqual 4 describe "deleting a tag group", -> describe "where the rule is not in the rule list for the tagGroup", -> @@ -58,8 +59,8 @@ describe "TagRulesCtrl", -> expect(scope.tagGroups[0].rules.indexOf(rule)).toEqual -1 it "updates tagGroup start indices", -> - expect(scope.tagGroups[0].startIndex).toEqual 0 - expect(scope.tagGroups[1].startIndex).toEqual 1 + expect(scope.tagGroups[0].startIndex).toEqual 1 + expect(scope.tagGroups[1].startIndex).toEqual 2 describe "without an id", -> rule = null @@ -75,5 +76,5 @@ describe "TagRulesCtrl", -> expect(scope.tagGroups[0].rules.indexOf(rule)).toEqual -1 it "updates tagGroup start indices", -> - expect(scope.tagGroups[0].startIndex).toEqual 0 - expect(scope.tagGroups[1].startIndex).toEqual 1 + expect(scope.tagGroups[0].startIndex).toEqual 1 + expect(scope.tagGroups[1].startIndex).toEqual 2 From 607b674c575498800506b1dccdb7eca7bd646b86 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 26 May 2016 13:46:54 +1000 Subject: [PATCH 095/110] Refactoring tag rule logic, placing in TagRuleApplicator lib class --- app/controllers/base_controller.rb | 11 +- app/controllers/shop_controller.rb | 24 +- app/helpers/enterprises_helper.rb | 12 +- app/models/enterprise.rb | 13 - app/models/spree/order_decorator.rb | 11 +- app/models/tag_rule.rb | 37 --- app/models/tag_rule/filter_order_cycles.rb | 26 +- app/models/tag_rule/filter_payment_methods.rb | 20 +- app/models/tag_rule/filter_products.rb | 25 +- .../tag_rule/filter_shipping_methods.rb | 20 +- lib/open_food_network/tag_rule_applicator.rb | 61 +++++ .../enterprises_controller_spec.rb | 7 +- spec/controllers/shop_controller_spec.rb | 118 ++++++--- .../consumer/shopping/checkout_spec.rb | 7 +- spec/helpers/enterprises_helper_spec.rb | 24 +- spec/helpers/injection_helper_spec.rb | 1 - .../tag_rule_applicator_spec.rb | 250 ++++++++++++++++++ spec/models/enterprise_spec.rb | 50 ---- spec/models/spree/order_spec.rb | 56 ++-- spec/models/tag_rule/discount_order_spec.rb | 6 +- .../tag_rule/filter_order_cycles_spec.rb | 51 ---- .../tag_rule/filter_payment_methods_spec.rb | 50 ---- spec/models/tag_rule/filter_products_spec.rb | 50 ---- .../tag_rule/filter_shipping_methods_spec.rb | 50 ---- spec/models/tag_rule_spec.rb | 192 -------------- 25 files changed, 475 insertions(+), 697 deletions(-) create mode 100644 lib/open_food_network/tag_rule_applicator.rb create mode 100644 spec/lib/open_food_network/tag_rule_applicator_spec.rb diff --git a/app/controllers/base_controller.rb b/app/controllers/base_controller.rb index 124dd7f38a..7bcb8bf3c9 100644 --- a/app/controllers/base_controller.rb +++ b/app/controllers/base_controller.rb @@ -1,3 +1,5 @@ +require 'open_food_network/tag_rule_applicator' + class BaseController < ApplicationController include Spree::Core::ControllerHelpers include Spree::Core::ControllerHelpers::RespondWith @@ -19,12 +21,9 @@ class BaseController < ApplicationController @order_cycles = OrderCycle.with_distributor(@distributor).active .order(@distributor.preferred_shopfront_order_cycle_order) - @distributor.apply_tag_rules( - type: "FilterOrderCycles", - subject: @order_cycles, - customer_tags: current_order.andand.customer.andand.tag_list || [], - shop: @distributor - ) + customer_tags = current_order.andand.customer.andand.tag_list + applicator = OpenFoodNetwork::TagRuleApplicator.new(@distributor, "FilterOrderCycles", customer_tags) + applicator.filter!(@order_cycles) # And default to the only order cycle if there's only the one if @order_cycles.count == 1 diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index b0324f14e0..e244a2a189 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -15,7 +15,7 @@ class ShopController < BaseController # If we add any more filtering logic, we should probably # move it all to a lib class like 'CachedProductsFilterer' - products_json = filtered_json(renderer.products_json) + products_json = filter(renderer.products_json) render json: products_json @@ -40,22 +40,22 @@ class ShopController < BaseController private def filtered_json(products_json) - tag_rules = relevant_tag_rules - return apply_tag_rules(tag_rules, products_json) if tag_rules.any? - products_json + if applicator.send(:rules).any? + filter(products_json) + else + products_json + end end - def apply_tag_rules(tag_rules, products_json) + def filter(products_json) products_hash = JSON.parse(products_json) - current_distributor.apply_tag_rules( - rules: tag_rules, - subject: products_hash, - customer_tags: current_order.andand.customer.andand.tag_list || [] - ) + applicator.filter!(products_hash) JSON.unparse(products_hash) end - def relevant_tag_rules - TagRule.for(current_distributor).of_type("FilterProducts") + def applicator + return @applicator unless @applicator.nil? + customer_tags = current_order.andand.customer.andand.tag_list + @applicator = OpenFoodNetwork::TagRuleApplicator.new(current_distributor, "FilterProducts", customer_tags) end end diff --git a/app/helpers/enterprises_helper.rb b/app/helpers/enterprises_helper.rb index b4375b2577..886546585c 100644 --- a/app/helpers/enterprises_helper.rb +++ b/app/helpers/enterprises_helper.rb @@ -5,13 +5,11 @@ module EnterprisesHelper def available_shipping_methods shipping_methods = current_distributor.shipping_methods - if current_distributor.present? - current_distributor.apply_tag_rules( - type: "FilterShippingMethods", - subject: shipping_methods, - customer_tags: current_order.andand.customer.andand.tag_list - ) - end + + customer_tags = current_order.andand.customer.andand.tag_list + applicator = OpenFoodNetwork::TagRuleApplicator.new(current_distributor, "FilterShippingMethods", customer_tags) + applicator.filter!(shipping_methods) + shipping_methods.uniq end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index f7e5a4b6f3..554529d7f2 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -344,13 +344,6 @@ class Enterprise < ActiveRecord::Base abn.present? end - def apply_tag_rules(context) - tag_rules_for(context).each do |rule| - rule.context = context - rule.apply - end - end - protected def devise_mailer @@ -459,10 +452,4 @@ class Enterprise < ActiveRecord::Base def initialize_permalink self.permalink = Enterprise.find_available_permalink(name) end - - def tag_rules_for(context) - rules = context[:rules] || tag_rules - return rules unless context[:type] - rules.merge(TagRule.of_type(context[:type])) - end end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 411e7e71a2..9125d33991 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -1,6 +1,7 @@ require 'open_food_network/enterprise_fee_calculator' require 'open_food_network/distribution_change_validator' require 'open_food_network/feature_toggle' +require 'open_food_network/tag_rule_applicator' ActiveSupport::Notifications.subscribe('spree.order.contents_changed') do |name, start, finish, id, payload| payload[:order].reload.update_distribution_charge! @@ -178,10 +179,6 @@ Spree::Order.class_eval do if order_cycle OpenFoodNetwork::EnterpriseFeeCalculator.new.create_order_adjustments_for self end - - if distributor.present? && customer.present? - distributor.apply_tag_rules(type: "DiscountOrder", subject: self, customer_tags: customer.andand.tag_list) - end end end @@ -214,7 +211,11 @@ Spree::Order.class_eval do def available_payment_methods return [] unless distributor.present? payment_methods = distributor.payment_methods.available(:front_end).all - distributor.apply_tag_rules( type: "FilterPaymentMethods", subject: payment_methods, customer_tags: customer.andand.tag_list) + + customer_tags = customer.andand.tag_list + applicator = OpenFoodNetwork::TagRuleApplicator.new(distributor, "FilterPaymentMethods", customer_tags) + applicator.filter!(payment_methods) + payment_methods end diff --git a/app/models/tag_rule.rb b/app/models/tag_rule.rb index 1c578d2848..ee1ef105a3 100644 --- a/app/models/tag_rule.rb +++ b/app/models/tag_rule.rb @@ -1,6 +1,4 @@ class TagRule < ActiveRecord::Base - attr_accessor :subject, :context - belongs_to :enterprise preference :customer_tags, :string, default: "" @@ -10,25 +8,6 @@ class TagRule < ActiveRecord::Base attr_accessible :enterprise, :enterprise_id, :is_default, :preferred_customer_tags scope :for, ->(enterprise) { where(enterprise_id: enterprise) } - scope :of_type, ->(type) { where(type: "TagRule::#{type}") } - - def context=(context) - raise "Context for tag rule cannot be nil" if context.nil? - raise "Subject for tag rule cannot be nil" if context[:subject].nil? - - @context = context - @subject = context[:subject] - end - - def apply - if relevant? - if customer_tags_match? - apply! - else - apply_default! if respond_to?(:apply_default!,true) - end - end - end def self.mapping_for(enterprises) self.for(enterprises).inject({}) do |mapping, rule| @@ -42,20 +21,4 @@ class TagRule < ActiveRecord::Base mapping end end - - private - - def relevant? - return false unless subject_class_matches? - if respond_to?(:additional_requirements_met?, true) - return false unless additional_requirements_met? - end - true - end - - def customer_tags_match? - context_customer_tags = context[:customer_tags] || [] - preferred_tags = preferred_customer_tags.split(",") - (context_customer_tags & preferred_tags).any? - end end diff --git a/app/models/tag_rule/filter_order_cycles.rb b/app/models/tag_rule/filter_order_cycles.rb index 2ed99320a5..de626ac70a 100644 --- a/app/models/tag_rule/filter_order_cycles.rb +++ b/app/models/tag_rule/filter_order_cycles.rb @@ -4,33 +4,19 @@ class TagRule::FilterOrderCycles < TagRule attr_accessible :preferred_matched_order_cycles_visibility, :preferred_exchange_tags - private - - # Warning: this should only EVER be called via TagRule#apply - def apply! - unless preferred_matched_order_cycles_visibility == "visible" - subject.reject!{ |oc| tags_match?(oc) } - end - end - - def apply_default! - if preferred_matched_order_cycles_visibility == "visible" - subject.reject!{ |oc| tags_match?(oc) } - end - end - def tags_match?(order_cycle) exchange_tags = exchange_for(order_cycle).andand.tag_list || [] preferred_tags = preferred_exchange_tags.split(",") ( exchange_tags & preferred_tags ).any? end - def exchange_for(order_cycle) - order_cycle.exchanges.outgoing.to_enterprise(context[:shop]).first + def reject_matched? + preferred_matched_order_cycles_visibility != "visible" end - def subject_class_matches? - subject.class == ActiveRecord::Relation && - subject.klass == OrderCycle + private + + def exchange_for(order_cycle) + order_cycle.exchanges.outgoing.to_enterprise(enterprise).first end end diff --git a/app/models/tag_rule/filter_payment_methods.rb b/app/models/tag_rule/filter_payment_methods.rb index da29426298..04a10889e0 100644 --- a/app/models/tag_rule/filter_payment_methods.rb +++ b/app/models/tag_rule/filter_payment_methods.rb @@ -4,29 +4,13 @@ class TagRule::FilterPaymentMethods < TagRule attr_accessible :preferred_matched_payment_methods_visibility, :preferred_payment_method_tags - private - - # Warning: this should only EVER be called via TagRule#apply - def apply! - unless preferred_matched_payment_methods_visibility == "visible" - subject.reject!{ |pm| tags_match?(pm) } - end - end - - def apply_default! - if preferred_matched_payment_methods_visibility == "visible" - subject.reject!{ |pm| tags_match?(pm) } - end - end - def tags_match?(payment_method) payment_method_tags = payment_method.andand.tag_list || [] preferred_tags = preferred_payment_method_tags.split(",") ( payment_method_tags & preferred_tags ).any? end - def subject_class_matches? - subject.class == Array && - subject.all? { |i| i.class < Spree::PaymentMethod } + def reject_matched? + preferred_matched_payment_methods_visibility != "visible" end end diff --git a/app/models/tag_rule/filter_products.rb b/app/models/tag_rule/filter_products.rb index 9df35e00a0..e1d3865036 100644 --- a/app/models/tag_rule/filter_products.rb +++ b/app/models/tag_rule/filter_products.rb @@ -5,25 +5,8 @@ class TagRule attr_accessible :preferred_matched_variants_visibility, :preferred_variant_tags - private - - # Warning: this should only EVER be called via TagRule#apply - def apply! - unless preferred_matched_variants_visibility == "visible" - subject.reject! do |product| - product["variants"].reject! { |v| tags_match?(v) } - product["variants"].empty? - end - end - end - - def apply_default! - if preferred_matched_variants_visibility == "visible" - subject.reject! do |product| - product["variants"].reject! { |v| tags_match?(v) } - product["variants"].empty? - end - end + def self.tagged_children_for(product) + product["variants"] end def tags_match?(variant) @@ -32,8 +15,8 @@ class TagRule (variant_tags & preferred_tags).any? end - def subject_class_matches? - subject.class == Array + def reject_matched? + preferred_matched_variants_visibility != "visible" end end end diff --git a/app/models/tag_rule/filter_shipping_methods.rb b/app/models/tag_rule/filter_shipping_methods.rb index 74438e560e..2091a14a63 100644 --- a/app/models/tag_rule/filter_shipping_methods.rb +++ b/app/models/tag_rule/filter_shipping_methods.rb @@ -4,19 +4,8 @@ class TagRule::FilterShippingMethods < TagRule attr_accessible :preferred_matched_shipping_methods_visibility, :preferred_shipping_method_tags - private - - # Warning: this should only EVER be called via TagRule#apply - def apply! - unless preferred_matched_shipping_methods_visibility == "visible" - subject.reject!{ |sm| tags_match?(sm) } - end - end - - def apply_default! - if preferred_matched_shipping_methods_visibility == "visible" - subject.reject!{ |sm| tags_match?(sm) } - end + def reject_matched? + preferred_matched_shipping_methods_visibility != "visible" end def tags_match?(shipping_method) @@ -24,9 +13,4 @@ class TagRule::FilterShippingMethods < TagRule preferred_tags = preferred_shipping_method_tags.split(",") ( shipping_method_tags & preferred_tags ).any? end - - def subject_class_matches? - subject.class == Array && - subject.all? { |i| i.class == Spree::ShippingMethod } - end end diff --git a/lib/open_food_network/tag_rule_applicator.rb b/lib/open_food_network/tag_rule_applicator.rb new file mode 100644 index 0000000000..181624a210 --- /dev/null +++ b/lib/open_food_network/tag_rule_applicator.rb @@ -0,0 +1,61 @@ +module OpenFoodNetwork + class TagRuleApplicator + attr_reader :enterprise, :rule_class, :customer_tags + + def initialize(enterprise, rule_type, customer_tags=[]) + raise "Enterprise cannot be nil" if enterprise.nil? + raise "Rule Type cannot be nil" if rule_type.nil? + + @enterprise = enterprise + @rule_class = "TagRule::#{rule_type}".constantize + @customer_tags = customer_tags || [] + end + + def filter!(subject) + return unless subject.respond_to?(:any?) && subject.any? + subject.reject! do |element| + if rule_class.respond_to?(:tagged_children_for) + children = rule_class.tagged_children_for(element) + children.reject! { |child| reject?(child) } + children.empty? + else + reject?(element) + end + end + end + + private + + def reject?(element) + customer_rules.each do |rule| + return rule.reject_matched? if rule.tags_match?(element) + end + + default_rules.each do |rule| + return rule.reject_matched? if rule.tags_match?(element) + end + + false + end + + def rules + return @rules unless @rules.nil? + @rules = rule_class.for(enterprise) + end + + def customer_rules + return @customer_matched_rules unless @customer_matched_rules.nil? + @customer_matched_rules = rules.select{ |rule| customer_tags_match?(rule) } + end + + def default_rules + return @default_rules unless @default_rules.nil? + @default_rules = rules.select(&:is_default?) + end + + def customer_tags_match?(rule) + preferred_tags = rule.preferred_customer_tags.split(",") + (customer_tags & preferred_tags).any? + end + end +end diff --git a/spec/controllers/enterprises_controller_spec.rb b/spec/controllers/enterprises_controller_spec.rb index 1ed68958f9..24837ceed9 100644 --- a/spec/controllers/enterprises_controller_spec.rb +++ b/spec/controllers/enterprises_controller_spec.rb @@ -40,12 +40,17 @@ describe EnterprisesController do order.update_attribute(:customer_id, customer.id) end - it "shows order cycles allowed by the rule" do + it "shows order cycles allowed by the rules" do create(:filter_order_cycles_tag_rule, enterprise: distributor, preferred_customer_tags: "wholesale", preferred_exchange_tags: "wholesale", preferred_matched_order_cycles_visibility: 'visible') + create(:filter_order_cycles_tag_rule, + enterprise: distributor, + is_default: true, + preferred_exchange_tags: "wholesale", + preferred_matched_order_cycles_visibility: 'hidden') spree_get :shop, {id: distributor} expect(assigns(:order_cycles)).to include order_cycle1, order_cycle2, order_cycle3 diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index 669e17db5f..777050847d 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -96,21 +96,20 @@ describe ShopController do describe "determining rule relevance" do let(:products_json) { double(:products_json) } + let(:applicator) { double(:applicator) } before do - # allow(controller).to receive(:products_json) { products_json } - allow(controller).to receive(:relevant_tag_rules) { relevant_tag_rules } - allow(controller).to receive(:apply_tag_rules) { "some filtered json" } + allow(applicator).to receive(:rules) { tag_rules } + allow(controller).to receive(:applicator) { applicator } + allow(controller).to receive(:filter) { "some filtered json" } end context "when no relevant rules exist" do - let(:relevant_tag_rules) { [] } - - before { allow(controller).to receive(:relevant_rules) { relevant_rules } } + let(:tag_rules) { [] } it "does not attempt to apply any rules" do controller.send(:filtered_json, products_json) - expect(expect(controller).to_not have_received(:apply_tag_rules)) + expect(expect(controller).to_not have_received(:filter)) end it "returns products as JSON" do @@ -120,11 +119,11 @@ describe ShopController do context "when relevant rules exist" do let(:tag_rule) { create(:filter_products_tag_rule, preferred_customer_tags: "tag1", preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "hidden" ) } - let(:relevant_tag_rules) { [tag_rule] } + let(:tag_rules) { [tag_rule] } it "attempts to apply any rules" do controller.send(:filtered_json, products_json) - expect(controller).to have_received(:apply_tag_rules).with(relevant_tag_rules, products_json) + expect(controller).to have_received(:filter).with(products_json) end it "returns filtered JSON" do @@ -133,49 +132,86 @@ describe ShopController do end end - describe "applying tag rules" do - let(:product1) { { id: 1, name: 'product 1', "variants" => [{ id: 4, "tag_list" => ["tag1"] }] } } - let(:product2) { { id: 2, name: 'product 2', "variants" => [{ id: 5, "tag_list" => ["tag1"] }, {id: 9, "tag_list" => ["tag2"]}] } } - let(:product3) { { id: 3, name: 'product 3', "variants" => [{ id: 6, "tag_list" => ["tag3"] }] } } + context "when FilterProducts tag rules are in effect" do + let!(:tagged_customer) { create(:customer, enterprise: distributor, tag_list: "member") } + let!(:untagged_customer) { create(:customer, enterprise: distributor, tag_list: "") } + let!(:order) { create(:order, distributor: distributor) } + let!(:tag_rule) { create(:filter_products_tag_rule, + enterprise: distributor, + preferred_customer_tags: "member", + preferred_variant_tags: "members-only") } + let!(:default_tag_rule) { create(:filter_products_tag_rule, + enterprise: distributor, + is_default: true, + preferred_variant_tags: "members-only") } + let(:product1) { { "id" => 1, "name" => 'product 1', "variants" => [{ "id" => 4, "tag_list" => ["members-only"] }] } } + let(:product2) { { "id" => 2, "name" => 'product 2', "variants" => [{ "id" => 5, "tag_list" => ["members-only"] }, {"id" => 9, "tag_list" => ["something"]}] } } + let(:product3) { { "id" => 3, "name" => 'product 3', "variants" => [{ "id" => 6, "tag_list" => ["something-else"] }] } } + let(:product2_without_v5) { { "id" => 2, "name" => 'product 2', "variants" => [{"id" => 9, "tag_list" => ["something"]}] } } let!(:products_array) { [product1, product2, product3] } let!(:products_json) { JSON.unparse( products_array ) } - let(:tag_rule) { create(:filter_products_tag_rule, preferred_customer_tags: "tag1", preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "hidden" ) } - let(:relevant_tag_rules) { [tag_rule] } before do allow(controller).to receive(:current_order) { order } - allow(tag_rule).to receive(:context=) - allow(tag_rule).to receive(:apply) - allow(distributor).to receive(:apply_tag_rules).and_call_original end - context "when a current order with a customer does not exist" do - let(:order) { double(:order, customer: nil) } + context "with a preferred visiblity of 'visible', default visibility of 'hidden'" do + before { tag_rule.update_attribute(:preferred_matched_variants_visibility, 'visible') } + before { default_tag_rule.update_attribute(:preferred_matched_variants_visibility, 'hidden') } - it "sets the context customer_tags as an empty array" do - controller.send(:apply_tag_rules, relevant_tag_rules, products_json) - expect(distributor).to have_received(:apply_tag_rules).with(rules: relevant_tag_rules, subject: JSON.parse(products_json), :customer_tags=>[]) - end - end + let(:filtered_products) { JSON.parse(controller.send(:filter, products_json)) } - context "when a customer does exist" do - let(:order) { double(:order, customer: double(:customer, tag_list: ["tag1", "tag2"])) } - - it "sets the context customer_tags" do - controller.send(:apply_tag_rules, relevant_tag_rules, products_json) - expect(distributor).to have_received(:apply_tag_rules).with(rules: relevant_tag_rules, subject: JSON.parse(products_json), :customer_tags=>["tag1", "tag2"]) - end - - context "applies the rule" do - before do - allow(tag_rule).to receive(:context=).and_call_original - allow(tag_rule).to receive(:apply).and_call_original + context "when the customer is nil" do + it "applies default action (hide)" do + expect(filtered_products).to include product2_without_v5, product3 + expect(filtered_products).to_not include product1, product2 end + end - it "applies the rule" do - result = controller.send(:apply_tag_rules, relevant_tag_rules, products_json) - expect(tag_rule).to have_received(:apply) - expect(result).to eq JSON.unparse([{ id: 2, name: 'product 2', variants: [{id: 9, tag_list: ["tag2"]}] }, product3]) + context "when the customer's tags match" do + before { order.update_attribute(:customer_id, tagged_customer.id) } + + it "applies the action (show)" do + expect(filtered_products).to include product1, product2, product3 + end + end + + context "when the customer's tags don't match" do + before { order.update_attribute(:customer_id, untagged_customer.id) } + + it "applies the default action (hide)" do + expect(filtered_products).to include product2_without_v5, product3 + expect(filtered_products).to_not include product1, product2 + end + end + end + + context "with a preferred visiblity of 'hidden', default visibility of 'visible'" do + before { tag_rule.update_attribute(:preferred_matched_variants_visibility, 'hidden') } + before { default_tag_rule.update_attribute(:preferred_matched_variants_visibility, 'visible') } + + let(:filtered_products) { JSON.parse(controller.send(:filter, products_json)) } + + context "when the customer is nil" do + it "applies default action (show)" do + expect(filtered_products).to include product1, product2, product3 + end + end + + context "when the customer's tags match" do + before { order.update_attribute(:customer_id, tagged_customer.id) } + + it "applies the action (hide)" do + expect(filtered_products).to include product2_without_v5, product3 + expect(filtered_products).to_not include product1, product2 + end + end + + context "when the customer's tags don't match" do + before { order.update_attribute(:customer_id, untagged_customer.id) } + + it "applies the default action (show)" do + expect(filtered_products).to include product1, product2, product3 end end end diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 9e9d830a50..e5e4871df0 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -118,10 +118,15 @@ feature "As a consumer I want to check out my cart", js: true do preferred_customer_tags: "local", preferred_shipping_method_tags: "local", preferred_matched_shipping_methods_visibility: 'visible') + create(:filter_shipping_methods_tag_rule, + enterprise: distributor, + is_default: true, + preferred_shipping_method_tags: "local", + preferred_matched_shipping_methods_visibility: 'hidden') visit checkout_path checkout_as_guest - # Rule in effect, disallows access to 'Local' + # Default rule in effect, disallows access to 'Local' page.should have_content "Frogs" page.should have_content "Donkeys" page.should_not have_content "Local" diff --git a/spec/helpers/enterprises_helper_spec.rb b/spec/helpers/enterprises_helper_spec.rb index 1dc32d63b3..7a9028e5d7 100644 --- a/spec/helpers/enterprises_helper_spec.rb +++ b/spec/helpers/enterprises_helper_spec.rb @@ -3,15 +3,19 @@ require 'spec_helper' describe EnterprisesHelper do describe "loading available shipping methods" do - context "when a FilterShippingMethods tag rule is in effect, with preferred visibility of 'visible'" do + context "when FilterShippingMethods tag rules are in effect" do let!(:distributor) { create(:distributor_enterprise) } - let!(:allowed_customer) { create(:customer, enterprise: distributor, tag_list: "local") } - let!(:disallowed_customer) { create(:customer, enterprise: distributor, tag_list: "") } + let!(:tagged_customer) { create(:customer, enterprise: distributor, tag_list: "local") } + let!(:untagged_customer) { create(:customer, enterprise: distributor, tag_list: "") } let!(:order) { create(:order, distributor: distributor) } let!(:tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: distributor, preferred_customer_tags: "local", preferred_shipping_method_tags: "local-delivery") } + let!(:default_tag_rule) { create(:filter_shipping_methods_tag_rule, + enterprise: distributor, + is_default: true, + preferred_shipping_method_tags: "local-delivery") } let!(:tagged_sm) { create(:shipping_method, require_ship_address: false, name: "Untagged", tag_list: "local-delivery") } let!(:untagged_sm) { create(:shipping_method, require_ship_address: false, name: "Tagged", tag_list: "") } @@ -20,8 +24,9 @@ describe EnterprisesHelper do allow(helper).to receive(:current_order) { order } end - context "with a preferred visiblity of 'visible" do + context "with a preferred visiblity of 'visible', default visibility of 'hidden'" do before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, 'visible') } + before { default_tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, 'hidden') } context "when the customer is nil" do it "applies default action (hide)" do @@ -31,7 +36,7 @@ describe EnterprisesHelper do end context "when the customer's tags match" do - before { order.update_attribute(:customer_id, allowed_customer.id) } + before { order.update_attribute(:customer_id, tagged_customer.id) } it "applies the action (show)" do expect(helper.available_shipping_methods).to include tagged_sm, untagged_sm @@ -39,7 +44,7 @@ describe EnterprisesHelper do end context "when the customer's tags don't match" do - before { order.update_attribute(:customer_id, disallowed_customer.id) } + before { order.update_attribute(:customer_id, untagged_customer.id) } it "applies the default action (hide)" do expect(helper.available_shipping_methods).to include untagged_sm @@ -48,8 +53,9 @@ describe EnterprisesHelper do end end - context "with a preferred visiblity of 'hidden" do + context "with a preferred visiblity of 'hidden', default visibility of 'visible'" do before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, 'hidden') } + before { default_tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, 'visible') } context "when the customer is nil" do it "applies default action (show)" do @@ -58,7 +64,7 @@ describe EnterprisesHelper do end context "when the customer's tags match" do - before { order.update_attribute(:customer_id, allowed_customer.id) } + before { order.update_attribute(:customer_id, tagged_customer.id) } it "applies the action (hide)" do expect(helper.available_shipping_methods).to include untagged_sm @@ -67,7 +73,7 @@ describe EnterprisesHelper do end context "when the customer's tags don't match" do - before { order.update_attribute(:customer_id, disallowed_customer.id) } + before { order.update_attribute(:customer_id, untagged_customer.id) } it "applies the default action (show)" do expect(helper.available_shipping_methods).to include tagged_sm, untagged_sm diff --git a/spec/helpers/injection_helper_spec.rb b/spec/helpers/injection_helper_spec.rb index d5ec204286..e7d775c657 100644 --- a/spec/helpers/injection_helper_spec.rb +++ b/spec/helpers/injection_helper_spec.rb @@ -30,7 +30,6 @@ describe InjectionHelper do shipping_methods = double(:shipping_methods, uniq: [sm]) current_distributor = double(:distributor, shipping_methods: shipping_methods) allow(helper).to receive(:current_distributor) { current_distributor } - allow(current_distributor).to receive(:apply_tag_rules).with({type: "FilterShippingMethods", subject: shipping_methods, customer_tags: nil}) helper.inject_available_shipping_methods.should match sm.id.to_s helper.inject_available_shipping_methods.should match sm.compute_amount(order).to_s end diff --git a/spec/lib/open_food_network/tag_rule_applicator_spec.rb b/spec/lib/open_food_network/tag_rule_applicator_spec.rb new file mode 100644 index 0000000000..0b560a3e0a --- /dev/null +++ b/spec/lib/open_food_network/tag_rule_applicator_spec.rb @@ -0,0 +1,250 @@ +require 'open_food_network/tag_rule_applicator' + +module OpenFoodNetwork + describe TagRuleApplicator do + let!(:enterprise) { create(:distributor_enterprise) } + let!(:oc_tag_rule) { create(:filter_order_cycles_tag_rule, enterprise: enterprise, preferred_customer_tags: "tag1", preferred_exchange_tags: "tag1", preferred_matched_order_cycles_visibility: "visible" )} + let!(:product_tag_rule1) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_customer_tags: "tag1", preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "visible" ) } + let!(:product_tag_rule2) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_customer_tags: "tag1", preferred_variant_tags: "tag3", preferred_matched_variants_visibility: "hidden" ) } + let!(:product_tag_rule3) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_customer_tags: "tag2", preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "visible" ) } + let!(:default_product_tag_rule) { create(:filter_products_tag_rule, enterprise: enterprise, is_default: true, preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "hidden" ) } + let!(:sm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise, preferred_customer_tags: "tag1", preferred_shipping_method_tags: "tag1", preferred_matched_shipping_methods_visibility: "visible" )} + + describe "initialisation" do + context "when enterprise is nil" do + let(:applicator) { OpenFoodNetwork::TagRuleApplicator.new(nil, "FilterProducts", ["tag1"]) } + it { expect{applicator}.to raise_error "Enterprise cannot be nil" } + end + + context "when rule_type is nil" do + let(:applicator) { OpenFoodNetwork::TagRuleApplicator.new(enterprise, nil, ["tag1"]) } + it { expect{applicator}.to raise_error "Rule Type cannot be nil" } + end + + context "when rule_type does not match an existing rule type" do + let(:applicator) { OpenFoodNetwork::TagRuleApplicator.new(enterprise, "FilterSomething", ["tag1"]) } + it { expect{applicator}.to raise_error NameError } + end + + context "when enterprise and rule_type are present" do + let(:applicator) { OpenFoodNetwork::TagRuleApplicator.new(enterprise, "FilterProducts", customer_tags) } + + context "when the customer tags are nil" do + let!(:customer_tags) { nil } + + it "sets customer tags to an empty array" do + expect(applicator.customer_tags).to eq [] + end + + it "does not match rules without customer tags" do + rule = double(:rule, preferred_customer_tags: "") + expect(applicator.send(:customer_tags_match?, rule)).to be false + end + end + + context "when customer tags are empty" do + let!(:customer_tags) { [] } + + it "sets customer tags to an empty array" do + expect(applicator.customer_tags).to eq [] + end + + it "does not match rules without customer tags" do + rule = double(:rule, preferred_customer_tags: "") + expect(applicator.send(:customer_tags_match?, rule)).to be false + end + end + + context "when customer_tags are present" do + let!(:customer_tags) { ["tag1"] } + + let(:rules) { applicator.send(:rules)} + let(:customer_rules) { applicator.send(:customer_rules)} + let(:default_rules) { applicator.send(:default_rules)} + + it "stores enterprise, rule_class and customer_tags as instance variables" do + expect(applicator.enterprise).to eq enterprise + expect(applicator.rule_class).to eq TagRule::FilterProducts + expect(applicator.customer_tags).to eq ["tag1"] + end + + it "selects only rules of the specified type" do + expect(rules).to include product_tag_rule1, product_tag_rule2, product_tag_rule3, default_product_tag_rule + expect(rules).not_to include oc_tag_rule, sm_tag_rule + end + + it "splits rules into those which match customer tags and those which don't" do + expect(customer_rules).to include product_tag_rule1, product_tag_rule2 + expect(customer_rules).not_to include default_product_tag_rule, product_tag_rule3, oc_tag_rule, sm_tag_rule + end + + it "splits out default rules" do + expect(default_rules).to include default_product_tag_rule + expect(default_rules).not_to include product_tag_rule1, product_tag_rule2, product_tag_rule3, oc_tag_rule, sm_tag_rule + end + end + end + end + + describe "filter!" do + let(:applicator) { OpenFoodNetwork::TagRuleApplicator.new(enterprise, "FilterProducts", []) } + + context "when the subject is nil" do + let(:subject) { double(:subject, reject!: false) } + + it "returns immediately" do + applicator.filter!(subject) + expect(subject).to_not have_received(:reject!) + end + end + + context "when subject is empty" do + let(:subject) { double(:subject, reject!: false) } + + it "returns immediately" do + applicator.filter!(subject) + expect(subject).to_not have_received(:reject!) + end + end + + context "when subject is an array" do + let(:element) { double(:element, ) } + let(:subject) { [element] } + + context "when rule_class reponds to tagged_children_for" do + let(:child1) { double(:child) } + let(:child2) { double(:child) } + let(:children) { [child1, child2] } + let(:rule_class) { double(:rule_class, tagged_children_for: children) } + + before{ allow(applicator).to receive(:rule_class) { rule_class } } + + context "when reject? returns true only for some children" do + before do + allow(applicator).to receive(:reject?).with(child1) { true } + allow(applicator).to receive(:reject?).with(child2) { false } + applicator.filter!(subject) + end + + it "rejects the specified children from the array" do + expect(children).to eq [child2] + end + + it "does not remove the element from the original subject" do + expect(subject).to eq [element] + end + end + + context "when reject? returns true for all children" do + before do + allow(applicator).to receive(:reject?).with(child1) { true } + allow(applicator).to receive(:reject?).with(child2) { true } + applicator.filter!(subject) + end + + it "removes all children from the array" do + expect(children).to eq [] + end + + it "removes the element from the original subject" do + expect(subject).to eq [] + end + end + end + + context "when rule_class doesn't respond to tagged_children_for" do + let(:rule_class) { double(:rule_class) } + + before{ allow(applicator).to receive(:rule_class) { rule_class } } + + context "when reject? returns false for the element" do + before do + allow(applicator).to receive(:reject?).with(element) { false } + applicator.filter!(subject) + end + + it "does not remove the element from the original subject" do + expect(subject).to eq [element] + end + end + + context "when reject? returns true for the element" do + before do + allow(applicator).to receive(:reject?).with(element) { true } + applicator.filter!(subject) + end + + it "removes the element from the original subject" do + expect(subject).to eq [] + end + end + end + end + end + + describe "reject?" do + let(:applicator) { OpenFoodNetwork::TagRuleApplicator.new(enterprise, "FilterProducts", ["tag1"]) } + let(:customer_rule) { double(:customer_rule, reject_matched?: "customer_rule.reject_matched?" )} + let(:default_rule) { double(:customer_rule, reject_matched?: "default_rule.reject_matched?" )} + let(:dummy) { double(:dummy) } + + before{ allow(applicator).to receive(:customer_rules) { [customer_rule] } } + before{ allow(applicator).to receive(:default_rules) { [default_rule] } } + + context "when a customer rule matches the tags of the element" do + before{ allow(customer_rule).to receive(:tags_match?).with(dummy) { true } } + + it "returns the value of customer_rule.reject_matched?" do + expect(applicator.send(:reject?, dummy)).to eq "customer_rule.reject_matched?" + end + end + + context "when no customer rules match the tags of the element" do + before{ allow(customer_rule).to receive(:tags_match?) { false } } + + context "when a default rule matches the tags of the element" do + before{ allow(default_rule).to receive(:tags_match?) { true } } + + it "returns the value of the default_rule.reject_matched?" do + expect(applicator.send(:reject?, dummy)).to eq "default_rule.reject_matched?" + end + end + + context "when a default rule matches the tags of the element" do + before{ allow(default_rule).to receive(:tags_match?) { false } } + + it "returns false" do + expect(applicator.send(:reject?, dummy)).to be false + end + end + end + end + + + describe "smoke test for products" do + let(:product1) { { id: 1, name: 'product 1', "variants" => [{ id: 4, "tag_list" => ["tag1"] }] } } + let(:product2) { { id: 2, name: 'product 2', "variants" => [{ id: 5, "tag_list" => ["tag1"] }, {id: 9, "tag_list" => ["tag2"]}] } } + let(:product3) { { id: 3, name: 'product 3', "variants" => [{ id: 6, "tag_list" => ["tag3"] }] } } + let!(:products_array) { [product1, product2, product3] } + + context "when customer tags don't match any rules" do + let(:applicator) { OpenFoodNetwork::TagRuleApplicator.new(enterprise, "FilterProducts", ["lalalala"]) } + + it "applies the default rule" do + applicator.filter!(products_array) + expect(products_array).to eq [{ id: 2, name: 'product 2', "variants" => [{id: 9, "tag_list" => ["tag2"]}] }, product3] + end + end + + context "when customer tags match one or more rules" do + let(:applicator) { OpenFoodNetwork::TagRuleApplicator.new(enterprise, "FilterProducts", ["tag1"]) } + + it "applies those rules" do + # product_tag_rule1 and product_tag_rule2 are being applied + applicator.filter!(products_array) + expect(products_array).to eq [product1, product2] + end + end + end + end +end diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index f0fd6bca4c..b8360094ec 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -859,54 +859,4 @@ describe Enterprise do end end end - - describe "finding tag rules" do - let(:enterprise) { create(:enterprise) } - let(:tag_rule1) { create(:filter_products_tag_rule, enterprise: enterprise) } - let(:tag_rule2) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise) } - let(:tag_rule3) { create(:tag_rule, enterprise: enterprise) } - let(:context) { { subject: "something" } } - - context "when a set of rules are passed in with the context" do - let(:tag_rules) { TagRule.where(id: [tag_rule1.id, tag_rule3.id]) } - before { context[:rules] = tag_rules } - - context "when a rule type has been passed in with the context" do - before { context[:type] = "FilterProducts" } - - it "returns rules of the specified type from within the list of passed-in rules" do - rules = enterprise.send(:tag_rules_for, context) - expect(rules).to include tag_rule1 - expect(rules).to_not include tag_rule2, tag_rule3 - end - end - - context "when a rule type has not been passed in with the context" do - it "returns the list of passed-in rules" do - rules = enterprise.send(:tag_rules_for, context) - expect(rules).to include tag_rule1, tag_rule3 - expect(rules).to_not include tag_rule2 - end - end - end - - context "when a set of rules are not passed in with the context" do - context "when a rule type has been passed in with the context" do - before { context[:type] = "FilterShippingMethods" } - - it "returns rules of the specified type belonging to the enterprise" do - rules = enterprise.send(:tag_rules_for, context) - expect(rules).to include tag_rule2 - expect(rules).to_not include tag_rule1, tag_rule3 - end - end - - context "when a rule type has not been passed in with the context" do - it "returns all rules belonging to the enterprise" do - rules = enterprise.send(:tag_rules_for, context) - expect(rules).to include tag_rule1, tag_rule2, tag_rule3 - end - end - end - end end diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 89ffd66e00..051c78896d 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -43,13 +43,17 @@ describe Spree::Order do end end - context "when a FilterPaymentMethods tag rule is in effect, with preferred visibility of 'visible'" do - let!(:allowed_customer) { create(:customer, enterprise: distributor, tag_list: "trusted") } - let!(:disallowed_customer) { create(:customer, enterprise: distributor, tag_list: "") } + context "when FilterPaymentMethods tag rules are in effect" do + let!(:tagged_customer) { create(:customer, enterprise: distributor, tag_list: "trusted") } + let!(:untagged_customer) { create(:customer, enterprise: distributor, tag_list: "") } let!(:tag_rule) { create(:filter_payment_methods_tag_rule, enterprise: distributor, preferred_customer_tags: "trusted", preferred_payment_method_tags: "trusted") } + let!(:default_tag_rule) { create(:filter_payment_methods_tag_rule, + enterprise: distributor, + is_default: true, + preferred_payment_method_tags: "trusted") } let(:tagged_pm) { pm1 } let(:untagged_pm) { pm2 } @@ -58,8 +62,9 @@ describe Spree::Order do distributor.payment_methods = [tagged_pm, untagged_pm] end - context "with a preferred visiblity of 'visible" do + context "with a preferred visiblity of 'visible', default visibility of 'hidden'" do before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, 'visible') } + before { default_tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, 'hidden') } context "when the customer is nil" do it "applies default action (hide)" do @@ -69,7 +74,7 @@ describe Spree::Order do end context "when the customer's tags match" do - before { order.update_attribute(:customer_id, allowed_customer.id) } + before { order.update_attribute(:customer_id, tagged_customer.id) } it "applies the action (show)" do expect(order.available_payment_methods).to include tagged_pm, untagged_pm @@ -77,7 +82,7 @@ describe Spree::Order do end context "when the customer's tags don't match" do - before { order.update_attribute(:customer_id, disallowed_customer.id) } + before { order.update_attribute(:customer_id, untagged_customer.id) } it "applies the default action (hide)" do expect(order.available_payment_methods).to include untagged_pm @@ -86,8 +91,9 @@ describe Spree::Order do end end - context "with a preferred visiblity of 'hidden" do + context "with a preferred visiblity of 'hidden', default visibility of 'visible'" do before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, 'hidden') } + before { default_tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, 'visible') } context "when the customer is nil" do it "applies default action (show)" do @@ -96,7 +102,7 @@ describe Spree::Order do end context "when the customer's tags match" do - before { order.update_attribute(:customer_id, allowed_customer.id) } + before { order.update_attribute(:customer_id, tagged_customer.id) } it "applies the action (hide)" do expect(order.available_payment_methods).to include untagged_pm @@ -105,7 +111,7 @@ describe Spree::Order do end context "when the customer's tags don't match" do - before { order.update_attribute(:customer_id, disallowed_customer.id) } + before { order.update_attribute(:customer_id, untagged_customer.id) } it "applies the default action (show)" do expect(order.available_payment_methods).to include tagged_pm, untagged_pm @@ -186,38 +192,6 @@ describe Spree::Order do subject.update_distribution_charge! end - - context "appying tag rules" do - let(:enterprise) { create(:distributor_enterprise) } - let(:customer) { create(:customer, enterprise: enterprise, tag_list: "tagtagtag") } - let(:tag_rule) { create(:tag_rule, enterprise: enterprise, preferred_customer_tags: "tagtagtag") } - let(:order) { create(:order_with_totals_and_distribution, distributor: enterprise, customer: customer) } - - before do - tag_rule.calculator.update_attribute(:preferred_flat_percent, -10) - end - - context "when the rule applies" do - it "applies the rule" do - order.update_distribution_charge! - order.reload - discount = order.adjustments.find_by_label("Discount") - expect(discount).to be_a Spree::Adjustment - expect(discount.amount).to eq (order.item_total / -10).round(2) - end - end - - context "when the rule does not apply" do - before { tag_rule.update_attribute(:preferred_customer_tags, "tagtag") } - - it "does not apply the rule" do - order.update_distribution_charge! - order.reload - discount = order.adjustments.find_by_label("Discount") - expect(discount).to be_nil - end - end - end end describe "looking up whether a line item can be provided by an order cycle" do diff --git a/spec/models/tag_rule/discount_order_spec.rb b/spec/models/tag_rule/discount_order_spec.rb index fd1a7e9251..93723ad84a 100644 --- a/spec/models/tag_rule/discount_order_spec.rb +++ b/spec/models/tag_rule/discount_order_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe TagRule::DiscountOrder, type: :model do let!(:tag_rule) { create(:tag_rule) } - describe "determining relevance based on additional requirements" do + pending "determining relevance based on additional requirements" do let(:subject) { double(:subject) } before do @@ -29,7 +29,7 @@ describe TagRule::DiscountOrder, type: :model do end end - describe "determining whether a the rule has already been applied to an order" do + pending "determining whether a the rule has already been applied to an order" do let!(:order) { create(:order) } let!(:adjustment) { order.adjustments.create({:amount => 12.34, :source => order, :originator => tag_rule, :label => 'discount' }, :without_protection => true) } @@ -47,7 +47,7 @@ describe TagRule::DiscountOrder, type: :model do end end - describe "applying the rule" do + pending "applying the rule" do # Assume that all validation is done by the TagRule base class let!(:line_item) { create(:line_item, price: 100.00) } diff --git a/spec/models/tag_rule/filter_order_cycles_spec.rb b/spec/models/tag_rule/filter_order_cycles_spec.rb index 53619a776d..4c08af7c7f 100644 --- a/spec/models/tag_rule/filter_order_cycles_spec.rb +++ b/spec/models/tag_rule/filter_order_cycles_spec.rb @@ -37,55 +37,4 @@ describe TagRule::FilterOrderCycles, type: :model do end end end - - describe "applying the rule" do - # Assume that all validation is done by the TagRule base class - - let(:enterprise) { create(:distributor_enterprise) } - let(:order_cycle1) { create(:simple_order_cycle, name: "order_cycle1", exchanges: [ create(:exchange, incoming: false, receiver: enterprise, tag_list: ["tag1", "something", "somethingelse"])]) } - let(:order_cycle2) { create(:simple_order_cycle, name: "order_cycle2", exchanges: [ create(:exchange, incoming: false, receiver: enterprise, tag_list: ["tag2"])]) } - let(:order_cycle3) { create(:simple_order_cycle, name: "order_cycle3", exchanges: [ create(:exchange, incoming: false, receiver: enterprise, tag_list: ["tag3"])]) } - let!(:order_cycle_hash) { [order_cycle1, order_cycle2, order_cycle3] } - - before do - tag_rule.update_attribute(:preferred_exchange_tags, "tag2") - tag_rule.context = {subject: order_cycle_hash, shop: enterprise} - end - - context "apply!" do - context "when showing matching exchanges" do - before { tag_rule.update_attribute(:preferred_matched_order_cycles_visibility, "visible") } - it "does nothing" do - tag_rule.send(:apply!) - expect(order_cycle_hash).to eq [order_cycle1, order_cycle2, order_cycle3] - end - end - - context "when hiding matching exchanges" do - before { tag_rule.update_attribute(:preferred_matched_order_cycles_visibility, "hidden") } - it "removes matching exchanges from the list" do - tag_rule.send(:apply!) - expect(order_cycle_hash).to eq [order_cycle1, order_cycle3] - end - end - end - - context "apply_default!" do - context "when showing matching exchanges" do - before { tag_rule.update_attribute(:preferred_matched_order_cycles_visibility, "visible") } - it "remove matching exchanges from the list" do - tag_rule.send(:apply_default!) - expect(order_cycle_hash).to eq [order_cycle1, order_cycle3] - end - end - - context "when hiding matching exchanges" do - before { tag_rule.update_attribute(:preferred_matched_order_cycles_visibility, "hidden") } - it "does nothing" do - tag_rule.send(:apply_default!) - expect(order_cycle_hash).to eq [order_cycle1, order_cycle2, order_cycle3] - end - end - end - end end diff --git a/spec/models/tag_rule/filter_payment_methods_spec.rb b/spec/models/tag_rule/filter_payment_methods_spec.rb index 8eec06f773..d8d30cefaa 100644 --- a/spec/models/tag_rule/filter_payment_methods_spec.rb +++ b/spec/models/tag_rule/filter_payment_methods_spec.rb @@ -30,54 +30,4 @@ describe TagRule::FilterPaymentMethods, type: :model do end end end - - describe "applying the rule" do - # Assume that all validation is done by the TagRule base class - - let(:sm1) { create(:payment_method, tag_list: ["tag1", "something", "somethingelse"]) } - let(:sm2) { create(:payment_method, tag_list: ["tag2"]) } - let(:sm3) { create(:payment_method, tag_list: ["tag3"]) } - let!(:payment_methods) { [sm1, sm2, sm3] } - - before do - tag_rule.update_attribute(:preferred_payment_method_tags, "tag2") - tag_rule.context = {subject: payment_methods} - end - - context "apply!" do - context "when showing matching payment methods" do - before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, "visible") } - it "does nothing" do - tag_rule.send(:apply!) - expect(payment_methods).to eq [sm1, sm2, sm3] - end - end - - context "when hiding matching payment methods" do - before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, "hidden") } - it "removes matching payment methods from the list" do - tag_rule.send(:apply!) - expect(payment_methods).to eq [sm1, sm3] - end - end - end - - context "apply_default!" do - context "when showing matching payment methods" do - before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, "visible") } - it "remove matching payment methods from the list" do - tag_rule.send(:apply_default!) - expect(payment_methods).to eq [sm1, sm3] - end - end - - context "when hiding matching payment methods" do - before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, "hidden") } - it "does nothing" do - tag_rule.send(:apply_default!) - expect(payment_methods).to eq [sm1, sm2, sm3] - end - end - end - end end diff --git a/spec/models/tag_rule/filter_products_spec.rb b/spec/models/tag_rule/filter_products_spec.rb index 185eb8fa47..0008db456d 100644 --- a/spec/models/tag_rule/filter_products_spec.rb +++ b/spec/models/tag_rule/filter_products_spec.rb @@ -30,54 +30,4 @@ describe TagRule::FilterProducts, type: :model do end end end - - describe "applying the rule" do - # Assume that all validation is done by the TagRule base class - - let(:product1) { { name: "product1", "variants" => [{ name: "v1", "tag_list" => ["tag1", "something", "somethingelse"]}] } } - let(:product2) { { name: "product2", "variants" => [{ name: "v2", "tag_list" => ["tag2"]}] } } - let(:product3) { { name: "product3", "variants" => [{ name: "v3", "tag_list" => ["tag3"]}] } } - let!(:product_hash) { [product1, product2, product3] } - - before do - tag_rule.update_attribute(:preferred_variant_tags, "tag2") - tag_rule.context = {subject: product_hash} - end - - context "apply!" do - context "when showing matching variants" do - before { tag_rule.update_attribute(:preferred_matched_variants_visibility, "visible") } - it "does nothing" do - tag_rule.send(:apply!) - expect(product_hash).to eq [product1, product2, product3] - end - end - - context "when hiding matching variants" do - before { tag_rule.update_attribute(:preferred_matched_variants_visibility, "hidden") } - it "removes matching variants from the list" do - tag_rule.send(:apply!) - expect(product_hash).to eq [product1, product3] - end - end - end - - context "apply_default!" do - context "when showing matching variants" do - before { tag_rule.update_attribute(:preferred_matched_variants_visibility, "visible") } - it "remove matching variants from the list" do - tag_rule.send(:apply_default!) - expect(product_hash).to eq [product1, product3] - end - end - - context "when hiding matching variants" do - before { tag_rule.update_attribute(:preferred_matched_variants_visibility, "hidden") } - it "does nothing" do - tag_rule.send(:apply_default!) - expect(product_hash).to eq [product1, product2, product3] - end - end - end - end end diff --git a/spec/models/tag_rule/filter_shipping_methods_spec.rb b/spec/models/tag_rule/filter_shipping_methods_spec.rb index 598d89c798..ff90ed8428 100644 --- a/spec/models/tag_rule/filter_shipping_methods_spec.rb +++ b/spec/models/tag_rule/filter_shipping_methods_spec.rb @@ -30,54 +30,4 @@ describe TagRule::FilterShippingMethods, type: :model do end end end - - describe "applying the rule" do - # Assume that all validation is done by the TagRule base class - - let(:sm1) { create(:shipping_method, tag_list: ["tag1", "something", "somethingelse"]) } - let(:sm2) { create(:shipping_method, tag_list: ["tag2"]) } - let(:sm3) { create(:shipping_method, tag_list: ["tag3"]) } - let!(:shipping_methods) { [sm1, sm2, sm3] } - - before do - tag_rule.update_attribute(:preferred_shipping_method_tags, "tag2") - tag_rule.context = {subject: shipping_methods} - end - - context "apply!" do - context "when showing matching shipping methods" do - before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, "visible") } - it "does nothing" do - tag_rule.send(:apply!) - expect(shipping_methods).to eq [sm1, sm2, sm3] - end - end - - context "when hiding matching shipping methods" do - before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, "hidden") } - it "removes matching shipping methods from the list" do - tag_rule.send(:apply!) - expect(shipping_methods).to eq [sm1, sm3] - end - end - end - - context "apply_default!" do - context "when showing matching shipping methods" do - before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, "visible") } - it "remove matching shipping methods from the list" do - tag_rule.send(:apply_default!) - expect(shipping_methods).to eq [sm1, sm3] - end - end - - context "when hiding matching shipping methods" do - before { tag_rule.update_attribute(:preferred_matched_shipping_methods_visibility, "hidden") } - it "does nothing" do - tag_rule.send(:apply_default!) - expect(shipping_methods).to eq [sm1, sm2, sm3] - end - end - end - end end diff --git a/spec/models/tag_rule_spec.rb b/spec/models/tag_rule_spec.rb index 32d2d532a4..06dac64194 100644 --- a/spec/models/tag_rule_spec.rb +++ b/spec/models/tag_rule_spec.rb @@ -8,196 +8,4 @@ describe TagRule, type: :model do expect(tag_rule).to validate_presence_of :enterprise end end - - describe 'setting the context' do - let(:subject) { double(:subject) } - let(:context) { { subject: subject, some_other_property: "yay"} } - - it "raises an error when context is nil" do - expect{ tag_rule.context = nil }.to raise_error "Context for tag rule cannot be nil" - end - - it "raises an error when subject is nil" do - expect{ tag_rule.context = {} }.to raise_error "Subject for tag rule cannot be nil" - end - - it "stores the subject and context provided as instance variables on the model" do - tag_rule.context = context - expect(tag_rule.subject).to eq subject - expect(tag_rule.context).to eq context - expect(tag_rule.instance_variable_get(:@subject)).to eq subject - expect(tag_rule.instance_variable_get(:@context)).to eq context - end - end - - describe "determining relevance based on subject and context" do - context "when the subject is nil" do - it "returns false" do - expect(tag_rule.send(:relevant?)).to be false - end - end - - context "when the subject is not nil" do - let(:subject) { double(:subject) } - - before do - tag_rule.context = {subject: subject} - allow(tag_rule).to receive(:customer_tags_match?) { :customer_tags_match_result } - allow(tag_rule).to receive(:subject_class) { Spree::Order} - end - - - context "when the subject class matches tag_rule#subject_class" do - before do - allow(subject).to receive(:class) { Spree::Order } - end - - context "when the rule does not repond to #additional_requirements_met?" do - before { allow(tag_rule).to receive(:respond_to?).with(:additional_requirements_met?, true) { false } } - - it "returns true" do - expect(tag_rule.send(:relevant?)).to be true - end - end - - context "when the rule reponds to #additional_requirements_met?" do - before { allow(tag_rule).to receive(:respond_to?).with(:additional_requirements_met?, true) { true } } - - context "and #additional_requirements_met? returns a truthy value" do - before { allow(tag_rule).to receive(:additional_requirements_met?) { "smeg" } } - - it "returns true immediately" do - expect(tag_rule.send(:relevant?)).to be true - end - end - - context "and #additional_requirements_met? returns true" do - before { allow(tag_rule).to receive(:additional_requirements_met?) { true } } - - it "returns true immediately" do - expect(tag_rule.send(:relevant?)).to be true - end - end - - context "and #additional_requirements_met? returns false" do - before { allow(tag_rule).to receive(:additional_requirements_met?) { false } } - - it "returns false immediately" do - expect(tag_rule.send(:relevant?)).to be false - end - end - end - end - - context "when the subject class does not match tag_rule#subject_class" do - before do - allow(subject).to receive(:class) { Spree::LineItem } - end - - it "returns false immediately" do - expect(tag_rule.send(:relevant?)).to be false - expect(tag_rule).to_not have_received :customer_tags_match? - end - end - end - - describe "determining whether specified customer tags match the given context" do - context "when the context has no customer tags specified" do - let(:context) { { subject: double(:something), not_tags: double(:not_tags) } } - - before { tag_rule.context = context } - - it "returns false" do - expect(tag_rule.send(:customer_tags_match?)).to be false - end - end - - context "when the context has customer tags specified" do - let(:context) { { subject: double(:something), customer_tags: ["member","local","volunteer"] } } - - before { tag_rule.context = context } - - context "when the rule has no preferred customer tags specified" do - before do - allow(tag_rule).to receive(:preferred_customer_tags) { "" } - end - - it "returns false" do - expect(tag_rule.send(:customer_tags_match?)).to be false - end - end - - context "when the rule has preferred customer tags specified that match ANY of the customer tags" do - before do - allow(tag_rule).to receive(:preferred_customer_tags) { "wholesale,some_tag,member" } - end - - it "returns false" do - expect(tag_rule.send(:customer_tags_match?)).to be true - end - end - - context "when the rule has preferred customer tags specified that match NONE of the customer tags" do - before do - allow(tag_rule).to receive(:preferred_customer_tags) { "wholesale,some_tag,some_other_tag" } - end - - it "returns false" do - expect(tag_rule.send(:customer_tags_match?)).to be false - end - end - end - end - - describe "applying a tag rule to a subject" do - before { allow(tag_rule).to receive(:apply!) } - - context "when the rule is deemed to be relevant" do - before { allow(tag_rule).to receive(:relevant?) { true } } - - context "and customer_tags_match? returns true" do - before { expect(tag_rule).to receive(:customer_tags_match?) { true } } - - it "applies the rule" do - tag_rule.apply - expect(tag_rule).to have_received(:apply!) - end - end - - context "when customer_tags_match? returns false" do - before { expect(tag_rule).to receive(:customer_tags_match?) { false } } - before { allow(tag_rule).to receive(:apply_default!) } - - context "and the rule responds to #apply_default!" do - before { allow(tag_rule).to receive(:respond_to?).with(:apply_default!, true) { true } } - - it "applies the default action" do - tag_rule.apply - expect(tag_rule).to_not have_received(:apply!) - expect(tag_rule).to have_received(:apply_default!) - end - end - - context "and the rule does not respond to #apply_default!" do - before { allow(tag_rule).to receive(:respond_to?).with(:apply_default!, true) { false } } - - it "does not apply the rule or the default action" do - tag_rule.apply - expect(tag_rule).to_not have_received(:apply!) - expect(tag_rule).to_not have_received(:apply_default!) - end - end - end - end - - context "when the rule is deemed not to be relevant" do - before { allow(tag_rule).to receive(:relevant?) { false } } - - it "does not apply the rule" do - tag_rule.apply - expect(tag_rule).to_not have_received(:apply!) - end - end - end - end end From 38e37a4dfafc06d9e44a4a4bdf5bd12a9968df73 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 26 May 2016 23:02:05 +1000 Subject: [PATCH 096/110] Using new jasmine2 syntax in variant override spec --- .../unit/admin/services/dirty_variant_overrides_spec.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee b/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee index 7a1dd4fab3..b3758ed20b 100644 --- a/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/dirty_variant_overrides_spec.js.coffee @@ -23,7 +23,7 @@ describe "maintaining a list of dirty variant overrides", -> describe "setting the value of an attribute", -> beforeEach -> - spyOn(DirtyVariantOverrides, "add").andCallThrough() + spyOn(DirtyVariantOverrides, "add").and.callThrough() describe "when a record for the given VO does not exist", -> beforeEach -> From 011c8c74d97243b2b4cf2479de41836e70475403 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 26 May 2016 23:25:00 +1000 Subject: [PATCH 097/110] Updating remove button character for tags in ngTagsInput --- spec/features/admin/variant_overrides_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 3dd2306243..1cb559a653 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -271,7 +271,7 @@ feature %q{ fill_in "variant-overrides-#{variant.id}-default_stock", with: '' within "tr#v_#{variant.id}" do vo.tag_list.each do |tag| - within "li.tag-item", text: "#{tag} ×" do + within "li.tag-item", text: "#{tag} ✖" do find("a.remove-button").trigger('click') end end From cb9e3b43f9cc0021d01404e84c8589c36e321e96 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 26 May 2016 23:54:54 +1000 Subject: [PATCH 098/110] Tag attributes are auto-initialized via directive if not present on object --- .../admin/order_cycles/services/order_cycle.js.coffee | 2 +- .../utils/directives/tags_with_translation.js.coffee | 9 ++++++--- app/serializers/api/admin/shipping_method_serializer.rb | 6 +++++- spec/javascripts/unit/order_cycle_spec.js.coffee | 2 +- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee index 5ab14dfdcd..65f0f67b71 100644 --- a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee @@ -48,7 +48,7 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, S this.order_cycle.incoming_exchanges.push({enterprise_id: new_supplier_id, incoming: true, active: true, variants: {}, enterprise_fees: []}) addDistributor: (new_distributor_id) -> - this.order_cycle.outgoing_exchanges.push({enterprise_id: new_distributor_id, incoming: false, active: true, variants: {}, enterprise_fees: [], tags: [], tag_list: ""}) + this.order_cycle.outgoing_exchanges.push({enterprise_id: new_distributor_id, incoming: false, active: true, variants: {}, enterprise_fees: []}) removeExchange: (exchange) -> if exchange.incoming diff --git a/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee index 221ee03c76..a79bc8f87e 100644 --- a/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee @@ -10,12 +10,15 @@ angular.module("admin.utils").directive "tagsWithTranslation", ($timeout) -> link: (scope, element, attrs) -> scope.findTags = undefined unless attrs.hasOwnProperty("findTags") + compileTagList = -> + scope.object[scope.tagListAttr] = (tag.text for tag in scope.object[scope.tagsAttr]).join(",") + $timeout -> + # Initialize properties if necessary scope.tagsAttr ||= "tags" scope.tagListAttr ||= "tag_list" - - compileTagList = -> - scope.object[scope.tagListAttr] = (tag.text for tag in scope.object[scope.tagsAttr]).join(",") + scope.object[scope.tagsAttr] ||= [] + compileTagList() scope.tagAdded = -> compileTagList() diff --git a/app/serializers/api/admin/shipping_method_serializer.rb b/app/serializers/api/admin/shipping_method_serializer.rb index 9fbb864d09..e160d97fdf 100644 --- a/app/serializers/api/admin/shipping_method_serializer.rb +++ b/app/serializers/api/admin/shipping_method_serializer.rb @@ -1,5 +1,9 @@ class Api::Admin::ShippingMethodSerializer < ActiveModel::Serializer - attributes :id, :name, :tags + attributes :id, :name, :tag_list, :tags + + def tag_list + object.tag_list.join(",") + end def tags object.tag_list.map{ |t| { text: t } } diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index df227c7960..4a924917c8 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -568,7 +568,7 @@ describe 'OrderCycle services', -> it 'adds the distributor to outgoing exchanges', -> OrderCycle.addDistributor('123') expect(OrderCycle.order_cycle.outgoing_exchanges).toEqual [ - {enterprise_id: '123', incoming: false, active: true, variants: {}, enterprise_fees: [], tags: [], tag_list: ""} + {enterprise_id: '123', incoming: false, active: true, variants: {}, enterprise_fees: []} ] describe 'removing exchanges', -> From a1c7a44fa010be9484db224e2d2aa4f8d6c00294 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 27 May 2016 13:53:25 +1000 Subject: [PATCH 099/110] Tag Rules can be rearranged in UI to set priority --- .../tag_rules_controller.js.coffee | 16 ++++----- .../admin/utils/directives/sortable.js.coffee | 36 +++++++++++++++++++ .../admin/tag_rules/tag_rule.html.haml | 5 +++ .../stylesheets/admin/tag_rules.css.scss | 7 ++++ app/models/tag_rule.rb | 4 ++- .../api/admin/enterprise_serializer.rb | 7 ++-- .../enterprises/form/_tag_rules.html.haml | 4 +-- ...20160527012603_add_priority_to_tag_rule.rb | 5 +++ db/schema.rb | 3 +- lib/open_food_network/tag_rule_applicator.rb | 2 +- spec/features/admin/tag_rules_spec.rb | 13 +++++-- .../tag_rule_applicator_spec.rb | 25 ++++++------- 12 files changed, 95 insertions(+), 32 deletions(-) create mode 100644 app/assets/javascripts/admin/utils/directives/sortable.js.coffee create mode 100644 db/migrate/20160527012603_add_priority_to_tag_rule.rb diff --git a/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee index 38e12182a0..45f98a2b57 100644 --- a/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee +++ b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee @@ -1,16 +1,16 @@ -angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, enterprise) -> +angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, $filter, enterprise) -> $scope.tagGroups = enterprise.tag_groups $scope.defaultTagGroup = enterprise.default_tag_group $scope.visibilityOptions = [ { id: "visible", name: "VISIBLE" }, { id: "hidden", name: "NOT VISIBLE" } ] - updateRuleCounts = -> + $scope.updateRuleCounts = -> index = $scope.defaultTagGroup.rules.length - for tagGroup in $scope.tagGroups + for tagGroup in $filter('orderBy')($scope.tagGroups, 'position') tagGroup.startIndex = index index = index + tagGroup.rules.length - updateRuleCounts() + $scope.updateRuleCounts() $scope.updateTagsRulesFor = (tagGroup) -> for tagRule in tagGroup.rules @@ -38,17 +38,17 @@ angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, ente newRule.peferred_exchange_tags = [] newRule.preferred_matched_order_cycles_visibility = "visible" tagGroup.rules.push(newRule) - updateRuleCounts() + $scope.updateRuleCounts() $scope.addNewTag = -> - $scope.tagGroups.push { tags: [], rules: [] } + $scope.tagGroups.push { tags: [], rules: [], position: $scope.tagGroups.length + 1 } $scope.deleteTagRule = (tagGroup, tagRule) -> index = tagGroup.rules.indexOf(tagRule) return unless index >= 0 if tagRule.id is null tagGroup.rules.splice(index, 1) - updateRuleCounts() + $scope.updateRuleCounts() else if confirm("Are you sure?") $http @@ -56,4 +56,4 @@ angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, ente url: "/admin/enterprises/#{enterprise.id}/tag_rules/#{tagRule.id}.json" .success -> tagGroup.rules.splice(index, 1) - updateRuleCounts() + $scope.updateRuleCounts() diff --git a/app/assets/javascripts/admin/utils/directives/sortable.js.coffee b/app/assets/javascripts/admin/utils/directives/sortable.js.coffee new file mode 100644 index 0000000000..2f2f100e31 --- /dev/null +++ b/app/assets/javascripts/admin/utils/directives/sortable.js.coffee @@ -0,0 +1,36 @@ +angular.module("admin.utils").directive "ofnSortable", ($timeout, $parse) -> + restrict: "E" + scope: + items: '@' + position: '@' + afterSort: '&' + handle: "@" + axis: "@" + link: (scope, element, attrs) -> + $timeout -> + scope.axis ||= "y" + scope.handle ||= ".handle" + getScopePos = $parse(scope.position) + setScopePos = getScopePos.assign + + element.sortable + handle: scope.handle + helper: 'clone' + axis: scope.axis + items: scope.items + appendTo: element + update: (event, ui) -> + sortableSiblings = ($(ss) for ss in ui.item.siblings(scope.items)) + offset = Math.min(ui.item.index(), sortableSiblings[0].index()) + newPos = ui.item.index() - offset + 1 + oldPos = getScopePos(ui.item.scope()) + if newPos < oldPos + for sibScope in sortableSiblings.map((ss) -> ss.scope()) + pos = getScopePos(sibScope) + setScopePos(sibScope, pos + 1) if pos >= newPos && pos < oldPos + else if newPos > oldPos + for sibScope in sortableSiblings.map((ss) -> ss.scope()) + pos = getScopePos(sibScope) + setScopePos(sibScope, pos - 1) if pos > oldPos && pos <= newPos + setScopePos(ui.item.scope(), newPos) + scope.afterSort() diff --git a/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml b/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml index 0a752ae17b..f3018927c4 100644 --- a/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml +++ b/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml @@ -16,6 +16,11 @@ name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", ng: { value: "rule.type" } } + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_priority", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][priority]", + ng: { value: "tagGroup.startIndex + $index" } } + %input{ type: "hidden", id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_is_default", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][is_default]", diff --git a/app/assets/stylesheets/admin/tag_rules.css.scss b/app/assets/stylesheets/admin/tag_rules.css.scss index 0540dfa89f..7feedb7c50 100644 --- a/app/assets/stylesheets/admin/tag_rules.css.scss +++ b/app/assets/stylesheets/admin/tag_rules.css.scss @@ -5,7 +5,14 @@ font-weight: bold; } +.customer_tag { + .header { + cursor: move; + } +} + .customer_tag, .default_rules { + background-color: #ffffff; border: 1px solid #cee1f4; margin-bottom: 40px; diff --git a/app/models/tag_rule.rb b/app/models/tag_rule.rb index ee1ef105a3..a5f2d93dee 100644 --- a/app/models/tag_rule.rb +++ b/app/models/tag_rule.rb @@ -5,9 +5,11 @@ class TagRule < ActiveRecord::Base validates :enterprise, presence: true - attr_accessible :enterprise, :enterprise_id, :is_default, :preferred_customer_tags + attr_accessible :enterprise, :enterprise_id, :is_default, :priority + attr_accessible :preferred_customer_tags scope :for, ->(enterprise) { where(enterprise_id: enterprise) } + scope :prioritised, -> { order('priority ASC') } def self.mapping_for(enterprises) self.for(enterprises).inject({}) do |mapping, rule| diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index 86b40c90ad..49a49b3250 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -9,9 +9,12 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer has_many :users, serializer: Api::Admin::UserSerializer def tag_groups - object.tag_rules.reject(&:is_default).each_with_object([]) do |tag_rule, tag_groups| + object.tag_rules.prioritised.reject(&:is_default).each_with_object([]) do |tag_rule, tag_groups| tag_group = find_match(tag_groups, tag_rule.preferred_customer_tags.split(",").map{ |t| { text: t } }) - tag_groups << tag_group if tag_group[:rules].empty? + if tag_group[:rules].empty? + tag_groups << tag_group + tag_group[:position] = tag_groups.count + end tag_group[:rules] << Api::Admin::TagRuleSerializer.new(tag_rule).serializable_hash end end diff --git a/app/views/admin/enterprises/form/_tag_rules.html.haml b/app/views/admin/enterprises/form/_tag_rules.html.haml index 5752b25b06..fd34477c6c 100644 --- a/app/views/admin/enterprises/form/_tag_rules.html.haml +++ b/app/views/admin/enterprises/form/_tag_rules.html.haml @@ -1,11 +1,11 @@ .row{ ng: { controller: "TagRulesCtrl" } } .eleven.columns.alpha.omega - .eleven.columns.alpha.omega + %ofn-sortable{ axis: "y", handle: ".header", items: '.customer_tag', position: "tagGroup.position", after: { sort: "updateRuleCounts()" } } .no_tags{ ng: { show: "tagGroups.length == 0" } } No tags apply to this enterprise yet = render 'admin/enterprises/form/tag_rules/default_rules' -# = render 'customer_tags' - .customer_tag{ ng: { repeat: "tagGroup in tagGroups" } } + .customer_tag{ id: "tg_{{tagGroup.position}}", ng: { repeat: "tagGroup in tagGroups" } } .header %table %colgroup diff --git a/db/migrate/20160527012603_add_priority_to_tag_rule.rb b/db/migrate/20160527012603_add_priority_to_tag_rule.rb new file mode 100644 index 0000000000..7080d7a9ac --- /dev/null +++ b/db/migrate/20160527012603_add_priority_to_tag_rule.rb @@ -0,0 +1,5 @@ +class AddPriorityToTagRule < ActiveRecord::Migration + def change + add_column :tag_rules, :priority, :integer, default: 99, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index f68f6c6a0a..002e367dd6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20160520065217) do +ActiveRecord::Schema.define(:version => 20160527012603) do create_table "account_invoices", :force => true do |t| t.integer "user_id", :null => false @@ -1164,6 +1164,7 @@ ActiveRecord::Schema.define(:version => 20160520065217) do t.datetime "created_at", :null => false t.datetime "updated_at", :null => false t.boolean "is_default", :default => false, :null => false + t.integer "priority", :default => 99, :null => false end create_table "taggings", :force => true do |t| diff --git a/lib/open_food_network/tag_rule_applicator.rb b/lib/open_food_network/tag_rule_applicator.rb index 181624a210..5225dfdd86 100644 --- a/lib/open_food_network/tag_rule_applicator.rb +++ b/lib/open_food_network/tag_rule_applicator.rb @@ -40,7 +40,7 @@ module OpenFoodNetwork def rules return @rules unless @rules.nil? - @rules = rule_class.for(enterprise) + @rules = rule_class.prioritised.for(enterprise) end def customer_rules diff --git a/spec/features/admin/tag_rules_spec.rb b/spec/features/admin/tag_rules_spec.rb index 45b28f603c..7c1aa55f9e 100644 --- a/spec/features/admin/tag_rules_spec.rb +++ b/spec/features/admin/tag_rules_spec.rb @@ -170,6 +170,9 @@ feature 'Tag Rules', js: true do select2_select 'VISIBLE', from: "enterprise_tag_rules_attributes_4_preferred_matched_shipping_methods_visibility" end + # Moving the Shipping Methods to top priority + find(".customer_tag#tg_4 .header", ).drag_to find(".customer_tag#tg_1 .header") + # # DiscountOrder rule # within "#tr_2" do # expect(page).to have_field "enterprise_tag_rules_attributes_2_calculator_attributes_preferred_flat_percent", with: '0' @@ -178,27 +181,31 @@ feature 'Tag Rules', js: true do click_button 'Update' - # FilterShippingMethods rule - expect(default_fsm_tag_rule.preferred_customer_tags).to eq "" + # DEFAULT FilterShippingMethods rule + expect(default_fsm_tag_rule.reload.preferred_customer_tags).to eq "" expect(default_fsm_tag_rule.preferred_shipping_method_tags).to eq "local,volunteers-only" expect(default_fsm_tag_rule.preferred_matched_shipping_methods_visibility).to eq "hidden" # FilterShippingMethods rule + expect(fsm_tag_rule.reload.priority).to eq 1 expect(fsm_tag_rule.preferred_customer_tags).to eq "local,volunteer" expect(fsm_tag_rule.preferred_shipping_method_tags).to eq "local,volunteers-only4" expect(fsm_tag_rule.preferred_matched_shipping_methods_visibility).to eq "visible" # FilterProducts rule + expect(fp_tag_rule.reload.priority).to eq 2 expect(fp_tag_rule.preferred_customer_tags).to eq "member,volunteer" expect(fp_tag_rule.preferred_variant_tags).to eq "member,volunteers-only1" expect(fp_tag_rule.preferred_matched_variants_visibility).to eq "hidden" # FilterPaymentMethods rule + expect(fpm_tag_rule.reload.priority).to eq 3 expect(fpm_tag_rule.preferred_customer_tags).to eq "trusted,volunteer" expect(fpm_tag_rule.preferred_payment_method_tags).to eq "trusted,volunteers-only2" expect(fpm_tag_rule.preferred_matched_payment_methods_visibility).to eq "visible" - # FilterPaymentMethods rule + # FilterOrderCycles rule + expect(foc_tag_rule.reload.priority).to eq 4 expect(foc_tag_rule.preferred_customer_tags).to eq "wholesale,volunteer" expect(foc_tag_rule.preferred_exchange_tags).to eq "wholesale,volunteers-only3" expect(foc_tag_rule.preferred_matched_order_cycles_visibility).to eq "hidden" diff --git a/spec/lib/open_food_network/tag_rule_applicator_spec.rb b/spec/lib/open_food_network/tag_rule_applicator_spec.rb index 0b560a3e0a..97551df96e 100644 --- a/spec/lib/open_food_network/tag_rule_applicator_spec.rb +++ b/spec/lib/open_food_network/tag_rule_applicator_spec.rb @@ -3,12 +3,12 @@ require 'open_food_network/tag_rule_applicator' module OpenFoodNetwork describe TagRuleApplicator do let!(:enterprise) { create(:distributor_enterprise) } - let!(:oc_tag_rule) { create(:filter_order_cycles_tag_rule, enterprise: enterprise, preferred_customer_tags: "tag1", preferred_exchange_tags: "tag1", preferred_matched_order_cycles_visibility: "visible" )} - let!(:product_tag_rule1) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_customer_tags: "tag1", preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "visible" ) } - let!(:product_tag_rule2) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_customer_tags: "tag1", preferred_variant_tags: "tag3", preferred_matched_variants_visibility: "hidden" ) } - let!(:product_tag_rule3) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_customer_tags: "tag2", preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "visible" ) } - let!(:default_product_tag_rule) { create(:filter_products_tag_rule, enterprise: enterprise, is_default: true, preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "hidden" ) } - let!(:sm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise, preferred_customer_tags: "tag1", preferred_shipping_method_tags: "tag1", preferred_matched_shipping_methods_visibility: "visible" )} + let!(:oc_tag_rule) { create(:filter_order_cycles_tag_rule, enterprise: enterprise, priority: 6, preferred_customer_tags: "tag1", preferred_exchange_tags: "tag1", preferred_matched_order_cycles_visibility: "visible" )} + let!(:product_tag_rule1) { create(:filter_products_tag_rule, enterprise: enterprise, priority: 5, preferred_customer_tags: "tag1", preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "visible" ) } + let!(:product_tag_rule2) { create(:filter_products_tag_rule, enterprise: enterprise, priority: 4, preferred_customer_tags: "tag1", preferred_variant_tags: "tag3", preferred_matched_variants_visibility: "hidden" ) } + let!(:product_tag_rule3) { create(:filter_products_tag_rule, enterprise: enterprise, priority: 3, preferred_customer_tags: "tag2", preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "visible" ) } + let!(:default_product_tag_rule) { create(:filter_products_tag_rule, enterprise: enterprise, priority: 2, is_default: true, preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "hidden" ) } + let!(:sm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise, priority: 1, preferred_customer_tags: "tag1", preferred_shipping_method_tags: "tag1", preferred_matched_shipping_methods_visibility: "visible" )} describe "initialisation" do context "when enterprise is nil" do @@ -68,19 +68,16 @@ module OpenFoodNetwork expect(applicator.customer_tags).to eq ["tag1"] end - it "selects only rules of the specified type" do - expect(rules).to include product_tag_rule1, product_tag_rule2, product_tag_rule3, default_product_tag_rule - expect(rules).not_to include oc_tag_rule, sm_tag_rule + it "selects only rules of the specified type, in order of priority" do + expect(rules).to eq [default_product_tag_rule, product_tag_rule3, product_tag_rule2, product_tag_rule1] end - it "splits rules into those which match customer tags and those which don't" do - expect(customer_rules).to include product_tag_rule1, product_tag_rule2 - expect(customer_rules).not_to include default_product_tag_rule, product_tag_rule3, oc_tag_rule, sm_tag_rule + it "splits rules into those which match customer tags and those which don't, in order of priority" do + expect(customer_rules).to eq [product_tag_rule2, product_tag_rule1] end it "splits out default rules" do - expect(default_rules).to include default_product_tag_rule - expect(default_rules).not_to include product_tag_rule1, product_tag_rule2, product_tag_rule3, oc_tag_rule, sm_tag_rule + expect(default_rules).to eq [default_product_tag_rule] end end end From 6dd05ca4dafa766d655bb049cd7b1f0ad5d24eb0 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 27 May 2016 15:04:43 +1000 Subject: [PATCH 100/110] Intermittent fail hunting --- app/models/spree/taxon_decorator.rb | 14 ++++++-------- spec/features/consumer/registration_spec.rb | 13 ++++++++++++- spec/models/spree/taxon_spec.rb | 19 +++++++++---------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/app/models/spree/taxon_decorator.rb b/app/models/spree/taxon_decorator.rb index a1e07cc53e..a051de98fa 100644 --- a/app/models/spree/taxon_decorator.rb +++ b/app/models/spree/taxon_decorator.rb @@ -24,10 +24,9 @@ Spree::Taxon.class_eval do joins(:products => :supplier). select('spree_taxons.*, enterprises.id AS enterprise_id'). each do |t| - - taxons[t.enterprise_id.to_i] ||= Set.new - taxons[t.enterprise_id.to_i] << t.id - end + taxons[t.enterprise_id.to_i] ||= Set.new + taxons[t.enterprise_id.to_i] << t.id + end taxons end @@ -43,10 +42,9 @@ Spree::Taxon.class_eval do where('o_exchanges.incoming = ?', false). select('spree_taxons.*, o_exchanges.receiver_id AS enterprise_id'). each do |t| - - taxons[t.enterprise_id.to_i] ||= Set.new - taxons[t.enterprise_id.to_i] << t.id - end + taxons[t.enterprise_id.to_i] ||= Set.new + taxons[t.enterprise_id.to_i] << t.id + end taxons end diff --git a/spec/features/consumer/registration_spec.rb b/spec/features/consumer/registration_spec.rb index 9b2602c1c6..cab6c7d327 100644 --- a/spec/features/consumer/registration_spec.rb +++ b/spec/features/consumer/registration_spec.rb @@ -44,10 +44,11 @@ feature "Registration", js: true do # Choosing a type expect(page).to have_content 'Last step to add My Awesome Enterprise!' - click_link 'producer-panel' + click_link_and_ensure('producer-panel', lambda { page.has_content? '#producer-panel.selected' } ) click_button 'Create Profile' # Enterprise should be created + # save_screenshot '/Users/rob/Desktop/ss.png' unless page.has_content? "Nice one!" expect(page).to have_content 'Nice one!' e = Enterprise.find_by_name('My Awesome Enterprise') expect(e.address.address1).to eq "123 Abc Street" @@ -129,4 +130,14 @@ feature "Registration", js: true do end end end + + def click_link_and_ensure(link_text, check) + # Buttons appear to be unresponsive for a while, so keep clicking them until content appears + using_wait_time 0.5 do + 10.times do + click_link link_text + break if check.call + end + end + end end diff --git a/spec/models/spree/taxon_spec.rb b/spec/models/spree/taxon_spec.rb index 926e5b3c5f..8fb4b22dc5 100644 --- a/spec/models/spree/taxon_spec.rb +++ b/spec/models/spree/taxon_spec.rb @@ -3,21 +3,20 @@ require 'spec_helper' module Spree describe Taxon do let(:e) { create(:supplier_enterprise) } - let(:t0) { p1.taxons.order('id ASC').first } - let(:t1) { create(:taxon) } - let(:t2) { create(:taxon) } + let!(:t1) { create(:taxon) } + let!(:t2) { create(:taxon) } describe "callbacks" do - let(:product) { create(:simple_product, taxons: [t1]) } + let!(:p2) { create(:simple_product, taxons: [t1]) } it "refreshes the products cache on save" do - expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product) + expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(p2) t1.name = 'asdf' t1.save end it "refreshes the products cache on destroy" do - expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product) + expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(p2) t1.destroy end end @@ -26,17 +25,17 @@ module Spree let!(:p1) { create(:simple_product, supplier: e, taxons: [t1, t2]) } it "finds taxons" do - Taxon.supplied_taxons.should == {e.id => Set.new([t0.id, t1.id, t2.id])} + Taxon.supplied_taxons.should == {e.id => Set.new(p1.taxons.map(&:id))} end end describe "finding all distributed taxons" do let!(:oc) { create(:simple_order_cycle, distributors: [e], variants: [p1.master]) } - let(:s) { create(:supplier_enterprise) } - let(:p1) { create(:simple_product, supplier: s, taxons: [t1, t2]) } + let!(:s) { create(:supplier_enterprise) } + let!(:p1) { create(:simple_product, supplier: s, taxons: [t1, t2]) } it "finds taxons" do - Taxon.distributed_taxons.should == {e.id => Set.new([t0.id, t1.id, t2.id])} + Taxon.distributed_taxons.should == {e.id => Set.new(p1.taxons.map(&:id))} end end end From 12e685bd923daf20709e59d8e3c8788875ca6a92 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 27 May 2016 16:22:11 +1000 Subject: [PATCH 101/110] Restricting number of tags for tag rules to one --- .../tags_with_translation.js.coffee | 7 ++++ .../filter_order_cycles_input.html.haml | 14 +++++-- .../filter_payment_methods_input.html.haml | 14 +++++-- .../tag_rules/filter_products_input.html.haml | 14 +++++-- .../filter_shipping_methods_input.html.haml | 14 +++++-- .../admin/tag_rules/tag_rule.html.haml | 2 +- .../templates/admin/tags_input.html.haml | 4 +- .../stylesheets/admin/tag_rules.css.scss | 8 ++++ .../enterprises/form/_tag_rules.html.haml | 4 +- spec/features/admin/tag_rules_spec.rb | 38 ++++++++++--------- 10 files changed, 80 insertions(+), 39 deletions(-) diff --git a/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee index a79bc8f87e..9722ab26af 100644 --- a/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee @@ -7,10 +7,15 @@ angular.module("admin.utils").directive "tagsWithTranslation", ($timeout) -> tagListAttr: "@?" findTags: "&" form: '=?' + onTagAdded: "&" + onTagRemoved: "&" + max: "=" link: (scope, element, attrs) -> scope.findTags = undefined unless attrs.hasOwnProperty("findTags") + scope.limitReached = false compileTagList = -> + scope.limitReached = scope.object[scope.tagsAttr].length >= scope.max if scope.max != undefined scope.object[scope.tagListAttr] = (tag.text for tag in scope.object[scope.tagsAttr]).join(",") $timeout -> @@ -21,10 +26,12 @@ angular.module("admin.utils").directive "tagsWithTranslation", ($timeout) -> compileTagList() scope.tagAdded = -> + scope.onTagAdded() compileTagList() scope.tagRemoved = -> # For some reason the tags input doesn't mark the form # as dirty when a tag is removed, which breaks the save bar scope.form.$setDirty(true) if typeof scope.form isnt 'undefined' + scope.onTagRemoved() compileTagList() diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles_input.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles_input.html.haml index 5cd8a4400d..fd8dac63c7 100644 --- a/app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles_input.html.haml +++ b/app/assets/javascripts/templates/admin/tag_rules/filter_order_cycles_input.html.haml @@ -1,4 +1,10 @@ -%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_order_cycles_visibility", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_order_cycles_visibility]", - ng: { model: "rule.preferred_matched_order_cycles_visibility"}, - data: 'visibilityOptions', "min-search" => 5 } +%div + %input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_order_cycles_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_order_cycles_visibility]", + ng: { model: "rule.preferred_matched_order_cycles_visibility", if: "!rule.is_default" }, + data: 'visibilityOptions', "min-search" => 5 } + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_order_cycles_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_order_cycles_visibility]", + ng: { value: "'hidden'", if: "rule.is_default" } } + %span.text-normal{ ng: { if: "rule.is_default" } } not visible diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods_input.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods_input.html.haml index 1446c6bcff..0dd533235d 100644 --- a/app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods_input.html.haml +++ b/app/assets/javascripts/templates/admin/tag_rules/filter_payment_methods_input.html.haml @@ -1,4 +1,10 @@ -%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_payment_methods_visibility", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_payment_methods_visibility]", - ng: { model: "rule.preferred_matched_payment_methods_visibility"}, - data: 'visibilityOptions', "min-search" => 5 } +%div + %input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_payment_methods_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_payment_methods_visibility]", + ng: { model: "rule.preferred_matched_payment_methods_visibility", if: "!rule.is_default" }, + data: 'visibilityOptions', "min-search" => 5 } + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_payment_methods_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_payment_methods_visibility]", + ng: { value: "'hidden'", if: "rule.is_default" } } + %span.text-normal{ ng: { if: "rule.is_default" } } not visible diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_products_input.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_products_input.html.haml index 9326af7d5a..da112c9db2 100644 --- a/app/assets/javascripts/templates/admin/tag_rules/filter_products_input.html.haml +++ b/app/assets/javascripts/templates/admin/tag_rules/filter_products_input.html.haml @@ -1,4 +1,10 @@ -%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_variants_visibility", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_variants_visibility]", - ng: { model: "rule.preferred_matched_variants_visibility"}, - data: 'visibilityOptions', "min-search" => 5 } +%div + %input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_variants_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_variants_visibility]", + ng: { model: "rule.preferred_matched_variants_visibility", if: "!rule.is_default" }, + data: 'visibilityOptions', "min-search" => 5 } + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_variants_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_variants_visibility]", + ng: { value: "'hidden'", if: "rule.is_default" } } + %span.text-normal{ ng: { if: "rule.is_default" } } not visible diff --git a/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods_input.html.haml b/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods_input.html.haml index af31e93cfa..a46b9abd14 100644 --- a/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods_input.html.haml +++ b/app/assets/javascripts/templates/admin/tag_rules/filter_shipping_methods_input.html.haml @@ -1,4 +1,10 @@ -%input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_shipping_methods_visibility", - name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_shipping_methods_visibility]", - ng: { model: "rule.preferred_matched_shipping_methods_visibility"}, - data: 'visibilityOptions', "min-search" => 5 } +%div + %input.fullwidth.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_shipping_methods_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_shipping_methods_visibility]", + ng: { model: "rule.preferred_matched_shipping_methods_visibility", if: "!rule.is_default" }, + data: 'visibilityOptions', "min-search" => 5 } + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_shipping_methods_visibility", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_shipping_methods_visibility]", + ng: { value: "'hidden'", if: "rule.is_default" } } + %span.text-normal{ ng: { if: "rule.is_default" } } not visible diff --git a/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml b/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml index f3018927c4..4427d284fd 100644 --- a/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml +++ b/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml @@ -38,7 +38,7 @@ %span.text-normal {{ opt[rule.type].textTop }} %td - %tags-with-translation{ object: "rule", "tags-attr" => "{{opt[rule.type].tagsAttr}}", "tag-list-attr" => "{{opt[rule.type].tagListAttr}}" } + %tags-with-translation{ object: "rule", max: 1, "tags-attr" => "{{opt[rule.type].tagsAttr}}", "tag-list-attr" => "{{opt[rule.type].tagListAttr}}" } %td.actions{ rowspan: 2 } %a{ ng: { click: "deleteTagRule(tagGroup, rule)" }, :class => "delete-tag-rule icon-trash no-text" } %tr diff --git a/app/assets/javascripts/templates/admin/tags_input.html.haml b/app/assets/javascripts/templates/admin/tags_input.html.haml index 15a69b650c..3c604b4b15 100644 --- a/app/assets/javascripts/templates/admin/tags_input.html.haml +++ b/app/assets/javascripts/templates/admin/tags_input.html.haml @@ -1,4 +1,6 @@ -%tags-input{ template: 'admin/tag.html', ng: { model: 'object[tagsAttr]' }, on: { tag: { added: 'tagAdded()', removed:'tagRemoved()' } } } +%tags-input{ template: 'admin/tag.html', + ng: { model: 'object[tagsAttr]', class: "{'limit-reached': limitReached}"}, + on: { tag: { added: 'tagAdded()', removed:'tagRemoved()' } } } %auto-complete{ ng: { if: "findTags" }, source: "findTags({query: $query})", template: "admin/tag_autocomplete.html", "min-length" => "0", diff --git a/app/assets/stylesheets/admin/tag_rules.css.scss b/app/assets/stylesheets/admin/tag_rules.css.scss index 7feedb7c50..a4bf6b7b2c 100644 --- a/app/assets/stylesheets/admin/tag_rules.css.scss +++ b/app/assets/stylesheets/admin/tag_rules.css.scss @@ -1,3 +1,11 @@ +tags-input { + &.limit-reached { + input, span.input { + display: none; + } + } +} + .no_tags { margin-bottom: 40px; color: #aeaeae; diff --git a/app/views/admin/enterprises/form/_tag_rules.html.haml b/app/views/admin/enterprises/form/_tag_rules.html.haml index fd34477c6c..e58f7c5a62 100644 --- a/app/views/admin/enterprises/form/_tag_rules.html.haml +++ b/app/views/admin/enterprises/form/_tag_rules.html.haml @@ -16,9 +16,7 @@ %h5 For customers tagged: %td - %tags-input{ ng: { model: 'tagGroup.tags'}, - min: { tags: "1" }, - on: { tag: { added: "updateTagsRulesFor(tagGroup)", removed: "updateTagsRulesFor(tagGroup)" } } } + %tags-with-translation{ object: "tagGroup", max: 1, on: { tag: { added: "updateTagsRulesFor(tagGroup)", removed: "updateTagsRulesFor(tagGroup)" } } } .no_rules{ ng: { show: "tagGroup.rules.length == 0" } } No rules apply to this tag yet diff --git a/spec/features/admin/tag_rules_spec.rb b/spec/features/admin/tag_rules_spec.rb index 7c1aa55f9e..63955de1f8 100644 --- a/spec/features/admin/tag_rules_spec.rb +++ b/spec/features/admin/tag_rules_spec.rb @@ -64,7 +64,7 @@ feature 'Tag Rules', js: true do click_button "Add Rule" within(".default_rules #tr_0") do find(:css, "tags-input .tags input").set "wholesale\n" - select2_select "NOT VISIBLE", from: "enterprise_tag_rules_attributes_0_preferred_matched_order_cycles_visibility" + expect(page).to have_content "not visible" end # New DiscountOrder Rule @@ -128,19 +128,21 @@ feature 'Tag Rules', js: true do expect(page).to have_selector '.customer_tag .header tags-input .tag-list ti-tag-item', text: "local", count: 1 expect(page).to have_selector '.customer_tag .header tags-input .tag-list ti-tag-item', text: "wholesale", count: 1 expect(page).to have_selector '.customer_tag .header tags-input .tag-list ti-tag-item', text: "trusted", count: 1 - all(:css, ".customer_tag .header tags-input .tags input").each { |node| node.set "volunteer\n" } + all(:css, ".customer_tag .header tags-input").each do |node| + node.find("li.tag-item a.remove-button").trigger('click') + node.find(".tags input").set "volunteer\n" + end # DEFAULT FilterShippingMethods rule within ".default_rules #tr_0" do - expect(first('tags-input .tag-list ti-tag-item')).to have_content "local" + within "li.tag-item", text: "local ✖" do find("a.remove-button").trigger('click') end find(:css, "tags-input .tags input").set "volunteers-only\n" - expect(page).to have_select2 "enterprise_tag_rules_attributes_0_preferred_matched_shipping_methods_visibility", selected: 'VISIBLE' - select2_select 'NOT VISIBLE', from: "enterprise_tag_rules_attributes_0_preferred_matched_shipping_methods_visibility" + expect(page).to have_content "not visible" end # FilterProducts rule within ".customer_tag #tr_1" do - expect(first('tags-input .tag-list ti-tag-item')).to have_content "member" + within "li.tag-item", text: "member ✖" do find("a.remove-button").trigger('click') end find(:css, "tags-input .tags input").set "volunteers-only1\n" expect(page).to have_select2 "enterprise_tag_rules_attributes_1_preferred_matched_variants_visibility", selected: 'VISIBLE' select2_select 'NOT VISIBLE', from: "enterprise_tag_rules_attributes_1_preferred_matched_variants_visibility" @@ -148,7 +150,7 @@ feature 'Tag Rules', js: true do # FilterPaymentMethods rule within ".customer_tag #tr_2" do - expect(first('tags-input .tag-list ti-tag-item')).to have_content "trusted" + within "li.tag-item", text: "trusted ✖" do find("a.remove-button").trigger('click') end find(:css, "tags-input .tags input").set "volunteers-only2\n" expect(page).to have_select2 "enterprise_tag_rules_attributes_2_preferred_matched_payment_methods_visibility", selected: 'NOT VISIBLE' select2_select 'VISIBLE', from: "enterprise_tag_rules_attributes_2_preferred_matched_payment_methods_visibility" @@ -156,7 +158,7 @@ feature 'Tag Rules', js: true do # FilterOrderCycles rule within ".customer_tag #tr_3" do - expect(first('tags-input .tag-list ti-tag-item')).to have_content "wholesale" + within "li.tag-item", text: "wholesale ✖" do find("a.remove-button").trigger('click') end find(:css, "tags-input .tags input").set "volunteers-only3\n" expect(page).to have_select2 "enterprise_tag_rules_attributes_3_preferred_matched_order_cycles_visibility", selected: 'VISIBLE' select2_select 'NOT VISIBLE', from: "enterprise_tag_rules_attributes_3_preferred_matched_order_cycles_visibility" @@ -164,7 +166,7 @@ feature 'Tag Rules', js: true do # FilterShippingMethods rule within ".customer_tag #tr_4" do - expect(first('tags-input .tag-list ti-tag-item')).to have_content "local" + within "li.tag-item", text: "local ✖" do find("a.remove-button").trigger('click') end find(:css, "tags-input .tags input").set "volunteers-only4\n" expect(page).to have_select2 "enterprise_tag_rules_attributes_4_preferred_matched_shipping_methods_visibility", selected: 'NOT VISIBLE' select2_select 'VISIBLE', from: "enterprise_tag_rules_attributes_4_preferred_matched_shipping_methods_visibility" @@ -183,31 +185,31 @@ feature 'Tag Rules', js: true do # DEFAULT FilterShippingMethods rule expect(default_fsm_tag_rule.reload.preferred_customer_tags).to eq "" - expect(default_fsm_tag_rule.preferred_shipping_method_tags).to eq "local,volunteers-only" + expect(default_fsm_tag_rule.preferred_shipping_method_tags).to eq "volunteers-only" expect(default_fsm_tag_rule.preferred_matched_shipping_methods_visibility).to eq "hidden" # FilterShippingMethods rule expect(fsm_tag_rule.reload.priority).to eq 1 - expect(fsm_tag_rule.preferred_customer_tags).to eq "local,volunteer" - expect(fsm_tag_rule.preferred_shipping_method_tags).to eq "local,volunteers-only4" + expect(fsm_tag_rule.preferred_customer_tags).to eq "volunteer" + expect(fsm_tag_rule.preferred_shipping_method_tags).to eq "volunteers-only4" expect(fsm_tag_rule.preferred_matched_shipping_methods_visibility).to eq "visible" # FilterProducts rule expect(fp_tag_rule.reload.priority).to eq 2 - expect(fp_tag_rule.preferred_customer_tags).to eq "member,volunteer" - expect(fp_tag_rule.preferred_variant_tags).to eq "member,volunteers-only1" + expect(fp_tag_rule.preferred_customer_tags).to eq "volunteer" + expect(fp_tag_rule.preferred_variant_tags).to eq "volunteers-only1" expect(fp_tag_rule.preferred_matched_variants_visibility).to eq "hidden" # FilterPaymentMethods rule expect(fpm_tag_rule.reload.priority).to eq 3 - expect(fpm_tag_rule.preferred_customer_tags).to eq "trusted,volunteer" - expect(fpm_tag_rule.preferred_payment_method_tags).to eq "trusted,volunteers-only2" + expect(fpm_tag_rule.preferred_customer_tags).to eq "volunteer" + expect(fpm_tag_rule.preferred_payment_method_tags).to eq "volunteers-only2" expect(fpm_tag_rule.preferred_matched_payment_methods_visibility).to eq "visible" # FilterOrderCycles rule expect(foc_tag_rule.reload.priority).to eq 4 - expect(foc_tag_rule.preferred_customer_tags).to eq "wholesale,volunteer" - expect(foc_tag_rule.preferred_exchange_tags).to eq "wholesale,volunteers-only3" + expect(foc_tag_rule.preferred_customer_tags).to eq "volunteer" + expect(foc_tag_rule.preferred_exchange_tags).to eq "volunteers-only3" expect(foc_tag_rule.preferred_matched_order_cycles_visibility).to eq "hidden" # DiscountOrder rule From 07384edb2d510f42d9c19ed580700cd4d167f280 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 27 May 2016 22:00:08 +1000 Subject: [PATCH 102/110] Can delete default tag rules --- .../admin/tag_rules/tag_rule.html.haml | 2 +- spec/features/admin/tag_rules_spec.rb | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml b/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml index 4427d284fd..7907b9dfda 100644 --- a/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml +++ b/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml @@ -40,7 +40,7 @@ %td %tags-with-translation{ object: "rule", max: 1, "tags-attr" => "{{opt[rule.type].tagsAttr}}", "tag-list-attr" => "{{opt[rule.type].tagListAttr}}" } %td.actions{ rowspan: 2 } - %a{ ng: { click: "deleteTagRule(tagGroup, rule)" }, :class => "delete-tag-rule icon-trash no-text" } + %a{ ng: { click: "deleteTagRule(tagGroup || defaultTagGroup, rule)" }, :class => "delete-tag-rule icon-trash no-text" } %tr %td %span.text-normal {{ opt[rule.type].textBottom }} diff --git a/spec/features/admin/tag_rules_spec.rb b/spec/features/admin/tag_rules_spec.rb index 63955de1f8..d956ff94b6 100644 --- a/spec/features/admin/tag_rules_spec.rb +++ b/spec/features/admin/tag_rules_spec.rb @@ -219,24 +219,23 @@ feature 'Tag Rules', js: true do end context "deleting" do - let!(:tag_rule) { create(:tag_rule, enterprise: enterprise, preferred_customer_tags: "member" ) } + let!(:tag_rule) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_customer_tags: "member" ) } + let!(:default_rule) { create(:filter_products_tag_rule, is_default: true, enterprise: enterprise ) } before do login_to_admin_section visit main_app.edit_admin_enterprise_path(enterprise) end - it "deletes rules from the database" do + it "deletes both default and customer rules from the database" do click_link "Tag Rules" - expect(page).to have_selector "#tr_0" - - expect{ - within "#tr_0" do - first("a.delete-tag-rule").click - end + expect do + within "#tr_1" do first("a.delete-tag-rule").click end + expect(page).to_not have_selector "#tr_1" + within "#tr_0" do first("a.delete-tag-rule").click end expect(page).to_not have_selector "#tr_0" - }.to change{TagRule.count}.by(-1) + end.to change{TagRule.count}.by(-2) end end end From 7028fbe288dfe53462818002fcd5272d4bb4a9aa Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Fri, 27 May 2016 23:26:31 +1000 Subject: [PATCH 103/110] Find current_customer via current_distributor and current_user rather than current_order Moving available_payment_methods to enterprises_helper, as per available_shipping_methods --- app/controllers/base_controller.rb | 3 +- app/controllers/shop_controller.rb | 3 +- app/helpers/enterprises_helper.rb | 19 ++- app/helpers/injection_helper.rb | 2 +- app/models/spree/order_decorator.rb | 16 -- app/models/spree/user_decorator.rb | 3 +- app/views/checkout/_order.rabl | 44 ----- app/views/checkout/_payment.html.haml | 2 +- .../enterprises_controller_spec.rb | 13 +- spec/controllers/shop_controller_spec.rb | 141 +++++++-------- .../consumer/shopping/checkout_spec.rb | 15 +- spec/helpers/enterprises_helper_spec.rb | 160 ++++++++++++++++-- spec/helpers/injection_helper_spec.rb | 13 +- spec/models/spree/order_spec.rb | 100 ----------- 14 files changed, 274 insertions(+), 260 deletions(-) delete mode 100644 app/views/checkout/_order.rabl diff --git a/app/controllers/base_controller.rb b/app/controllers/base_controller.rb index 7bcb8bf3c9..58bf679dc3 100644 --- a/app/controllers/base_controller.rb +++ b/app/controllers/base_controller.rb @@ -21,8 +21,7 @@ class BaseController < ApplicationController @order_cycles = OrderCycle.with_distributor(@distributor).active .order(@distributor.preferred_shopfront_order_cycle_order) - customer_tags = current_order.andand.customer.andand.tag_list - applicator = OpenFoodNetwork::TagRuleApplicator.new(@distributor, "FilterOrderCycles", customer_tags) + applicator = OpenFoodNetwork::TagRuleApplicator.new(@distributor, "FilterOrderCycles", current_customer.andand.tag_list) applicator.filter!(@order_cycles) # And default to the only order cycle if there's only the one diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index e244a2a189..091a501ca5 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -55,7 +55,6 @@ class ShopController < BaseController def applicator return @applicator unless @applicator.nil? - customer_tags = current_order.andand.customer.andand.tag_list - @applicator = OpenFoodNetwork::TagRuleApplicator.new(current_distributor, "FilterProducts", customer_tags) + @applicator = OpenFoodNetwork::TagRuleApplicator.new(current_distributor, "FilterProducts", current_customer.andand.tag_list) end end diff --git a/app/helpers/enterprises_helper.rb b/app/helpers/enterprises_helper.rb index 886546585c..cc2b966b01 100644 --- a/app/helpers/enterprises_helper.rb +++ b/app/helpers/enterprises_helper.rb @@ -3,16 +3,31 @@ module EnterprisesHelper @current_distributor ||= current_order(false).andand.distributor end + def current_customer + return nil unless spree_current_user && current_distributor + @current_customer ||= spree_current_user.customer_of(current_distributor) + end + def available_shipping_methods + return [] unless current_distributor.present? shipping_methods = current_distributor.shipping_methods - customer_tags = current_order.andand.customer.andand.tag_list - applicator = OpenFoodNetwork::TagRuleApplicator.new(current_distributor, "FilterShippingMethods", customer_tags) + applicator = OpenFoodNetwork::TagRuleApplicator.new(current_distributor, "FilterShippingMethods", current_customer.andand.tag_list) applicator.filter!(shipping_methods) shipping_methods.uniq end + def available_payment_methods + return [] unless current_distributor.present? + payment_methods = current_distributor.payment_methods.available(:front_end).all + + applicator = OpenFoodNetwork::TagRuleApplicator.new(current_distributor, "FilterPaymentMethods", current_customer.andand.tag_list) + applicator.filter!(payment_methods) + + payment_methods + end + def managed_enterprises Enterprise.managed_by(spree_current_user) end diff --git a/app/helpers/injection_helper.rb b/app/helpers/injection_helper.rb index 2d42d43429..50b9c93fca 100644 --- a/app/helpers/injection_helper.rb +++ b/app/helpers/injection_helper.rb @@ -23,7 +23,7 @@ module InjectionHelper end def inject_available_payment_methods - inject_json_ams "paymentMethods", current_order.available_payment_methods, + inject_json_ams "paymentMethods", available_payment_methods, Api::PaymentMethodSerializer, current_order: current_order end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 9125d33991..a4995274b3 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -207,18 +207,6 @@ Spree::Order.class_eval do adjustments.payment_fee.map(&:amount).sum end - # Show payment methods for this distributor - def available_payment_methods - return [] unless distributor.present? - payment_methods = distributor.payment_methods.available(:front_end).all - - customer_tags = customer.andand.tag_list - applicator = OpenFoodNetwork::TagRuleApplicator.new(distributor, "FilterPaymentMethods", customer_tags) - applicator.filter!(payment_methods) - - payment_methods - end - # Does this order have shipments that can be shipped? def ready_to_ship? self.shipments.any?{|s| s.can_ship?} @@ -231,10 +219,6 @@ Spree::Order.class_eval do end end - def available_shipping_methods(display_on = nil) - Spree::ShippingMethod.all_available(self, display_on) - end - def shipping_tax adjustments(:reload).shipping.sum &:included_tax end diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index afd32bd294..562b0d10ca 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -38,7 +38,8 @@ Spree.user_class.class_eval do end def customer_of(enterprise) - customers.of(enterprise).first + return nil unless enterprise + customers.find_by_enterprise_id(enterprise) end def send_signup_confirmation diff --git a/app/views/checkout/_order.rabl b/app/views/checkout/_order.rabl deleted file mode 100644 index 778f4428d0..0000000000 --- a/app/views/checkout/_order.rabl +++ /dev/null @@ -1,44 +0,0 @@ -#NOTE: when adding new fields for user input, it may want to be cached in localStorage -# If so, make sure to add it to controller attribute caching - -object current_order -attributes :id, :email, :shipping_method_id, :user_id - -node :display_total do - current_order.display_total.money.to_f -end - -node :payment_method_id do - current_order.payments.first.andand.payment_method_id -end - -child current_order.bill_address => :bill_address do - attributes :phone, :firstname, :lastname, :address1, :address2, :city, :country_id, :state_id, :zipcode -end - -child current_order.ship_address => :ship_address do - attributes :phone, :firstname, :lastname, :address1, :address2, :city, :country_id, :state_id, :zipcode -end - -# This is actually totally decoupled data and should be injected separately into their -# own services - -node :shipping_methods do - Hash[current_distributor.shipping_methods.uniq.collect { |method| - [method.id, { - require_ship_address: method.require_ship_address, - price: method.compute_amount(current_order).to_f, - name: method.name, - description: method.description - }] - }] -end - -node :payment_methods do - Hash[current_order.available_payment_methods.collect { - |method| [method.id, { - name: method.name, - method_type: method.method_type - }] - }] -end diff --git a/app/views/checkout/_payment.html.haml b/app/views/checkout/_payment.html.haml index c0578a8a4c..8d387145d5 100644 --- a/app/views/checkout/_payment.html.haml +++ b/app/views/checkout/_payment.html.haml @@ -17,7 +17,7 @@ -# The problem being how to render the partials .row .small-6.columns - - current_order.available_payment_methods.each do |method| + - available_payment_methods.each do |method| .row .small-12.columns %label diff --git a/spec/controllers/enterprises_controller_spec.rb b/spec/controllers/enterprises_controller_spec.rb index 24837ceed9..a4e66313dd 100644 --- a/spec/controllers/enterprises_controller_spec.rb +++ b/spec/controllers/enterprises_controller_spec.rb @@ -32,13 +32,10 @@ describe EnterprisesController do end context "using FilterOrderCycles tag rules" do + let(:user) { create(:user) } let!(:order_cycle3) { create(:simple_order_cycle, distributors: [distributor], orders_open_at: 3.days.ago, orders_close_at: 4.days.from_now) } let!(:oc3_exchange) { order_cycle3.exchanges.outgoing.to_enterprise(distributor).first } - let!(:customer) { create(:customer, enterprise: distributor) } - - before do - order.update_attribute(:customer_id, customer.id) - end + let(:customer) { create(:customer, user: user, enterprise: distributor) } it "shows order cycles allowed by the rules" do create(:filter_order_cycles_tag_rule, @@ -55,6 +52,11 @@ describe EnterprisesController do spree_get :shop, {id: distributor} expect(assigns(:order_cycles)).to include order_cycle1, order_cycle2, order_cycle3 + allow(controller).to receive(:spree_current_user) { user } + + spree_get :shop, {id: distributor} + expect(assigns(:order_cycles)).to include order_cycle1, order_cycle2, order_cycle3 + oc3_exchange.update_attribute(:tag_list, "wholesale") spree_get :shop, {id: distributor} @@ -62,7 +64,6 @@ describe EnterprisesController do expect(assigns(:order_cycles)).not_to include order_cycle3 customer.update_attribute(:tag_list, ["wholesale"]) - order.reload spree_get :shop, {id: distributor} expect(assigns(:order_cycles)).to include order_cycle1, order_cycle2, order_cycle3 diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index 777050847d..c3784df428 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -132,86 +132,95 @@ describe ShopController do end end - context "when FilterProducts tag rules are in effect" do - let!(:tagged_customer) { create(:customer, enterprise: distributor, tag_list: "member") } - let!(:untagged_customer) { create(:customer, enterprise: distributor, tag_list: "") } - let!(:order) { create(:order, distributor: distributor) } - let!(:tag_rule) { create(:filter_products_tag_rule, - enterprise: distributor, - preferred_customer_tags: "member", - preferred_variant_tags: "members-only") } - let!(:default_tag_rule) { create(:filter_products_tag_rule, - enterprise: distributor, - is_default: true, - preferred_variant_tags: "members-only") } - let(:product1) { { "id" => 1, "name" => 'product 1', "variants" => [{ "id" => 4, "tag_list" => ["members-only"] }] } } - let(:product2) { { "id" => 2, "name" => 'product 2', "variants" => [{ "id" => 5, "tag_list" => ["members-only"] }, {"id" => 9, "tag_list" => ["something"]}] } } - let(:product3) { { "id" => 3, "name" => 'product 3', "variants" => [{ "id" => 6, "tag_list" => ["something-else"] }] } } - let(:product2_without_v5) { { "id" => 2, "name" => 'product 2', "variants" => [{"id" => 9, "tag_list" => ["something"]}] } } - let!(:products_array) { [product1, product2, product3] } - let!(:products_json) { JSON.unparse( products_array ) } + describe "loading available order cycles" do + let(:user) { create(:user) } + before { allow(controller).to receive(:spree_current_user) { user } } - before do - allow(controller).to receive(:current_order) { order } - end + context "when FilterProducts tag rules are in effect" do + let(:customer) { create(:customer, user: user, enterprise: distributor) } + let!(:tag_rule) { create(:filter_products_tag_rule, + enterprise: distributor, + preferred_customer_tags: "member", + preferred_variant_tags: "members-only") } + let!(:default_tag_rule) { create(:filter_products_tag_rule, + enterprise: distributor, + is_default: true, + preferred_variant_tags: "members-only") } + let(:product1) { { "id" => 1, "name" => 'product 1', "variants" => [{ "id" => 4, "tag_list" => ["members-only"] }] } } + let(:product2) { { "id" => 2, "name" => 'product 2', "variants" => [{ "id" => 5, "tag_list" => ["members-only"] }, {"id" => 9, "tag_list" => ["something"]}] } } + let(:product3) { { "id" => 3, "name" => 'product 3', "variants" => [{ "id" => 6, "tag_list" => ["something-else"] }] } } + let(:product2_without_v5) { { "id" => 2, "name" => 'product 2', "variants" => [{"id" => 9, "tag_list" => ["something"]}] } } + let!(:products_array) { [product1, product2, product3] } + let!(:products_json) { JSON.unparse( products_array ) } - context "with a preferred visiblity of 'visible', default visibility of 'hidden'" do - before { tag_rule.update_attribute(:preferred_matched_variants_visibility, 'visible') } - before { default_tag_rule.update_attribute(:preferred_matched_variants_visibility, 'hidden') } + before do + allow(controller).to receive(:current_order) { order } + end - let(:filtered_products) { JSON.parse(controller.send(:filter, products_json)) } + context "with a preferred visiblity of 'visible', default visibility of 'hidden'" do + before { tag_rule.update_attribute(:preferred_matched_variants_visibility, 'visible') } + before { default_tag_rule.update_attribute(:preferred_matched_variants_visibility, 'hidden') } - context "when the customer is nil" do - it "applies default action (hide)" do - expect(filtered_products).to include product2_without_v5, product3 - expect(filtered_products).to_not include product1, product2 + let(:filtered_products) { JSON.parse(controller.send(:filter, products_json)) } + + context "when the customer is nil" do + it "applies default action (hide)" do + expect(controller.current_customer).to be nil + expect(filtered_products).to include product2_without_v5, product3 + expect(filtered_products).to_not include product1, product2 + end + end + + context "when the customer's tags match" do + before { customer.update_attribute(:tag_list, 'member') } + + it "applies the action (show)" do + expect(controller.current_customer).to eq customer + expect(filtered_products).to include product1, product2, product3 + end + end + + context "when the customer's tags don't match" do + before { customer.update_attribute(:tag_list, 'something') } + + it "applies the default action (hide)" do + expect(controller.current_customer).to eq customer + expect(filtered_products).to include product2_without_v5, product3 + expect(filtered_products).to_not include product1, product2 + end end end - context "when the customer's tags match" do - before { order.update_attribute(:customer_id, tagged_customer.id) } + context "with a preferred visiblity of 'hidden', default visibility of 'visible'" do + before { tag_rule.update_attribute(:preferred_matched_variants_visibility, 'hidden') } + before { default_tag_rule.update_attribute(:preferred_matched_variants_visibility, 'visible') } - it "applies the action (show)" do - expect(filtered_products).to include product1, product2, product3 + let(:filtered_products) { JSON.parse(controller.send(:filter, products_json)) } + + context "when the customer is nil" do + it "applies default action (show)" do + expect(controller.current_customer).to be nil + expect(filtered_products).to include product1, product2, product3 + end end - end - context "when the customer's tags don't match" do - before { order.update_attribute(:customer_id, untagged_customer.id) } + context "when the customer's tags match" do + before { customer.update_attribute(:tag_list, 'member') } - it "applies the default action (hide)" do - expect(filtered_products).to include product2_without_v5, product3 - expect(filtered_products).to_not include product1, product2 + it "applies the action (hide)" do + expect(controller.current_customer).to eq customer + expect(filtered_products).to include product2_without_v5, product3 + expect(filtered_products).to_not include product1, product2 + end end - end - end - context "with a preferred visiblity of 'hidden', default visibility of 'visible'" do - before { tag_rule.update_attribute(:preferred_matched_variants_visibility, 'hidden') } - before { default_tag_rule.update_attribute(:preferred_matched_variants_visibility, 'visible') } + context "when the customer's tags don't match" do + before { customer.update_attribute(:tag_list, 'something') } - let(:filtered_products) { JSON.parse(controller.send(:filter, products_json)) } - - context "when the customer is nil" do - it "applies default action (show)" do - expect(filtered_products).to include product1, product2, product3 - end - end - - context "when the customer's tags match" do - before { order.update_attribute(:customer_id, tagged_customer.id) } - - it "applies the action (hide)" do - expect(filtered_products).to include product2_without_v5, product3 - expect(filtered_products).to_not include product1, product2 - end - end - - context "when the customer's tags don't match" do - before { order.update_attribute(:customer_id, untagged_customer.id) } - - it "applies the default action (show)" do - expect(filtered_products).to include product1, product2, product3 + it "applies the default action (show)" do + expect(controller.current_customer).to eq customer + expect(filtered_products).to include product1, product2, product3 + end end end end diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index e5e4871df0..cbb4d12554 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -106,6 +106,9 @@ feature "As a consumer I want to check out my cart", js: true do end context "using FilterShippingMethods" do + let(:user) { create(:user) } + let(:customer) { create(:customer, user: user, enterprise: distributor) } + it "shows shipping methods allowed by the rule" do # No rules in effect toggle_shipping @@ -131,10 +134,16 @@ feature "As a consumer I want to check out my cart", js: true do page.should have_content "Donkeys" page.should_not have_content "Local" - customer = create(:customer, enterprise: distributor, tag_list: "local") - order.update_attribute(:customer_id, customer.id) + quick_login_as(user) + visit checkout_path + + # Default rule in still effect, disallows access to 'Local' + page.should have_content "Frogs" + page.should have_content "Donkeys" + page.should_not have_content "Local" + + customer.update_attribute(:tag_list, "local") visit checkout_path - checkout_as_guest # #local Customer can access 'Local' shipping method page.should have_content "Frogs" diff --git a/spec/helpers/enterprises_helper_spec.rb b/spec/helpers/enterprises_helper_spec.rb index 7a9028e5d7..8e8fa3ed5e 100644 --- a/spec/helpers/enterprises_helper_spec.rb +++ b/spec/helpers/enterprises_helper_spec.rb @@ -1,13 +1,37 @@ require 'spec_helper' describe EnterprisesHelper do + let(:user) { create(:user) } + let(:distributor) { create(:distributor_enterprise) } + let(:some_other_distributor) { create(:distributor_enterprise) } + + before { allow(helper).to receive(:spree_current_user) { user } } + describe "loading available shipping methods" do + let!(:sm1) { create(:shipping_method, require_ship_address: false, distributors: [distributor]) } + let!(:sm2) { create(:shipping_method, require_ship_address: false, distributors: [some_other_distributor]) } + + context "when the order has no current_distributor" do + before do + allow(helper).to receive(:current_distributor) { nil } + end + + it "returns an empty array" do + expect(helper.available_shipping_methods).to eq [] + end + end + + context "when no tag rules are in effect" do + before { allow(helper).to receive(:current_distributor) { distributor } } + + it "finds the shipping methods for the current distributor" do + expect(helper.available_shipping_methods).to_not include sm2 + expect(helper.available_shipping_methods).to include sm1 + end + end context "when FilterShippingMethods tag rules are in effect" do - let!(:distributor) { create(:distributor_enterprise) } - let!(:tagged_customer) { create(:customer, enterprise: distributor, tag_list: "local") } - let!(:untagged_customer) { create(:customer, enterprise: distributor, tag_list: "") } - let!(:order) { create(:order, distributor: distributor) } + let(:customer) { create(:customer, user: user, enterprise: distributor) } let!(:tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: distributor, preferred_customer_tags: "local", @@ -16,12 +40,13 @@ describe EnterprisesHelper do enterprise: distributor, is_default: true, preferred_shipping_method_tags: "local-delivery") } - let!(:tagged_sm) { create(:shipping_method, require_ship_address: false, name: "Untagged", tag_list: "local-delivery") } - let!(:untagged_sm) { create(:shipping_method, require_ship_address: false, name: "Tagged", tag_list: "") } + let!(:tagged_sm) { sm1 } + let!(:untagged_sm) { sm2 } before do + tagged_sm.update_attribute(:tag_list, 'local-delivery') distributor.shipping_methods = [tagged_sm, untagged_sm] - allow(helper).to receive(:current_order) { order } + allow(helper).to receive(:current_distributor) { distributor } end context "with a preferred visiblity of 'visible', default visibility of 'hidden'" do @@ -30,23 +55,26 @@ describe EnterprisesHelper do context "when the customer is nil" do it "applies default action (hide)" do + expect(helper.current_customer).to be nil expect(helper.available_shipping_methods).to include untagged_sm expect(helper.available_shipping_methods).to_not include tagged_sm end end context "when the customer's tags match" do - before { order.update_attribute(:customer_id, tagged_customer.id) } + before { customer.update_attribute(:tag_list, 'local') } it "applies the action (show)" do + expect(helper.current_customer).to eq customer expect(helper.available_shipping_methods).to include tagged_sm, untagged_sm end end context "when the customer's tags don't match" do - before { order.update_attribute(:customer_id, untagged_customer.id) } + before { customer.update_attribute(:tag_list, 'something') } it "applies the default action (hide)" do + expect(helper.current_customer).to eq customer expect(helper.available_shipping_methods).to include untagged_sm expect(helper.available_shipping_methods).to_not include tagged_sm end @@ -59,27 +87,137 @@ describe EnterprisesHelper do context "when the customer is nil" do it "applies default action (show)" do + expect(helper.current_customer).to be nil expect(helper.available_shipping_methods).to include tagged_sm, untagged_sm end end context "when the customer's tags match" do - before { order.update_attribute(:customer_id, tagged_customer.id) } + before { customer.update_attribute(:tag_list, 'local') } it "applies the action (hide)" do + expect(helper.current_customer).to eq customer expect(helper.available_shipping_methods).to include untagged_sm expect(helper.available_shipping_methods).to_not include tagged_sm end end context "when the customer's tags don't match" do - before { order.update_attribute(:customer_id, untagged_customer.id) } + before { customer.update_attribute(:tag_list, 'something') } it "applies the default action (show)" do + expect(helper.current_customer).to eq customer expect(helper.available_shipping_methods).to include tagged_sm, untagged_sm end end end end end + + describe "loading available payment methods" do + let!(:pm1) { create(:payment_method, distributors: [distributor])} + let!(:pm2) { create(:payment_method, distributors: [some_other_distributor])} + + context "when the order has no current_distributor" do + before do + allow(helper).to receive(:current_distributor) { nil } + end + + it "returns an empty array" do + expect(helper.available_payment_methods).to eq [] + end + end + + context "when no tag rules are in effect" do + before { allow(helper).to receive(:current_distributor) { distributor } } + + it "finds the payment methods for the current distributor" do + expect(helper.available_payment_methods).to_not include pm2 + expect(helper.available_payment_methods).to include pm1 + end + end + + context "when FilterPaymentMethods tag rules are in effect" do + let(:customer) { create(:customer, user: user, enterprise: distributor) } + let!(:tag_rule) { create(:filter_payment_methods_tag_rule, + enterprise: distributor, + preferred_customer_tags: "trusted", + preferred_payment_method_tags: "trusted") } + let!(:default_tag_rule) { create(:filter_payment_methods_tag_rule, + enterprise: distributor, + is_default: true, + preferred_payment_method_tags: "trusted") } + let(:tagged_pm) { pm1 } + let(:untagged_pm) { pm2 } + + before do + tagged_pm.update_attribute(:tag_list, 'trusted') + distributor.payment_methods = [tagged_pm, untagged_pm] + allow(helper).to receive(:current_distributor) { distributor } + end + + context "with a preferred visiblity of 'visible', default visibility of 'hidden'" do + before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, 'visible') } + before { default_tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, 'hidden') } + + context "when the customer is nil" do + it "applies default action (hide)" do + expect(helper.current_customer).to be nil + expect(helper.available_payment_methods).to include untagged_pm + expect(helper.available_payment_methods).to_not include tagged_pm + end + end + + context "when the customer's tags match" do + before { customer.update_attribute(:tag_list, 'trusted') } + + it "applies the action (show)" do + expect(helper.current_customer).to eq customer + expect(helper.available_payment_methods).to include tagged_pm, untagged_pm + end + end + + context "when the customer's tags don't match" do + before { customer.update_attribute(:tag_list, 'something') } + + it "applies the default action (hide)" do + expect(helper.current_customer).to eq customer + expect(helper.available_payment_methods).to include untagged_pm + expect(helper.available_payment_methods).to_not include tagged_pm + end + end + end + + context "with a preferred visiblity of 'hidden', default visibility of 'visible'" do + before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, 'hidden') } + before { default_tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, 'visible') } + + context "when the customer is nil" do + it "applies default action (show)" do + expect(helper.current_customer).to be nil + expect(helper.available_payment_methods).to include tagged_pm, untagged_pm + end + end + + context "when the customer's tags match" do + before { customer.update_attribute(:tag_list, 'trusted') } + + it "applies the action (hide)" do + expect(helper.current_customer).to eq customer + expect(helper.available_payment_methods).to include untagged_pm + expect(helper.available_payment_methods).to_not include tagged_pm + end + end + + context "when the customer's tags don't match" do + before { customer.update_attribute(:tag_list, 'something') } + + it "applies the default action (show)" do + expect(helper.current_customer).to eq customer + expect(helper.available_payment_methods).to include tagged_pm, untagged_pm + end + end + end + end + end end diff --git a/spec/helpers/injection_helper_spec.rb b/spec/helpers/injection_helper_spec.rb index e7d775c657..acec197b20 100644 --- a/spec/helpers/injection_helper_spec.rb +++ b/spec/helpers/injection_helper_spec.rb @@ -26,17 +26,20 @@ describe InjectionHelper do it "injects shipping_methods" do sm = create(:shipping_method) - helper.stub(:current_order).and_return order = create(:order) - shipping_methods = double(:shipping_methods, uniq: [sm]) - current_distributor = double(:distributor, shipping_methods: shipping_methods) - allow(helper).to receive(:current_distributor) { current_distributor } + current_distributor = create(:distributor_enterprise, shipping_methods: [sm]) + order = create(:order, distributor: current_distributor) + allow(helper).to receive(:current_order) { order } + allow(helper).to receive(:spree_current_user) { nil } helper.inject_available_shipping_methods.should match sm.id.to_s helper.inject_available_shipping_methods.should match sm.compute_amount(order).to_s end it "injects payment methods" do pm = create(:payment_method) - helper.stub_chain(:current_order, :available_payment_methods).and_return [pm] + current_distributor = create(:distributor_enterprise, payment_methods: [pm]) + order = create(:order, distributor: current_distributor) + allow(helper).to receive(:current_order) { order } + allow(helper).to receive(:spree_current_user) { nil } helper.inject_available_payment_methods.should match pm.id.to_s helper.inject_available_payment_methods.should match pm.name end diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 051c78896d..114c7d5fac 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -21,106 +21,6 @@ describe Spree::Order do end end - describe "Payment methods" do - let(:distributor) { create(:distributor_enterprise) } - let(:some_other_distributor) { create(:distributor_enterprise) } - let(:order) { create(:order, distributor: distributor) } - let!(:pm1) { create(:payment_method, distributors: [distributor])} - let!(:pm2) { create(:payment_method, distributors: [some_other_distributor])} - - context "when the order has no distributor" do - let(:order_without_distributor) { create(:order, distributor: nil) } - - it "returns an empty array" do - expect(order_without_distributor.available_payment_methods).to eq [] - end - end - - context "when no tag rules are in effect" do - it "finds the payment methods for the current distributor" do - order.available_payment_methods.include?(pm2).should == false - order.available_payment_methods.include?(pm1).should == true - end - end - - context "when FilterPaymentMethods tag rules are in effect" do - let!(:tagged_customer) { create(:customer, enterprise: distributor, tag_list: "trusted") } - let!(:untagged_customer) { create(:customer, enterprise: distributor, tag_list: "") } - let!(:tag_rule) { create(:filter_payment_methods_tag_rule, - enterprise: distributor, - preferred_customer_tags: "trusted", - preferred_payment_method_tags: "trusted") } - let!(:default_tag_rule) { create(:filter_payment_methods_tag_rule, - enterprise: distributor, - is_default: true, - preferred_payment_method_tags: "trusted") } - let(:tagged_pm) { pm1 } - let(:untagged_pm) { pm2 } - - before do - tagged_pm.update_attribute(:tag_list, 'trusted') - distributor.payment_methods = [tagged_pm, untagged_pm] - end - - context "with a preferred visiblity of 'visible', default visibility of 'hidden'" do - before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, 'visible') } - before { default_tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, 'hidden') } - - context "when the customer is nil" do - it "applies default action (hide)" do - expect(order.available_payment_methods).to include untagged_pm - expect(order.available_payment_methods).to_not include tagged_pm - end - end - - context "when the customer's tags match" do - before { order.update_attribute(:customer_id, tagged_customer.id) } - - it "applies the action (show)" do - expect(order.available_payment_methods).to include tagged_pm, untagged_pm - end - end - - context "when the customer's tags don't match" do - before { order.update_attribute(:customer_id, untagged_customer.id) } - - it "applies the default action (hide)" do - expect(order.available_payment_methods).to include untagged_pm - expect(order.available_payment_methods).to_not include tagged_pm - end - end - end - - context "with a preferred visiblity of 'hidden', default visibility of 'visible'" do - before { tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, 'hidden') } - before { default_tag_rule.update_attribute(:preferred_matched_payment_methods_visibility, 'visible') } - - context "when the customer is nil" do - it "applies default action (show)" do - expect(order.available_payment_methods).to include tagged_pm, untagged_pm - end - end - - context "when the customer's tags match" do - before { order.update_attribute(:customer_id, tagged_customer.id) } - - it "applies the action (hide)" do - expect(order.available_payment_methods).to include untagged_pm - expect(order.available_payment_methods).to_not include tagged_pm - end - end - - context "when the customer's tags don't match" do - before { order.update_attribute(:customer_id, untagged_customer.id) } - - it "applies the default action (show)" do - expect(order.available_payment_methods).to include tagged_pm, untagged_pm - end - end - end - end - end - describe "updating the distribution charge" do let(:order) { build(:order) } From 98f8f7b89fd67420662a8ffd151661f067c2fc31 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Sat, 28 May 2016 01:16:07 +1000 Subject: [PATCH 104/110] Adding new help-modal dialog, use for explaining tag rule UI --- .../directives/new_customer_dialog.js.coffee | 13 ++----------- .../directives/new_rule_dialog.js.coffee | 13 ++----------- .../utils/directives/help-modal.js.coffee | 17 +++++++++++++++++ .../utils/services/dialog_defaults.js.coffee | 10 ++++++++++ .../javascripts/admin/utils/utils.js.coffee | 2 +- .../admin/modals/tag_rule_help.html.haml | 18 ++++++++++++++++++ app/assets/stylesheets/admin/dialog.css.scss | 5 +++++ .../stylesheets/admin/tag_rules.css.scss | 3 +++ .../form/tag_rules/_default_rules.html.haml | 3 ++- 9 files changed, 60 insertions(+), 24 deletions(-) create mode 100644 app/assets/javascripts/admin/utils/directives/help-modal.js.coffee create mode 100644 app/assets/javascripts/admin/utils/services/dialog_defaults.js.coffee create mode 100644 app/assets/javascripts/templates/admin/modals/tag_rule_help.html.haml create mode 100644 app/assets/stylesheets/admin/dialog.css.scss diff --git a/app/assets/javascripts/admin/customers/directives/new_customer_dialog.js.coffee b/app/assets/javascripts/admin/customers/directives/new_customer_dialog.js.coffee index 02276875b9..46292ae85c 100644 --- a/app/assets/javascripts/admin/customers/directives/new_customer_dialog.js.coffee +++ b/app/assets/javascripts/admin/customers/directives/new_customer_dialog.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.customers").directive 'newCustomerDialog', ($compile, $injector, $templateCache, $window, CurrentShop, Customers) -> +angular.module("admin.customers").directive 'newCustomerDialog', ($compile, $injector, $templateCache, DialogDefaults, CurrentShop, Customers) -> restrict: 'A' scope: true link: (scope, element, attr) -> @@ -27,16 +27,7 @@ angular.module("admin.customers").directive 'newCustomerDialog', ($compile, $inj template = $compile($templateCache.get('admin/new_customer_dialog.html'))(scope) # Set Dialog options - template.dialog - show: { effect: "fade", duration: 400 } - hide: { effect: "fade", duration: 300 } - autoOpen: false - resizable: false - width: $window.innerWidth * 0.4; - modal: true - open: (event, ui) -> - $('.ui-widget-overlay').bind 'click', -> - $(this).siblings('.ui-dialog').find('.ui-dialog-content').dialog('close') + template.dialog(DialogDefaults) # Link opening of dialog to click event on element element.bind 'click', (e) -> diff --git a/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee b/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee index df09bfd74b..1d3cb02f7a 100644 --- a/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee +++ b/app/assets/javascripts/admin/tag_rules/directives/new_rule_dialog.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.tagRules").directive 'newTagRuleDialog', ($compile, $templateCache, $window) -> +angular.module("admin.tagRules").directive 'newTagRuleDialog', ($compile, $templateCache, DialogDefaults) -> restrict: 'A' scope: tagGroup: '=' @@ -18,16 +18,7 @@ angular.module("admin.tagRules").directive 'newTagRuleDialog', ($compile, $templ scope.ruleType = scope.ruleTypes[0].id # Set Dialog options - template.dialog - show: { effect: "fade", duration: 400 } - hide: { effect: "fade", duration: 300 } - autoOpen: false - resizable: false - width: $window.innerWidth * 0.4; - modal: true - open: (event, ui) -> - $('.ui-widget-overlay').bind 'click', -> - $(this).siblings('.ui-dialog').find('.ui-dialog-content').dialog('close') + template.dialog(DialogDefaults) # Link opening of dialog to click event on element element.bind 'click', (e) -> diff --git a/app/assets/javascripts/admin/utils/directives/help-modal.js.coffee b/app/assets/javascripts/admin/utils/directives/help-modal.js.coffee new file mode 100644 index 0000000000..b458f85179 --- /dev/null +++ b/app/assets/javascripts/admin/utils/directives/help-modal.js.coffee @@ -0,0 +1,17 @@ +angular.module("admin.utils").directive 'helpModal', ($compile, $templateCache, $window, DialogDefaults) -> + restrict: 'C' + scope: + template: '@' + link: (scope, element, attr) -> + # Compile modal template + template = $compile($templateCache.get(scope.template))(scope) + + # Load Dialog Options + template.dialog(DialogDefaults) + + # Link opening of dialog to click event on element + element.bind 'click', (e) -> template.dialog('open') + + scope.close = -> + template.dialog('close') + return diff --git a/app/assets/javascripts/admin/utils/services/dialog_defaults.js.coffee b/app/assets/javascripts/admin/utils/services/dialog_defaults.js.coffee new file mode 100644 index 0000000000..7afb65e82d --- /dev/null +++ b/app/assets/javascripts/admin/utils/services/dialog_defaults.js.coffee @@ -0,0 +1,10 @@ +angular.module("admin.utils").factory "DialogDefaults", ($window) -> + show: { effect: "fade", duration: 400 } + hide: { effect: "fade", duration: 300 } + autoOpen: false + resizable: false + width: $window.innerWidth * 0.4; + modal: true + open: (event, ui) -> + $('.ui-widget-overlay').bind 'click', -> + $(this).siblings('.ui-dialog').find('.ui-dialog-content').dialog('close') diff --git a/app/assets/javascripts/admin/utils/utils.js.coffee b/app/assets/javascripts/admin/utils/utils.js.coffee index 094d3a5849..0b04480ff3 100644 --- a/app/assets/javascripts/admin/utils/utils.js.coffee +++ b/app/assets/javascripts/admin/utils/utils.js.coffee @@ -1 +1 @@ -angular.module("admin.utils", ["ngSanitize"]) \ No newline at end of file +angular.module("admin.utils", ["templates", "ngSanitize"]) \ No newline at end of file diff --git a/app/assets/javascripts/templates/admin/modals/tag_rule_help.html.haml b/app/assets/javascripts/templates/admin/modals/tag_rule_help.html.haml new file mode 100644 index 0000000000..6a0d720a5f --- /dev/null +++ b/app/assets/javascripts/templates/admin/modals/tag_rule_help.html.haml @@ -0,0 +1,18 @@ +#tag-rule-help + .margin-bottom-30.text-center + .text-big Tag Rules + + .margin-bottom-30 + .text-normal Overview + %p Tag rules provide a way to describe which items are visible or otherwise to which customers. Items can be Shipping Methods, Payment Methods, Products and Order Cycles. + + .margin-bottom-30 + .text-normal 'By Default...' Rules + %p Default rules allow you to hide items so that they are not visible by default. This behaviour can then be overriden by non-default rules for customers with particular tags. + + .margin-bottom-30 + .text-normal 'Customers Tagged...' Rules + %p By creating rules related to a specific customer tag, you can override the default behaviour (whether it be to show or to hide items) for customers with the specified tag. + + .text-center + %input.button.red.icon-plus{ type: 'button', value: 'Got it', ng: { click: 'close()' } } diff --git a/app/assets/stylesheets/admin/dialog.css.scss b/app/assets/stylesheets/admin/dialog.css.scss new file mode 100644 index 0000000000..832270d07d --- /dev/null +++ b/app/assets/stylesheets/admin/dialog.css.scss @@ -0,0 +1,5 @@ +.ui-dialog { + p { + line-height: 2; + } +} diff --git a/app/assets/stylesheets/admin/tag_rules.css.scss b/app/assets/stylesheets/admin/tag_rules.css.scss index a4bf6b7b2c..05decd6027 100644 --- a/app/assets/stylesheets/admin/tag_rules.css.scss +++ b/app/assets/stylesheets/admin/tag_rules.css.scss @@ -35,6 +35,9 @@ tags-input { tr { td { border: none; + h5 { + display: inline-block; + } } } } diff --git a/app/views/admin/enterprises/form/tag_rules/_default_rules.html.haml b/app/views/admin/enterprises/form/tag_rules/_default_rules.html.haml index c8fdcc2a8a..09c78f9c8c 100644 --- a/app/views/admin/enterprises/form/tag_rules/_default_rules.html.haml +++ b/app/views/admin/enterprises/form/tag_rules/_default_rules.html.haml @@ -6,7 +6,8 @@ %tr %td %h5 - By Default: + By Default + %i.text-big.icon-question-sign.help-modal{ template: 'admin/modals/tag_rule_help.html' } .no_rules{ ng: { show: "defaultTagGroup.rules.length == 0" } } No default rules apply yet .tag_rule{ ng: { repeat: "rule in defaultTagGroup.rules" } } From 6f375306c231a98f3d005be4de067a8bff3de2ff Mon Sep 17 00:00:00 2001 From: Mike iLL Kilmer Date: Tue, 31 May 2016 21:13:03 -0500 Subject: [PATCH 105/110] Some US-Specific copy. (#1018) Add locale en-US.yml --- config/locales/en-US.yml | 1048 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 1048 insertions(+) create mode 100644 config/locales/en-US.yml diff --git a/config/locales/en-US.yml b/config/locales/en-US.yml new file mode 100644 index 0000000000..e86100ba93 --- /dev/null +++ b/config/locales/en-US.yml @@ -0,0 +1,1048 @@ +# English language file +# --------------------- +# +# Originally adapted from source language file maintained by the Australian OFN team. +# Visit Transifex to translate this file into other languages: +# +# https://www.transifex.com/open-food-foundation/open-food-network/ +# +# If you translate this file in a text editor, please share your results with us by +# +# - uploading the file to Transifex or +# - opening a pull request at GitHub. +# +# +# See http://community.openfoodnetwork.org/t/localisation-ofn-in-your-language/397 + +en-US: + activerecord: + # Overridden here due to a bug in spree i18n (Issue #870) + weight_measurement_unit: "Weight (per lb)" + attributes: + spree/order: + payment_state: Payment State + shipment_state: Shipment State + devise: + failure: + invalid: | + Invalid email or password. + Were you a guest last time? Perhaps you need to create an account or reset your password. + enterprise_confirmations: + enterprise: + confirmed: Thankyou, your email address has been confirmed. + not_confirmed: Your email address could not be confirmed. Perhaps you have already completed this step? + confirmation_sent: "Confirmation email sent!" + confirmation_not_sent: "Could not send a confirmation email." + home: "OFN" + title: Open Food Network + welcome_to: 'Welcome to ' + site_meta_description: "We begin from the ground up. With farmers and growers ready to tell their stories proudly and truly. With distributors ready to connect people with products fairly and honestly. With buyers who believe that better weekly shopping decisions can…" + search_by_name: Search by name or city... + producers: 'US Producers' + producers_join: US producers are now welcome to join the Open Food Network. #FIXME + charges_sales_tax: Charges GST? + print_invoice: "Print Invoice" + send_invoice: "Send Invoice" + resend_confirmation: "Resend Confirmation" + view_order: "View Order" + edit_order: "Edit Order" + ship_order: "Ship Order" + cancel_order: "Cancel Order" + confirm_send_invoice: "An invoice for this order will be sent to the customer. Are you sure you want to continue?" + confirm_resend_order_confirmation: "Are you sure you want to resend the order confirmation email?" + must_have_valid_business_number: "%{enterprise_name} must have a valid ABN before invoices can be sent." + invoice: "Invoice" + percentage_of_sales: "%{percentage} of sales" + percentage_of_turnover: "Percentage of turnover" + monthly_cap_excl_tax: "monthly cap (excl. GST)" + capped_at_cap: "capped at %{cap}" + per_month: "per month" + free: "free" + plus_tax: "plus GST" + total_monthly_bill_incl_tax: "Total Monthly Bill (Incl. Tax)" + say_no: "No" + say_yes: "Yes" + + sort_order_cycles_on_shopfront_by: "Sort Order Cycles On Shopfront By" + + + admin: + # General form elements + quick_search: Quick Search + clear_all: Clear All + producer: Producer + shop: Shop + product: Product + variant: Variant + + columns: Columns + actions: Actions + viewing: "Viewing: %{current_view_name}" + + whats_this: What's this? + + tag_has_rules: "Existing rules for this tag: %{num}" + has_one_rule: "has one rule" + has_n_rules: "has %{num} rules" + + customers: + index: + add_customer: "Add customer" + customer_placeholder: "customer@example.org" + inventory: + title: Inventory + description: Use this page to manage inventories for your enterprises. Any product details set here will override those set on the 'Products' page + sku: SKU + price: Price + on_hand: On Hand + on_demand: On Demand? + enable_reset: Enable Stock Level Reset? + inherit: Inherit? + add: Add + hide: Hide + select_a_shop: Select A Shop + review_now: Review Now + new_products_alert_message: There are %{new_product_count} new products available to add to your inventory. + currently_empty: Your inventory is currently empty + no_matching_products: No matching products found in your inventory + no_hidden_products: No products have been hidden from this inventory + no_matching_hidden_products: No hidden products match your search criteria + no_new_products: No new products are available to add to this inventory + no_matching_new_products: No new products match your search criteria + inventory_powertip: This is your inventory of products. To add products to your inventory, select 'New Products' from the Viewing dropdown. + hidden_powertip: These products have been hidden from your inventory and will not be available to add to your shop. You can click 'Add' to add a product to you inventory. + new_powertip: These products are available to be added to your inventory. Click 'Add' to add a product to your inventory, or 'Hide' to hide it from view. You can always change your mind later! + + + order_cycle: + choose_products_from: "Choose Products From:" + + enterprise: + select_outgoing_oc_products_from: Select outgoing OC products from + + enterprises: + form: + primary_details: + shopfront_requires_login: "Shopfront requires login?" + shopfront_requires_login_tip: "Choose whether customers must login to view the shopfront." + shopfront_requires_login_false: "Public" + shopfront_requires_login_true: "Require customers to login" + + home: + hubs: + show_closed_shops: "Show closed shops" + hide_closed_shops: "Hide closed shops" + show_on_map: "Show all on the map" + shared: + register_call: + selling_on_ofn: "Interested in getting on the Open Food Network?" + register: "Register here" + shop: + messages: + login: "login" + register: "register" + contact: "contact" + require_customer_login: "This shop is for customers only." + require_login_html: "Please %{login} if you have an account already. Otherwise, %{register} to become a customer." + require_customer_html: "Please %{contact} %{enterprise} to become a customer." + + # Printable Invoice Columns + invoice_column_item: "Item" + invoice_column_qty: "Qty" + invoice_column_tax: "GST" + invoice_column_price: "Price" + + logo: "Logo (640x130)" #FIXME + logo_mobile: "Mobile logo (75x26)" #FIXME + logo_mobile_svg: "Mobile logo (SVG)" #FIXME + home_hero: "Hero image" + home_show_stats: "Show statistics" + footer_logo: "Logo (220x76)" #FIXME + footer_facebook_url: "Facebook URL" + footer_twitter_url: "Twitter URL" + footer_instagram_url: "Instagram URL" + footer_linkedin_url: "LinkedIn URL" + footer_googleplus_url: "Google Plus URL" + footer_pinterest_url: "Pinterest URL" + footer_email: "Email" + footer_links_md: "Links" + footer_about_url: "About URL" + footer_tos_url: "Terms of Service URL" + + name: Name + first_name: First Name + last_name: Last Name + email: Email + phone: Phone + next: Next + address: Address + address2: Address (contd.) + city: City + state: State + postcode: Postcode + country: Country + unauthorized: Unauthorized + terms_of_service: "Terms of service" + on_demand: On demand + none: None + + label_shops: "Shops" + label_map: "Map" + label_producers: "Producers" + label_groups: "Groups" + label_about: "About" + label_shopping: "Shopping" + label_login: "Login" + label_logout: "Logout" + label_signup: "Sign up" + label_administration: "Administration" + label_admin: "Admin" + label_account: "Account" + label_more: "Show more" + label_less: "Show less" + label_notices: "Notices" + + items: "items" + cart_headline: "Your shopping cart" + total: "Total" + checkout: "Checkout now" + cart_updating: "Updating cart..." + cart_empty: "Cart empty" + cart_edit: "Edit your cart" + + card_number: Card Number + card_securitycode: "Security Code" + card_expiry_date: Expiry Date + + ofn_cart_headline: "Current cart for:" + ofn_cart_distributor: "Distributor:" + ofn_cart_oc: "Order cycle:" + ofn_cart_from: "From:" + ofn_cart_to: "To:" + ofn_cart_product: "Product:" + ofn_cart_quantitiy: "Quantity:" + ofn_cart_send: "Buy me" + + ie_warning_headline: "Your browser is out of date :-(" + ie_warning_text: "For the best Open Food Network experience, we strongly recommend upgrading your browser:" + ie_warning_chrome: Download Chrome + ie_warning_firefox: Download Firefox + ie_warning_ie: Upgrade Internet Explorer + ie_warning_other: "Can't upgrade your browser? Try Open Food Network on your smartphone :-)" + + footer_global_headline: "OFN Global" + footer_global_home: "Home" + footer_global_news: "News" + footer_global_about: "About" + footer_global_contact: "Contact" + + footer_sites_headline: "OFN Sites" + footer_sites_developer: "Developer" + footer_sites_community: "Community" + footer_sites_userguide: "User Guide" + + footer_secure: "Secure and trusted." + footer_secure_text: "Open Food Network uses SSL encryption (2048 bit RSA) everywhere to keep your shopping and payment information private. Our servers do not store your credit card details and payments are processed by PCI-compliant services." + + footer_contact_headline: "Keep in touch" + footer_contact_email: "Email us" + + footer_nav_headline: "Navigate" + footer_join_headline: "Join us" + footer_join_producers: "Producers sign-up" + footer_join_hubs: "Hubs sign-up" + footer_join_groups: "Groups sign-up" + footer_join_partners: "Food systems partners" + + footer_legal_call: "Read our" + footer_legal_tos: "Terms and conditions" + footer_legal_visit: "Find us on" + footer_legal_text_html: "Open Food Network is a free and open source software platform. Our content is licensed with %{content_license} and our code with %{code_license}." + + home_shop: Shop Now + + brandstory_headline: "Food, unincorporated." + brandstory_intro: "Sometimes the best way to fix the system is to start a new one…" + brandstory_part1: "We begin from the ground up. With farmers and growers ready to tell their stories proudly and truly. With distributors ready to connect people with products fairly and honestly. With buyers who believe that better weekly shopping decisions can seriously change the world." + brandstory_part2: "Then we need a way to make it real. A way to empower everyone who grows, sells and buys food. A way to tell all the stories, to handle all the logistics. A way to turn transaction into transformation every day." + brandstory_part3: "So we build an online marketplace that levels the playing field. It’s transparent, so it creates real relationships. It’s open source, so it’s owned by everyone. It scales to regions and nations, so people start versions across the world." + brandstory_part4: "It works everywhere. It changes everything." + brandstory_part5_strong: "We call it Open Food Network." + brandstory_part6: "We all love food. Now we can love our food system too." + + system_headline: "Here's how it works." + system_step1: "1. Search" + system_step1_text: "Search our diverse, independent shops for seasonal local food. Search by neighbourhood and food category, or whether you prefer delivery or pickup." + system_step2: "2. Shop" + system_step2_text: "Transform your transactions with affordable local food from diverse producers and hubs. Know the stories behind your food and the people who make it!" + system_step3: "3. Pick-up / Delivery" + system_step3_text: "Hang on for your delivery, or visit your producer or hub for a more personal connection with your food. Food shopping as diverse as nature intended it." + + cta_headline: "Shopping that makes the world a better place." + cta_label: "I'm Ready" + + stats_headline: "We're creating a new food system." + stats_producers: "food producers" + stats_shops: "food shops" + stats_shoppers: "food shoppers" + stats_orders: "food orders" + + checkout_title: Checkout + checkout_now: Checkout now + checkout_order_ready: Order ready for + checkout_hide: Hide + checkout_expand: Expand + checkout_headline: "Ok, ready to checkout?" + checkout_as_guest: "Checkout as guest" + checkout_details: "Your details" + checkout_billing: "Billing info" + checkout_shipping: Shipping info + checkout_method_free: Free + checkout_address_same: Shipping address same as billing address? + checkout_ready_for: "Ready for:" + checkout_instructions: "Any comments or special instructions?" + checkout_payment: Payment + checkout_send: Place order now + checkout_your_order: Your order + checkout_cart_total: Cart total + checkout_shipping_price: Shipping + checkout_total_price: Total + checkout_back_to_cart: "Back to Cart" + + order_paid: PAID + order_not_paid: NOT PAID + order_total: Total order + order_payment: "Paying via:" + order_billing_address: Billing address + order_delivery_on: Delivery on + order_delivery_address: Delivery address + order_special_instructions: "Your notes:" + order_pickup_time: Ready for collection + order_pickup_instructions: Collection Instructions + order_produce: Produce + order_total_price: Total + order_includes_tax: (includes tax) + order_payment_paypal_successful: Your payment via PayPal has been processed successfully. + order_hub_info: Hub Info + + bom_tip: "Use this page to alter product quantities across multiple orders. Products may also be removed from orders entirely, if required." + bom_shared: "Shared Resource?" + bom_page_title: "Bulk Order Management" + bom_no: "Order no." + bom_date: "Order date" + bom_cycle: "Order cycle" + bom_max: "Max" + bom_hub: "Hub" + bom_variant: "Product: Unit" + bom_final_weigth_volume: "Weight/Volume" + bom_quantity: "Quantity" + bom_actions_delete: "Delete Selected" + bom_loading: "Loading orders" + bom_no_results: "No orders found." + bom_order_error: "Some errors must be resolved before you can update orders.\nAny fields with red borders contain errors." + + unsaved_changes_warning: "Unsaved changes exist and will be lost if you continue." + unsaved_changes_error: "Fields with red borders contain errors." + + products: "Products" + products_in: "in %{oc}" + products_at: "at %{distributor}" + products_elsewhere: "Products found elsewhere" + + email_welcome: "Welcome" + email_confirmed: "Thank you for confirming your email address." + email_registered: "is now part of" + email_userguide_html: "The User Guide with detailed support for setting up your Producer or Hub is here: +%{link}" + email_admin_html: "You can manage your account by logging into the %{link} or by clicking on the cog in the top right hand side of the homepage, and selecting Administration." + email_community_html: "We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next. +%{link}" + email_help: "If you have any difficulties, check out our FAQs, browse the forum or post a 'Support' topic and someone will help you out!" + email_confirmation_greeting: "Hi, %{contact}!" + email_confirmation_profile_created: "A profile for %{name} has been successfully created! +To activate your Profile we need to confirm this email address." + email_confirmation_click_link: "Please click the link below to confirm your email and to continue setting up your profile." + email_confirmation_link_label: "Confirm this email address »" + email_confirmation_help_html: "After confirming your email you can access your administration account for this enterprise. +See the %{link} to find out more about %{sitename}'s features and to start using your profile or online store." + email_confirmation_userguide: "User Guide" + email_social: "Connect with Us:" + email_contact: "Email us:" + email_signoff: "Cheers," + email_signature: "%{sitename} Team" + + email_confirm_customer_greeting: "Hi %{name}," + email_confirm_customer_intro_html: "Thanks for shopping at %{distributor}!" + email_confirm_customer_number_html: "Order confirmation #%{number}" + email_confirm_customer_details_html: "Here are your order details from %{distributor}:" + email_confirm_customer_signoff: "Kind regards," + email_confirm_shop_greeting: "Hi %{name}," + email_confirm_shop_order_html: "Well done! You have a new order for %{distributor}!" + email_confirm_shop_number_html: "Order confirmation #%{number}" + email_order_summary_item: "Item" + email_order_summary_quantity: "Qty" + email_order_summary_price: "Price" + email_order_summary_subtotal: "Subtotal:" + email_order_summary_total: "Total:" + email_payment_paid: PAID + email_payment_not_paid: NOT PAID + email_payment_summary: Payment summary + email_payment_method: "Paying via:" + email_shipping_delivery_details: Delivery details + email_shipping_delivery_time: "Delivery on:" + email_shipping_delivery_address: "Delivery address:" + email_shipping_collection_details: Collection details + email_shipping_collection_time: "Ready for collection:" + email_shipping_collection_instructions: "Collection instructions:" + email_special_instructions: "Your notes:" + + email_signup_greeting: Hello! + email_signup_welcome: "Welcome to %{sitename}!" + email_signup_login: Your login + email_signup_email: Your login email is + email_signup_shop_html: "You can start shopping online now at %{link}." + email_signup_text: "Thanks for joining the network. + If you are a customer, we look forward to introducing you to many fantastic farmers, wonderful food hubs and delicious food! + If you are a producer or food enterprise, we are excited to have you as a part of the network." + email_signup_help_html: "We welcome all your questions and feedback; you can use the Send Feedback button on the site or email us at" + + shopping_oc_closed: Orders are closed + shopping_oc_closed_description: "Please wait until the next cycle opens (or contact us directly to see if we can accept any late orders)" + shopping_oc_last_closed: "The last cycle closed %{distance_of_time} ago" + shopping_oc_next_open: "The next cycle opens in %{distance_of_time}" + shopping_tabs_about: "About %{distributor}" + shopping_tabs_contact: "Contact" + shopping_contact_address: "Address" + shopping_contact_web: "Contact" + shopping_contact_social: "Follow" + shopping_groups_part_of: "is part of:" + shopping_producers_of_hub: "%{hub}'s producers:" + + enterprises_next_closing: "Next order closing" + enterprises_ready_for: "Ready for" + enterprises_choose: "Choose when you want your order:" + + hubs_buy: "Shop for:" + hubs_shopping_here: "Shopping here" + hubs_orders_closed: "Orders closed" + hubs_profile_only: "Profile only" + hubs_delivery_options: "Delivery options" + hubs_pickup: "Pickup" + hubs_delivery: "Delivery" + hubs_producers: "Our producers" + hubs_filter_by: "Filter by" + hubs_filter_type: "Type" + hubs_filter_delivery: "Delivery" + hubs_matches: "Did you mean?" + hubs_intro: Shop in your local area + hubs_distance: Closest to + hubs_distance_filter: "Show me shops near %{location}" + + products_clear_all: Clear all + products_showing: "Showing:" + products_with: with + products_search: "Search by product or producer" + products_loading: "Loading products..." + products_updating_cart: "Updating cart..." + products_cart_empty: "Cart empty" + products_edit_cart: "Edit your cart" + products_from: from + products_change: "No changes to save." + products_update_error: "Saving failed with the following error(s):" + products_update_error_msg: "Saving failed." + products_update_error_data: "Save failed due to invalid data:" + products_changes_saved: "Changes saved." + + search_no_results_html: "Sorry, no results found for %{query}. Try another search?" + + components_profiles_popover: "Profiles do not have a shopfront on the Open Food Network, but may have their own physical or online shop elsewhere" + components_profiles_show: "Show profiles" + components_filters_nofilters: "No filters" + components_filters_clearfilters: "Clear all filters" + + groups_title: Groups + groups_headline: Groups / regions + groups_text: "Every producer is unique. Every business has something different to offer. Our groups are collectives of producers, hubs and distributors who share something in common like location, farmers market or philosophy. This makes your shopping experience easier. So explore our groups and have the curating done for you." + groups_search: "Search name or keyword" + groups_no_groups: "No groups found" + groups_about: "About Us" + + groups_producers: "Our producers" + groups_hubs: "Our hubs" + groups_contact_web: Contact + groups_contact_social: Follow + groups_contact_address: Address + groups_contact_email: Email us + groups_contact_website: Visit our website + groups_contact_facebook: Follow us on Facebook + groups_signup_title: Sign up as a group + groups_signup_headline: Groups sign up + groups_signup_intro: "We're an amazing platform for collaborative marketing, the easiest way for your members and stakeholders to reach new markets. We're non-profit, affordable, and simple." + groups_signup_email: Email us + groups_signup_motivation1: We transform food systems fairly. + groups_signup_motivation2: It's why we get out of bed every day. We're a global non-profit, based on open source code. We play fair. You can always trust us. + groups_signup_motivation3: We know you have big ideas, and we want to help. We'll share our knowledge, networks and resources. We know that isolation doesn't create change, so we'll partner with you. + groups_signup_motivation4: We meet you where you are. + groups_signup_motivation5: You might be an alliance of food hubs, producers, or distributors, and an industry body, or a local government. + groups_signup_motivation6: Whatever your role in your local food movement, we're ready to help. However you come to wonder what Open Food Network would look like or is doing in your part of the world, let's start the conversation. + groups_signup_motivation7: We make food movements make more sense. + groups_signup_motivation8: You need to activate and enable your networks, we offer a platform for conversation and action. You need real engagement. We’ll help reach all the players, all the stakeholders, all the sectors. + groups_signup_motivation9: You need resourcing. We’ll bring all our experience to bear. You need cooperation. We’ll better connect you to a global network of peers. + groups_signup_pricing: Group Account + groups_signup_studies: Case Studies + groups_signup_contact: Ready to discuss? + groups_signup_contact_text: "Get in touch to discover what OFN can do for you:" + groups_signup_detail: "Here's the detail." + + login_invalid: "Invalid email or password" + + modal_hubs: "Food Hubs" + modal_hubs_abstract: Our food hubs are the point of contact between you and the people who make your food! + modal_hubs_content1: You can search for a convenient hub by location or name. Some hubs have multiple points where you can pick-up your purchases, and some will also provide delivery options. Each food hub is a sales point with independent business operations and logistics - so variations between hubs are to be expected. + modal_hubs_content2: You can only shop at one food hub at a time. + + modal_groups: "Groups / Regions" + modal_groups_content1: These are the organisations and relationships between hubs which make up the Open Food Network. + modal_groups_content2: Some groups are clustered by location or council, others by non-geographic similarities. + + modal_how: "How it works" + modal_how_shop: Shop the Open Food Network + modal_how_shop_explained: Search for a food hub near you to start shopping! You can expand each food hub to see what kinds of goodies are available, and click through to start shopping. (You can only shop one food hub at a time.) + modal_how_pickup: Pick-ups, delivery and shipping costs + modal_how_pickup_explained: Some food hubs deliver to your door, while others require you to pick-up your purchases. You can see which options are available on the homepage, and select which you'd like at the shopping and check-out pages. Delivery will cost more, and pricing differs from hub-to-hub. Each food hub is a sales point with independent business operations and logisitics - so variations between hubs are to be expected. + modal_how_more: Learn more + modal_how_more_explained: "If you want to learn more about the Open Food Network, how it works, and get involved, check out:" + + modal_producers: "Producers" + modal_producers_explained: "Our producers make all the delicious food you can shop for on the Open Food Network." + + ocs_choice_hub: "Hub:" + ocs_choice_oc: "Order Cycle:" + ocs_choice_text: "You have not yet picked where you will get your order from." + ocs_closed_headline: Orders are currently closed for this hub + ocs_closed_time: "The last cycle closed %{time} ago." + ocs_closed_contact: "Please contact your hub directly to see if they accept late orders, or wait until the next cycle opens." + ocs_closed_opens: "The next order cycle opens in %{time}" + ocs_closed_email: "Email: %{email}" + ocs_closed_phone: "Phone: %{phone}" + ocs_pickup_time: "Your order will be ready on %{pickup_time}" + ocs_change_date: "Change Collection Date" + ocs_change_date_notice: "(This will reset your cart)" + ocs_close_time: "ORDERS CLOSE" + ocs_when_headline: When do you want your order? + ocs_when_text: No products are displayed until you select a date. + ocs_when_closing: "Closing On" + ocs_when_choose: "Choose Order Cycle" + ocs_list: "List View" + + producers_about: About us + producers_buy: Shop for + producers_contact: Contact + producers_contact_phone: Call + producers_contact_social: Follow + producers_buy_at_html: "Shop for %{enterprise} products at:" + producers_filter: Filter by + producers_filter_type: Type + producers_title: Producers + producers_headline: Find local producers + producers_signup_title: Sign up as a producer + producers_signup_headline: Food producers, empowered. + producers_signup_motivation: Sell your food and tell your stories to diverse new markets. Save time and money on every overhead. We support innovation without the risk. We've levelled the playing field. + producers_signup_send: Join now + producers_signup_enterprise: Enterprise Accounts + producers_signup_studies: Stories from our producers. + producers_signup_cta_headline: Join now! + producers_signup_cta_action: Join now + producers_signup_detail: Here's the detail. + producer: Producer + + products_item: Item + products_description: Description + products_variant: Variant + products_quantity: Quantity + products_availabel: Available? + products_producer: "Producer" + products_price: "Price" + products_sku: "SKU" + products_name: "name" + products_unit: "unit" + products_on_hand: "on hand" + products_on_demand: "On demand?" + products_category: "Category" + products_tax_category: "tax category" + products_available_on: "Available On" + products_inherit: "Inherit?" + products_inherits_properties: "Inherits Properties?" + products_stock_level_reset: "Enable Stock Level Reset?" + + register_title: Register + + shops_title: Shops + shops_headline: Shopping, transformed. + shops_text: Food grows in cycles, farmers harvest in cycles, and we order food in cycles. If you find an order cycle closed, check back soon. + shops_signup_title: Sign up as a hub + shops_signup_headline: Food hubs, unlimited. + shops_signup_motivation: Whatever your model, we support you. However you change, we're with you. We're non-profit, independent, and open-sourced. We're the software partners you've dreamed of. + shops_signup_action: Join now + shops_signup_pricing: Enterprise Accounts + shops_signup_stories: Stories from our hubs. + shops_signup_help: We're ready to help. + shops_signup_help_text: You need a better return. You need new buyers and logistics partners. You need your story told across wholesale, retail, and the kitchen table. + shops_signup_detail: Here's the detail. + + orders_fees: Fees... + orders_edit_title: Shopping Cart + orders_edit_headline: Your shopping cart + orders_edit_time: Order ready for + orders_edit_continue: Continue shopping + orders_edit_checkout: Checkout + orders_form_empty_cart: "Empty cart" + orders_form_subtotal: Produce subtotal + orders_form_admin: Admin and handling + orders_form_total: Total + orders_oc_expired_headline: Orders have closed for this order cycle + orders_oc_expired_text: "Sorry, orders for this order cycle closed %{time} ago! Please contact your hub directly to see if they can accept late orders." + orders_oc_expired_text_others_html: "Sorry, orders for this order cycle closed %{time} ago! Please contact your hub directly to see if they can accept late orders %{link}." + orders_oc_expired_text_link: "or see the other order cycles available at this hub" + orders_oc_expired_email: "Email:" + orders_oc_expired_phone: "Phone:" + orders_show_title: Order Confirmation + orders_show_time: Order ready on + orders_show_number: Order confirmation + + products_cart_distributor_choice: "Distributor for your order:" + products_cart_distributor_change: "Your distributor for this order will be changed to %{name} if you add this product to your cart." + products_cart_distributor_is: "Your distributor for this order is %{name}." + products_distributor_error: "Please complete your order at %{link} before shopping with another distributor." + products_oc: "Order cycle for your order:" + products_oc_change: "Your order cycle for this order will be changed to %{name} if you add this product to your cart." + products_oc_is: "Your order cycle for this order is %{name}." + products_oc_error: "Please complete your order from %{link} before shopping in a different order cycle." + products_oc_current: "your current order cycle" + products_max_quantity: Max quantity + products_distributor: Distributor + products_distributor_info: When you select a distributor for your order, their address and pickup times will be displayed here. + + # keys used in javascript + password: Password + remember_me: Remember Me + are_you_sure: "Are you sure?" + orders_open: Orders open + closing: "Closing " + going_back_to_home_page: "Taking you back to the home page" + creating: Creating + updating: Updating + failed_to_create_enterprise: "Failed to create your enterprise." + failed_to_create_enterprise_unknown: "Failed to create your enterprise.\nPlease ensure all fields are completely filled out." + failed_to_update_enterprise_unknown: "Failed to update your enterprise.\nPlease ensure all fields are completely filled out." + order_not_saved_yet: "Your order hasn't been saved yet. Give us a few seconds to finish!" + filter_by: "Filter by" + hide_filters: "Hide filters" + one_filter_applied: "1 filter applied" + x_filters_applied: " filters applied" + submitting_order: "Submitting your order: please wait" + confirm_hub_change: "Are you sure? This will change your selected hub and remove any items in your shopping cart." + confirm_oc_change: "Are you sure? This will change your selected order cycle and remove any items in your shopping cart." + location_placeholder: "Type in a location..." + error_required: "can't be blank" + error_number: "must be number" + error_email: "must be email address" + item_handling_fees: "Item Handling Fees (included in item totals)" + january: "January" + february: "February" + march: "March" + april: "April" + may: "May" + june: "June" + july: "July" + august: "August" + september: "September" + october: "October" + november: "November" + december: "December" + email_not_found: "Email address not found" + email_required: "You must provide an email address" + logging_in: "Hold on a moment, we're logging you in" + signup_email: "Your email" + choose_password: "Choose a password" + confirm_password: "Confirm password" + action_signup: "Sign up now" + welcome_to_ofn: "Welcome to the Open Food Network!" + signup_or_login: "Start By Signing Up (or logging in)" + have_an_account: "Already have an account?" + action_login: "Log in now." + forgot_password: "Forgot Password?" + password_reset_sent: "An email with instructions on resetting your password has been sent!" + reset_password: "Reset password" + registration_greeting: "Greetings!" + who_is_managing_enterprise: "Who is responsible for managing %{enterprise}?" + enterprise_contact: "Primary Contact" + enterprise_contact_required: "You need to enter a primary contact." + enterprise_email_address: "Email address" + enterprise_phone: "Phone number" + back: "Back" + continue: "Continue" + limit_reached_headline: "Oh no!" + limit_reached_message: "You have reached the limit!" + limit_reached_text: "You have reached the limit for the number of enterprises you are allowed to own on the" + limit_reached_action: "Return to the homepage" + select_promo_image: "Step 3. Select Promo Image" + promo_image_tip: "Tip: Shown as a banner, preferred size is 1200×260px" + promo_image_label: "Choose a promo image" + action_or: "OR" + promo_image_drag: "Drag and drop your promo here" + review_promo_image: "Step 4. Review Your Promo Banner" + review_promo_image_tip: "Tip: for best results, your promo image should fill the available space" + promo_image_placeholder: "Your logo will appear here for review once uploaded" + uploading: "Uploading..." + select_logo: "Step 1. Select Logo Image" + logo_tip: "Tip: Square images will work best, preferably at least 300×300px" + logo_label: "Choose a logo image" + logo_drag: "Drag and drop your logo here" + review_logo: "Step 2. Review Your Logo" + review_logo_tip: "Tip: for best results, your logo should fill the available space" + logo_placeholder: "Your logo will appear here for review once uploaded" + enterprise_about_headline: "Nice one!" + enterprise_about_message: "Now let's flesh out the details about" + enterprise_success: "Success! %{enterprise} added to the Open Food Network " + enterprise_registration_exit_message: "If you exit this wizard at any stage, you need to click the confirmation link in the email you have received. This will take you to your admin interface where you can continue setting up your profile." + enterprise_description: "Short Description" + enterprise_description_placeholder: "A short sentence describing your enterprise" + enterprise_long_desc: "Long Description" + enterprise_long_desc_placeholder: "This is your opportunity to tell the story of your enterprise - what makes you different and wonderful? We'd suggest keeping your description to under 600 characters or 150 words." + enterprise_long_desc_length: "%{num} characters / up to 600 recommended" + enterprise_abn: "ABN" + enterprise_abn_placeholder: "eg. 99 123 456 789" + enterprise_acn: "ACN" + enterprise_acn_placeholder: "eg. 123 456 789" + enterprise_tax_required: "You need to make a selection." + enterprise_final_step: "Final step!" + enterprise_social_text: "How can people find %{enterprise} online?" + website: "Website" + website_placeholder: "eg. openfoodnetwork.org.au" + facebook: "Facebook" + facebook_placeholder: "eg. www.facebook.com/PageNameHere" + linkedin: "LinkedIn" + linkedin_placeholder: "eg. www.linkedin.com/YourNameHere" + twitter: "Twitter" + twitter_placeholder: "eg. @twitter_handle" + instagram: "Instagram" + instagram_placeholder: "eg. @instagram_handle" + registration_greeting: "Hi there!" + registration_intro: "You can now create a profile for your Producer or Hub" + registration_action: "Let's get started!" + registration_checklist: "You'll need" + registration_time: "5-10 minutes" + registration_enterprise_address: "Enterprise address" + registration_contact_details: "Primary contact details" + registration_logo: "Your logo image" + registration_promo_image: "Landscape image for your profile" + registration_about_us: "'About Us' text" + registration_outcome_headline: "What do I get?" + registration_outcome1_html: "Your profile helps people find and contact you on the Open Food Network." + registration_outcome2: "Use this space to tell the story of your enterprise, to help drive connections to your social and online presence. " + registration_outcome3: "It's also the first step towards trading on the Open Food Network, or opening an online store." + registration_finished_headline: "Finished!" + registration_finished_thanks: "Thanks for filling out the details for %{enterprise}." + registration_finished_login: "You can change or update your enterprise at any stage by logging into Open Food Network and going to Admin." + registration_finished_activate: "Activate %{enterprise}." + registration_finished_activate_instruction_html: "We've sent a confirmation email to %{email} if it hasn't been activated before.
    +Please follow the instructions there to make your enterprise visible on the Open Food Network." + registration_finished_action: "Open Food Network home" + registration_type_headline: "Last step to add %{enterprise}!" + registration_type_question: "Are you a producer?" + registration_type_producer: "Yes, I'm a producer" + registration_type_no_producer: "No, I'm not a producer" + registration_type_error: "Please choose one. Are you are producer?" + registration_type_producer_help: "Producers make yummy things to eat and/or drink. You're a producer if you grow it, raise it, brew it, bake it, ferment it, milk it or mould it." + registration_type_no_producer_help: "If you’re not a producer, you’re probably someone who sells and distributes food. You might be a hub, coop, buying group, retailer, wholesaler or other." + create_profile: "Create Profile" + registration_images_headline: "Thanks!" + registration_images_description: "Let's upload some pretty pictures so your profile looks great! :)" + registration_detail_headline: "Let's Get Started" + registration_detail_enterprise: "Woot! First we need to know a little bit about your enterprise:" + registration_detail_producer: "Woot! First we need to know a little bit about your farm:" + registration_detail_name_enterprise: "Enterprise Name:" + registration_detail_name_producer: "Farm Name:" + registration_detail_name_placeholder: "e.g. Charlie's Awesome Farm" + registration_detail_name_error: "Please choose a unique name for your enterprise" + registration_detail_address1: "Address line 1:" + registration_detail_address1_placeholder: "e.g. 123 Cranberry Drive" + registration_detail_address1_error: "Please enter an address" + registration_detail_address2: "Address line 2:" + registration_detail_suburb: "City:" + registration_detail_suburb_placeholder: "e.g. New York City" + registration_detail_suburb_error: "Please enter a city" + registration_detail_postcode: "Postcode:" + registration_detail_postcode_placeholder: "e.g. 10019" + registration_detail_postcode_error: "Postcode required" + registration_detail_state: "State:" + registration_detail_state_error: "State required" + registration_detail_country: "Country:" + registration_detail_country_error: "Please select a country" + fees: "Fees" + item_cost: "Item cost" + bulk: "Bulk" + shop_variant_quantity_min: "min" + shop_variant_quantity_max: "max" + contact: "Contact" + follow: "Follow" + shop_for_products_html: "Shop for %{enterprise} products at:" + change_shop: "Change shop to:" + shop_at: "Shop now at:" + price_breakdown: "Full price breakdown" + admin_fee: "Admin fee" + sales_fee: "Sales fee" + packing_fee: "Packing fee" + transport_fee: "Transport fee" + fundraising_fee: "Fundraising fee" + price_graph: "Price graph" + included_tax: "Included tax" + remove_tax: "Remove tax" + balance: "Balance" + transaction: "Transaction" + transaction_date: "Date" #Transaction is only in key to avoid conflict with :date + payment_state: "Payment status" + shipping_state: "Shipping status" + value: "Value" + balance_due: "Balance due" + credit: "Credit" + Paid: "Paid" + Ready: "Ready" + you_have_no_orders_yet: "You have no orders yet" + running_balance: "Running balance" + outstanding_balance: "Outstanding balance" + admin_entreprise_relationships: "Enterprise Relationships" + admin_entreprise_relationships_everything: "Everything" + admin_entreprise_relationships_permits: "permits" + admin_entreprise_relationships_seach_placeholder: "Search" + admin_entreprise_relationships_button_create: "Create" + admin_entreprise_groups: "Enterprise Groups" + admin_entreprise_groups_name: "Name" + admin_entreprise_groups_owner: "Owner" + admin_entreprise_groups_on_front_page: "On front page ?" + admin_entreprise_groups_entreprise: "Enterprises" + admin_entreprise_groups_primary_details: "Primary Details" + admin_entreprise_groups_data_powertip: "The primary user responsible for this group." + admin_entreprise_groups_data_powertip_logo: "This is the logo for the group" + admin_entreprise_groups_data_powertip_promo_image: "This image is displayed at the top of the Group profile" + admin_entreprise_groups_about: "About" + admin_entreprise_groups_images: "Images" + admin_entreprise_groups_contact: "Contact" + admin_entreprise_groups_contact_phone_placeholder: "eg. 212 765 3210" + admin_entreprise_groups_contact_address1_placeholder: "eg. 100 Martin Luther King Drive" + admin_entreprise_groups_contact_city: "City" + admin_entreprise_groups_contact_city_placeholder: "eg. NYC, NY" + admin_entreprise_groups_contact_zipcode: "Postcode" + admin_entreprise_groups_contact_zipcode_placeholder: "eg. 10019" + admin_entreprise_groups_contact_state_id: "State" + admin_entreprise_groups_contact_country_id: "Country" + admin_entreprise_groups_web: "Web Resources" + admin_entreprise_groups_web_twitter: "eg. @the_prof" + admin_entreprise_groups_web_website_placeholder: "eg. www.truffles.com" + admin_order_cycles: "Admin Order Cycles" + open: "Open" + close: "Close" + supplier: "Supplier" + coordinator: "Coordinator" + distributor: "Distributor" + product: "Products" + enterprise_fees: "Enterprise Fees" + fee_type: "Fee Type" + tax_category: "Tax Category" + calculator: "Calculator" + calculator_values: "Calculator values" + new_order_cycles: "New Order Cycles" + select_a_coordinator_for_your_order_cycle: "select a coordinator for your order cycle" + edit_order_cycle: "Edit Order Cycle" + roles: "Roles" + update: "Update" + add_producer_property: "Add producer property" + admin_settings: "Settings" + update_invoice: "Update Invoices" + finalise_invoice: "Finalise Invoices" + finalise_user_invoices: "Finalise User Invoices" + finalise_user_invoice_explained: "Use this button to finalize all invoices in the system for the previous calendar month. This task can be set up to run automatically once a month." + manually_run_task: "Manually Run Task " + update_user_invoices: "Update User Invoices" + update_user_invoice_explained: "Use this button to immediately update invoices for the month to date for each enterprise user in the system. This task can be set up to run automatically every night." + auto_finalise_invoices: "Auto-finalise invoices monthly on the 2nd at 1:30am" + auto_update_invoices: "Auto-update invoices nightly at 1:00am" + in_progress: "In Progress" + started_at: "Started at" + queued: "Queued" + scheduled_for: "Scheduled for" + customers: "Customers" + please_select_hub: "Please select a Hub" + loading_customers: "Loading Customers" + no_customers_found: "No customers found" + go: "Go" + hub: "Hub" + accounts_administration_distributor: "accounts administration distributor" + accounts_and_billing: "Accounts & Billing" + producer: "Producer" + product: "Product" + price: "Price" + on_hand: "On hand" + save_changes: "Save Changes" + spree_admin_overview_enterprises_header: "My Enterprises" + spree_admin_overview_enterprises_footer: "MANAGE MY ENTERPRISES" + spree_admin_enterprises_hubs_name: "Name" + spree_admin_enterprises_create_new: "CREATE NEW" + spree_admin_enterprises_shipping_methods: "Shipping Methods" + spree_admin_enterprises_fees: "Enterprise Fees" + spree_admin_enterprises_none_create_a_new_enterprise: "CREATE A NEW ENTERPRISE" + spree_admin_enterprises_none_text: "You don't have any enterprises yet" + spree_admin_enterprises_producers_name: "Name" + spree_admin_enterprises_producers_total_products: "Total Products" + spree_admin_enterprises_producers_active_products: "Active Products" + spree_admin_enterprises_producers_order_cycles: "Products in OCs" + spree_admin_enterprises_producers_order_cycles_title: "" + spree_admin_enterprises_tabs_hubs: "HUBS" + spree_admin_enterprises_tabs_producers: "PRODUCERS" + spree_admin_enterprises_producers_manage_order_cycles: "MANAGE ORDER CYCLES" + spree_admin_enterprises_producers_manage_products: "MANAGE PRODUCTS" + spree_admin_enterprises_producers_orders_cycle_text: "You don't have any active order cycles." + spree_admin_enterprises_any_active_products_text: "You don't have any active products." + spree_admin_enterprises_create_new_product: "CREATE A NEW PRODUCT" + spree_admin_order_cycles: "Order Cycles" + spree_admin_order_cycles_tip: "Order cycles determine when and where your products are available to customers." + dashbord: "Dashboard" + spree_admin_single_enterprise_alert_mail_confirmation: "Please confirm the email address for" + spree_admin_single_enterprise_alert_mail_sent: "We've sent an email to" + spree_admin_overview_action_required: "Action Required" + spree_admin_overview_check_your_inbox: "Please check you inbox for furher instructions. Thanks!" + change_package: "Change Package" + spree_admin_single_enterprise_hint: "Hint: To allow people to find you, turn on your visibility under" + your_profil_live: "Your profile live" + on_ofn_map: "on the Open Food Network map" + see: "See" + live: "live" + manage: "Manage" + resend: "Resend" + add_and_manage_products: "Add & manage products" + add_and_manage_order_cycles: "Add & manage order cycles" + manage_order_cycles: "Manage order cycles" + manage_products: "Manage products" + edit_profile_details: "Edit profile details" + edit_profile_details_etc: "Change your profile description, images, etc." + start_date: "Start Date" + end_date: "End Date" + order_cycle: "Order Cycle" + group_buy_unit_size: "Group Buy Unit Size" + total_qtt_ordered: "Total Quantity Ordered" + max_qtt_ordered: "Max Quantity Ordered" + current_fulfilled_units: "Current Fulfilled Units" + max_fulfilled_units: "Max Fulfilled Units" + bulk_management_warning: "WARNING: Some variants do not have a unit value" + ask: "Ask?" + no_orders_found: "No orders found." + order_no: "Order No." + weight_volume: "Weight/Volume" + remove_tax: "Remove tax" + tax_settings: "Tax Settings" + products_require_tax_category: "products require tax category" + admin_shared_address_1: "Address" + admin_shared_address_2: "Address (cont.)" + admin_share_city: "City" + admin_share_zipcode: "Postcode" + admin_share_country: "Country" + admin_share_state: "State" + hub_sidebar_hubs: "Hubs" + hub_sidebar_none_available: "None Available" + hub_sidebar_manage: "Manage" + hub_sidebar_at_least: "At least one hub must be selected" + hub_sidebar_blue: "blue" + hub_sidebar_red: "red" + shop_trial_in_progress: "Your shopfront trial expires in %{days}." + shop_trial_expired: "Good news! We have decided to extend shopfront trials until further notice (probably around March 2015)." #FIXME + report_customers_distributor: "Distributor" + report_customers_supplier: "Supplier" + report_customers_cycle: "Order Cycle" + report_customers_type: "Report Type" + report_customers_csv: "Download as csv" + report_producers: "Producers: " + report_type: "Report Type: " + report_hubs: "Hubs: " + report_payment: "Payment Methods: " + report_distributor: "Distributor: " + report_payment_by: 'Payments By Type' + report_itemised_payment: 'Itemised Payment Totals' + report_payment_totals: 'Payment Totals' + report_all: 'all' + report_order_cycle: "Order Cycle: " + report_entreprises: "Enterprises: " + report_users: "Users: " + initial_invoice_number: "Initial invoice number:" + invoice_date: "Invoice date:" + due_date: "Due date:" + account_code: "Account code:" + equals: "Equals" + contains: "contains" + discount: "Discount" + filter_products: "Filter Products" + delete_product_variant: "The last variant cannot be deleted!" + progress: "progress" + saving: "Saving.." + success: "success" + failure: "failure" + unsaved_changes_confirmation: "Unsaved changes will be lost. Continue anyway?" + one_product_unsaved: "Changes to one product remain unsaved." + products_unsaved: "Changes to %{n} products remain unsaved." + add_manager: "Add a manager" + is_already_manager: "is already a manager!" + no_change_to_save: " No change to save" + add_manager: "Add a manager" + users: "Users" + about: "About" + images: "Images" + contact: "Contact" + web: "Web" + primary_details: "Primary Details" + adrdress: "Address" + contact: "Contact" + social: "Social" + business_details: "Business Details" + properties: "Properties" + shipping_methods: "Shipping Methods" + payment_methods: "Payment Methods" + enterprise_fees: "Enterprise Fees" + inventory_settings: "Inventory Settings" + tag_rules: "Tag Rules" + shop_preferences: "Shop Preferences" + validation_msg_relationship_already_established: "^That relationship is already established." + validation_msg_at_least_one_hub: "^At least one hub must be selected" + validation_msg_product_category_cant_be_blank: "^Product Category cant be blank" + validation_msg_tax_category_cant_be_blank: "^Tax Category can't be blank" + validation_msg_is_associated_with_an_exising_customer: "is associated with an existing customer" + + spree: + shipment_states: + backorder: backorder + partial: partial + pending: pending + ready: ready + shipped: shipped + payment_states: + balance_due: balance due + completed: completed + checkout: checkout + credit_owed: credit owed + failed: failed + paid: paid + pending: pending + processing: processing + void: void + order_state: + address: address + adjustments: adjustments + awaiting_return: awaiting return + canceled: canceled + cart: cart + complete: complete + confirm: confirm + delivery: delivery + payment: payment + resumed: resumed + returned: returned + skrill: skrill From 89c3758bae93f95fa4c23d99167136bf16b56998 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 1 Jun 2016 16:15:44 +1000 Subject: [PATCH 106/110] Avoid race conditions in feature specs --- spec/features/consumer/shopping/shopping_spec.rb | 1 + spec/features/consumer/shops_spec.rb | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index 9b03259ec5..1e69e97b1c 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -254,6 +254,7 @@ feature "As a consumer I want to shop with a distributor", js: true do variant.update_attributes! on_hand: 0 # -- Messaging + expect(page).to have_input "variants[#{variant.id}]" fill_in "variants[#{variant.id}]", with: '1' wait_until { !cart_dirty } diff --git a/spec/features/consumer/shops_spec.rb b/spec/features/consumer/shops_spec.rb index 9625640fe7..df44955232 100644 --- a/spec/features/consumer/shops_spec.rb +++ b/spec/features/consumer/shops_spec.rb @@ -36,9 +36,8 @@ feature 'Shops', js: true do it "should show closed shops after clicking the button" do create(:simple_product, distributors: [d1, d2]) visit shops_path - click_link "Show closed shops" - page.should have_selector 'hub.inactive' - page.should have_selector 'hub.inactive', text: d2.name + click_link_and_ensure("Show closed shops", -> { page.has_selector? 'hub.inactive' }) + page.should have_selector 'hub.inactive', text: d2.name end it "should link to the hub page" do @@ -52,4 +51,14 @@ feature 'Shops', js: true do open_enterprise_modal producer modal_should_be_open_for producer end + + def click_link_and_ensure(link_text, check) + # Buttons appear to be unresponsive for a while, so keep clicking them until content appears + using_wait_time 0.5 do + 10.times do + click_link link_text + break if check.call + end + end + end end From 489feb4e019448d9b7b427c0c3ebdecdd53cc494 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 1 Jun 2016 14:40:56 +1000 Subject: [PATCH 107/110] Longer default page size for feature specs --- spec/features/admin/order_cycles_spec.rb | 12 ------------ spec/spec_helper.rb | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 2733aeaeaf..1fa85f06e7 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -229,9 +229,6 @@ feature %q{ page.should have_field 'order_cycle_outgoing_exchange_1_pickup_time', with: 'time 1' page.should have_field 'order_cycle_outgoing_exchange_1_pickup_instructions', with: 'instructions 1' - # Make the whole page visible - page.driver.resize(1280, 3600) - # And the distributors should have products page.all('table.exchanges tbody tr.distributor').each_with_index do |row, i| row.find('td.products').click @@ -276,9 +273,6 @@ feature %q{ scenario "updating an order cycle", js: true do - # Make the page long enough to avoid the save bar overlaying the form - page.driver.resize(1280, 3600) - # Given an order cycle with all the settings oc = create(:order_cycle) initial_variants = oc.variants.sort_by &:id @@ -655,9 +649,6 @@ feature %q{ end scenario "editing an order cycle" do - # Make the page long enough to avoid the save bar overlaying the form - page.driver.resize(1280, 3600) - oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: distributor_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) visit edit_admin_order_cycle_path(oc) @@ -927,9 +918,6 @@ feature %q{ end scenario "updating an order cycle" do - # Make the page long enough to avoid the save bar overlaying the form - page.driver.resize(1280, 3600) - # Given an order cycle with pickup time and instructions fee1 = create(:enterprise_fee, name: 'my fee', enterprise: enterprise) fee2 = create(:enterprise_fee, name: 'that fee', enterprise: enterprise) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a477ad31e1..f4c5b73fac 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -35,7 +35,7 @@ require 'capybara/poltergeist' Capybara.javascript_driver = :poltergeist Capybara.register_driver :poltergeist do |app| - options = {phantomjs_options: ['--load-images=no'], window_size: [1280, 800], timeout: 2.minutes} + options = {phantomjs_options: ['--load-images=no'], window_size: [1280, 3600], timeout: 2.minutes} # Extend poltergeist's timeout to allow ample time to use pry in browser thread #options.merge! {timeout: 5.minutes} # Enable the remote inspector: Use page.driver.debug to open a remote debugger in chrome From caf35d0ad71553140f497c3cf081bc31ab9b3e19 Mon Sep 17 00:00:00 2001 From: Steve Pettitt Date: Sat, 21 May 2016 11:15:45 +0100 Subject: [PATCH 108/110] Add a spec to exclude canceled orders (failing) --- spec/mailers/producer_mailer_spec.rb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/spec/mailers/producer_mailer_spec.rb b/spec/mailers/producer_mailer_spec.rb index 7291714e03..884ba5cc6f 100644 --- a/spec/mailers/producer_mailer_spec.rb +++ b/spec/mailers/producer_mailer_spec.rb @@ -14,6 +14,7 @@ describe ProducerMailer do let(:p2) { create(:product, price: 23.45, supplier: s2) } let(:p3) { create(:product, price: 34.56, supplier: s1) } let(:p4) { create(:product, price: 45.67, supplier: s1) } + let(:p5) { create(:product, price: 56.78, supplier: s1) } let(:order_cycle) { create(:simple_order_cycle) } let!(:incoming_exchange) { order_cycle.exchanges.create! sender: s1, receiver: d1, incoming: true, receival_instructions: 'Outside shed.' } @@ -33,6 +34,16 @@ describe ProducerMailer do order.save order end + let!(:order_canceled) do + order = create(:order, distributor: d1, order_cycle: order_cycle, state: 'complete') + order.line_items << create(:line_item, variant: p5.variants.first) + order.finalize! + order.cancel + puts order.inspect + order.finalize! + order.save + order + end let(:mail) { ActionMailer::Base.deliveries.last } before do @@ -75,8 +86,11 @@ describe ProducerMailer do mail.body.encoded.should_not include p3.name end + it "does not include canceled orders" do + mail.body.encoded.should_not include p5.name + end + it "includes the total" do - # puts mail.text_part.body.encoded mail.body.encoded.should include 'Total: $50.00' body_as_html(mail).find("tr.total-row") .should have_selector("td", text: "$50.00") From a504a10b521c98d8eb7925d4900738cda88d55e8 Mon Sep 17 00:00:00 2001 From: Steve Pettitt Date: Sat, 21 May 2016 13:32:25 +0100 Subject: [PATCH 109/110] Change to explicitly check complete state (default Spree::Order.complete scope checks only for presence of 'completed_at' timestamp) --- app/mailers/producer_mailer.rb | 2 +- spec/mailers/producer_mailer_spec.rb | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/mailers/producer_mailer.rb b/app/mailers/producer_mailer.rb index 30fad48f58..102ac60417 100644 --- a/app/mailers/producer_mailer.rb +++ b/app/mailers/producer_mailer.rb @@ -33,7 +33,7 @@ class ProducerMailer < Spree::BaseMailer joins(:order => :order_cycle, :variant => :product). where('order_cycles.id = ?', order_cycle). merge(Spree::Product.in_supplier(producer)). - merge(Spree::Order.complete) + merge(Spree::Order.by_state('complete')) end def total_from_line_items(line_items) diff --git a/spec/mailers/producer_mailer_spec.rb b/spec/mailers/producer_mailer_spec.rb index 884ba5cc6f..c964342073 100644 --- a/spec/mailers/producer_mailer_spec.rb +++ b/spec/mailers/producer_mailer_spec.rb @@ -39,8 +39,6 @@ describe ProducerMailer do order.line_items << create(:line_item, variant: p5.variants.first) order.finalize! order.cancel - puts order.inspect - order.finalize! order.save order end From 365c6b3a83dab6dd1aafe693e6e6e463464c7deb Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 8 Jun 2016 13:06:57 +1000 Subject: [PATCH 110/110] Streamlining button clicking on registration spec --- spec/features/consumer/registration_spec.rb | 42 +++++++-------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/spec/features/consumer/registration_spec.rb b/spec/features/consumer/registration_spec.rb index cab6c7d327..5cd34206a5 100644 --- a/spec/features/consumer/registration_spec.rb +++ b/spec/features/consumer/registration_spec.rb @@ -22,7 +22,8 @@ feature "Registration", js: true do expect(URI.parse(current_url).path).to eq registration_path # Done reading introduction - click_button_and_ensure_content "Let's get started!", "Woot! First we need to know a little bit about your enterprise:" + page.has_content? + click_and_ensure(:button, "Let's get started!", lambda { page.has_content? 'Woot!' }) # Filling in details fill_in 'enterprise_name', with: "My Awesome Enterprise" @@ -33,23 +34,20 @@ feature "Registration", js: true do fill_in 'enterprise_zipcode', with: '3070' select 'Australia', from: 'enterprise_country' select 'VIC', from: 'enterprise_state' - click_button 'Continue' + click_and_ensure(:button, "Continue", lambda { page.has_content? 'Who is responsible for managing My Awesome Enterprise?' }) + # Filling in Contact Details - expect(page).to have_content 'Who is responsible for managing My Awesome Enterprise?' fill_in 'enterprise_contact', with: 'Saskia Munroe' page.should have_field 'enterprise_email_address', with: user.email fill_in 'enterprise_phone', with: '12 3456 7890' - click_button 'Continue' + click_and_ensure(:button, "Continue", lambda { page.has_content? 'Last step to add My Awesome Enterprise!' }) # Choosing a type - expect(page).to have_content 'Last step to add My Awesome Enterprise!' - click_link_and_ensure('producer-panel', lambda { page.has_content? '#producer-panel.selected' } ) - click_button 'Create Profile' + click_and_ensure(:link, 'producer-panel', lambda { page.has_content? '#producer-panel.selected' } ) + click_and_ensure(:button, "Create Profile", lambda { page.has_content? 'Nice one!' }) # Enterprise should be created - # save_screenshot '/Users/rob/Desktop/ss.png' unless page.has_content? "Nice one!" - expect(page).to have_content 'Nice one!' e = Enterprise.find_by_name('My Awesome Enterprise') expect(e.address.address1).to eq "123 Abc Street" expect(e.sells).to eq "unspecified" @@ -62,10 +60,9 @@ feature "Registration", js: true do fill_in 'enterprise_abn', with: '12345' fill_in 'enterprise_acn', with: '54321' choose 'Yes' # enterprise_charges_sales_tax - click_button 'Continue' + click_and_ensure(:button, "Continue", lambda { page.has_content? 'Step 1. Select Logo Image' }) # Enterprise should be updated - expect(page).to have_content "Let's upload some pretty pictures so your profile looks great!" e.reload expect(e.description).to eq "Short description" expect(e.long_description).to eq "Long description" @@ -75,21 +72,20 @@ feature "Registration", js: true do # Images # Move from logo page - click_button 'Continue' + click_and_ensure(:button, "Continue", lambda { page.has_content? 'Step 3. Select Promo Image' }) + # Move from promo page - click_button 'Continue' + click_and_ensure(:button, "Continue", lambda { page.has_content? 'How can people find My Awesome Enterprise online?' }) # Filling in social - expect(page).to have_content 'How can people find My Awesome Enterprise online?' fill_in 'enterprise_website', with: 'www.shop.com' fill_in 'enterprise_facebook', with: 'FaCeBoOk' fill_in 'enterprise_linkedin', with: 'LiNkEdIn' fill_in 'enterprise_twitter', with: '@TwItTeR' fill_in 'enterprise_instagram', with: '@InStAgRaM' - click_button 'Continue' + click_and_ensure(:button, "Continue", lambda { page.has_content? 'Finished!' }) # Done - expect(page).to have_content "Finished!" expect(page).to have_content "We've sent a confirmation email to #{user.email} if it hasn't been activated before." e.reload expect(e.website).to eq "www.shop.com" @@ -121,21 +117,11 @@ feature "Registration", js: true do expect(page).to have_content content end - def click_button_and_ensure_content(button_text, content) + def click_and_ensure(type, text, check) # Buttons appear to be unresponsive for a while, so keep clicking them until content appears using_wait_time 0.5 do 10.times do - click_button button_text - break if page.has_content? content - end - end - end - - def click_link_and_ensure(link_text, check) - # Buttons appear to be unresponsive for a while, so keep clicking them until content appears - using_wait_time 0.5 do - 10.times do - click_link link_text + send("click_#{type}", text) break if check.call end end