From 923d06da7317fd4bf32c79bac4db8bbe9fe8a92a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20=20Di=CC=81az=20Corte=CC=81s?= Date: Tue, 13 Oct 2015 19:35:09 -0300 Subject: [PATCH 1/9] PROSA-15 - Refactor Compila 2.4.2, pero se cae al editar, hay que revisar extensivamente. --- activator | 2 +- ...ch-1.3.2.jar => activator-launch-1.3.6.jar | Bin 1213547 -> 1213544 bytes app/Filters.scala | 19 ++ app/Global.scala | 8 - app/controllers/Application.scala | 14 +- app/controllers/AuthConfigImpl.scala | 10 +- app/controllers/AuthController.scala | 37 ++-- app/controllers/AuthorsController.scala | 7 +- app/controllers/BlogsController.scala | 90 +++++----- app/controllers/BlogsGuestController.scala | 10 +- app/controllers/DBElement.scala | 23 --- app/controllers/ImagesController.scala | 23 ++- app/controllers/ImportController.scala | 53 ++++-- app/controllers/PostsController.scala | 165 ++++++++++-------- app/controllers/PostsGuestController.scala | 54 ++++-- app/models/Author.scala | 14 -- app/models/Blog.scala | 35 +--- app/models/HasId.scala | 8 - app/models/HasOwner.scala | 7 - app/models/Image.scala | 11 -- app/models/Owned.scala | 12 -- app/models/Post.scala | 18 -- app/services/AuthorService.scala | 51 ++++-- app/services/BlogService.scala | 39 ++++- app/services/DbService.scala | 92 ++++++++-- app/services/EntityService.scala | 39 ----- app/services/ImageService.scala | 18 +- app/services/PostService.scala | 56 ++++-- app/tools/PostAux.scala | 30 ++-- app/views/blogs_form.scala.html | 2 +- app/views/blogs_index.scala.html | 2 +- app/views/blogs_menu.scala.html | 2 +- app/views/change_password.scala.html | 2 +- app/views/index.scala.html | 2 +- app/views/login.scala.html | 2 +- app/views/main.scala.html | 2 +- app/views/main_editor.scala.html | 2 +- app/views/post_index.scala.html | 2 +- app/views/posts_atom.scala.xml | 2 +- app/views/posts_edit.scala.html | 2 +- app/views/posts_import.scala.html | 2 +- app/views/posts_new.scala.html | 2 +- app/views/posts_view.scala.html | 2 +- app/views/tags/blog_footer.scala.html | 2 +- app/views/tags/footer.scala.html | 2 +- app/views/tags/post_footer.scala.html | 2 +- build.sbt | 40 +++-- conf/application.conf | 48 ++--- project/build.properties | 2 +- project/plugins.sbt | 12 +- 50 files changed, 578 insertions(+), 503 deletions(-) rename activator-launch-1.3.2.jar => activator-launch-1.3.6.jar (85%) create mode 100644 app/Filters.scala delete mode 100644 app/Global.scala delete mode 100644 app/controllers/DBElement.scala delete mode 100644 app/models/HasId.scala delete mode 100644 app/models/HasOwner.scala delete mode 100644 app/models/Owned.scala diff --git a/activator b/activator index 8039d1d..b19a051 100755 --- a/activator +++ b/activator @@ -288,7 +288,7 @@ declare -a java_args declare -a app_commands declare -r real_script_path="$(realpath "$0")" declare -r activator_home="$(realpath "$(dirname "$real_script_path")")" -declare -r app_version="1.3.2" +declare -r app_version="1.3.6" declare -r app_launcher="${activator_home}/activator-launch-${app_version}.jar" declare -r script_name=activator diff --git a/activator-launch-1.3.2.jar b/activator-launch-1.3.6.jar similarity index 85% rename from activator-launch-1.3.2.jar rename to activator-launch-1.3.6.jar index 0947327c403fb3fdf27939e9b83712753b252247..f6f665f82f9b5e0fae9c748cc3f2a1d83eb68a2f 100644 GIT binary patch delta 62888 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 delta 62592 zcmZ@h1$-3O)0|uGZugQ9h>-*jlHd*r?gS`aAc5kN;O_3w07Hux{uCz=%4;cJrFfw& zo=}QgCk!~gZ@+Nu5`S5)!b zOAVI$A0z%hb#Sk^_lVoYSaOLMO)MfV)0>+Lq^Idj?@3N?sVfBWg+1o?;o}X3poPm} zeIMRZSH|p>4*loZqTo<%Pmt5k0LVFlaAmJE@IAOjk8mgCBzi3AHS`uT4z;DH>fLnz zt-SywCpiPsGxQdrE@v}}MPSJLCM~jfiDANJ#pLjbo7WO92Ws33u%xFMd^MKy)0UIv z40vC#+g>1DF@_LbX`|%1Y%Bx?f*rQ^dAQ{XbsT0`9pf>lpEOF1myP8~(nOO+Wlv8q zNrL945=e%jDUo6>3ONH6+k3KG ze#Go`25r308A8W1vG*&ONHd@$eI%M$B9 z_BgtZb%fD%Q%8h|QOg^K5a?Lc;S9EM{g~I>QKNDV!9MWy*`+7ULqDjmo7e>vRCt^;^ z%=NBy7!~hzv&g;%alJqKI9)F&Tti%^|5|Lg1<83mO>a@W#Dw+n%7YidLGA80-np&W z;^_}xp8KQy?N5$pwC@$a%lXUA?XcXgDy8(Td(ut8~Wdi zmuq`t!NKj1%m4Y0%Ts1dW?oas*`Nazo^@REYrwgzB57Y3V%N8ulk;Otwb{DOgB#4u z`=r>Ij7M|szwYw=n_~Tg1IvuZvf775&rzI;- zyYwm3ZHvxypWW`<#-xSQj#i##3;KFZ)39^%4zB*T=%OS28`t@ved(yOLt6|hSmnZU zow6*V|Dz=lJ-6Mrb}Z0%{(;#o_uCe%{9~?r!^#Th?g!0yJZAZCJDRmWakA*SiOJX9 z)~6-=j(IX6e)+{RrMI?=Sh}^v)CPt&Ump6s*Du$8vz(aHKDm%*-p-a|LStQZr=(3B z{`6`KSDP8b7pz^kw&a_XJJ+jE>Nv3Y(M#;pPjnA!y=a;9M|JhKF?#pV8 zE9Pu&Hs!n2jO*^!^@sO}o__6a^WU3JoHpgN7sJ0_VR`Y}7a2nfKKlJ(`oFPJxUA4Nd*I5jZS6n9wwGjGL@)#oz6yW_gL=@UCxGIy67F9;xGyuL(c z_X@`ZRr-U;VVNx|hl%^lw5q}48sfwBVd)1Rhh*-r{#r;#-#N2#=JT3k#64UJFoeN) z&^a^LBor5{nWt)B7uWD^vMwz1Y(1lqEaQNG8=b`J=5@HsKy*0rd_<h z%4p~3VVOhQeMPvq4lWX(Y}1ElcI`AoAYtG$1pM=HDM68*wK6RI>ON;?i>_sfXwF1^ zSmqbWO$l%(M_)O!WVb(r&P-d+u5>-HPlC9H=q$AURR1mnC>4ScP>LH0W)4dGo#dfe zVX&&a-jbO*_-FD|gDM+zPMH5pv1Iliy;wvgJ*{dOoOq_FnPn$@C1htlm^?>F%p5;; z4!M-{q)KMP8QlrDb7p6vVKm7aF`FSam`F7fM9t_(hEc zN`wkdzEE75#`M+#k=c21OCeOt&&*RxE0ad&=Ys?xJT7bq%Ur#@s-VuyT6shu)03Wr z!CR|Nfeb};WoED5FEHLDIk5JCV1q+?T^KY!g^@~qtynX6t-md3GuLcbBFHkUZHl*x zG~)dR##b-|z~^riN9MZBy2SEe_d2>>y62L(=KjLFow^`c^%jG)?Q0qBntLY*CF!SX zhh<*f*TR=8v-yE#;$AZzPP{{%ohuUQha25EzpBC^oodfK`|Bvt5O8>sA$aK#y*a&k znmKdG@3Vyntt)3np5H70nI|ti71zA9!2Gc~3!J*3Gef*VHD^A*v`najnK`=3nJ2DF z_s35SLj?+C8PvGUvDs5e(9W4Rw?nOXDCM*;y_Z)^TnQ()bm0+p#gm`c~nAL+$4<`WlVhXrO#r|XY<35A(SPgc_PlYg?rHSc6R zHn7m5Ix>yVn+XIgoa!w>kYiEvWq$cD<*b&7nX_Lei;kL^^Y*PU0P?%^(GaZH{V%uu z7^bbAu)H*}`$pYqQd1*Q9}hK>jaK&3th-KNQSq6}rZWq6Z1)Hlw^ru{u&ZG##i8pV z7})Uu-8Q8 zTNeovovMX357*@-)SEfs@D5vv&5hFiqKLfhyto>JGP?ajH5#uq4VznD7f8U97b>$W z6?Kbz0JWftTXnM|Rdnmg9Nq~Y{OEuTx2m#r)pT0~2F5iogt62bx@E<374u@o73E{i zdg^A7T;Dkvy9O?s%E@LG3-(O1O>w}kV*@~T$?N;Y7N55&5%u$&I8=y6b88TEZqhw zZrpR&6r=_)-D2Ixq9XwfC~OYsijY*E&V2OA$L@ZnTOz#UbAm9oX1UHF2yr;~Hq)h* z5~vk;_HMPVlVD_+pbJC>XgV1VV&NnOyA^A;QMbq@(g8~p!@4?=D6l~oCR8~BU?iE! zbcJbX^g5Ui9qfKU_mgl?%S1kmrl$_Jz^i;}2unMnJ1xX$rt)GK!bbj}lT!>n&tcn+ z>J|tOK+G}N=wrGG0?IHTlnkugDP1)om<>JSlXg7H3`_ZWflkkP+XCK(VImCuT~|&R z%eGz6oe{Emm1nCj>5d5iBo;7KWt*?)Xs~2BmjIC?K23S^hVCb;NN|)H#!f!;$zUwV zG&r!if9pOa4ZPV|t$%bKg`HH_mOfNG?9MZtUbG^6{8BeW+;Df;H}7=)NRk%7Hah)0 zf@tL#Tf)q22E9pe(vZXAEcy~cEyI9~p_^+CeObD}iaA=yOdr@M493K$E_TGNr@@-X zDGV^39B^fX-VK*xFejR+`bL6{H+nex+M^#Vg2dv(^wMXoPw;`4eI23ym$j_(V-z>-Wl-Ca=8C*0kO)ROR{#P35G3<@lpz8XL zesp|H#T}r+s%1}2eH9W~D?$Hnu5O0I!2seBgSU4KJXS=tv5U3!KL|YLQ#d$_s-bYH zuKte54jwCQFitJ1=4Foh`k_J*AM4nfhWgjk!}!dV?N8FL7mbHSx-whcMt@Z3&~gx` z{I2x3wasj0JN+rK@bt;f&wJ@jqy*`)*5dI9jCT;^q)`s?pW>3o8Q1}N-@f%>Av zqUBn|AiY8KBX0)q$scPzRR4tdwZs@>)4N#OaDAcxGd4m$U)=B^FPilqrEe=BS{$yA z)>BDgsP8v{Wti5zDHr(6N)#IwlP(*S&&&AKG8j@zVW?9o^EEytK77WV3({;n`c>$!W5=zk-FaGHy&upnmt8E06c zdteu3rB3QA5Sup3*6P5lQ~L4#O~%)O>1z@kSS$5bvPbsE;lJrmk|3JzrRUUkLd`&( zg-!TFzs)~gn`P-&2o8+2HaTV?*YtM?XzLjcSKlfQR_eN*CID>j4ZR!e(Ok+=0* z$RyqsplNwE@1A@5MA04i7?B8v%3}jBcqChn;nMJyP^YbLV9Z*b%Jfh4GYO0ZNLwqX z@2{SpUHV7XO@_(rjUbVB0}~7k3!5r#(rrD%nA2@Y6-}vy8Rz#5 zr}1A970=eRU;`5#GhFt11lAk5=2g*wdHD@;(B%ab9Otbz)+Nk99TqzX7L9O&93ik{ z|4g#5wc=T=7{gX!5W_X4oO1K!HpAt1s<6ac=wc12B9Xnb#Q1na9U%dD2>7%Y;%Z=- zWaSJS1h+Oq0*;xbwN93aeVi;oy#?T54K;}ADjV90!6yCSPhn8CrmC=+35Iz>sb)pR z`qE2=ZGAaJ)M?&p_?`|FsD(jV)YU)(0y~{z*r(&WKe*RdpY{mxqFj1nS_IqQ-7tlW z#Z(M~>RC8iMJA|`Y(Wo$Ls-i&TXqP-^lF>J;e3Kxj;-%w7)ek*&+pXFAeWK|fCGtW zQNDqOVMN9&+Obr_LLnMb1{;b^uOl zhG82#r#Bb8d?71-+}eFXEALe5`Q?>oGa9;EU$46DcAp~eMw}kA>B5tvy>C^H`h7;8 zDxIDd-`KF+kL`~ih%CN-q@Hh}m3W!&$N3nhQn-oczkybIh~Xy@_*X*==fpLRup#XJ zP(yLyDO{O}kg~%Kr3iw&Aux7?VR9}LGmbO_6Bg@6Fq9u<*e67Ca^@Uu=taE~chF$L zc*99w3K%_s1Ok@%U=}jbK3DWWm%IUq)Fr6gx#{kT++3+{f zlxk=WVWB$=a<5I}a_%&&C0)@yO(E>q9>Z86hc)}!@PHhcbjcV3?e-Z)3Foy36A$(o z770Md1BOWWV!vUR58xMZ9edF5gSgfR0}mP2<^tH>Lk1UV#YoXG#9n@9m_e8sKNyCK zK^9s@aF>o7$`QKt37=*{{|d41P8w)`12_cITBsK2blQ+3R3kJ(NB~rw59a=A_(oXj zo`g&jX|tpwDB2%d1^2O({3kwQOFoU z^Dv_m5kE~k`{?gfrR;?c~>=@{aDr5UEr1;K-7$SMkj3P zid$pdY8aaf6ln5K)H0qIc5B_26SGqZM%=&6eWao}smb`YwlPCMF%IGU>7^6iCSyB( zUB|eKID}*;)K5`8Y(qWcXN1#2|8WE3DuQUCui6SjU!sw5r*L2EItUGBuI5I!fCA54 zXwnwbXI?ifKfPNSvq{jkf2<*ZERpc2o9bZmKQgW%oR;y(7bo2(N!z=lq#xQFR}wY0 zR|uQX(fBz*G_xyyY&=apF(tto!U`rEs}oI+6eCrF$Zp2(MX0pYeb>!6f+)2dZPU}J zuyvmpmk@Z+8&r#6xy;oDsLs8OJ&8sunAv@Ndg8t@#sW~G7y9r?KjUgasHIud0Aml@ z_S4GT&%M+b_WcN%n}D_yfVAGKk)0f6q$eW`r4ALcSjBP1VRVBPm{zP>t%1rDjc0_j zDDIKC=9$C7CL1>iUOw1_K>5Bn4SqVsSf0QrvH)v7&G;ihG^bAWMW-6gFjgb5mMek% z)KK>AOrQ4B3dh}f7|&_5jb_or4D(R74&KBu{d}X7glUkv3*LuJUucwL*W-TJfgXH{ zx&CaC@uF}{K%WUf>bK~Cb+JqswsDD(mK6{kk0Z?J^~QXxDHw6zE4OE9X}0n+;}wzK z3`;=+aRZuHaa)X$Oux=Z8>kWtS5vX)tzK_zMJ`E}k{gV24~f9AFb!FL{_;Or+H5lJ z_mjj>xm?1K5H?|(@uD9gXjU%XVLVS@Ekq}GV{SDcj4F5RH1_@w$699^yZC__vW)hF z02jih?(uPlWV6t(jqQn=8^{)X<6|2(%daBX*xLD6*L_AE#g{i4JQ|7*FVhbh9|$_k z#e8hlVPh{EEwNEFIxEZ_hVz4jpN!k63AGfr|6=@_r1C1nvQ8RfguC(G}ah@FtvjvW=~1#{`9h!yltmCriI&JVNejZ_eZ!jRXFSyT)5$ z8Ov)(2%Gc7*pD32GJfV8V-y@4jY1Nh8?TaqSmr80$uYRA)$4^Zi@-Z8V`0>XRwL(HyH~t#fyNk2cf2Mbc4nW6FhMBhDB;{!J$bQoHEfSTA%~ESqf0Q zi$xbSEh2{}IBk+I1Qt$K%}ig|B%e#vDQc1%L{dnBSW}`<&U%(K?Vy{mIMV?ko@K?G z))HoVIa3K?JxIkF)6@v2t7N(_xOkYW!NIxsFs@b=(+vWnu$=JDo7OCNwu$n5&to5rJ_=5yKEn10!E1d0$Cm;YlVrA=UWT%*G=4xTUEf@p~cjfwWy;5~vgPH>~0sS?G4%z%S)Fk72!$ zp)LO-Rai!2AZ?y%Wd}N&Y}9<%bv3Y)y-qgKV>dpAdBG4q4?EhA-F-S5c95eamh>}E zZxhWS04rq-C=2jW1nXmJOVrxrH@2VYuqat*RSx;mc?6n>Om4`RYFaKzwALTHEI=!A zQcbT(6wb(E0Ks+^IoOmb{9%6#GffmU3R<5To$B7b7wNLY&o{k_^$grsR>1cD2TD*PW)jVw%9AY}6jpBr)RdIbgEVc*>KHp`=0%AzMpF zggBveKZ}ihddBpX5T)f0Oa0vxMSk#%V(bqeh-m8qU|)eb7IndNSuBnDR0DhVN}QBe z&NiJ822r7jVRdhtXhq38O)MynO>TDct|^VO2FqD4c*z4(6N;bE8^a1bHl_Xt^@%4w zgRqo6^_giPQDdpim0jkAk4h;!`;|}raxlXRD)*eV5fmK^VK_^YiSDg`E3u3`GMK+6 zlLgoc@oOEtLD!N(!_7fHcpGGXAzBYJIu^3lVx_K> z*F0PJrF9mK;c-6m&w>`)t|Ysc-%MNW-fYw8{`dj~uL(1=Tw*Dq#5&am3&YLd=3*F% z8AGO#+3a`OFn^fN}|O4^9S_5W)7w62y_ zN?o&Dn`<(X+nbFnuDB5bR=q z_cd?)036J+`kPk^Fq=BiEEjsREi^Y4rkUx12{i?qF~}@mCBvzvw-d4Wq2_}^DiAd& z+p!jv8*Xkd;>fT9&@`0lm6fH9G4~KOvYRl|c(bg9nrowXU@z}G!91N(h_@b#n)08$ z0l{WAcB*+P+2$?3`>^*lm~Or)S^`B^V7+IUpNU!zxcG~KcutqG-E4*$J5>i;|Ec*q zGV(ow&pE-iCFbiS0n;az9a>|iHx|8d5i)J;*gA8Dpk`>nByx$H1Iha_my}GL>o?eC zzA6acGmIhCL9$5;yij0f9S@mr5uYYAYBzS<>Br0&#PXgSV|Sw)V^5m1iI2Mh=!OF> z>{bKVx(hy?f^cl%W%EN~Ic&C60EC#?!CU6Nq)M|OX%FsCeSOz_kEq|X00|uI&Qo)y zKi{@JX!*5&&DZ{eF9-jqhrKme=tV<@7Q|5Zh*Sl7oFZB#^X7#q~cGF#*v z!`i%-DwBwin_cK&kyQj~NoP3``P~$6l`*E>QwTFd~ zqgtb{Ie_D6@qQNhOhHRv1c$R411uj2+W<2sX6a3f9oCGoIN9km%U3kP;(`Rq1*r$I zyBr&3Sx1eE>vRpzkW~(&Tp%nwjOtsDv-m6rc&FFEx5iukN5&F0JUWE+_RLhv7vwh9 zladgB2-R+xW9cnkef6%KQBpD1eT5~7tickSOMP#pMUIPFG>}?_E=N#7{8|gG+j-F_ z3A>Nr?)z8kEc2+{HL&|T1W(^+X-Nt&<}r}`9j<}$ZMGO`aMs!r!GWwDv(SRdTYnL( zu-e~RLP?0W2IpWH{XI5C#y-n+!sW(Wfw&(nR(AG)<(cSK?9^e)aJp&oy+uwl-uz(M zDI~DnKUw7dycWLfpDboJ>!_uWc*n`>^y!xt3pDu&%faLG7TO$SEiPJCi7>DyS1q)s z#lF2}p~A*+1wfBR8FqzRBAeK;JC-fN8j)Hm+i}lAn;UHH1B=|1(K<}jG3+quk1P$z zLu|_xLM9)s@vXq87WvMAHtkxYvxc(5&n*{8p9Ckquyht+w*I9>zKMgYJo%y&-qdGb zy|a8LX!$M??tkg5Q~b1p`!r4*ub#wN)KHW49^o+dWg$anwL|?=SncZ7FMQLG`c3Xk;x}%Ni$Q!Z1<45jVi)!0K~2BDJk;ZAl8X z-i;G*v4#eWeMxq{zI6bRXx?S~j!utnY^_WyGzosx(i-9)B8|FMd+S*8S5W6;uR2?w zi@uWQ0{#6K{hGNo@2;>wdw}-odEm70jsbR#;n; zHN1%Maf&ruWtDr?n#(y?uqEEDv6d&7`Hm#SZ?Q(Ok^i$Uq;|(rP!f>Z0z0nagR@Rs ztW@E60g^b=o^4jzg9dGXGA#@DCx6JW&Lt(@HrrzLusL5@!^lu?b8&F?9_wil6~0qe z5~9vxT&*T}MtTiyPtaQaWRFspXfYA0!$Cz=%#&RHADF8ILfi4|5} z$C1x--dciKc)#afTH)<=Y?P@Nd~ybdH!qIWykwPIa@y#S@fVH`t*=<4NQ*W;r`}Kl zAnFEA4p_F;PB@y*AU=?p|FYI018_DN1E+3Vt!&f{YY2f4-?!o<{HFDH5n*jWN4fy` z<(Bn|fNEqHZ{omx@{u(VT1+y=LGe4*X#$V)b+2?-a0|1j@-6iL=^bka(&I;fFK^wm z(jEgt;dCR%0_J{b9Zvjw`s>~EMz9J89$D#K3T@$a@D7eqCm&laa4yFxo^1g>Si!wU zL@`9k$Z9F zve&Py^v9{tQfG^U`1`mYboHKUhTd;QT5~0U2T$6@>;Krr3t7Ho! z4y{?x%ow(>itPp&Agm9E6Hjppb3eiM2^lVM9=O`T=79KTX#MiqHn{}UR&CsMh6J)_ z;RCc>r;?tlZb)cvGqZJ#Z2wSE!B#EDPB*jB)*D}glmmp|U*)y1$%k87I%=_v{1@Le z!mJ5k$t`VN$V7@C`?r;CDuMAKtJeay=Of!}0uwcKexc^$;v8&cTU#eVz%Z<|+U3sy zV<4(MeuyHw84_AL+mZz#Xoev!FT8uH+F?f*+jG$ff=xrI>}l8cQh_Z@u}vaDI9A1g z)>|5Pw;dLfEQVd{IW-+0Pgwjwn_MYsODBW`GFO_dun@+o46?~bcKEy!2j+n`HeVRq}3Z4KFk@(M$&(G$Q9oUzH#cv43@H`N1w z7(6N~d)D@iu!*4_T0B9SB!`!7p3&oGOaAcbZrm?T#66=2|DNQ6?W~9(w-jHuuvx9t zW+(7dI{Z5)u+Zdjvax^K^uht?@@H=2U_hPl;EfW%j@+=lq*iKq-K&TFbJsSRxOkIt z?QHu)+d7K%ggA${q0_VM7Wmifaj`{DZ9OR(*luJc>D6dkSUhIfY{57M{cDpins!Z5 zyaKgvvB2z?wpYSS_gPnNQsHM>`jfebGIPNi$5*ZF)kwRR3GpVojqTIfo0BU%#tgx2 zh>$~o6T*F)Cm%a*wyz-VGJNgJGWs^PyOheYePEZ&bJ>_{D{JUa+3K^4_&*VNc@79${qzeOunAuhpIw6VXS(vqc%}vK=5l(T*#pi z$J#()#-KfDt|nw<35VQg*JMv@?l8fO(vH7#i5WJ=Ug0RKRlz~qCk(wVK&r4OU;#&F zwx+scw1_>6s^d^8Db-uKG=MZ_cod2~a9n-I@05{fZ(%r;-(!QMFi#LHZ{qk%*sK*K z{6v_Y?P%)QK#ptugM%SoxTh%F+T1Zg;25Un3ULEk|3X4DWQSv(DyJGsY9IpPST3@h`K;}@Zi z>5e(%Rw>4-u=deCcJib{&Py~mxCm%-+HqZ^ixw_~;`*&9rc+uhrc=%Q0XU^SDVgjfuGgsk&G{N8gI*ShGFA z45=lt7xk|ha8}f6k`}~9)en$+^>K{?4wI#rvt?ms_+JH|*XXAaB}>&egcuPUCq%_#(Z6-u8Ua_g2T(5wMk1U`f%!VPjdl0{)B@B5@+( zV_EYuSn~5;4Hzb(z%U9wk&F+BZ4h1-W4rCIfG&cFVYp5)doK0B`Blz9)$UDjW48U<{>+mVz1% z1&6W3cL8#vQ5#jcVD`@7j35^5WhK3;TorIk?`3n&rES-it(}!ve2`N<62}l!Vh=-{ zugG8IUr0%(^*_`f!uEwb)j{x4i*Wt&I2gj$qvUj<*W^r z)`WqT(OG8^=W1fnW`rm;lI0Y2zR4$=6f3t^5{pfA7NxjqLmN`(XAA2(YYFo~m=+1u zs$y}9Zs3$Q)x>GS_*7>|vk5ZUTp!Oflq9A(ql`8vO`ItW?S|iXD~df z>j`ELGM(}d=lNKMJNZu9eP(FwViJ$lsXROIty6B;VTl+-{ZLEN9Q-&eT&Rx`DSy=Y zJr!iF^*D~jo_Fr1X3&N;4Xj*s?iGp{Dw;{Y;{4(PrRa61UQqGYiiL#67#sM@8Ae$t zz%G_^+u0~cv=_E`F-R-p@~}~FoNI+)EGhs$1}kU;s|ijw_5{P=P?ylmhfPhHi>(ZE zJs@QocwrM_sY4#VnEha!RIp$pYqw1to5$jgmDD-1~T1cIwM?%`~z?)p@Oi6z!_O&}4Zk8`kR z2`-h4!?yGh*1QD{bZE~UA%|CVO}iDG4O|%_WZ3A%xbZeHC1I+&8@fEC45w5+64AW; zEUmFi{(Tfy3Lku26&GfCQ`ZR6h{Z5hQB_z3#4cxxVyO&wyCX4mK#jwVz8#A$Ha2l zx#rT4hf7>97!VxHzVG77N4<{kPLa7rhBBv-89Z4q2W;AhiP^DFT)hazm*s?lcWrS- zQ?m{Btwp_DEy!LiOv%0Q+r(QD2bKD`XfBeQAU4*buS>oqfJIqKW%K*Hx(bEt$w1c# zVKl=k(_Y*F2CJC%HRCqwV{0-)sr|KRb`;b_d4b>QzN;Ku=iONtWy*0_Oabw6-tqmq_MAqak z$XfYJmwa-L=9Ggu8(rer7icvo8@~?Cc5HId-|To}%E5qOJ4@f{qK907vqIc!=eR4o zFdi?v;;b%myK6et2+aXOAj%Lsl<5kku<#bx^Ns5v#X@T-G}-~PQ?LRg>~oDIVy!1> zxF;z%7@3c_Vo8_Ql=#K!a5nfymwZ3nYeEk!&+gw`0dS%lCe8elF8PcBttqB`zne8X z?b;{IU^vvsKLODS0)FG$$xz66im#S$*<5z=Cmt94{=4fpG4f#O<<3jw_gDZpJ1pqV z&A#BOBJ_YzUyv=lBm0}~@?yq7*`PT|m?l;RdHZvPv*1en7R6L{T zo91w}FAh%QySv+j34YBIR2j*NOmJ5)|JO%sTEirrJDMR;6j{SJ3rQ@yGspdgP{Pmx ztZKU3Nio*CjajrT0Bng4|8t+Cyzp9SP|Tdb z&IW(!{(;&Xrzgb#1>0HbW*>bxal(SnWhyV-`Piv#ZfaO|Vuw4J_<01qgj_i*w975u z;doDo54yGM$D89IDE|no-z^vSO6p zB}9~Q3nB_~MtNe`g62viQp>}s2||+#va(6a1hEN1UKU|*Iw*@MI9fSgI0mcqZyzg$ z55Q5ZNLPg(fitwGJ3T-ktGkXxtJPHHAlX4E9y*4hTPWH|vqLk{-Boe38^e?++ONh| zD?`Dy!m4rDYg^4y3R3=1_?C{xesg%GBIkQTBip2k(*Q6 z7^St|f(ba4jhlea5i_A{rU7_@3SiR}ZKfbkxbHGU`J&x6hjl}M*6`z|E{?7;!pt?d|si` zHel6FB^16puc(AxaT_56((G;}sfkW+&m+%efuIWt9TEX8FDTWBDgANX6Cvj>Ds*%NAWdW9MtODU=FBam{qB-dj3AG%BP8ImLZ_81 z<$&bN3LU`!nu4vDeH3UmzP{{Zqb9ii6(7OFt|;|Mi^g#IiVwq@tEi^PRfSG{08PZ8 zt3D!-rY?!NcU4KC8w_74G|f`zum?bND9q1N=nxzDHp_=WpRLp+?grUP1G-t_bEEnG zCfmpNxNAPq8+Facr5~>;4TTVmFYZqtzLY;DmIttr4{1Gd?Tu{D>pqcPab0OnXl$Ei zbW`syAEVR%QkoLx=3hQ$mAm0{qlM}78$Mw=;)~WIUi_v{#M|FgYLQ&bZkYt9Zz`n- zW4xtQrW-Btop1R>UW2Ung`lR|{tV@B`!J;3R_MGNz%E25qJVk_oAuFch0a3Z5IQ^t zn%z;#kX)_rt-hn=Bgi|>eg2LTCPx`^w&50oL|y4*vho48*7Q1ae=sB{$Y#*dUb;yV42(nnnDbCkB? zx=W7IQe1zPqcj!Qmd8pBaoyPGdfsEDtAPLgSQ#j;d;YDo5ZB-St)z)?NsCgQs1KT5K=KK>8>9b)cuEbmjv`|(qyp@46Gs(dW21D;7;?R~D- zKa+8I@Jz<1*>h!(;Qju&(oS4Q|10A&?q8*rfFJo+86d7Jyud*#*S2LZq%ZegC@BKo z>ZNR#wJ&9Sp1o8W3!3_`B=2Xhqz%tr$@o`!tuzqy(_c#)Uc6R%3V4qy)=r*Y3A6$KJh_?cVa8q*?z?+HmU~#db+KRW#j_>1yp<=}S(kq%=mSQqoS> z5j9-UsfogvD7{Jvxj?V>5b&3Jm6E@nLG2{q8w@HnR)kTd1RiBnDS3V|s+5p5O%lGq zBCTamwqZPL-1R zl2fHbt?iO}H@IY5sBU>Z)-7#5?N)0F8-f&-I>HD=>N=;WX@aJ)D)lZ`|$h-0{eKC?BX&Q5q2_+v2Z4>3g*xscT}8l>dE@jK2~rdB+8-R7h_GOB)jN z%63`kbDfn}$|(^db7N?T%=4c@WE=|RlQJjf^RXwNq>0Wib*1@SAIvXfRVY;2IX6`O zM8x)4sM7AY-drj}M|o^q(#lSt_Pglrqm& zlxyy_a}8 zdlr6TdjtOR;!pl71oLaFF%xp>K1$AB^f@B;;Ljt8@apjpgLc$mpSadN1`qEsxODga zDMN-~ZzfR9`Bu zULTcz1nujq;X?Un4syX#AF2Z!6ayFQlZi_kAml8BH&BZUz9tAVJeg$x=WP7`bm=-O z#cLrat_52gs09S^84hy5g9d8h(z#7$9q(Wj^8k7mt1;W6x zs!jO%;7Z9u>kP!dCZ>UBIx?}hLZu%0@%`q-vzGQjIz@dg}QDNJIhG^SR zur-poP^uAvtkB2@G=PIz!Ym)qHV$eGM}0sK5M*eu(EuqM4PhnT&wh61zVl~11#$&i z?w|d4p=D#Wu&`)sV>L|wVms7of}fMz;e)uy5%)8^2xk-3Eqq;6_D;ci=%*Fizlfi& zo1tbCwQO>3(1(pUy74QF-KV%&p!m_%`Pz_Y2=94uty2f|`0w63YthB8laQ+${_^>8 zSA^3|)I!4kmrc~LLdW+Bf!LQvj`~lDP^Bq`a5S`ON|^>@I4B9g2ehAqqTqrL=p6^$ zgRo|jF|iqfGN5xawSWjq*}N+kUB|GT#<2LO!1u7KnM{FW$ZE*>&H(oB4Fx8=-)!dH z>?Uq`b=vnALy{y0N?Cy5F>UBj^3i=6naqK+-h5Gk^8O>$y z+|(SMY6L$tS4$MiZNs}``=mEQN&V2NP(Nu)PGaJnF$6)e7OEl|qh<^At;p$I41P*p z5rSI2`R2$a6cC5P`~@{QqmhTf!4|SxpF~0WzQ4lwBsByk_EX&ja}BCjW%crDC}SNO zMh;1!P`-*%qaYs!5M{Do2mrVcDrHaB>%psk#&tZa++kArX8s)a-}E;sw+Fz>+559GGM zcZcLK5Zewzob}ECFWwoVOTAx}s4cNWBg+1bGFdbyOAML*}EFQ8Q6M^$!GG5B5?Y zMQR}?i;MX_gd^~q>G-vKfhgVyqc{-ib&|8L0SGdjEntKP1&sMW@E^`D?Q#M24*Q^k z>!RUwCs{9aAEU}*1vxAnIu|s$wDK&>ue^K9U%#V>$vCw8SDin^NGul+4axY{57g=`^Qn1f#1<;avEk66q%rJ+oZ87L zLQ6wx7yJ|KhNaQw(*uZqX%|Fh#BpR4)aW9uoAmCBKQ?1CT*b*!VL$6m!@w?T8PVp& zH#MkO8R>4L-!=T`PQc+Vav`O3MMY^^Q z7l8*Z1b`)3j$z@+h+UgN*uW3`PU95oYkMiaNLCAoW@>i+=?dJDkT9Wk+Ob1_Z3OP+olGrYZqsQ?r4ixEQF36AN~QNt90RUM5A9Oy35 z@t+_%_+ul)eQeDCL3zyg(D~Yk5ov+HqJF8NPc}k$vauMv^KtnY3e$ChTc61Jq_YRg z59(%w(%p>VAILuzv%O+9l>G<({L9T%WNF?LKS|*3ZVZMsJ*34uxWvJqc+tWCJ=DJ7 zH0670H03@>s}4O8TfMi2jRNSYhKgy+puqgA!qApg_$%n=@4-GsxX{N~=!50RZ|@G4 z;05m}CQYOtO9?2{ON|x1Y*s?KG3D?Tfo3?^M*GpdgS1|1DUsSg-2So5FDP#l$}8qa zH)#Y6>V01vmYe_&tuxLr$1qr6P~`P;V(y7pFMg`@?& zQBuiqUP*^~OBI(9Vfb>q5e|#4gh=Gh@A(N`@ZS}x?5l(03(x(r$U zC!B?+8KKrRJTc{7hFtzWNgRKt5R!lSK~%7N^p#P0$Tdv_dp|ivtlke@_-M8f*3QNB z#KSm(VfjN`j+BJ_@nac%J;nr*>9 zW)2TgZK4~m>wf6XK9r^4pz1FxVVjo)PZ+U4oX^s5=O7f=W``F$qpj3rAB>3JJ7Lmb zwJ1MogghQwckQ?YRB{`CCH-neAWR&rRu-ln-~zVo_6m5g`#+|SgYPvu$Yb}{2a@XU z@v?n9LODv;v>zXW+6R8)MJPk%1f^)ZQ5*P?gx}!LKNovI z;!v53sY6jfjeU?c6nAp^?=z}e;)#s5z3aW~ca%^Gjq#WAFI9#ch^Fa-MjZOBsFzry^I3p&v+@zU$(PXDFpUN(u3c{!XYh zO0K3hEUtZ$zyGG-)|)@weVB@L!sczGFkZ=Ubd+2(=GQ%Yc^Wq{2IB^l|4vp*aF3Qd zS=C3Qp?#kiVG@3|>%Uj?^;g!PGwLTa^&}=tIX`p%ex@-N<;TM072{Lj<)2c1M)KoG zUe=F1^hGW?9v2fV$g7S%_sNe){tJ?q@guMODwiBjkO>mx)t8K|$Bp?=dapMcdASb; z?oT5oSLA1b?!ljbx0(OW2piuSBi>JCNE*wtRR@R1sxgqDGnqeVL(AplUq_)pK7;mO z!2%o_tJW6P@oXdJgqA+u{pd0>+lpyfFRsHLBhDsXtR+F7(<%BlX}MIv1o#@;`rE<(Z-nNnZ1oK%yy^i-s4 z?kDf#k`O&rZ48g5NbO}4=FQ^+#yR|T^`llHYO2~#_^@ay`tV!4*N22@SmcMN%e<&^ zA+2U6>I}pd<6HXj-`H}*5+^*FRVeBCZ>X;kdR#@4;?*`-G)=zha(No+yi?gL0M8Mk zWFDWVQZK&UwFU)L_7e~VDbwWx-YwHnKzfbbz~j+Ekc~zQm(&@Et&%|4A~11=oW?ES z*j{zKSUh7W5bpV4Uvg~8`h;~t(o9)4$IV3SkVanYy+U#ZaG4|iZOpr8B;dhA7KofB zRhF8C*h@_zaW;-lU1!P68pRQ{2dbi~%1Nmw`ZMTpQghhy8%_QB$jP@WI6FMrzsU8bUg2da3Tm)WP^)a2t_o6q@qvOtE4|cs zD3(`&NQFg!7N1__#lBu;!atz}Sp_e1?7r1rEI%m=@-CC5qQo-92CXG*ap?E6G~Imn z`|_*NblnH0r>rLyGaU61c$Eum@;@&YPu8N^^Y2a1$*x#;$`{O&C)TfN`)u%1^AooK zPu#+JS`%si?9WhO)J`9gBcG`r;rm67t+&SqyKcY4#(s|2XZwAy-9DGt(Hv|1k+6B; z(C1R`DUMA#Ls;*DU7Bg4(=z*B#M_`zR!1nj=7`HVga`pVCd>{wD`i06agy0j zd`R%fFdMX3B}uxhLXyO1J|zEJC6yoK*oXi6V9l%LL_B&mVpqQM!Qx?Jc6j@h6hDcR zw0P@7^7Crx`8AGxod;6ZsKua$&ivs}ep|nAz;Y~pzhJ=qhv@B)yhctnvPS zn0EFU%s4OS)9dfAtjK5Ofmp0c`Q!{XU?8#3kT(ziF6vLDT8JF}YrFkHUTQp~jM5>t z+RuqYQ0@)PBL718B-l%hhnLaR?(JH%bALWBqT&~_XEgr;5ntu^BBp;KW3-whx`ufX zc!(J(?l3wjcs-hFMKfdkoN68JrN)EJJR)lkhgOP?N6MS{^Y1tfBD~aN)~SJ_n{CYV z&45=ZlfMb*AF!VxW1U(>B_ZjvD!!?Bkdd$DYj>@24^ zqF-Atf*<|{O|s=eBIe!~ftv#I=V1@&mrM)LMqvD%AIkp(!32{NN5+A*aUehl7_z?btk3TE&Ux;skmRsaPe8m=& zlF^q-DXzT@CMMK*t`RRgIL{kqp^NZYD5 z6H+ttmfZY5#9zjrf3CGp^M())o^!yr#pO%2`M05ktp|CN1P`DK(vnyBS@zP4r@S&e zL&g4EC$C}QHo0}OztOl?d}_G~lgYn|+#3dqwxRQp+hyk~a%sr@)=1qDC!+qZ%*+_+ zr5?3i%`0puRK5D&)scD>Qu~j>)nVs$xkdeOJ9=_voY#{gJ7oDZAFMew6%8AT`?mhE zj-KG9#)I$Z{o_?TP|W?wUIdJ%suz7mZIL1X+)kA}odwVE*I#3ylGe?ntgeqc=4A+P8k zGNmzBGErxZ|Eudtz;imfa1gS7+sz)?NysX8A|kOxh$QwUmc&v!K`B89T19QYHbYYq zs??fLYKx$?7O}MmLhQQ)wI#y;&Uf9JJNf?K^L)>P_nb3l&YYP!v)nnSWB^(B*pjdL zP5=!GWMKMEZo&;8_ZDSB4>uE|^e&XC3wIhEls5kQ)<*wR(55f65lz^I=y!1wKG|`5 z3*A)kfmCsaJ@o-G?80sor4JWH^!n zE8fWfvh}eE;qu&Hj4C*q7&sp{0uN$`!kDp;Ch;Rk;%99#l6Urdd-x>knvA-Nb=X}) zB_L_H!A)tbCa;6)XQSHwC{vs`ZW>f9C{sgD@8%QEzfF>IPoeAlVA79o+A}_Tyt!;q z!#D;hkw=R?s629pE7frzv-a@FG?SuL-Bf7t9zL&pF!Fk<(5SsU=1trSUN=7#n!T4x z-cM1VK#peYGuYdHKBMp2E3Mu(vIdG&bqeJqWZ*tt__Tcx;MZ7%9@@tb|B5IY7^Xr; zhgsMPA$80BAgS9*MKYolNEYws4z-Gs1h-X@ARnQw^07xo9;gCh7!|uA-j@PzV;P_$ zEAzN_wd?cifN#)ZbjMH}c~|#Qu^^414f&AAm9jYisWTE~5}JsRbi%q(@t^bk&3DX& z#0vONfG|S23{tVEmzw*jz}G`mD6$?hfj-ei<2FOh0*vwEDD!NjiUr9KwZh^zC7((i z%}Cg7v{MvVltO_3f{ci!_YHz3{Oj@^iVQF*GJPs#3#8c+*)TXLesVbt0iWa7R;ckv zl6}ySpjhUgvXx=YKtsb6aiVgXL8ZFWWQeREZeQAL$1K?JBmy9@+MClDbq#`Si&~O> zh__D8A=K{MxhfK5US#`eHiv;C&>OqGNuRQRHc?sSMF+ed_1aJtvV3nC#ryAJGj75u#ipJB!}eSM zhrvlWT^(ASdZ#)ny&FIA$a&b^$^ybp^4;V_{JrYm_e2s6@wktPF6SignE90M@FS5f zL}}2;DVIl`M9FpF1LEK?;faa`sWxrNtiQO_C4WIGd8Q&c_m_dKY)dH`TOy+7r12^4 z#vMEXTUakwumCtlWeA=}Tf%i0;|QQ%rTTA&AsyntSG)6UZLbYN&(zwqstg^p_~ zZv`Z{s(6ux)PiK4=Z&$Aitp{EB0)-0b}eo8>fRa8pzkC$8N*o1vxMV{VKy(XwIi0GW9&wmYmH+h|>QjuLIK}?lP8QGh<`mD-{)58T;vN;PLx+F(S3c7y5YJU zH26JQxGqLVhRSS6-fFrkimsn__X6B+5JD@_%43Jg zY{>A+LZ<7Np*^;3j0Of^LENu!PZ`$c-~%hj+6Y$`N4~u&DwZN_Vv|Q}_^|ua6-cc! zNhAqX{JU%Kz_?OWC=HbpH)bAAR)IhEGxP?J49T zT0tQcX_%@)Q&N%mRsBg~&{dGkoGz2lEVE?HRleQxP5p;A@qB?d1q7i6-!7m;F?ZbvHt@Vn!?wg$oA z&0%oS7E#3O4xbPc??B3t%d!;Y;AMvac@$_~C<92q%SJEPy8z6t$pEtNItVw)XHnF) zNOs+QcX{A=l>+06WkAWr%T~N+y0u##1fM>JpEz6}AYJYm!j$$}b`Mh8-J??MjH!9a z$$N%ciUa(i$&Js!AomhKvC`k&XDrqv=ssUD#@vU%|2$x*F^8||bn>dzyr#MTQ}WLb z8LuG!gt{yjd4eBMfHfAlzg ze;-u^Y~mSrG#UStpK<3rMd^uE$(r}*7RYYQ_Uo*k0T^h`0DRYGj-0MkbEVoBJGF78 zoyNwbjCkzmQh_uDHcgJqo*p?vrS`T^p~#Oc2~rz(+v!4YV!Xglyh_&9lJeCiNSGa6ijnOmE*? zRz7r_q+!%pWt8wyFTVt`P!*$wD9(sl6MZRP_~`GD*HClF}k`GnV_PvC@|kUx1{SJ0p?Ewin#Hw*Ij+x*60; zurf1iO`w3RJQEX}O&e$tZfJHraE@e^;lXOrYN zhA5?5p7J|Atq4_Fj0s%a!AiisJgq#kN$ZWdr^%-`ygJ&qPAN9AjTs8J_o!e=(o)4t!I( z%RuQnes#L$djR1*WuV`C{!B(@hofzJ!GxjkTXDJbFUfw--`#pd1?2Xl0vghM)fUEj zDV`T!26XTMiu%hLtFy}Z;mf?@+s!(W5~}ptfsBUjX?d1|xm&WVTT(gSpZ@Aotiw9k zf1Y>c_*gSoran>5U8RVMu^2@GPtt0Xm1z_~oM&AaP1_xADjuGhkELui$=DD4@r|_X zznv{XJsrOQ!J*HR?H_oY|LgXN$i&4)GYR`tqF-KLA(v2W zF4++0m6%Smn@VgtwGKF0R0T#FjJ_)>+bzpj2u6p7lrbe!B zgUP}zi!Sfqv(z0ID67OGCDB zcG=C}X6(e-*w7$Om2Z*V|3jv}R8?Z<2Y%9b6Q;q>B2af9!8!Jvm8p?s+_Xy^z8Gz# z1=||@`~CdTGLvT|3YC$uR3 zYO4mtY{9RtP}GBeSW)anpU&nmOUKJ9Sx9KEI1SlM(brcP%3_XxZ|(Ol!jSP0ZX*_= zxk{;>NXx5Mb|kJkcg+6PAuIB>tOBx~vz68s3Z&jM0!pfLwx8|zbm7Y+_*g#_CWeN4 z@F*EGKy(d>Z3!pWfV>IMRVY%SYe`NGu1h{8>HOMQq>>TcNr^^DOUJ2mVYoH;)e#K% ziqyA|*wM^L3kW&=mJ3l*rL!9oeXHMUe-0Apm&9Upm_Tk>NUfA6%R7E6A%M=W=py2r zo?dPgPgAA`lBbpuTln^F={m*=Vh`XaMv=}W$5LXiB9~i2$@x`CsI}xukX_x;q?2|3 zLNm`DQs}u=s8HP+)n&HDRuT)<<9GL;I3MH1KJ*xoI-Q)ilEM`u23NVU>J8{>!a;%s zsl!PT9BU2z_S(w@Aq%@TP1DYH5>8X}i6cYx#-UCzo29|A=neEp!#!lNj2vf~ zr4N$0D|6=z8^|*AlL2IKXOI6CQeajHxuuaDjp^Hovn?M`f^C5&H6Hc?Ls7V+;{u57|$OkwRXnzlRq3-i{}0Bq^8-*K)&rM}aF{ndBNY zCp?Q99#ix~Pl~#e(izq+q@|8m^VFJvMuXtF4dJ=s`1Dm@RWi3R|%Q&=5pl5GaghFca`i_M^@(I|C<2PT6hpeyEz<(kuk z^ej9+pE+X^eZ*ianpq5ymXd5qYN^y~{=E2&7>&(tVvks)+q{Tc!HpCzvUVa7F?`Ax z5!nBGn)+dtzVf$UKHro24w4I*Vkfa%ghziUJ@!p)+^y(dnR-3RwUgM!djB`iqAntk z4_#KducaL#+aZu_Jp|vNbC=?9QT8yymNznb?TxjkFth!1Cu^zO3a^zj z(EfokZ)aydxsISfw-8dlCWf4OA;=ok%8j#~qv)i16!j%nkw~(aD^mC zyfj7C?bya9NKkh>MT6VRXm&VkP~yr5!fS;A31=WX8fF&zN<0az$@|n_Zr{Bb0h;3& zG{xC=S}YaqMsj13RatGPVyIKKOPpM=vzrvE=o{w-piTlg>?sA33^$2=*|&uPC%cez zcbMd%o5Z&Fj$Arjy8}vpik~>@_2@2(iF23Of$zcr!>i9lomSx|hS}w0mb=6@j1Re^ zxNALS0iL~VEQrp7H?xNa(CNKpW%}YDiu%(iP|;5YI(bP3a>9d8eMJ<_OqGS+Ooh-O zPcF2DC(yRjDe6OJPPZ{#(6lSu{L2IwxemIyc%t5h9P*T66fJaKkmNO&X%VgfizpEH zEiEE|>RlW-CQ=_Q~iYZywrj zdeLno?WV)gGU8IGUOua(f%Np@dW`actg#2FEbNN=NbHPezh>CaZy<|q^oc$;=Me3r z&ZOK&V)yW0yz27d3VK_81T*56(fz|@yB|Dqi9hYWwRp)t)k4X2j>~9i9ZW-0kK1^X zf&ttNUbR8;_Af?aMN$Hg{;T6D8yB*)HmA#?bSn#Fx`F~5NAg^SR!~%bj;Wz1?R_Qo z5<#*r&_?IQGVDo&i){_!8NkapP3dkI%5+XwZCr_`A4fy{fF8J}D&s3ZiN)jFvYx-) z3+rsh)`{2$K9XEN-Umvl6uUdJ6#qJW^|@-U{qo)zvR@!b5gRrT=Oxs81au>ArAUM2u^8Ct~bP2*_6Y49~Ac=J)UuAL+ zvfI3<4_MMo2BH*K6^pTTuh*3f3g6Sjw&Pc!KF+9*xVP5CO~r>w;ZCrdt?A+KtM9Ex zTB6YDuxfjFDgV>NiwVHA=`XRnpMSvttM2e&C@xU?*t!XA91;K$b6*t+G&edObM(n2 z&iW2;@)FO=QABE8%I85sgONJ(ae&0Ga+^>6yD%FAh&>`jvGLp+lk>q+5QzABk??BbeQ^{z#CU9kOTJ<>=(jEl<^@b^OqDO z`hTv2{ubPS!!%fSKiWf#_UB<6hf1}s$7j0al*Q+k!$mXfE9*LFom?|xV`s0k)tL0X zS$@N84!|wT@Du0qZd+yE1zT+$jDv_`n!%nQoi2l%KFKFG{Wwx3RATvtI)(#-S0AaR>Z9VpQ2mh?^P`xP}Gs6G?3V3yKD-~dBy;H@}L24%?}i9 zP$Hv;mTD|WOe4vrQOuD4`K)7mcdMdb@V$tcKYE$CH<@D;YJarESbN`+^$-EwQ6$T@q@q!RjQ8hw+>KbstCHbNUgANAR%^f5US? zMwaJJ8fO!q9Nld3ZF<4i4y@uS$URq?twR$&YdtLf)$1iF7s9Gy@H361H{rq0xh7Ec zil;2V#Y^L0+~!eyt$9bFrK`r8eA`rF&j#dBbfOQ_SwpHclh|Fq1+SjW`xg2h zg%)BpclydK+FtOBvCa7UdNJ6m{#u8j22xf>qZc-QKW^=ucpnmvqh1Mu@gCv9S!(Zq zC`|xa(t@{DdUHta+*U?W348Lq8TZr@ik?DHPkD_RwFu)uS#!tN?-#)}KElDoX|-J^ znPpWNZ}+?~$ZC?Hl9iUAF_4+fc;nB!qPejadebL8ItcpL>&i6r5%gc(0#c*9$>_J; z;7!@8o$xk|>POXwC#{C_#tQ<^^kjv;FpEDIyKIR8!~Yr>_jO6$K5 zr`1W8r9`ykyKoDawU`u$p|uHSBymOAgQU0QOUl!9*Z;vtSpb|Gx2q#~Yez%?Jv2r} z=KxR!va8#R!s$yBKlG`5N#1K5s}deFkKl8pL$l_?D^PoS@lG6lmyys`eDs~#3T3`p zCQHe0#TTkqDX@4sNsdBbWf#f!07D}I9A6;=A6952l8=0~iis9b^i!scHu_29A_T8b zDEfYtjE1a+E6~tZX@&+vF0P?F<&W`eBedLWk`yJyD|Dy#wkkf1S=$25C3d8VO6$hR;ZLX?u3){HmY%lN?NjwNwOkmwrNZ+s3*^!Fn2q;JIx3yD*I(F z<@F%B?GV;ZZ7bDPyr=QcXID{{M2rxkxxUBKaE#^o?-+ba+Dh!Pju(+;(=UQ=F!;pr zb^GrkpXrk`_njUsr+0-mgI{dQ`(#-=DPGYnr`x@Q3qZ3Fy-%FyX6>gwiLM=u0Mx5J zKOfpP`E}2l7=2p0RF3|J<*Qg`w3pb$k*xNx?Tr&Ew6MM8ujp`p#m&X(7%+THBHDj1 zt5^cAXxzwvXzuD`q9OHa5hH1&^nt4sIChf(M*S^`TL;NOdB(8xZpE4v@QMQbf(4)J zep|CV*}|EO=^%M3)brM@dASW#Pw^9H_2@eql6*%KOo}>i#h!MMYF3NCi%a+&vEYuk z)#^yiSU$jajsd*wA&0F<=|hbz$-<_kQiW}lrg6n*nz0oc(<8LjBZ@c7fys(J%*^@= zRdBQ=hE9?{iR;LZRCJkne_jGr4>*OmRae`UQhSke9VHK?hew^dn138D@!DjTNhJDC z5_?_oP3i5Ir5NXJTr1B;-+0MV4|-|w25Ki>v%F5U)q_r9Jl z!ARVQR{}>5P&6i-p_XJ!f@FsW*12bg#(|`1gosFa^NtE9jlHHm8icmI)H zKr$wtR2hsDxa@eXBbnKiha=0+OrJ9w1<`>^jBipC7>hZn(p6&5uLg95z@)A!lyud) z8l@`TX)z`Fv%89|+M2j-AlchXMN-;JTT@WqZ)L%j zQm9VfX>2Vx;*JL$rRP(o@&Zr{9zMgI5dzLxwTF`hvB_^nfX@4`-+q+0sMuSIm8l zqHZHqsB2G&Jue*B6KJ>5Ds=H^ZEZpPAxiRSEF&?HpmAEfz5z#3%4psTBwtJ*hkHr2 z$kbkvuQGP^Sa@$E{o?BdtRQYEA0dZ&@hyQBRlfW9A2ib;j9;CFrs^?;Np&Q>d-Idg z8NE^FpfrjGkqv48qxnvcw9f4XQ@TL`vB!TmQx)1GjZbfEiY-1OTAv)!uflVibcCv>mo>Po(QRXmZ(T>tkmMuqC_47 z|42#d;bk>C+@DKiOZ|*~`2P^cPY;NvcgA_m|j< zMqwYm@S>lC{RcnMH=nJSsRK4>O+VQy+qI$hR~a)~q%v7loRmaIt~2NoW$^qf1SK zfZuswC zx9nj3shCm4W!28ZGWDxu9zl0;zB%+)1nTCx$|VOKlc`4>(>e*sBhv<<NM!AVr$rE4CajyI~bCxUsOq2GMG=ddntPEZxxDi zBw>Ve9|Ds3msBK4L-;aq21Peqrl>dBKZLJhuTfyf6%`xGbs!Cg@`{5^#z-8= zs-gU>^LB;J-l_;3LSVTrg17y1gR!tPsrp|)R?piqu;jMZL#RV0Md#dQY)+))3%->pRjum_=D>v+kiY5DrwxRJBfYK#j z41*%;%Ty#c%d~DL>&ZUDLDv0)Najek4CjVF36g3fkoGM7iMAy3W9$iFqq1*p4az>D_k-Nl9mtkMjgcahE^9Y*(9R!0gM{*x z!ZLrdW{ec4(9M~Vb>T7S?to5Q7B(b?vHX@+!dMt#%2uOVEy>xfI@51e4^xsqbBrXB z#B-cfOQ~pu;|fhb80HDaFfrV^v7J}cg4Eujb0<0Dcw6R=gUH<7pG$Q~k=W^ay%ZqT z_Zbl%GA>18`4loK(lhU~+#9&*`ngABj0cc;;4@3)c)q?EK#_nyJ{PlhJTK-fMeL6l zk#OQaf%{Cv1RytWeCAm>f%D{0q_Fri5_(IgC$TU1y>ZY ziJrul87Y%6H1w)Q#!sdp#9LimQZR{Glax~Qa&?OOkb0B(M&PK~eI8$di!Q_%*Hh5` z5lNXW4N_*d>|@S_CqQEhuMiJ0+gZy}XIbkVNYGc@A6k5cGE=lNnv{z3E8Td#70J`; z-HZp?is;(M&#WE*6>zhwQg6I#V4}4oSkfcJ#$_<2H&p%SXCEu5wAMtW!=%d;iG8z> zw|en<`qAM~6S`n0mHc~Ccq^1of$Dj#G8#6OZ{a6T1+dIV2C}E}i`fs7BA+%uB|hLM zo`AK9ld03<^akO=m3Jy+{T(l(un#+%p4fNq;Zzu79eSZ?>#cocYS>y!Zl>~!C#?om zJ4LTLeFU2rcq|}>X?#Do?=)KG09lG|px%k(PU9_hkfJ@3DO!)%Oy~W_YdV1Zu`=7h z={$Ift3Kwy2snNaPKm|n>vW1ttu(w|DCsE`m^q2*p(9~4cyaMFfL56=3a})rL83TK zYu|&rGEw0zXjsukcmvt!<gj)Z(T(@Ezh`y#nb~FW{awh%aS=ZW$Cf8rn(Y0 zJq>!~ZZ@LJ4_Z|xhthb}?{1y*8x1^tpoO?z>AO{yw`3+ZG3w3ahl2_Dd)0~C7|L$B z?WMqjTwYMcOny0KTG~!8`d!XnsH(U{?6!k)xR7k@u#?ksBrQpq#Rtm8vmkW(PMM^1 zHf{?Y+^KgLw&Sa$gCy@TBk>>~PwMf4q-w{0BqbSjf)&IPU7w-Aj*|?qBF|33pD3?l z^8MdE%q)V5yrF$xVQ}ekik0a}=FOH^&enaiA@%pu46Ud1_CY$$9(!1zz$C0if`BvmS88HQpGzv1{+IObjtQNrW1clL+hM-Lyc92U4ZWhr Gzy1perP0v< diff --git a/app/Filters.scala b/app/Filters.scala new file mode 100644 index 0000000..7e6d2d7 --- /dev/null +++ b/app/Filters.scala @@ -0,0 +1,19 @@ +import javax.inject.Inject + +import com.mohiva.play.htmlcompressor.HTMLCompressorFilter +import com.mohiva.play.xmlcompressor.XMLCompressorFilter +import play.api.http.HttpFilters +import play.api.mvc.EssentialFilter + +/** + * Filters for HTML Compressor + * Created by ediaz on 11-10-15. + */ +class Filters @Inject() (htmlCompressorFilter: HTMLCompressorFilter, xmlCompressorFilter: XMLCompressorFilter) + extends HttpFilters { + + override def filters: Seq[EssentialFilter] = Seq( + htmlCompressorFilter, + xmlCompressorFilter + ) +} diff --git a/app/Global.scala b/app/Global.scala deleted file mode 100644 index d24667d..0000000 --- a/app/Global.scala +++ /dev/null @@ -1,8 +0,0 @@ -import play.api.mvc.WithFilters -import com.mohiva.play.htmlcompressor.HTMLCompressorFilter -import com.mohiva.play.xmlcompressor.XMLCompressorFilter - -object Global extends WithFilters(HTMLCompressorFilter(), XMLCompressorFilter()) { - - -} \ No newline at end of file diff --git a/app/controllers/Application.scala b/app/controllers/Application.scala index eefcc44..6ca3c6b 100644 --- a/app/controllers/Application.scala +++ b/app/controllers/Application.scala @@ -1,17 +1,23 @@ package controllers +import javax.inject.Inject import jp.t2v.lab.play2.auth.OptionalAuthElement -import models.Guest -import play.api.mvc._ +import models.{Post, Guest} +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.Controller +import play.api.db.slick.DatabaseConfigProvider import services.PostService import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global -object Application extends Controller with DBElement with OptionalAuthElement with AuthConfigImpl { +class Application @Inject()(val messagesApi:MessagesApi, dbConfigProvider:DatabaseConfigProvider) extends Controller with OptionalAuthElement with AuthConfigImpl with I18nSupport { val MAX_POSTS = 10 def index = AsyncStack { implicit request => - Future.successful(Ok(views.html.index("Prosa", PostService.last(MAX_POSTS), loggedIn.getOrElse(Guest)))) + PostService.last(MAX_POSTS).flatMap { posts => + Future.successful(Ok(views.html.index("Prosa", posts, loggedIn.getOrElse(Guest)))) + } } def untrail(path: String) = AsyncStack { implicit request => diff --git a/app/controllers/AuthConfigImpl.scala b/app/controllers/AuthConfigImpl.scala index a12e190..c53cb4b 100644 --- a/app/controllers/AuthConfigImpl.scala +++ b/app/controllers/AuthConfigImpl.scala @@ -3,7 +3,6 @@ package controllers import jp.t2v.lab.play2.auth.{CookieTokenAccessor, AuthConfig} import models._ import play.api.Play.current -import play.api.db.slick.DB import play.api.mvc.Results._ import play.api.mvc._ import services.AuthorService @@ -21,11 +20,8 @@ trait AuthConfigImpl extends AuthConfig { val sessionTimeoutInSeconds = 3600 - def resolveUser(id:Id)(implicit ctx:ExecutionContext) : Future[Option[User]] = Future { - DB.withSession { implicit session => + def resolveUser(id:Id)(implicit ctx:ExecutionContext) : Future[Option[User]] = AuthorService.findById(id) - } - } def loginSucceeded(request:RequestHeader)(implicit ctx:ExecutionContext) : Future[Result] = { val uri = request.session.get("access_uri").getOrElse(routes.BlogsGuestController.index().url.toString) @@ -37,7 +33,9 @@ trait AuthConfigImpl extends AuthConfig { def authenticationFailed(request:RequestHeader)(implicit ctx:ExecutionContext) : Future[Result] = Future.successful(Redirect(routes.AuthController.login()).withSession("access_uri" -> request.uri)) - def authorizationFailed(request:RequestHeader)(implicit ctx:ExecutionContext) = Future(Redirect(routes.AuthController.login())) + override def authorizationFailed(request:RequestHeader, user: User, authority: Option[Authority])(implicit ctx:ExecutionContext) = { + Future(Redirect(routes.AuthController.login())) + } def authorize(user:User, authority:Authority)(implicit ctx:ExecutionContext) = Future.successful((Permission.valueOf(user.permission), authority) match { case (Administrator, _) => true diff --git a/app/controllers/AuthController.scala b/app/controllers/AuthController.scala index 5917137..7cda5af 100644 --- a/app/controllers/AuthController.scala +++ b/app/controllers/AuthController.scala @@ -1,35 +1,26 @@ package controllers +import javax.inject.Inject + import jp.t2v.lab.play2.auth.LoginLogout -import models.Author -import org.mindrot.jbcrypt.BCrypt -import play.api.Play.current import play.api.data.Form import play.api.data.Forms._ +import play.api.db.slick.DatabaseConfigProvider +import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, Controller} import services.AuthorService - +import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future +case class LoginData(username:String, password:String) -object AuthController extends Controller with LoginLogout with AuthConfigImpl { - import scala.concurrent.ExecutionContext.Implicits.global +class AuthController @Inject() (val messagesApi: MessagesApi, dbConfigProvider: DatabaseConfigProvider) extends Controller with LoginLogout with AuthConfigImpl with I18nSupport { - val loginForm = Form { - mapping("nickname" -> nonEmptyText, "password" -> text)(authenticateAuthor)(_.map(u => (u.nickname, ""))) - .verifying("Invalid email or password", result => result.isDefined) - } - def authenticateAuthor(nickname:String, password:String) : Option[Author] = { - play.api.db.slick.DB.withSession { implicit session => - AuthorService.findByNickname(nickname).flatMap { author => - if (BCrypt.checkpw(password, author.password)) - Some(author) - else - None - } - } - } + val loginForm = Form( + mapping("nickname" -> nonEmptyText, "password" -> text + ) (LoginData.apply)(LoginData.unapply) + ) def login = Action { implicit request => Ok(views.html.login(loginForm)) @@ -42,7 +33,11 @@ object AuthController extends Controller with LoginLogout with AuthConfigImpl { def authenticate = Action.async { implicit request => loginForm.bindFromRequest.fold( formWithErrors => Future.successful(BadRequest(views.html.login(formWithErrors))), - author => gotoLoginSucceeded(author.get.id) + data => + AuthorService.authenticate(data.username, data.password).flatMap { + case Some(user) => gotoLoginSucceeded(user.id) + case None => Future.successful(BadRequest(views.html.login(loginForm.fill(data).withGlobalError("user.not_authenticated")))) + } ) } diff --git a/app/controllers/AuthorsController.scala b/app/controllers/AuthorsController.scala index 13f5a0c..1a9c991 100644 --- a/app/controllers/AuthorsController.scala +++ b/app/controllers/AuthorsController.scala @@ -1,15 +1,18 @@ package controllers +import javax.inject.Inject + import jp.t2v.lab.play2.auth.AuthElement import models.Writer import org.mindrot.jbcrypt.BCrypt import play.api.data.Form import play.api.data.Forms._ -import play.api.i18n.Messages +import play.api.db.slick.DatabaseConfigProvider +import play.api.i18n.{I18nSupport, MessagesApi, Messages} import play.api.mvc.Controller import services.AuthorService -object AuthorsController extends Controller with DBElement with TokenValidateElement with AuthElement with AuthConfigImpl { +class AuthorsController @Inject() (val messagesApi: MessagesApi, dbConfigProvider: DatabaseConfigProvider) extends Controller with TokenValidateElement with AuthElement with AuthConfigImpl with I18nSupport { val changePasswordForm = Form( tuple( diff --git a/app/controllers/BlogsController.scala b/app/controllers/BlogsController.scala index 0a520f2..a67c96c 100644 --- a/app/controllers/BlogsController.scala +++ b/app/controllers/BlogsController.scala @@ -1,19 +1,22 @@ package controllers +import javax.inject.Inject import jp.t2v.lab.play2.auth.AuthElement import models.{BlogStatus, Editor} -import play.api.Play.current import play.api.data.Form import play.api.data.Forms._ -import play.api.db.slick.DB -import play.api.i18n.Messages +import play.api.db.slick.DatabaseConfigProvider +import play.api.i18n.{I18nSupport, MessagesApi, Messages} import play.api.mvc.Controller import services.{AuthorService, BlogService} import tools.PostAux +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future -object BlogsController extends Controller with DBElement with TokenValidateElement with AuthElement with AuthConfigImpl { +case class BlogData(id:Option[String], name:String,alias:String,description:String,image:Option[String],logo:Option[String],url:Option[String], disqus:Option[String], googleAnalytics:Option[String], useAvatarAsLogo:Option[Boolean], status:Int) + +class BlogsController @Inject() (val messagesApi: MessagesApi, dbConfigProvider: DatabaseConfigProvider) extends Controller with TokenValidateElement with AuthElement with AuthConfigImpl with I18nSupport { - case class BlogData(id:Option[String], name:String,alias:String,description:String,image:Option[String],logo:Option[String],url:Option[String], disqus:Option[String], googleAnalytics:Option[String], useAvatarAsLogo:Option[Boolean], status:Int) val blogForm = Form( mapping( @@ -30,64 +33,61 @@ object BlogsController extends Controller with DBElement with TokenValidateEleme "status" -> number(min=BlogStatus.INACTIVE.id, max=BlogStatus.PUBLISHED.id) ) (BlogData.apply)(BlogData.unapply) - .verifying(Messages("blogs.error.duplicate_alias"), result => result match { - case blogData => checkAliasNotExists(blogData.id, blogData.alias) - } - ) ) def checkAliasName(alias:String) = alias.matches("[a-zA-Z0-9_-]+") && alias.length() <= 32 - /// this is ugly - def checkAliasNotExists(idOpt:Option[String], alias:String) = { - DB.withSession { implicit session => - idOpt match { - case None => - BlogService.findByAlias(alias).isEmpty - case Some(id) => - BlogService.findById(id).map { blog => - if (blog.alias == alias) - true - else - BlogService.findByAlias(alias).isEmpty - }.getOrElse(BlogService.findByAlias(alias).isEmpty) - } + def create = AsyncStack(AuthorityKey -> Editor, IgnoreTokenValidation -> None) { implicit request => + AuthorService.findById(loggedIn.id).flatMap { + case Some(author) => + Future.successful(Ok(views.html.blogs_form(None, blogForm, loggedIn, PostAux.avatarUrl(author.email)))) + case None => + Future.successful(Redirect(routes.BlogsGuestController.index())) } } - def create = StackAction(AuthorityKey -> Editor, IgnoreTokenValidation -> None) { implicit request => - val ownerEmail = AuthorService.findById(loggedIn.id).map { _.email }.orNull - Ok(views.html.blogs_form(None, blogForm, loggedIn, PostAux.avatarUrl(ownerEmail))) - } - - def edit(id:String) = StackAction(AuthorityKey -> Editor, IgnoreTokenValidation -> None) { implicit request => - BlogService.findById(id).map { blog => - val form = blogForm.fill(BlogData(Some(blog.id), blog.name, blog.alias, blog.description, blog.image, blog.logo, blog.url, blog.disqus, blog.googleAnalytics, blog.useAvatarAsLogo, blog.status.id)) - val ownerEmail = AuthorService.findById(blog.owner).map { _.email }.orNull - Ok(views.html.blogs_form(Some(blog), form, loggedIn, PostAux.avatarUrl(ownerEmail))) - }.getOrElse (Redirect(routes.BlogsGuestController.index()).flashing("error" -> Messages("blogs.error.not_found"))) + def edit(id:String) = AsyncStack(AuthorityKey -> Editor, IgnoreTokenValidation -> None) { implicit request => + BlogService.findById(id).flatMap { + case Some(blog) => + val form = blogForm.fill(BlogData(Some(blog.id), blog.name, blog.alias, blog.description, blog.image, blog.logo, blog.url, blog.disqus, blog.googleAnalytics, blog.useAvatarAsLogo, blog.status.id)) + AuthorService.findById(blog.owner).map { + case Some(owner) => + Ok(views.html.blogs_form(Some(blog), form, loggedIn, PostAux.avatarUrl(owner.email))) + case None => + Redirect(routes.BlogsGuestController.index()).flashing("error" -> Messages("blogs.error.not_found")) + } + case None => + Future.successful(Redirect(routes.BlogsGuestController.index()).flashing("error" -> Messages("blogs.error.not_found"))) + } } - def save = StackAction(AuthorityKey -> Editor, IgnoreTokenValidation -> None) { implicit request => + def save = AsyncStack(AuthorityKey -> Editor, IgnoreTokenValidation -> None) { implicit request => blogForm.bindFromRequest.fold( - formWithErrors => BadRequest(views.html.blogs_form(None, formWithErrors, loggedIn, null)), - blogData => { - BlogService.create(loggedIn, blogData.name, blogData.alias, blogData.description, blogData.image, blogData.logo, blogData.url, blogData.disqus, blogData.googleAnalytics, blogData.useAvatarAsLogo) - Redirect(routes.BlogsGuestController.index()).flashing("success" -> Messages("blogs.success.created")) - } + formWithErrors => Future.successful(BadRequest(views.html.blogs_form(None, formWithErrors, loggedIn, null))), + blogData => + BlogService.findByAlias(blogData.alias).flatMap { + case None => + BlogService.create(loggedIn, blogData.name, blogData.alias, blogData.description, blogData.image, blogData.logo, blogData.url, blogData.disqus, blogData.googleAnalytics, blogData.useAvatarAsLogo) + Future.successful(Redirect(routes.BlogsGuestController.index()).flashing("success" -> Messages("blogs.success.created"))) + case Some(blog) => + Future.successful(BadRequest(views.html.blogs_form(None, blogForm.fill(blogData).withGlobalError("blg"), loggedIn, null))) + } ) } - def update(id:String) = StackAction(AuthorityKey -> Editor, IgnoreTokenValidation -> None) { implicit request => - BlogService.findById(id).map { blog => + def update(id:String) = AsyncStack(AuthorityKey -> Editor, IgnoreTokenValidation -> None) { implicit request => + BlogService.findById(id).flatMap { + case Some(blog) => blogForm.bindFromRequest.fold( - formWithErrors => BadRequest(views.html.blogs_form(Some(blog), formWithErrors, loggedIn, null)), + formWithErrors => Future.successful(BadRequest(views.html.blogs_form(Some(blog), formWithErrors, loggedIn, null))), blogData => { BlogService.update(blog, blogData.name, blogData.alias, blogData.description, blogData.image, blogData.logo, blogData.url, blogData.disqus, blogData.googleAnalytics, blogData.useAvatarAsLogo, BlogStatus(blogData.status)) - Redirect(routes.BlogsGuestController.index()).flashing("success" -> Messages("blogs.success.updated")) + Future.successful(Redirect(routes.BlogsGuestController.index()).flashing("success" -> Messages("blogs.success.updated"))) } ) - }.getOrElse (Redirect(routes.BlogsGuestController.index()).flashing("error" -> Messages("blogs.error.not_found"))) + case None => + Future.successful(Redirect(routes.BlogsGuestController.index()).flashing("error" -> Messages("blogs.error.not_found"))) + } } } diff --git a/app/controllers/BlogsGuestController.scala b/app/controllers/BlogsGuestController.scala index c3598f1..f34930a 100644 --- a/app/controllers/BlogsGuestController.scala +++ b/app/controllers/BlogsGuestController.scala @@ -1,16 +1,22 @@ package controllers +import javax.inject.Inject import jp.t2v.lab.play2.auth.OptionalAuthElement import models.{Guest, Visitor} +import play.api.db.slick.DatabaseConfigProvider +import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.Controller import services.BlogService import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global -object BlogsGuestController extends Controller with DBElement with OptionalAuthElement with AuthConfigImpl { +class BlogsGuestController @Inject() (val messagesApi: MessagesApi, dbConfigProvider: DatabaseConfigProvider) extends Controller with OptionalAuthElement with AuthConfigImpl with I18nSupport { def index(pageNum:Int=0) = AsyncStack { implicit request => val user : Visitor = loggedIn.getOrElse(Guest) - Future.successful(Ok(views.html.blogs_index("Blogs", BlogService.list(user, page = pageNum), user))) + BlogService.listForVisitor(user, page=pageNum).flatMap { page => + Future.successful(Ok(views.html.blogs_index("Blogs", page, user))) + } } } \ No newline at end of file diff --git a/app/controllers/DBElement.scala b/app/controllers/DBElement.scala deleted file mode 100644 index 70ad683..0000000 --- a/app/controllers/DBElement.scala +++ /dev/null @@ -1,23 +0,0 @@ -package controllers - -import jp.t2v.lab.play2.stackc.{RequestAttributeKey, RequestWithAttributes, StackableController} -import play.api.Play.current -import play.api.db.slick._ -import play.api.mvc.{Controller, Result} -import scala.concurrent.Future -import scala.language.implicitConversions - -trait DBElement extends StackableController { - self:Controller => - - case object DBSessionKey extends RequestAttributeKey[Session] - - abstract override def proceed[A](req:RequestWithAttributes[A])(f:RequestWithAttributes[A] => Future[Result]) : Future[Result] = { - DB.withSession { implicit session => - super.proceed(req.set(DBSessionKey, session))(f) - } - } - - implicit def dbSession(implicit req: RequestWithAttributes[_]): Session = req.get(DBSessionKey).get - -} diff --git a/app/controllers/ImagesController.scala b/app/controllers/ImagesController.scala index c954f4c..b004870 100644 --- a/app/controllers/ImagesController.scala +++ b/app/controllers/ImagesController.scala @@ -1,19 +1,24 @@ package controllers import java.io.File - +import javax.inject.Inject import jp.t2v.lab.play2.auth.AuthElement +import jp.t2v.lab.play2.stackc.StackableController import models.Writer import play.api.Logger import play.api.data.Form import play.api.data.Forms._ +import play.api.db.slick.DatabaseConfigProvider +import play.api.i18n.{MessagesApi, I18nSupport} import play.api.libs.json._ import play.api.mvc.Controller import services.ImageService import tools.ContentManager +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future -object ImagesController extends Controller with DBElement with AuthElement with AuthConfigImpl { +class ImagesController @Inject() (val messagesApi: MessagesApi, dbConfigProvider: DatabaseConfigProvider) extends Controller with AuthElement with AuthConfigImpl with I18nSupport { val createForm = Form( tuple( @@ -59,20 +64,22 @@ object ImagesController extends Controller with DBElement with AuthElement with } -object ContentController extends Controller with DBElement { +class ContentController @Inject() (val messagesApi: MessagesApi, dbConfigProvider: DatabaseConfigProvider) extends Controller with StackableController { /** * This method get temporal file, you should configure a CDN in application.conf */ - def getImage(id: String) = StackAction { + def getImage(id: String) = AsyncStack { implicit request => - ImageService.findById(id).map { - img => + ImageService.findById(id).flatMap { + case Some(img) => val source = scala.io.Source.fromFile(img.filename)(scala.io.Codec.ISO8859) val byteArray = source.map(_.toByte).toArray source.close() - Ok(byteArray).as(img.contentType) - } getOrElse NotFound + Future.successful(Ok(byteArray).as(img.contentType)) + case None => + Future.successful(NotFound) + } } } diff --git a/app/controllers/ImportController.scala b/app/controllers/ImportController.scala index 171a0df..dc5b3fd 100644 --- a/app/controllers/ImportController.scala +++ b/app/controllers/ImportController.scala @@ -1,38 +1,53 @@ package controllers +import javax.inject.Inject + import jp.t2v.lab.play2.auth.AuthElement import models.Editor import play.api.data.Form import play.api.data.Forms._ -import play.api.i18n.Messages +import play.api.db.slick.DatabaseConfigProvider +import play.api.i18n.{I18nSupport, MessagesApi, Messages} import play.api.mvc.Controller -import services.{BlogService, PostService} +import services.{AuthorService, BlogService, PostService} +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future -object ImportController extends Controller with DBElement with TokenValidateElement with AuthElement with AuthConfigImpl { +class ImportController @Inject() (val messagesApi: MessagesApi, dbConfigProvider: DatabaseConfigProvider) extends Controller with TokenValidateElement with AuthElement with AuthConfigImpl with I18nSupport { val BlogNotFound = Redirect(routes.BlogsGuestController.index()).flashing("error" -> Messages("blogs.error.not_found")) - def importPosts(alias:String) = StackAction(AuthorityKey -> Editor, IgnoreTokenValidation -> None) { implicit request => - BlogService.findByAlias(alias).map { blog => - Ok(views.html.posts_import(blog, blog.author, loggedIn)) - } getOrElse BlogNotFound + def importPosts(alias:String) = AsyncStack(AuthorityKey -> Editor, IgnoreTokenValidation -> None) { implicit request => + BlogService.findByAlias(alias).flatMap { + case Some(blog) => + AuthorService.findById(blog.owner).flatMap { author => + Future.successful(Ok(views.html.posts_import(blog, author, loggedIn))) + } + case None => + Future.successful(BlogNotFound) + } } val fileFormatForm = Form(single("file_format" -> nonEmptyText)) - def loadPosts(alias:String) = StackAction(parse.multipartFormData, AuthorityKey -> Editor) { implicit request => - BlogService.findByAlias(alias).map { blog => - request.body.file("file").map { file => - fileFormatForm.bindFromRequest.fold( - formWithErrors => BadRequest(views.html.posts_import(blog, blog.author, loggedIn)), - formOk => { - PostService.importPosts(loggedIn, blog, file.ref.file, formOk) - Redirect(routes.PostsController.index(alias)).flashing("success" -> Messages("posts.success.imported")) - } - ) - } getOrElse BlogNotFound - } getOrElse BlogNotFound + def loadPosts(alias:String) = AsyncStack(parse.multipartFormData, AuthorityKey -> Editor) { implicit request => + BlogService.findByAlias(alias).flatMap { + case Some(blog) => + request.body.file("file").map { file => + fileFormatForm.bindFromRequest.fold( + formWithErrors => + AuthorService.findById(blog.owner).flatMap { author => + Future.successful(BadRequest(views.html.posts_import(blog, author, loggedIn))) + }, + formOk => { + PostService.importPosts(loggedIn, blog, file.ref.file, formOk) + Future.successful(Redirect(routes.PostsController.index(alias)).flashing("success" -> Messages("posts.success.imported"))) + } + ) + } getOrElse Future.successful(BlogNotFound) + case None => Future.successful(BlogNotFound) + } } } diff --git a/app/controllers/PostsController.scala b/app/controllers/PostsController.scala index 5ab5f76..fdba3ab 100644 --- a/app/controllers/PostsController.scala +++ b/app/controllers/PostsController.scala @@ -1,43 +1,57 @@ package controllers +import javax.inject.Inject + import jp.t2v.lab.play2.auth.AuthElement import models._ import play.api.data.Form import play.api.data.Forms._ -import play.api.i18n.Messages +import play.api.db.slick.DatabaseConfigProvider +import play.api.i18n.{I18nSupport, MessagesApi, Messages} import play.api.mvc.Controller import services.{AuthorService, BlogService, PostService} import tools.PostAux - +import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future -object PostsController extends Controller with DBElement with TokenValidateElement with AuthElement with AuthConfigImpl { +case class PostData(image:Option[String], title:String, subtitle:Option[String], content:String, draft:Boolean, publish:Option[Boolean]) + +class PostsController @Inject() (val messagesApi: MessagesApi, dbConfigProvider: DatabaseConfigProvider) extends Controller with TokenValidateElement with AuthElement with AuthConfigImpl with I18nSupport { val blogNotFound = Redirect(routes.BlogsGuestController.index()).flashing("error" -> Messages("blogs.error.not_found")) val indexView = views.html.post_index - def index(alias:String, pageNum:Int=0) = AsyncStack(AuthorityKey -> Writer, IgnoreTokenValidation -> None) { implicit request => - Future.successful { - BlogService.findByAlias(alias).map { blog => - Ok(indexView(blog, blog.author, PostService.list(blog, draft = false, page = pageNum), drafts = false, loggedIn, AuthorService.getAvatar(blog.owner))) - } getOrElse blogNotFound - } + def index(alias:String, pageNum:Int=0) = AsyncStack(AuthorityKey -> models.Writer, IgnoreTokenValidation -> None) { implicit request => + BlogService.findByAlias(alias).flatMap { + case Some(blog) => + AuthorService.findById(blog.owner).flatMap { author => + AuthorService.getAvatar(blog.owner).flatMap { avatar => + PostService.listForBlog(blog, draft = false, page = pageNum).map { list => + Ok(indexView(blog, author, list, drafts = false, loggedIn, avatar)) + } + } + } + case None => Future.successful(blogNotFound) + } } val BlogNotFound = Redirect(routes.BlogsGuestController.index()).flashing("error" -> Messages("blogs.error.not_found")) def PostNotFound(alias:String) = Redirect(routes.PostsGuestController.index(alias)).flashing("error" -> Messages("posts.error.not_found")) - def drafts(alias:String, pageNum:Int=0) = AsyncStack(AuthorityKey -> Writer, IgnoreTokenValidation -> None) { implicit request => - Future.successful { - BlogService.findByAlias(alias).map { blog => - Ok(indexView(blog, blog.author, PostService.list(blog, draft = true, page = pageNum), drafts = true, loggedIn, PostAux.avatarUrl(loggedIn.email))) - } getOrElse BlogNotFound - } + def drafts(alias:String, pageNum:Int=0) = AsyncStack(AuthorityKey -> models.Writer, IgnoreTokenValidation -> None) { implicit request => + BlogService.findByAlias(alias).flatMap { + case Some(blog) => + AuthorService.findById(blog.owner).flatMap { author => + PostService.listForBlog(blog, draft = true, page = pageNum).map { list => + Ok(indexView(blog, author, list, drafts = true, loggedIn, PostAux.avatarUrl(loggedIn.email))) + } + } + case None => Future.successful(BlogNotFound) + } } - case class PostData(image:Option[String], title:String, subtitle:Option[String], content:String, draft:Boolean, publish:Option[Boolean]) val postForm = Form( mapping ( @@ -50,82 +64,89 @@ object PostsController extends Controller with DBElement with TokenValidateEleme )(PostData.apply)(PostData.unapply) ) - def create(alias:String) = AsyncStack(AuthorityKey -> Writer, IgnoreTokenValidation -> None) { implicit request => - Future.successful { - BlogService.findByAlias(alias).map { blog => - Ok(views.html.posts_new(blog, postForm, loggedIn)) - } getOrElse BlogNotFound + def create(alias:String) = AsyncStack(AuthorityKey -> models.Writer, IgnoreTokenValidation -> None) { implicit request => + BlogService.findByAlias(alias).flatMap { + case Some(blog) => + Future.successful(Ok(views.html.posts_new(blog, postForm, loggedIn))) + case None => Future.successful(BlogNotFound) } } - def save(alias:String) = AsyncStack(AuthorityKey -> Writer) { implicit request => - Future.successful { - BlogService.findByAlias(alias).map { blog => + def save(alias:String) = AsyncStack(AuthorityKey -> models.Writer) { implicit request => + BlogService.findByAlias(alias).flatMap { + case None => Future.successful(BlogNotFound) + case Some(blog) => postForm.bindFromRequest.fold( - formWithErrors => BadRequest(views.html.posts_new(blog, formWithErrors, loggedIn)), + formWithErrors => Future.successful(BadRequest(views.html.posts_new(blog, formWithErrors, loggedIn))), postData => { val post = PostService.create(loggedIn, blog, postData.title, postData.subtitle, postData.content, postData.draft, postData.image) if (postData.draft) - Redirect(routes.PostsController.drafts(blog.alias)).flashing("success" -> Messages("posts.success.created")) + Future.successful(Redirect(routes.PostsController.drafts(blog.alias)).flashing("success" -> Messages("posts.success.created"))) else - Redirect(routes.PostsGuestController.index(alias)).flashing("success" -> Messages("posts.success.created")) - } - ) - } getOrElse BlogNotFound + Future.successful(Redirect(routes.PostsGuestController.index(alias)).flashing("success" -> Messages("posts.success.created"))) + }) } } - def edit(alias:String, id:String) = AsyncStack(AuthorityKey -> Writer, IgnoreTokenValidation -> None) { implicit request => - Future.successful { - BlogService.findByAlias(alias).map { blog => - PostService.findById(id).map { post => - if (post.author != loggedIn.id) - Redirect(routes.PostsGuestController.index(alias)).flashing("error" -> Messages("posts.error.not_found")) - else - Ok(views.html.posts_edit(blog, post, postForm.fill(PostData(post.image, post.title, post.subtitle, post.content, post.draft, Some(post.published.isDefined))), loggedIn)) - } getOrElse PostNotFound(alias) - } getOrElse BlogNotFound + def edit(alias:String, id:String) = AsyncStack(AuthorityKey -> models.Writer, IgnoreTokenValidation -> None) { implicit request => + BlogService.findByAlias(alias).flatMap { + case None => Future.successful(BlogNotFound) + case Some(blog) => + PostService.findById(blog.id).map { + case None => BlogNotFound + case Some(post) => + if (post.author != loggedIn.id) + Redirect(routes.PostsGuestController.index(alias)).flashing("error" -> Messages("posts.error.not_found")) + else + Ok(views.html.posts_edit(blog, post, postForm.fill(PostData(post.image, post.title, post.subtitle, post.content, post.draft, Some(post.published.isDefined))), loggedIn)) + } } } - def update(alias:String, id:String) = AsyncStack(AuthorityKey -> Writer) { implicit request => - Future.successful { - BlogService.findByAlias(alias).map { blog => - PostService.findById(id).map { post => - postForm.bindFromRequest.fold( - formWithErrors => BadRequest(views.html.posts_new(blog, formWithErrors, loggedIn)), - postData => { - PostService.update(post, postData.title, postData.subtitle, postData.content, postData.draft, postData.image, postData.publish.getOrElse(false)) - Redirect(routes.PostsController.edit(alias, id)).flashing("success" -> Messages("posts.success.saved")) - } - ) - } getOrElse PostNotFound(alias) - } getOrElse BlogNotFound + def update(alias:String, id:String) = AsyncStack(AuthorityKey -> models.Writer) { implicit request => + BlogService.findByAlias(alias).flatMap { + case None => Future.successful(BlogNotFound) + case Some(blog) => + PostService.findById(id).map { + case None => BlogNotFound + case Some(post) => + postForm.bindFromRequest.fold( + formWithErrors => BadRequest(views.html.posts_new(blog, formWithErrors, loggedIn)), + postData => { + PostService.update(post, postData.title, postData.subtitle, postData.content, postData.draft, postData.image, postData.publish.getOrElse(false)) + Redirect(routes.PostsController.edit(alias, id)).flashing("success" -> Messages("posts.success.saved")) + } + ) + } } } def delete(alias:String, id:String) = AsyncStack(AuthorityKey -> Writer) { implicit request => - Future.successful { - BlogService.findByAlias(alias).map { blog => - PostService.findById(id).map { post => - PostService.delete(post) - if (post.draft) - Redirect(routes.PostsController.drafts(alias)).flashing("success" -> Messages("posts.success.deleted")) - else - Redirect(routes.PostsGuestController.index(alias)).flashing("success" -> Messages("posts.success.deleted")) - } getOrElse PostNotFound(alias) - } getOrElse BlogNotFound - } + BlogService.findByAlias(alias).flatMap { + case None => Future.successful(BlogNotFound) + case Some(blog) => + PostService.findById(id).map { + case None => BlogNotFound + case Some(post) => + PostService.delete(post.id) + if (post.draft) + Redirect(routes.PostsController.drafts(alias)).flashing("success" -> Messages("posts.success.deleted")) + else + Redirect(routes.PostsGuestController.index(alias)).flashing("success" -> Messages("posts.success.deleted")) + } + } } - def unpublish(alias:String, id:String) = AsyncStack(AuthorityKey -> Writer) { implicit request => - Future.successful { - BlogService.findByAlias(alias).map { blog => - PostService.findById(id).map { post => - PostService.update(post.copy(draft = true, published = None)) - Redirect(routes.PostsController.edit(alias, id)).flashing("success" -> Messages("posts.success.unpublished")) - } getOrElse PostNotFound(alias) - } getOrElse BlogNotFound + def unpublish(alias:String, id:String) = AsyncStack(AuthorityKey -> models.Writer) { implicit request => + BlogService.findByAlias(alias).flatMap { + case None => Future.successful(BlogNotFound) + case Some(blog) => + PostService.findById(id).map { + case None => BlogNotFound + case Some(post) => + PostService.update(post.copy(draft = true, published = None)) + Redirect(routes.PostsController.edit(alias, id)).flashing("success" -> Messages("posts.success.unpublished")) + } } } diff --git a/app/controllers/PostsGuestController.scala b/app/controllers/PostsGuestController.scala index 8f9a2e7..0d925ba 100644 --- a/app/controllers/PostsGuestController.scala +++ b/app/controllers/PostsGuestController.scala @@ -1,43 +1,59 @@ package controllers +import javax.inject.Inject + import jp.t2v.lab.play2.auth.OptionalAuthElement import models._ -import play.api.i18n.Messages +import play.api.db.slick.DatabaseConfigProvider +import play.api.i18n.{MessagesApi, I18nSupport, Messages} import play.api.mvc.Controller import services.{AuthorService, BlogService, PostService} import tools.PostAux import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global -object PostsGuestController extends Controller with DBElement with OptionalAuthElement with AuthConfigImpl { +class PostsGuestController @Inject() (val messagesApi: MessagesApi, dbConfigProvider: DatabaseConfigProvider) extends Controller with OptionalAuthElement with AuthConfigImpl with I18nSupport { val BlogNotFound = Redirect(routes.BlogsGuestController.index()).flashing("error" -> Messages("blogs.error.not_found")) val indexView = views.html.post_index def index(alias:String, pageNum:Int=0) = AsyncStack { implicit request => - Future.successful( - BlogService.findByAlias(alias).filter(blog => blog.status == BlogStatus.PUBLISHED).map { blog => - Ok(indexView(blog, blog.author, PostService.list(blog, draft = false, page = pageNum), drafts = false, loggedIn.getOrElse(Guest), AuthorService.getAvatar(blog.owner))) - } getOrElse BlogNotFound - ) + BlogService.findByAlias(alias).flatMap { + case None => Future.successful(BlogNotFound) + case Some(blog) => + AuthorService.findById(blog.owner).flatMap { author => + AuthorService.getAvatar(blog.owner).flatMap { avatar => + PostService.listForBlog(blog, draft = false, page = pageNum).map { list => + Ok(indexView(blog, author, list, drafts = false, loggedIn.getOrElse(Guest), avatar)) + } + } + } + } } def view(alias:String, year:Int, month:Int, day:Int, slug:String) = AsyncStack { implicit request => - Future.successful( - BlogService.findByAlias(alias).map { blog => - PostService.find(blog, slug, year, month, day).map { post => - Ok(views.html.posts_view(blog, blog.author, post, loggedIn.getOrElse(Guest))) - } getOrElse NotFound - } getOrElse BlogNotFound - ) + BlogService.findByAlias(alias).flatMap { + case None => Future.successful(BlogNotFound) + case Some(blog) => + PostService.find(blog, slug, year, month, day).flatMap { + case None => Future.successful(NotFound) + case Some(post) => + AuthorService.findById(blog.owner).map { author => + Ok(views.html.posts_view(blog, author, post, loggedIn.getOrElse(Guest))) + } + } + } } def atom(alias:String) = AsyncStack { implicit request => - Future.successful( - BlogService.findByAlias(alias).map { blog => - Ok(views.xml.posts_atom(blog, PostService.last(blog, 10))) - } getOrElse BlogNotFound - ) + BlogService.findByAlias(alias).flatMap { + case None => Future.successful(BlogNotFound) + case Some(blog) => + PostService.last(blog, 10).map { list => + Ok(views.xml.posts_atom(blog, list)) + } + } } } diff --git a/app/models/Author.scala b/app/models/Author.scala index c2b96ed..1a636a3 100644 --- a/app/models/Author.scala +++ b/app/models/Author.scala @@ -1,6 +1,5 @@ package models -import play.api.db.slick.Config.driver.simple._ sealed trait Visitor @@ -16,18 +15,5 @@ case class Author( bio:Option[String] ) extends Visitor with Identifiable -class AuthorEntity(tag:Tag) extends Table[Author](tag, "author") with HasId { - - def id = column[String]("id", O.PrimaryKey) - def nickname = column[String]("nickname") - def email = column[String]("email") - def password = column[String]("password") - def permission = column[String]("permission") - def fullname = column[String]("fullname", O.Nullable) - def bio = column[String]("bio", O.Nullable) - - def * = (id,nickname,email,password,permission,fullname.?,bio.?) <> (Author.tupled, Author.unapply) - -} diff --git a/app/models/Blog.scala b/app/models/Blog.scala index d1d5f74..72c69d7 100644 --- a/app/models/Blog.scala +++ b/app/models/Blog.scala @@ -1,14 +1,14 @@ package models -import play.api.db.slick.Config.driver.simple._ -import play.api.i18n.Messages import services.AuthorService +import slick.driver.PostgresDriver.api._ + object BlogStatus extends Enumeration { - val CREATED = Value(0, Messages("blog.status.created")) - val PUBLISHED = Value(1, Messages("blog.status.published")) - val INACTIVE = Value(-1, Messages("blog.status.published"))// <- reserved for administator + val CREATED = Value(0, "blog.status.created") + val PUBLISHED = Value(1, "blog.status.published") + val INACTIVE = Value(-1, "blog.status.published")// <- reserved for administator implicit val BlogStatusMapper = MappedColumnType.base[BlogStatus.Value, Int]( s => s.id, @@ -29,30 +29,7 @@ case class Blog( googleAnalytics:Option[String], status:BlogStatus.Value, // owner:String -) extends Identifiable { - - def author(implicit s:Session) = AuthorService.findById(owner) - -} - -class BlogEntity(tag:Tag) extends Table[Blog](tag, "blog") with HasId { - - def id = column[String]("id", O.PrimaryKey) - def name = column[String]("name") - def alias = column[String]("alias") - def description = column[String]("description") - def image = column[String]("image", O.Nullable) - def logo = column[String]("logo", O.Nullable) - def url = column[String]("url", O.Nullable) - def useAvatarAsLogo = column[Boolean]("use_avatar_as_logo", O.Nullable) - def disqus = column[String]("disqus", O.Nullable) - def googleAnalytics = column[String]("google_analytics", O.Nullable) - def status = column[BlogStatus.Value]("status") - def owner = column[String]("owner", O.Length(45, varying = true)) - - def * = (id,name,alias,description,image.?, logo.?, url.?, useAvatarAsLogo.?, disqus.?, googleAnalytics.?, status, owner) <> (Blog.tupled, Blog.unapply) -} - +) extends Identifiable diff --git a/app/models/HasId.scala b/app/models/HasId.scala deleted file mode 100644 index d8c4e2e..0000000 --- a/app/models/HasId.scala +++ /dev/null @@ -1,8 +0,0 @@ -package models - -import play.api.db.slick.Config.driver.simple._ - -trait HasId { - - def id: Column[String] - } diff --git a/app/models/HasOwner.scala b/app/models/HasOwner.scala deleted file mode 100644 index 4f3e32d..0000000 --- a/app/models/HasOwner.scala +++ /dev/null @@ -1,7 +0,0 @@ -package models - -import play.api.db.slick.Config.driver.simple._ - -trait HasOwner extends HasId { - def owner: Column[String] - } diff --git a/app/models/Image.scala b/app/models/Image.scala index 5529552..22c911c 100644 --- a/app/models/Image.scala +++ b/app/models/Image.scala @@ -1,17 +1,6 @@ package models -import play.api.db.slick.Config.driver.simple._ - case class Image(id:String, filename:String, contentType:String, url:Option[String]) extends Identifiable -class ImageEntity(tag:Tag) extends Table[Image](tag, "image") with HasId { - - def id = column[String]("id", O.PrimaryKey) - def filename = column[String]("filename") - def contentType = column[String]("contenttype") - def url = column[String]("url", O.Nullable) - - def * = (id,filename,contentType,url.?) <> (Image.tupled, Image.unapply) -} diff --git a/app/models/Owned.scala b/app/models/Owned.scala deleted file mode 100644 index 8aec95c..0000000 --- a/app/models/Owned.scala +++ /dev/null @@ -1,12 +0,0 @@ -package models - -import play.api.db.slick.Config.driver.simple._ -import services.AuthorService - -trait Owned extends Identifiable { - - val owner: String - - def author(implicit s: Session) = AuthorService.findById(owner).get - - } diff --git a/app/models/Post.scala b/app/models/Post.scala index 7be20fc..9cb4920 100644 --- a/app/models/Post.scala +++ b/app/models/Post.scala @@ -1,9 +1,7 @@ package models import java.sql.Timestamp - import org.joda.time.DateTime -import play.api.db.slick.Config.driver.simple._ case class Post(id:String, blog:String, image:Option[String], title:String, subtitle:Option[String], content:String, slug:Option[String], draft:Boolean, created:Option[Timestamp], published:Option[Timestamp], author:String) extends Identifiable { @@ -11,22 +9,6 @@ case class Post(id:String, blog:String, image:Option[String], title:String, subt } -class PostEntity(tag:Tag) extends Table[Post](tag, "post") with HasId { - - def id = column[String]("id", O.PrimaryKey, O.NotNull) - def blog = column[String]("blog", O.Length(45, varying = true)) - def image = column[String]("image") - def title = column[String]("title") - def subtitle = column[String]("subtitle") - def content = column[String]("content") - def slug = column[String]("slug") - def draft = column[Boolean]("draft") - def created = column[Timestamp]("created") - def published = column[Timestamp]("published") - def author = column[String]("author", O.Length(45, varying = true)) - - def * = (id, blog, image.?, title, subtitle.?, content, slug.?, draft, created.?, published.?, author) <> (Post.tupled, Post.unapply) -} diff --git a/app/services/AuthorService.scala b/app/services/AuthorService.scala index 6887351..26cbb86 100644 --- a/app/services/AuthorService.scala +++ b/app/services/AuthorService.scala @@ -1,37 +1,68 @@ package services -import models.{Author, AuthorEntity} +import models.Author import org.mindrot.jbcrypt.BCrypt -import play.api.db.slick.Config.driver.simple._ +import slick.driver.PostgresDriver.api._ import tools.{PostAux, IdGenerator} +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future /** * AuthorService * Created by ediaz on 21-05-15. */ + + +class AuthorEntity(tag:Tag) extends Table[Author](tag, "author") with HasId { + + def id = column[String]("id", O.PrimaryKey) + def nickname = column[String]("nickname") + def email = column[String]("email") + def password = column[String]("password") + def permission = column[String]("permission") + def fullname = column[Option[String]]("fullname") + def bio = column[Option[String]]("bio") + + def * = (id,nickname,email,password,permission,fullname,bio) <> (Author.tupled, Author.unapply) + +} + object AuthorService extends EntityService[Author] { type EntityType = AuthorEntity val items = TableQuery[AuthorEntity] lazy val authors = items - def findByNickname(nickname: String)(implicit s:Session) = authors.filter(_.nickname === nickname).firstOption + def authenticate(username: String, password:String) = + findByNickname(username).map { + case None => None + case Some(a) => + if (BCrypt.checkpw(password, a.password)) + Some(a) + else + None + } + + + def findByNickname(nickname: String) : Future[Option[Author]] = + dbConfig.db.run(authors.filter(a => a.nickname === nickname).result.headOption) def create(nickname:String, email:String, password:String, permission:String)(implicit s:Session) = { val pass = BCrypt.hashpw(password, BCrypt.gensalt()) insert(Author(IdGenerator.nextId(classOf[Author]), nickname, email, pass, permission, None, None)) } - def changePassword(author:Author, newPassword:String)(implicit s:Session) { + def changePassword(author:Author, newPassword:String) { update(author.copy(password = BCrypt.hashpw(newPassword, BCrypt.gensalt()))) } - def getAvatar(authorId:String)(implicit s:Session) = - findById(authorId).map { author => - PostAux.avatarUrl(author.email) - }.orNull + def getAvatar(authorId:String) = + findById(authorId).map { + case Some(a) => PostAux.avatarUrl(a.email) + case None => null + } - def queryFilter(qry: String, c: AuthorEntity) = c.nickname like "%" + qry + "%" + override def queryFilter(qry: String, c: AuthorEntity) = c.nickname like "%" + qry + "%" - def queryOrder(c: EntityType) = c.nickname.asc + override def queryOrder(c: EntityType) = c.nickname.asc } diff --git a/app/services/BlogService.scala b/app/services/BlogService.scala index 9037238..7f4b09b 100644 --- a/app/services/BlogService.scala +++ b/app/services/BlogService.scala @@ -1,8 +1,30 @@ package services import models._ -import play.api.db.slick.Config.driver.simple._ import tools.IdGenerator +import slick.driver.PostgresDriver.api._ +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future + + +class BlogEntity(tag:Tag) extends Table[Blog](tag, "blog") with HasId { + + def id = column[String]("id", O.PrimaryKey) + def name = column[String]("name") + def alias = column[String]("alias") + def description = column[String]("description") + def image = column[Option[String]]("image") + def logo = column[Option[String]]("logo") + def url = column[Option[String]]("url") + def useAvatarAsLogo = column[Option[Boolean]]("use_avatar_as_logo") + def disqus = column[Option[String]]("disqus") + def googleAnalytics = column[Option[String]]("google_analytics") + def status = column[BlogStatus.Value]("status") + def owner = column[String]("owner", O.Length(45, varying = true)) + + def * = (id,name,alias,description,image, logo, url, useAvatarAsLogo, disqus, googleAnalytics, status, owner) <> (Blog.tupled, Blog.unapply) +} + object BlogService extends DbService[Blog]{ @@ -12,22 +34,23 @@ object BlogService extends DbService[Blog]{ lazy val blogs = items - def list(user:Visitor, page: Int = 0, pageSize: Int = 10)(implicit s:Session) : Page[Blog] = { + def listForVisitor(user:Visitor, page: Int = 0, pageSize: Int = 10) : Future[Page[Blog]] = { val offset = pageSize * page val query = (for { blog <- blogs if blog.status === BlogStatus.PUBLISHED || user.isInstanceOf[Author]} yield blog).sortBy(_.name.asc).drop(offset).take(pageSize) - val totalRows = count() - val result = query.list.map(row => row) - Page(result, page, offset, totalRows, pageSize) + val totalRows = count + val result = dbConfig.db.run(query.result) + result flatMap (items => totalRows map (rows => Page(items, page, offset, rows, pageSize))) } - def findByAlias(alias:String)(implicit s:Session) : Option[Blog] = blogs.filter(_.alias === alias).firstOption + def findByAlias(alias:String) : Future[Option[Blog]] = + dbConfig.db.run(blogs.filter(_.alias === alias).result.headOption) - def create(owner:Author, name:String,alias:String,description:String,image:Option[String],logo:Option[String],url:Option[String], disqus:Option[String], gogleAnalytics:Option[String], useAvatarAsLogo:Option[Boolean])(implicit s:Session) { + def create(owner:Author, name:String,alias:String,description:String,image:Option[String],logo:Option[String],url:Option[String], disqus:Option[String], gogleAnalytics:Option[String], useAvatarAsLogo:Option[Boolean]) { val blog = Blog(IdGenerator.nextId(classOf[Blog]), name, alias, description, image, logo, url, useAvatarAsLogo, disqus, gogleAnalytics, BlogStatus.CREATED, owner.id) insert(blog) } - def update(blog:Blog, name:String,alias:String,description:String,image:Option[String],logo:Option[String],url:Option[String], disqus:Option[String], gogleAnalytics:Option[String], useAvatarAsLogo:Option[Boolean], status:BlogStatus.Value)(implicit s:Session) { + def update(blog:Blog, name:String,alias:String,description:String,image:Option[String],logo:Option[String],url:Option[String], disqus:Option[String], gogleAnalytics:Option[String], useAvatarAsLogo:Option[Boolean], status:BlogStatus.Value) { update (blog.copy(name=name, alias=alias, useAvatarAsLogo=useAvatarAsLogo, description=description, image=image, logo=logo, url=url, disqus=disqus, googleAnalytics=gogleAnalytics, status=status)) } diff --git a/app/services/DbService.scala b/app/services/DbService.scala index fdd34ae..ebe8dbd 100644 --- a/app/services/DbService.scala +++ b/app/services/DbService.scala @@ -1,41 +1,95 @@ package services -import models.{HasId, Identifiable} -import play.api.db.slick.Config.driver.simple._ +import models.Identifiable +import play.api.Play +import play.api.db.slick.DatabaseConfigProvider +import play.api.libs.concurrent.Execution.Implicits.defaultContext +import slick.driver.JdbcProfile +import slick.driver.PostgresDriver.api._ +import slick.lifted.ColumnOrdered import tools.IdGenerator +import scala.concurrent.Future +trait HasId { -trait DbService[C <: Identifiable] { + def id: Rep[String] +} - type EntityType <: Table[C] + +trait HasOwner extends HasId { + def owner: Rep[String] +} + +trait DAOService[Entity <: Identifiable, I] { + + def insert(item: Entity): Future[Int] + def update(item: Entity): Future[Int] + def delete(id: I): Future[Int] + def findById(id: I): Future[Option[Entity]] + def count: Future[Int] + def list(page: Int = 0, pageSize: Int = 10, orderBy: Int = 1, filter: String = "%"): Future[Page[Entity]] + +} + +trait DbService[Entity <: Identifiable] extends DAOService[Entity,String] { + + type EntityType <: Table[Entity] type TableType = TableQuery[EntityType] val items: TableType - def genId(c: Class[C]) = IdGenerator.nextId(c) + def genId(c: Class[Entity]) = IdGenerator.nextId(c) - def findById(id: String)(implicit s: Session) = { - items.filter(e => e.asInstanceOf[HasId].id === id).firstOption - } + protected val dbConfig = DatabaseConfigProvider.get[JdbcProfile](Play.current) - def count()(implicit s: Session): Int = Query(items.length).first + override def count : Future[Int] = dbConfig.db.run(items.length.result) - def insert(item: EntityType#TableElementType)(implicit s: Session) { - items.insert(item) + private def filterQuery(id:String) : Query[EntityType, Entity, Seq] = items.filter(i => i.asInstanceOf[HasId].id === id) - } - def update(item: EntityType#TableElementType)(implicit s: Session) { - items.filter(e => e.asInstanceOf[HasId].id === item.asInstanceOf[Identifiable].id).update(item) + def findById(id: String) : Future[Option[Entity]]= dbConfig.db.run(filterQuery(id).result.headOption) + + override def insert(item: Entity) : Future[Int] = dbConfig.db.run(items += item) + + override def update(item: Entity) : Future[Int] = dbConfig.db.run(filterQuery(item.id).update(item)) + + override def delete(id:String) : Future[Int] = dbConfig.db.run(filterQuery(id).delete) + + override def list(page: Int = 0, pageSize: Int = 10, orderBy: Int = 1, filter: String = "%"): Future[Page[Entity]] = { + val offset = pageSize * page + val query = (for {item <- items} yield item).drop(offset).take(pageSize) + val totalRows = count + val result = dbConfig.db.run(query.result) + result flatMap (items => totalRows map (rows => Page(items, page, offset, rows, pageSize))) } +} + + +trait EntityService[T <: Identifiable] extends DbService[T] { - def delete(item: EntityType#TableElementType)(implicit s: Session) { - items.filter(e => e.asInstanceOf[HasId].id === item.asInstanceOf[Identifiable].id).delete + def queryFilter(qry: String, c: EntityType): Rep[Boolean] + + def queryOrder(c: EntityType): ColumnOrdered[String] + + def countQuery(qryStr: String): Future[Int] = + dbConfig.db.run(items.filter(c => queryFilter(qryStr, c)).length.result) + + def pageQuery(page: Int, offset: Int, pageSize: Int, filter: Option[String]): Query[EntityType, T, Seq] = { + (if (filter.isEmpty) for {i <- items} yield i + else for {i <- items if queryFilter(filter.get, i)} yield i) + .sortBy(queryOrder) + .drop(offset) + .take(pageSize) } - def delete(id: String)(implicit s: Session) { - items.filter(e => e.asInstanceOf[HasId].id === id).delete + + def search(queryStr: String, page: Int = 0, pageSize: Int = 50)(implicit s: Session): Future[Page[T]] = { + val offset = pageSize * page + val query = pageQuery(page, offset, pageSize, Some(queryStr)) + val totalRows = countQuery(queryStr) + val result = dbConfig.db.run(query.result) + result flatMap (items => totalRows map (rows => Page(items, page, offset, rows, pageSize))) } -} \ No newline at end of file +} diff --git a/app/services/EntityService.scala b/app/services/EntityService.scala index e60c4fe..dd31c76 100644 --- a/app/services/EntityService.scala +++ b/app/services/EntityService.scala @@ -1,42 +1,3 @@ package services import models.Identifiable -import play.api.db.slick.Config.driver.simple._ - -import scala.slick.lifted.ColumnOrdered - -trait EntityService[T <: Identifiable] extends DbService[T] { - - def queryFilter(qry: String, c: EntityType): Column[Boolean] - - def queryOrder(c: EntityType): ColumnOrdered[String] - - def countQuery(qryStr: String)(implicit s: Session): Int = { - Query(items.filter(c => queryFilter(qryStr, c)).length).first - } - - def pageQuery(page: Int, offset: Int, pageSize: Int, filter: Option[String]): Query[EntityType, T, Seq] = { - (if (filter.isEmpty) for {i <- items} yield i - else for {i <- items if queryFilter(filter.get, i)} yield i) - .sortBy(queryOrder) - .drop(offset) - .take(pageSize) - } - - def list(page: Int = 0, pageSize: Int = 10)(implicit s: Session): Page[T] = { - val offset = pageSize * page - val query = pageQuery(page, offset, pageSize, None) - val totalRows = count() - val result = query.list.map(row => row) - Page(result, page, offset, totalRows, pageSize) - } - - def search(queryStr: String, page: Int = 0, pageSize: Int = 50)(implicit s: Session): Page[T] = { - val offset = pageSize * page - val query = pageQuery(page, offset, pageSize, Some(queryStr)) - val totalRows = countQuery(queryStr) - val result = query.list.map(row => row) - Page(result, page, offset, totalRows, pageSize) - } - -} diff --git a/app/services/ImageService.scala b/app/services/ImageService.scala index 07a74a1..412eb88 100644 --- a/app/services/ImageService.scala +++ b/app/services/ImageService.scala @@ -1,16 +1,28 @@ package services -import models.{Image, ImageEntity} -import play.api.db.slick.Config.driver.simple._ +import slick.driver.PostgresDriver.api._ + +import models.{Image} import tools.IdGenerator + +class ImageEntity(tag:Tag) extends Table[Image](tag, "image") with HasId { + + def id = column[String]("id", O.PrimaryKey) + def filename = column[String]("filename") + def contentType = column[String]("contenttype") + def url = column[Option[String]]("url") + + def * = (id,filename,contentType,url) <> (Image.tupled, Image.unapply) +} + object ImageService extends DbService[Image] { type EntityType = ImageEntity val items = TableQuery[ImageEntity] lazy val images = items - def addImage(filename:String, contentType:String)(implicit s:Session) = { + def addImage(filename:String, contentType:String)= { val image = Image(IdGenerator.nextId(classOf[Image]), filename, contentType, None) insert(image) image diff --git a/app/services/PostService.scala b/app/services/PostService.scala index 1c62fcc..1c6bb3d 100644 --- a/app/services/PostService.scala +++ b/app/services/PostService.scala @@ -2,15 +2,32 @@ package services import java.io.File import java.sql.Timestamp - import models._ import org.joda.time.{DateTime, Period} -import play.api.db.slick.Config.driver.simple._ import play.api.libs.json.{JsArray, JsObject, Json} import tools.IdGenerator - +import scala.concurrent.Future import scala.io.Source +import slick.driver.PostgresDriver.api._ +import scala.concurrent.ExecutionContext.Implicits.global + + +class PostEntity(tag:Tag) extends Table[Post](tag, "post") with HasId { + + def id = column[String]("id", O.PrimaryKey) + def blog = column[String]("blog", O.Length(45, varying = true)) + def image = column[String]("image") + def title = column[String]("title") + def subtitle = column[String]("subtitle") + def content = column[String]("content") + def slug = column[String]("slug") + def draft = column[Boolean]("draft") + def created = column[Timestamp]("created") + def published = column[Timestamp]("published") + def author = column[String]("author", O.Length(45, varying = true)) + def * = (id, blog, image.?, title, subtitle.?, content, slug.?, draft, created.?, published.?, author) <> (Post.tupled, Post.unapply) +} object PostService extends DbService[Post]{ @@ -19,37 +36,38 @@ object PostService extends DbService[Post]{ val items = TableQuery[PostEntity] lazy val posts = items - def last(n:Int)(implicit s:Session) : List[(Post,Blog)] = { - val q = (for { (p,b) <- posts innerJoin BlogService.blogs on (_.blog === _.id) if p.draft === false && b.status === BlogStatus.PUBLISHED} yield (p,b) ).sortBy(_._1.published.desc).take(n) - q.list.map(p => p) + def last(n:Int) : Future[Seq[(Post,Blog)]] = { + val q = (for { (p,b) <- posts join BlogService.blogs on (_.blog === _.id) if p.draft === false && b.status === BlogStatus.PUBLISHED} yield (p,b) ).sortBy(_._1.published.desc).take(n) + dbConfig.db.run(q.result) } - def last(blog:Blog, n:Int)(implicit s:Session) : List[(Post,Author)] = { - val q = (for { (p,a) <- posts innerJoin AuthorService.authors on (_.author === _.id) if p.blog === blog.id && p.draft === false } yield (p,a) ).sortBy(_._1.published.desc).take(n) - q.list.map(p => p) + def last(blog:Blog, n:Int) : Future[Seq[(Post,Author)]] = { + val q = (for { (p,a) <- posts join AuthorService.authors on (_.author === _.id) if p.blog === blog.id && p.draft === false } yield (p,a) ).sortBy(_._1.published.desc).take(n) + dbConfig.db.run(q.result) } - def list(blog:Blog, draft:Boolean, page:Int=0, pageSize:Int=10)(implicit s:Session) : Page[Post] = { + def listForBlog(blog:Blog, draft:Boolean, page:Int=0, pageSize:Int=10) : Future[Page[Post]] = { val offset = pageSize * page val query = (for { p <- posts if (p.blog === blog.id) && (p.draft === draft) } yield p).sortBy(if (draft) _.created.desc else _.published.desc).drop(offset).take(pageSize) val totalRows = count(blog, draft) - val result = query.list.map(row => row) - Page(result, page, offset, totalRows, pageSize) + val result = dbConfig.db.run(query.result) + result flatMap (items => totalRows map (rows => Page(items, page, offset, rows, pageSize))) } - def count(blog:Blog, draft:Boolean)(implicit s:Session) = Query((for { p <- posts if (p.blog === blog.id) && (p.draft === draft) } yield p).length).first + def count(blog:Blog, draft:Boolean) : Future[Int] = + dbConfig.db.run((for { p <- posts if (p.blog === blog.id) && (p.draft === draft) } yield p).length.result) - def find(blog:Blog, slug:String, year:Int, month:Int, day:Int)(implicit s:Session) = { + def find(blog:Blog, slug:String, year:Int, month:Int, day:Int) : Future[Option[Post]] = { val ini = new DateTime(year, month, day, 0, 0) val end = ini.plus(Period.days(1)) val tini = new Timestamp(ini.getMillis) val tend = new Timestamp(end.getMillis) val query = for {p <- posts if (p.blog === blog.id) && (p.published >= tini) && (p.published <= tend) && (p.slug === slug)} yield p - query.firstOption + dbConfig.db.run(query.result.headOption) } - def create(author:Author, blog:Blog, title:String, subtitle:Option[String], content:String, draft:Boolean, image:Option[String])(implicit s:Session) = { + def create(author:Author, blog:Blog, title:String, subtitle:Option[String], content:String, draft:Boolean, image:Option[String]) = { def published = if (draft) None else Some(new Timestamp(DateTime.now.getMillis)) def slug = if (draft) None else Some(tools.PostAux.slugify(title)) def post = Post(id=IdGenerator.nextId(classOf[Post]), blog=blog.id, title=title, subtitle=subtitle, @@ -61,19 +79,19 @@ object PostService extends DbService[Post]{ post } - def update(post:Post, title:String, subtitle:Option[String], content:String, draft:Boolean, image:Option[String], publish:Boolean)(implicit s:Session) { + def update(post:Post, title:String, subtitle:Option[String], content:String, draft:Boolean, image:Option[String], publish:Boolean) { def published = if (publish) Some(post.published.getOrElse(new Timestamp(DateTime.now.getMillis))) else None def slug = if (publish) Some(post.slug.getOrElse(tools.PostAux.slugify(title))) else None def isDraft = !publish update(post.copy(title=title, subtitle=subtitle, content=content, draft=isDraft, image=image, published=published, slug=slug)) } - def importPosts(author:Author, blog:Blog, file:File, format:String)(implicit s:Session) { + def importPosts(author:Author, blog:Blog, file:File, format:String) { if (format == "ghost") importGhostFormat(author, blog, file) } - def importGhostFormat(author:Author, blog:Blog, file:File)(implicit s:Session) { + def importGhostFormat(author:Author, blog:Blog, file:File){ val data = Source.fromFile(file)(scala.io.Codec.UTF8).mkString val json = Json.parse(data) val jsonPosts = (json \ "data" \ "posts").as[JsArray] diff --git a/app/tools/PostAux.scala b/app/tools/PostAux.scala index d646b88..437f320 100644 --- a/app/tools/PostAux.scala +++ b/app/tools/PostAux.scala @@ -11,7 +11,7 @@ import scala.annotation.tailrec object PostAux { def canonical(blog:Blog, post:Post) = { - val base = blog.url.getOrElse(Messages("prosa.canonical.url")) + val base = blog.url.getOrElse("prosa.canonical.url") if (base.endsWith("/")) base.stripSuffix("/") + slug(blog.alias, post, drafts=false) else @@ -82,20 +82,20 @@ object PostAux { } - lazy val year = Messages("dates.year") - lazy val years = Messages("dates.years") - lazy val month = Messages("dates.month") - lazy val months = Messages("dates.months") - lazy val week = Messages("dates.week") - lazy val weeks = Messages("dates.weeks") - lazy val day = Messages("dates.day") - lazy val days = Messages("dates.days") - lazy val hour = Messages("dates.hour") - lazy val hours = Messages("dates.hours") - lazy val minute = Messages("dates.minute") - lazy val minutes = Messages("dates.minutes") - lazy val second = Messages("dates.second") - lazy val seconds = Messages("dates.seconds") + lazy val year = "dates.year" + lazy val years = "dates.years" + lazy val month = "dates.month" + lazy val months = "dates.months" + lazy val week = "dates.week" + lazy val weeks = "dates.weeks" + lazy val day = "dates.day" + lazy val days = "dates.days" + lazy val hour = "dates.hour" + lazy val hours = "dates.hours" + lazy val minute = "dates.minute" + lazy val minutes = "dates.minutes" + lazy val second = "dates.second" + lazy val seconds = "dates.seconds" def formatElapsed(date:Option[java.util.Date]) = { date match { diff --git a/app/views/blogs_form.scala.html b/app/views/blogs_form.scala.html index 4b3aa7b..99b16af 100644 --- a/app/views/blogs_form.scala.html +++ b/app/views/blogs_form.scala.html @@ -1,4 +1,4 @@ -@(blog:Option[Blog], form: Form[BlogsController.BlogData], user:Visitor, avatarUrl:String)(implicit token:controllers.PreventingCsrfToken, flash:Flash) +@(blog:Option[Blog], form: Form[BlogData], user:Visitor, avatarUrl:String)(implicit messages:Messages, token:controllers.PreventingCsrfToken, flash:Flash) @main("Blog", "blog", None, None, None, None, user){ @views.html.blogs_menu(None, user) }{ diff --git a/app/views/blogs_index.scala.html b/app/views/blogs_index.scala.html index 19497c1..a9c93e3 100644 --- a/app/views/blogs_index.scala.html +++ b/app/views/blogs_index.scala.html @@ -1,4 +1,4 @@ -@(caption:String, page:services.Page[Blog], user:Visitor)(implicit flash:Flash) +@(caption:String, page:services.Page[Blog], user:Visitor)(implicit flash:Flash, messages:Messages) @main(caption, null, None, Some(Messages("prosa.canonical.url")), None, None, user) { @views.html.blogs_menu(None, user) }{ diff --git a/app/views/blogs_menu.scala.html b/app/views/blogs_menu.scala.html index a633906..fdeebcb 100644 --- a/app/views/blogs_menu.scala.html +++ b/app/views/blogs_menu.scala.html @@ -1,4 +1,4 @@ -@(blogOpt:Option[Blog], user:Visitor) +@(blogOpt:Option[Blog], user:Visitor)(implicit messages:Messages) @blogOpt.map { blog =>