From ee2ca7fbaf6c513bd714f09bddfec45d6c8af73c Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sun, 7 Sep 2014 00:33:46 +0200 Subject: [PATCH] Added account settings management and avatars --- data/avatars/.gitignore | 3 + data/avatars/blank.png | Bin 0 -> 6425 bytes data/config.ini | 21 ++- public_html/css/forms.css | 12 ++ public_html/index.html | 4 + public_html/js/Auth.js | 10 ++ public_html/js/Controls/FileDropper.js | 54 ++++++ .../js/Presenters/RegistrationPresenter.js | 22 +-- .../Presenters/UserAccountRemovalPresenter.js | 78 +++++++++ .../UserAccountSettingsPresenter.js | 154 ++++++++++++++++++ .../UserBrowsingSettingsPresenter.js | 46 ++++++ public_html/js/Presenters/UserPresenter.js | 60 +++---- public_html/js/Util.js | 4 +- public_html/templates/account-removal.tpl | 4 +- public_html/templates/account-settings.tpl | 130 ++++++++------- public_html/templates/browsing-settings.tpl | 4 +- public_html/templates/login-form.tpl | 9 +- public_html/templates/registration-form.tpl | 16 +- public_html/templates/user.tpl | 16 +- src/Controllers/UserAvatarController.php | 69 ++++++++ src/Controllers/UserController.php | 75 +++++++-- src/Controllers/ViewProxies/UserViewProxy.php | 3 +- src/Entities/User.php | 5 + src/FormData/RegistrationFormData.php | 4 +- src/FormData/UserEditFormData.php | 24 +++ src/Helpers/EnumHelper.php | 35 +++- src/Helpers/HttpHelper.php | 6 + src/Privilege.php | 10 ++ src/Services/FileService.php | 93 +++++++++++ .../IThumbnailGenerator.php | 7 + .../ImageGdThumbnailGenerator.php | 98 +++++++++++ .../ImageImagickThumbnailGenerator.php | 78 +++++++++ .../ImageThumbnailGenerator.php | 28 ++++ src/Services/ThumbnailService.php | 30 ++++ src/Services/UserService.php | 83 +++++++++- src/di.php | 8 +- tests/AbstractTestCase.php | 17 ++ tests/Services/FileServiceTest.php | 16 ++ tests/Services/UserServiceTest.php | 15 +- tests/files/.gitignore | 2 + 40 files changed, 1178 insertions(+), 175 deletions(-) create mode 100644 data/avatars/.gitignore create mode 100644 data/avatars/blank.png create mode 100644 public_html/js/Controls/FileDropper.js create mode 100644 public_html/js/Presenters/UserAccountRemovalPresenter.js create mode 100644 public_html/js/Presenters/UserAccountSettingsPresenter.js create mode 100644 public_html/js/Presenters/UserBrowsingSettingsPresenter.js create mode 100644 src/Controllers/UserAvatarController.php create mode 100644 src/FormData/UserEditFormData.php create mode 100644 src/Services/FileService.php create mode 100644 src/Services/ThumbnailGenerators/IThumbnailGenerator.php create mode 100644 src/Services/ThumbnailGenerators/ImageGdThumbnailGenerator.php create mode 100644 src/Services/ThumbnailGenerators/ImageImagickThumbnailGenerator.php create mode 100644 src/Services/ThumbnailGenerators/ImageThumbnailGenerator.php create mode 100644 src/Services/ThumbnailService.php create mode 100644 tests/Services/FileServiceTest.php create mode 100644 tests/files/.gitignore diff --git a/data/avatars/.gitignore b/data/avatars/.gitignore new file mode 100644 index 00000000..1bc68f63 --- /dev/null +++ b/data/avatars/.gitignore @@ -0,0 +1,3 @@ +* +!blank.png +!.gitignore diff --git a/data/avatars/blank.png b/data/avatars/blank.png new file mode 100644 index 0000000000000000000000000000000000000000..54c5e1e121fa2f1afbbd8380adec47050c775deb GIT binary patch literal 6425 zcma)gcTf}Dw{4_D5Rl%aNl^&B2Z++8V}Ot#5JCykdnfdwbm>)^0z$;l1nEUUjDUPd z6GB(I(wmga_q%W2yf<_IxM$Cveb!!U&Hn40Gjm=T8*1OCVy6NC0Jn7>Yd`^jTcCfF z;-&$xWFC8aQ%D?D4O9Vux>Vo=iu4BG@_}lr0cuA$H*Nrr-D9W$01zw)07S$90OvQY zh;;zKPXYkgv;hFXxc~r*XJP9T_4a2=oewd)2^dOk+@002PxfAiKqgs_Vn z>5z_ws%hYlouWNGmyZqIC)d|r+{oHY9|~UH7)}~Z4aPhFu9tXC-3RgNJTY`?i^d5c z(4XPg=Y-|}KbQBFZWc=0n;k4QD%_5O(iu9x$a-gt?$kw+nVpbS~tuyrV24(-0Pd zm*|yf%jMZYg=MshS>6}g2*_v2fMOKZ4sA0+;2zVZ6K;o-&)lrjyuAud}wftY`w1AHyerQ|l7D z8cKkk1_-}~rZbWI2F{+Ywnv!nqAO8Bhok<7#+1;^c8C_UC5-7AljoTb=!{PQ1vN91 z<=Nj|9@FRjBrg}-F$=*5g!v+*i3S7|1xn1Czb!6Z@#P=`1m#J>e7bAZG=BU)n|9R4D{-BL{06ZcN{Na4Ba z^aS}5&;!#~VJ9XFo3-O$|6n1Tk2#@%znUn=lH${&PF^lI;1n485{0f^sj+MZTLDDO z%u14cx|-kiG-I9nL(-_SwWQizDq+zqmPQ1ZBV4*Wf-qg;5x)xH1Ki@$4a;Y45mWoQ zq)@^(K#)*hAtp<6-z*-Pj;Lnj@q7!xSM%_pSZL>ynig(Le}`Cj!seAZ-+10KmwQWc z2?9x`ZnG^_uCNl^b4&N=X#9>h8qsuS^^xgAbVH7)iS#!VSYfit;ryuaiFleGc5ZD) z@ul>@p<{?!oPfa|+r2E|{&!m+*t2QIKG<-M{b1D|6hOYD_$JmXcBtP>Fm3h{LA@zs zI44XRXv;nkiH<$vH>)(DFqrk*f%WdoG1Xc!^jXEu5E4jiEpKh`DLPf|DEJSf>;F<^L*W#5|=kQ zmCvzs7fr%unqRWcKnbPF@E}cm-`IhvhX9D7ea{4=oyn*1#oG@DB~i@+;WGr?-?I60 zNzMI%{*x}~f+%9MwdJ9D;^luRLogi9mz_-Q{{rhNCJj}t+-{HL=6E`5@PTU5^}CaZ z0D=YdIqcD3wGSMoCIfb5!d4YH@_uP@f{|`il*AAQN|YYHcuI`oSbAFk18!6~$>3%26>4>)FqzfL({R$iZ2)x?8I{i+aGEm#`k^ z)bGvX9x9os52=Cp$fO$LD*{`C70nei{k)+10kYiYXQQcx0r*`rQMQNUkNh>EPTrSg zpi$rN)3wCcgIE-*L2I@tryxpz|6U(CN7TWc#70D)RKw)PM%HS9iReJ8wn=KTcjtn8 z^A*bP9j$@pDVft9uN9MM)TomErBdL^#dc^kmZt`NxEfDxi5cHq=#ejywe^!diB%h; zt7-Vwzq)b|=TP2h@9F~)$%>9~#u1mTZNje6tQh=M&d>Hp-NoM&ZACE`sGz^ft6L&= z9UjTGJG=Ld=ktFS2$RDv22|4~Jb)V4rRwczmhN2Hi?uHxCnYjeJwJaj=}~j>7L0_- zHnuWax^gkE^)0%7opsFP*wz?{fSEaGC}Fj_e;E86aH2GPpFBF(lN$D(gx7fm_q|zn zMu8I18)b==Ua|4nh4~CatLrY02T#cGYOhwd^&N>5Dg6svjH`+5-=WTPZ-a!tsOLBp zI+QiZwPaY@=v1&CwBnhv>q^Z&cGK(YNJ$2P0~vPLeF{{!ulO=WytAATGr_J%4u?J} z`Ti=38#Z-^bQqHd#3s=4U&{^4M8!|{jKG-@OLILY6^{!xI(fUjCAr~zJ?uX%%Jw2e zQiIj$m5t5jf3|w88|1DMi!|}PJWw9Lt9-@DEe!>EJ}sQg>!qR-TUcd4N1kNbyvQ3s z09=A!s_tDfWv{01OBsXnN zubN~Onncgold4=9!~BUR|7ECg=^d$KJ6e}n)XuQypT(2XzF!#T)wuDMB)Fuk@jlP! zZD7nq5&AVkpGkO%f~$sx;GhLbd<)%|nhnlQ4mdN_Wy@$`M_ZKD&7TY9(_$MVi(OQJ zAtSOw6wF}?!@dFJvsB7MmFGgcM6X41L_r+VQ381-+j67J$Y<(XgeMTq!0m5k;8BeF zd&%#uBSPL0r&c(*DU3QtA@`aI44t%V;YcNtJJOP7FmvnT7So#CI0YoP$|}boJX-@! zD>g-D*`D#e*m0%tzFpbI2^^j(kpE1-^1$g^nOfDN)bMSCsn#{YRlB%)cF0Kxi`Jib zGMb5NPA2=7J+qgObqnQ#7PdTzYi%1U!;m@Za*e#>WOL?Y+nyz-ziMci?3yd^-+Q`r zm1}kT<&l`G=dOOE@?t09TIb~amc<}mweTp%)J`1VRt-RcJdT` zVkSqtEN5($bUafbFEpzl-f#1waflPfIXvRE6>5^UnLd>?rIu3JG3vbj(D(U{#{O+G z3&x^|*E!&043oh%ZXi5mg{hQd_OrNvv+U|Ou7lu$X0}aMY`L%98+ItAAY~hcLT0}M zSC6*JI6?DcPr*z}Ch})YAD#{= zV6%F_CM2|@SgSrpJMC0=fo9V8G?S}I(4Hw8U|aof3~tu%j^D|pXV}JRYv@Gc1E_z6 zQcW`u))-K4X_s>UA3>S5{vn0H4hLLPap6*0i>w7t?SuNTq@pqRwpxYek(7}sxHVdw zL6O^}vngLSzovi*-Fp}hig$^QEgy-+@;DEkWz2eUUm($}?H)$gCSJm;?6vxtp*&^? z(q^@r3tMe>dk2H*&Ma}6?xtIyK|G1yDfC$1)tdMQS=N727k4*l8AnIYy!0;CA-b7% zpM!gU;v`@A%aCiobhx=w+y+E2{GK@&zSD)HlDzw{ZAV7kvh$rR-TsB~Vl%9how;eE zVaQiW`*^0YTo>dF{e<&h$z4ktw zKR+(3!Vwdx4bH2PKA(|{u(KRWw4izX7K}GK`s0>CNn*5F8g6B;S8ch(MX|y*jWnP5g#_+e zLs(hho6cr<2+~ z{;L^Vo+RS>JS~(LbXJC0KCRu8+*$m}GE)>sIvTGodVMmJ?4H8ZrI}4^wDV%#@T#u+P$fQB@qd7hVxs=Csyc$ ze|P)4*(doVYC;;_?R`3ZgMXwk9q z0gV^<;NVTS`#I(pF%bT&V2*``*~lJn2W~fB;(QC2cB@ADE1hO#LXI>qn`OdR%yR-G zZF!XUjx7FT7iZ{m|HEC+aG$W?Dv2J!G^)2tV?#T`!*+IrsR-Frl{F_F?^TNsPb0F6 z06rUogOy+QdlMh^i{>t#eNB7{41|aUzeDt7yeYDkf_kgI@*9LW)lNIZA1wPqRPL-#Vu3pZ zoU$=B{*9W=6OZ}Kj&ABby%^J7da3)l;QR3bZtFaa&o@pMk=~Iz$A||hQ9}C_PCUQc z71Y4>!d2{(=`S+@Vb8Ywu3}?`f~)Qy8v@$OfseCzI&%kcBWE`jpJtAWX@{Y@)t#yb z>M9A7=&d$A)31nq%FI1YmGNGaW3CyXgl!XZq^5ezFu^^ z_qjt!K5u#h`G?VeecqS%`AsX=JKK=1aOs$fd+UQ+ z>`)|SBpo|Bf24~7la_G{+i$tc=On2<)NE{^FIz zY0h(E|e)+-sz7%knxdVjb?|IVP9Z@gShy3FIvPMj6VJAXIyOZ;)D z+hw-7Xm;^|lsW_0CxD{R^OpF=z#m|kis_@XrJ{|fA*#vm53TNBIM@#B><|CN=BxGWiC@m2mE%HN zle#SkXD_aOGRH&5nq}P%h1$mqD-BAJbI1$7;o!HCrvu6b3|_fPSamNVkH#P;ji#*$ zY>qo>E`Yl@oi6}3of!;HSvs)W5P0{eoo$4=KiYaH#Fm)^w~?i%cR~{n+n3p(qyS_3k`SM+p{c+ zPmw$mBO+yRH|VyR9BqOd6v5>_|cHBt(?W z`Tt8V2|yD z;cu_2miDT;SJIQ*?!G&GB_N){WpZHy0|JYdS65FG?$natA*`1OR)z!y*kzbhPrtJ) z?xH;a02%x}C{fII(j^>46YhIQ1$X87^mI^G#B8|TMO|Kzm%l)@sY^H^EklRp$DuLz zT+5XNXuC=;q^(o2Qj;uW98!b8maHW=A5@;-oV`KR@GR@$!6{>yXq2lGh^RKPIHmZg zowp1LA0P)VO!tfERy!ZG$rm#TS0{X{d9Iz{cA8s$Edko`(tsWY$S!$DGZX|LAT}zp z&qoGrTn}3vhkMEF|C*u{<6OfA&l>n1^wb+1+1E9FbQ~rUk%6R)z}e{t zesDWUE0UZ$Id3Dw&GFDkaZK9Iwu^?V=JQqOyb4^_@YRN5RO*iaqZyAkR|%XDzbiZ9 zX$W-o!dop!8^d!?v)TCCLthubri&>e(` zZIfG4*YwvR&)ZiwSWiYYEu0Qa_Vz9N?8I_48t7optkwwkxIzX?OP?YS{~{>X&4NE@ zuPIH%8TBL#_@TV57vGG5qNNovK!)@?Ujn@#l!=1OpqN;za+*r1C+%c{*OO%q0?9%G zyMg-yv83`TpcsetreVn#I6P>f2gg`UnH@;+A%(-`x`X00a^josR6k8NIxM@j#aadv zT!-!#{I0|z(JoL=V{lQ3M*QK<{n6=m?m)Wic8pupecNBEeh{B#x?}lyw}DRc&1w0W z{kjAtdDPn?(BI4O(u5b$5@(j{i(duA3OYnaB9p6Cz7 z2Z-ar5@pcFZ>2!0RK?Rh7o6{5%Tw-lP(1dOs4f~bjmzBamSp8V^3Es4?b;^R@yH5z zpUCRbTQEot0Hwbw{bl*nOC>H}XYF0_c?y2}h3&=u&#-A6C)U^W9vS=z=@OE6WLe`; zQq0sLxnoW=DU8FFB7=T(Jn=n1ZxM0nO}|)2m{t1zr%!6WdA=|Q&dD&lAlvYbniL`= zK6+dEgFL<`s_C1o72YeQdb*eSw}XIFOhhbphn;1}bv-(jIg z4aO6#C+>_v3)FS;L5EDTE-{yQyx>0r@deu+JnP8nf`GQ=>wZUsWBfJ{6dlge8#g&4 zfMFI?%KwjBLaQlc&G_oC7jufKaDPripKj(r$9UI8CwjwCWhS!k7M@CDk0J0-Ku@Ms zx;gYDn1eaLbg{W&T+jS&=;hBt;SJbM&U^5*)7gLaicNUfU2G87YIx8X6@0JfKq)}0 zql-P50snq&VXQA_?DQv0oxvA>dR#1G3FH5Xo$y%5{R_cPhp{Aub|@rD>~$pLd-lP~ z#ywruh5e)$ir^voWXb>}(W(o#T|v?@SZ(t&Kyr}e)|Kz?S$Hzd#hC4EqCo~};rj-qIkov(*C dKq^oQBpE3AZ|a;^G~Q?dI+}(WwQ4p|{{;cxMf?B& literal 0 HcmV?d00001 diff --git a/data/config.ini b/data/config.ini index 942ee782..cb7a6194 100644 --- a/data/config.ini +++ b/data/config.ini @@ -8,12 +8,25 @@ secret = change minPasswordLength = 5 [security.privileges] -register = anonymous -listUsers = regularUser, powerUser, moderator, administrator -deleteOwnAccount = regularUser, powerUser, moderator, administrator -deleteAllAccounts = administrator +register = anonymous +listUsers = regularUser, powerUser, moderator, administrator +deleteOwnAccount = regularUser, powerUser, moderator, administrator +deleteAllAccounts = administrator +changeOwnName = regularUser, powerUser, moderator, administrator +changeOwnAvatarStyle = regularUser, powerUser, moderator, administrator +changeOwnEmailAddress = regularUser, powerUser, moderator, administrator +changeOwnName = regularUser, powerUser, moderator, administrator +changeOwnPassword = regularUser, powerUser, moderator, administrator +changeAllAvatarStyles = moderator, administrator +changeAllEmailAddresses = moderator, administrator +changeAllNames = moderator, administrator +changeAllPasswords = moderator, administrator +changeAccessRank = administrator [users] minUserNameLength = 1 maxUserNameLength = 32 usersPerPage = 20 + +[misc] +thumbnailCropStyle = outside diff --git a/public_html/css/forms.css b/public_html/css/forms.css index e91e2aca..0b8b4181 100644 --- a/public_html/css/forms.css +++ b/public_html/css/forms.css @@ -44,3 +44,15 @@ input[type=button] { font-family: 'Droid Sans', sans-serif; font-size: 17px; } + +.file-handler { + border: 3px dashed #eee; + padding: 0.3em 0.5em; + line-height: 140% !important; + text-align: center; + cursor: pointer; +} +.file-handler.active { + border-color: #6a2; + background-color: #eeffcc; +} diff --git a/public_html/index.html b/public_html/index.html index 38966ca9..6c4d92a9 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -33,6 +33,7 @@ + @@ -40,6 +41,9 @@ + + + diff --git a/public_html/js/Auth.js b/public_html/js/Auth.js index 4b2dfef4..472e6886 100644 --- a/public_html/js/Auth.js +++ b/public_html/js/Auth.js @@ -5,6 +5,16 @@ App.Auth = function(jQuery, util, api, appState, promise) { var privileges = { register: 'register', listUsers: 'listUsers', + viewAllEmailAddresses: 'viewAllEmailAddresses', + changeAccessRank: 'changeAccessRank', + changeOwnAvatarStyle: 'changeOwnAvatarStyle', + changeOwnEmailAddress: 'changeOwnEmailAddress', + changeOwnName: 'changeOwnName', + changeOwnPassword: 'changeOwnPassword', + changeAllAvatarStyles: 'changeAllAvatarStyles', + changeAllEmailAddresses: 'changeAllEmailAddresses', + changeAllNames: 'changeAllNames', + changeAllPasswords: 'changeAllPasswords', deleteOwnAccount: 'deleteOwnAccount', deleteAllAccounts: 'deleteAllAccounts', }; diff --git a/public_html/js/Controls/FileDropper.js b/public_html/js/Controls/FileDropper.js new file mode 100644 index 00000000..9fcf71f5 --- /dev/null +++ b/public_html/js/Controls/FileDropper.js @@ -0,0 +1,54 @@ +var App = App || {}; +App.Controls = App.Controls || {}; + +App.Controls.FileDropper = function( + $fileInput, + allowMultiple, + onChange, + jQuery) { + + var $dropDiv = jQuery('
'); + $dropDiv.html((allowMultiple ? 'Drop files here!' : 'Drop file here!') + '
Or just click on this box.'); + $dropDiv.insertBefore($fileInput); + $fileInput.attr('multiple', allowMultiple); + $fileInput.hide(); + + $fileInput.change(function(e) + { + addFiles(this.files); + }); + + $dropDiv.on('dragenter', function(e) + { + $dropDiv.addClass('active'); + }).on('dragleave', function(e) + { + $dropDiv.removeClass('active'); + }).on('dragover', function(e) + { + e.preventDefault(); + }).on('drop', function(e) + { + e.preventDefault(); + addFiles(e.originalEvent.dataTransfer.files); + }).on('click', function(e) + { + $fileInput.show().focus().trigger('click').hide(); + $dropDiv.addClass('active'); + }); + + function getFiles() { + return files; + } + + function addFiles(files) { + $dropDiv.removeClass('active'); + if (!allowMultiple && files.length > 1) { + alert('Cannot select multiple files.'); + return; + } + onChange(files); + } +} + +App.DI.register('fileDropper', App.Controls.FileDropper); diff --git a/public_html/js/Presenters/RegistrationPresenter.js b/public_html/js/Presenters/RegistrationPresenter.js index f11755a5..810641d1 100644 --- a/public_html/js/Presenters/RegistrationPresenter.js +++ b/public_html/js/Presenters/RegistrationPresenter.js @@ -31,20 +31,20 @@ App.Presenters.RegistrationPresenter = function( e.preventDefault(); messagePresenter.hideMessages($messages); - registrationData = { - userName: $el.find('[name=user]').val(), - password: $el.find('[name=password1]').val(), - passwordConfirmation: $el.find('[name=password2]').val(), + formData = { + userName: $el.find('[name=userName]').val(), + password: $el.find('[name=password]').val(), + passwordConfirmation: $el.find('[name=passwordConfirmation]').val(), email: $el.find('[name=email]').val(), }; - if (!validateRegistrationData(registrationData)) + if (!validateRegistrationFormData(formData)) return; - api.post('/users', registrationData) + api.post('/users', formData) .then(function(response) { registrationSuccess(response); - }).catch(function(response) { + }).fail(function(response) { registrationFailure(response); }); } @@ -62,18 +62,18 @@ App.Presenters.RegistrationPresenter = function( messagePresenter.showError($messages, apiResponse.json && apiResponse.json.error || apiResponse); } - function validateRegistrationData(registrationData) { - if (registrationData.userName.length == 0) { + function validateRegistrationFormData(formData) { + if (formData.userName.length == 0) { messagePresenter.showError($messages, 'User name cannot be empty.'); return false; } - if (registrationData.password.length == 0) { + if (formData.password.length == 0) { messagePresenter.showError($messages, 'Password cannot be empty.'); return false; } - if (registrationData.password != registrationData.passwordConfirmation) { + if (formData.password != formData.passwordConfirmation) { messagePresenter.showError($messages, 'Passwords must be the same.'); return false; } diff --git a/public_html/js/Presenters/UserAccountRemovalPresenter.js b/public_html/js/Presenters/UserAccountRemovalPresenter.js new file mode 100644 index 00000000..a9749663 --- /dev/null +++ b/public_html/js/Presenters/UserAccountRemovalPresenter.js @@ -0,0 +1,78 @@ +var App = App || {}; +App.Presenters = App.Presenters || {}; + +App.Presenters.UserAccountRemovalPresenter = function( + jQuery, + util, + promise, + api, + auth, + router, + messagePresenter) { + + var target; + var template; + var user; + var privileges = {}; + + function init(args) { + return promise.make(function(resolve, reject) { + user = args.user; + target = args.target; + + privileges.canDeleteAccount = + auth.hasPrivilege(auth.privileges.deleteAllAccounts) || + (auth.hasPrivilege(auth.privileges.deleteOwnAccount) && auth.isLoggedIn(user.name)); + + promise.wait(util.promiseTemplate('account-removal')).then(function(html) { + template = _.template(html); + render(); + resolve(); + }); + }); + } + + function render() { + $el = jQuery(target); + $el.html(template({ + user: user, + canDeleteAccount: privileges.canDeleteAccount})); + + $el.find('form').submit(accountRemovalFormSubmitted); + } + + function getPrivileges() { + return privileges; + } + + function accountRemovalFormSubmitted(e) { + e.preventDefault(); + $messages = jQuery(target).find('.messages'); + messagePresenter.hideMessages($messages); + if (!$el.find('input[name=confirmation]:visible').prop('checked')) { + messagePresenter.showError($messages, 'Must confirm to proceed.'); + return; + } + api.delete('/users/' + user.name) + .then(function() { + auth.logout(); + var $messageDiv = messagePresenter.showInfo($messages, 'Account deleted. Back to main page'); + $messageDiv.find('a').click(mainPageLinkClicked); + }).fail(function(response) { + messagePresenter.showError($messages, response.json && response.json.error || response); + }); + } + + function mainPageLinkClicked(e) { + e.preventDefault(); + router.navigateToMainPage(); + } + + return { + init: init, + render: render, + getPrivileges: getPrivileges + }; +}; + +App.DI.register('userAccountRemovalPresenter', App.Presenters.UserAccountRemovalPresenter); diff --git a/public_html/js/Presenters/UserAccountSettingsPresenter.js b/public_html/js/Presenters/UserAccountSettingsPresenter.js new file mode 100644 index 00000000..52922283 --- /dev/null +++ b/public_html/js/Presenters/UserAccountSettingsPresenter.js @@ -0,0 +1,154 @@ +var App = App || {}; +App.Presenters = App.Presenters || {}; + +App.Presenters.UserAccountSettingsPresenter = function( + jQuery, + util, + promise, + api, + auth, + messagePresenter) { + + var $messages; + var target; + var template; + var user; + var privileges; + var avatarContent; + + function init(args) { + return promise.make(function(resolve, reject) { + user = args.user; + target = args.target; + + privileges = { + canChangeAccessRank: + auth.hasPrivilege(auth.privileges.changeAccessRank), + canChangeAvatarStyle: + auth.hasPrivilege(auth.privileges.changeAllAvatarStyles) || + (auth.hasPrivilege(auth.privileges.changeOwnAvatarStyle) && auth.isLoggedIn(user.name)), + canChangeName: + auth.hasPrivilege(auth.privileges.changeAllNames) || + (auth.hasPrivilege(auth.privileges.changeOwnName) && auth.isLoggedIn(user.name)), + canChangeEmailAddress: + auth.hasPrivilege(auth.privileges.changeAllEmailAddresses) || + (auth.hasPrivilege(auth.privileges.changeOwnEmailAddress) && auth.isLoggedIn(user.name)), + canChangePassword: + auth.hasPrivilege(auth.privileges.changeAllPasswords) || + (auth.hasPrivilege(auth.privileges.changeOwnPassword) && auth.isLoggedIn(user.name)), + }; + + promise.wait(util.promiseTemplate('account-settings')).then(function(html) { + template = _.template(html); + render(); + resolve(); + }); + }); + } + + function render() { + var $el = jQuery(target); + $el.html(template(_.extend({user: user}, privileges))); + $el.find('form').submit(accountSettingsFormSubmitted); + $el.find('form [name=avatar-style]').change(avatarStyleChanged); + avatarStyleChanged(); + new App.Controls.FileDropper($el.find('[name=avatar-content]'), false, avatarContentChanged, jQuery); + } + + function getPrivileges() { + return privileges; + } + + function avatarStyleChanged(e) { + var $el = jQuery(target); + var $target = $el.find('.avatar-content .file-handler'); + if ($el.find('[name=avatar-style]:checked').val() == 'manual') { + $target.show(); + } else { + $target.hide(); + } + } + + function avatarContentChanged(files) { + if (files.length == 1) { + var reader = new FileReader(); + reader.onloadend = function() { + avatarContent = reader.result; + var $el = jQuery(target); + var $target = $el.find('.avatar-content .file-handler'); + $target.html(files[0].name); + } + reader.readAsDataURL(files[0]); + } + } + + function accountSettingsFormSubmitted(e) { + e.preventDefault(); + var $el = jQuery(target); + var $messages = jQuery(target).find('.messages'); + messagePresenter.hideMessages($messages); + var formData = {}; + + if (privileges.canChangeAvatarStyle) { + formData.avatarStyle = $el.find('[name=avatar-style]:checked').val(); + if (avatarContent) + formData.avatarContent = avatarContent; + } + if (privileges.canChangeName) { + formData.userName = $el.find('[name=userName]').val(); + } + if (privileges.canChangeEmailAddress) { + formData.email = $el.find('[name=email]').val(); + } + if (privileges.canChangePassword) { + formData.password = $el.find('[name=password]').val(); + formData.passwordConfirmation = $el.find('[name=passwordConfirmation]').val(); + } + if (privileges.canChangeAccessRank) { + formData.accessRank = $el.find('[name=access-rank]').val(); + } + + if (!validateAccountSettingsFormData($messages, formData)) { + return; + } + + if (!formData.password) { + delete formData.password; + delete formData.passwordConfirmation; + } + + api.put('/users/' + user.name, formData) + .then(function(response) { + editSuccess($messages, response); + }).fail(function(response) { + editFailure($messages, response); + }); + } + + function editSuccess($messages, apiResponse) { + //todo: tell user if it turned out that he needs to confirm his e-mail + var message = 'Account settings updated!'; + messagePresenter.showInfo($messages, message); + } + + function editFailure($messages, apiResponse) { + messagePresenter.showError($messages, apiResponse.json && apiResponse.json.error || apiResponse); + } + + function validateAccountSettingsFormData($messages, formData) { + if (formData.password != formData.passwordConfirmation) { + messagePresenter.showError($messages, 'Passwords must be the same.'); + return false; + } + + return true; + } + + return { + init: init, + render: render, + getPrivileges: getPrivileges, + }; +} + +App.DI.register('userAccountSettingsPresenter', App.Presenters.UserAccountSettingsPresenter); diff --git a/public_html/js/Presenters/UserBrowsingSettingsPresenter.js b/public_html/js/Presenters/UserBrowsingSettingsPresenter.js new file mode 100644 index 00000000..1a58a9be --- /dev/null +++ b/public_html/js/Presenters/UserBrowsingSettingsPresenter.js @@ -0,0 +1,46 @@ +var App = App || {}; +App.Presenters = App.Presenters || {}; + +App.Presenters.UserBrowsingSettingsPresenter = function( + jQuery, + util, + promise, + auth) { + + var target; + var template; + var user; + var privileges = {}; + + function init(args) { + return promise.make(function(resolve, reject) { + user = args.user; + target = args.target; + + privileges.canChangeBrowsingSettings = auth.isLoggedIn(user.name) && user.name == auth.getCurrentUser().name; + + promise.wait(util.promiseTemplate('browsing-settings')).then(function(html) { + template = _.template(html); + render(); + resolve(); + }); + }); + } + + function render() { + $el = jQuery(target); + $el.html(template({user: user})); + } + + function getPrivileges() { + return privileges; + } + + return { + init: init, + render: render, + getPrivileges: getPrivileges, + }; +} + +App.DI.register('userBrowsingSettingsPresenter', App.Presenters.UserBrowsingSettingsPresenter); diff --git a/public_html/js/Presenters/UserPresenter.js b/public_html/js/Presenters/UserPresenter.js index f6c4b38a..bfd04bc2 100644 --- a/public_html/js/Presenters/UserPresenter.js +++ b/public_html/js/Presenters/UserPresenter.js @@ -8,14 +8,14 @@ App.Presenters.UserPresenter = function( api, auth, topNavigationPresenter, + userBrowsingSettingsPresenter, + userAccountSettingsPresenter, + userAccountRemovalPresenter, messagePresenter) { var $el = jQuery('#content'); var $messages = $el; var template; - var accountSettingsTemplate; - var accountRemovalTemplate; - var browsingSettingsTemplate; var user; var userName; @@ -25,23 +25,22 @@ App.Presenters.UserPresenter = function( promise.waitAll( util.promiseTemplate('user'), - util.promiseTemplate('account-settings'), - util.promiseTemplate('account-removal'), - util.promiseTemplate('browsing-settings'), api.get('/users/' + userName)) .then(function( userHtml, - accountSettingsHtml, - accountRemovalHtml, - browsingSettingsHtml, response) { + $messages = $el.find('.messages'); template = _.template(userHtml); - accountSettingsTemplate = _.template(accountSettingsHtml); - accountRemovalTemplate = _.template(accountRemovalHtml); - browsingSettingsTemplate = _.template(browsingSettingsHtml); user = response.json; - render(); + var extendedContext = _.extend(args, {user: user}); + + promise.waitAll( + userBrowsingSettingsPresenter.init(_.extend(extendedContext, {target: '#browsing-settings-target'})), + userAccountSettingsPresenter.init(_.extend(extendedContext, {target: '#account-settings-target'})), + userAccountRemovalPresenter.init(_.extend(extendedContext, {target: '#account-removal-target'}))) + .then(render); + }).fail(function(response) { $el.empty(); messagePresenter.showError($messages, response.json && response.json.error || response); @@ -49,37 +48,16 @@ App.Presenters.UserPresenter = function( } function render() { - var context = { + $el.html(template({ user: user, - canDeleteAccount: auth.hasPrivilege(auth.privileges.deleteAllAccounts) || - (auth.isLoggedIn(userName) && auth.hasPrivilege(auth.privileges.deleteOwnAccount)), - }; - $el.html(template(context)); - $el.find('.browsing-settings').html(browsingSettingsTemplate(context)); - $el.find('.account-settings').html(accountSettingsTemplate(context)); - $el.find('.account-removal').html(accountRemovalTemplate(context)); - $el.find('.account-removal form').submit(accountRemovalFormSubmitted); - $messages = $el.find('.messages'); + canChangeBrowsingSettings: userBrowsingSettingsPresenter.getPrivileges().canChangeBrowsingSettings, + canChangeAccountSettings: _.any(userAccountSettingsPresenter.getPrivileges()), + canDeleteAccount: userAccountRemovalPresenter.getPrivileges().canDeleteAccount})); + userBrowsingSettingsPresenter.render(); + userAccountSettingsPresenter.render(); + userAccountRemovalPresenter.render(); }; - function accountRemovalFormSubmitted(e) { - e.preventDefault(); - $messages = $el.find('.account-removal .messages'); - messagePresenter.hideMessages($messages); - if (!$el.find('.account-removal input[name=confirmation]:visible').prop('checked')) { - messagePresenter.showError($messages, 'Must confirm to proceed.'); - return; - } - api.delete('/users/' + user.name) - .then(function() { - auth.logout(); - var $messageDiv = messagePresenter.showInfo($messages, 'Account deleted. Back to main page'); - $messageDiv.find('a').click(mainPageLinkClicked); - }).fail(function(response) { - messagePresenter.showError($messages, response.json && response.json.error || response); - }); - } - return { init: init, render: render diff --git a/public_html/js/Util.js b/public_html/js/Util.js index 9127554d..a178cf77 100644 --- a/public_html/js/Util.js +++ b/public_html/js/Util.js @@ -44,7 +44,7 @@ App.Util = (function(jQuery, promise) { } else { lastContentPresenter.reinit.call(presenter, args); } - }; + } function promiseTemplate(templateName) { return promiseTemplateFromCache(templateName) @@ -83,7 +83,7 @@ App.Util = (function(jQuery, promise) { resolve(data); }, error: function(xhr, textStatus, errorThrown) { - console.log(Error('Error while loading template ' + templateName + ': ' + errorThrown)); + console.log(Error('Error while loading template ' + templateName + ': ' + errorThrown)); reject(); }, }); diff --git a/public_html/templates/account-removal.tpl b/public_html/templates/account-removal.tpl index 7792b45a..0eb928ff 100644 --- a/public_html/templates/account-removal.tpl +++ b/public_html/templates/account-removal.tpl @@ -1,4 +1,4 @@ -