From 76d99c3469abd31ec28facb73599535b59e5e41a Mon Sep 17 00:00:00 2001 From: zyl <3116318851@qq.com> Date: Thu, 27 Nov 2025 23:40:10 +0800 Subject: [PATCH] =?UTF-8?q?=E7=8E=8B=E8=8A=B8=E4=BB=A3=E7=A0=81=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/workspace.xml | 4 +- src/accounts/accounts/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 141 bytes .../__pycache__/admin.cpython-310.pyc | Bin 0 -> 3572 bytes .../accounts/__pycache__/apps.cpython-310.pyc | Bin 0 -> 530 bytes .../__pycache__/forms.cpython-310.pyc | Bin 0 -> 5259 bytes .../__pycache__/models.cpython-310.pyc | Bin 0 -> 2515 bytes .../accounts/__pycache__/urls.cpython-310.pyc | Bin 0 -> 835 bytes .../user_login_backend.cpython-310.pyc | Bin 0 -> 1102 bytes .../__pycache__/utils.cpython-310.pyc | Bin 0 -> 3582 bytes .../__pycache__/views.cpython-310.pyc | Bin 0 -> 10211 bytes src/accounts/accounts/admin.py | 101 ++++++ src/accounts/accounts/apps.py | 12 + src/accounts/accounts/forms.py | 166 ++++++++++ .../accounts/migrations/0001_initial.py | 38 +++ ...s_remove_bloguser_created_time_and_more.py | 55 ++++ src/accounts/accounts/migrations/__init__.py | 0 .../__pycache__/0001_initial.cpython-310.pyc | Bin 0 -> 1484 bytes ...user_created_time_and_more.cpython-310.pyc | Bin 0 -> 1209 bytes .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 152 bytes src/accounts/accounts/models.py | 60 ++++ .../accounts/templatetags/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 154 bytes src/accounts/accounts/tests.py | 296 +++++++++++++++++ src/accounts/accounts/urls.py | 36 +++ src/accounts/accounts/user_login_backend.py | 55 ++++ src/accounts/accounts/utils.py | 79 +++++ src/accounts/accounts/views.py | 306 ++++++++++++++++++ 28 files changed, 1207 insertions(+), 1 deletion(-) create mode 100644 src/accounts/accounts/__init__.py create mode 100644 src/accounts/accounts/__pycache__/__init__.cpython-310.pyc create mode 100644 src/accounts/accounts/__pycache__/admin.cpython-310.pyc create mode 100644 src/accounts/accounts/__pycache__/apps.cpython-310.pyc create mode 100644 src/accounts/accounts/__pycache__/forms.cpython-310.pyc create mode 100644 src/accounts/accounts/__pycache__/models.cpython-310.pyc create mode 100644 src/accounts/accounts/__pycache__/urls.cpython-310.pyc create mode 100644 src/accounts/accounts/__pycache__/user_login_backend.cpython-310.pyc create mode 100644 src/accounts/accounts/__pycache__/utils.cpython-310.pyc create mode 100644 src/accounts/accounts/__pycache__/views.cpython-310.pyc create mode 100644 src/accounts/accounts/admin.py create mode 100644 src/accounts/accounts/apps.py create mode 100644 src/accounts/accounts/forms.py create mode 100644 src/accounts/accounts/migrations/0001_initial.py create mode 100644 src/accounts/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py create mode 100644 src/accounts/accounts/migrations/__init__.py create mode 100644 src/accounts/accounts/migrations/__pycache__/0001_initial.cpython-310.pyc create mode 100644 src/accounts/accounts/migrations/__pycache__/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.cpython-310.pyc create mode 100644 src/accounts/accounts/migrations/__pycache__/__init__.cpython-310.pyc create mode 100644 src/accounts/accounts/models.py create mode 100644 src/accounts/accounts/templatetags/__init__.py create mode 100644 src/accounts/accounts/templatetags/__pycache__/__init__.cpython-310.pyc create mode 100644 src/accounts/accounts/tests.py create mode 100644 src/accounts/accounts/urls.py create mode 100644 src/accounts/accounts/user_login_backend.py create mode 100644 src/accounts/accounts/utils.py create mode 100644 src/accounts/accounts/views.py diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 07bef71e..783800c8 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -38,12 +38,13 @@ "RunOnceActivity.OpenDjangoStructureViewOnStart": "true", "RunOnceActivity.ShowReadmeOnStart": "true", "RunOnceActivity.git.unshallow": "true", - "git-widget-placeholder": "zyl__branch", + "git-widget-placeholder": "master", "ignore.virus.scanning.warn.message": "true", "node.js.detected.package.eslint": "true", "node.js.detected.package.tslint": "true", "node.js.selected.package.eslint": "(autodetect)", "node.js.selected.package.tslint": "(autodetect)", + "nodejs_package_manager_path": "npm", "vue.rearranger.settings.migration": "true" } }]]> @@ -90,6 +91,7 @@ 1760256904468 + diff --git a/src/accounts/accounts/__init__.py b/src/accounts/accounts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/accounts/accounts/__pycache__/__init__.cpython-310.pyc b/src/accounts/accounts/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b17775872e747de2513d93da573abe897caeb77a GIT binary patch literal 141 zcmd1j<>g`kf~>b6GC=fW5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!H4i&acOWl2VU zUO-WPR%&udj7wHxUV6S$PJVh!VsdhRX&ryk0@&Ee@O9{FKt1R6CGK K#Y{kgg#iF%i6B}4 literal 0 HcmV?d00001 diff --git a/src/accounts/accounts/__pycache__/admin.cpython-310.pyc b/src/accounts/accounts/__pycache__/admin.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..14e3399ab6a971442e855e0a0295449acaf7d7f7 GIT binary patch literal 3572 zcmaJ^-A^3X6`yZ*VRxZ~I=0*oR7Fv@N|56ArBM~Rb>gb3f-E)7!^qNfvUd!FXLsqH z*}6uwTD}a#3#PH-U>m!vgKZpz1*>k1ftZK>7xS8(+1)3<=c(u1%Pbp%Gw9yEA2a8k zdw%Df-yxq&whQpsr7qj(5rl7W()uVcISI}ECv-$0q9bHQT@-OFIZ{^EWj2-_C9CQx z8!Ir@bd8Nwr!5=P<1p4po70}{&^tuo_X3HL_@qGMmN=A{ko6?2w37~4>9ACor(m8S zNth=s8RnfN^(P_S*#I4cwCJnn@-*whApU39qQ50sI|q{#PEHM)xdH1m?88bDS8`_7 zI&E7HfrZ3?<$2cMJ;O0aE;d9iE%rMnKY(v$P<-oym&eEb3v`yCi$u^RBI>fG45^kz zqzMs5ZA6}v^w{2-(y|tZYp6tnYiJXa-cH&`Y*NrWNMc$zC$bC2br>sxo`mZq{hmmZ zQ`9m&JD85nXqv7VeshF#`a>pvJ%NC1$XbjyTSUU+VWU+ zs}z1&ZhTRyua7l~+x2JvdcIR?6vrD=bCH_iYB{)bxps3hSoX;u{j;~#?3D|$6=e?X~(cb8VUUR^-a|djX;GBf!f>$_E(7n z_L4`AMT)fAtM7fwGXZPW%9^&*pVs`CVW1NjhTm=&**qyYI8GSGUkj$gcW4Yw@m1JF zMbDwTxDN&T3aqm3+c)2R`|O#E-e5j=hUSN?jCYn>8Z|g;W-|Fg&U4S2=!$*A7kvel zcoo&xpotMUM;v^s!UW+_#|{6^4Xmk%(pSAL8Cvg0VCz(&M8NG;vfq zFVo|mv@ClHktg8IaaaRuD22NqfEgcsZ_I!RI)KOIyLr zSCNaLobdBv`0snw%@y#4`m?LS)IEfoIY_v;9xU9iZWim)kHIynn{#{|%-je+z76yL^9z-R$ zso(jb>DaIaVe%fOdFqQsTJ{y!a?a5s@B&5i?;}XjkNi$1%!m#R_h<`JhMaLKvdnrutfnK^KX`)Xq88g z!kvB1%@=6KO3U;|Fr`1iHKhPK`5`2E!^k+0O$>vhi9pg`=+e;K1av8_OHw2aVoVWV zk`(aZwD>_fMN{wr?ZmDNI={^@NInCynN!7qKPY1himu@&&+MhKKjelB{9@d%FqY|$ zvFl~;?dh1Y94FF~C6Cwe6{c|oi9 zR*{GdtRe%eD2z)KqE5zim2rs%uB7?hO)qXTVqqBsF~DA6lIm_Hy!JF=ftNB#ZRa|$ z0H=}aZaExVk1|DV`$0In%e^ePG*$b$@?9=|Idg=w#c*ks^9NJyfEf^%lh9oBjK~c8 zL||#Qr~?IK3LJU$;H4QIAj&2pFqAR=u%|#ADU==a(J;@$j&&34AbHu1_?g9n#` z()9?Q5|7E+%vZHX+u@zbMiIyzGdhH2SQ_I&&I!9Iq<;Ax6a%&GwZ>8rS({z-hK+2i@+aIM~)wYX;Wa^ix|ERA2Fh( zD2HN-XaQ|M8h-&BzlPQXEv59R6ju*8V6zhWIqXIb$g4mdsz7i)sQ!qo$B6dZ37nW*|@ABQ+8e&-8Sp?7G5zWu2eti{ao9PKxkZj_|a(3oCMhtxe zZa^`G`R$O8p@_~8*g3xgIM_0V@-}cZ{Tb{6>?iO|d_66W$Zy&NfM*~764y~ee>!m} zatMI0V0q7Sq8>8|F>vZ992<@#wma;Y7ya(mnc>*E^R6F*YSW^2Zop5vmPs>%2Cw5< z@Za~cx8R^ZLUWM>SkNjExU$lr$T6AXZhyMV*9a@888_(1_@pnB&w14TsLw2TgMEzT zs0{D>hwh{9F2y2`=aZv*LJFSkxP2ZqbFRZ`7>b1=CmoHIp)RjUtY&92nk3>>`(2MyWZ&O{y*sUzD585 literal 0 HcmV?d00001 diff --git a/src/accounts/accounts/__pycache__/apps.cpython-310.pyc b/src/accounts/accounts/__pycache__/apps.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f5bfa32bc240a8ddb2ec046deeb65fd9d28a8864 GIT binary patch literal 530 zcmYjOO;5r=5Z$FkD<8%KKY(i!;}0;#fRcD4B;Fd5P1CKYfo`R6;3fevCZHZTdD0W= zK{Un$@#}x|Dy4Ap?#bB}kV$5;^XARl*&P;*h7j0tYvsTUAoR_Xc@rT?fLn106j7W< z1*~9rZsJM&d;22m)#`aRuG`E?tcaYOKGH)1?1h^H0pd$rS1l?E> zXh0FE04k!roYXW{HcFPSQN93bC=RAhaHrpP+qdpjeSFe#KOWqtx1aYWAcOw-@cGth zG-rh2b#4Zd!M)vhNouD&>*_o+kG$T6SgYm7SeQL_dz&coH<^=u~Kw7Y}e@$mX| z)NMGIEw}UN$=K3#mb4tSB|;3fpc7(;2q`dH&hvAGkfXAipHvnJp-cl<OE|Y ebBnN0H!@=swX*wThS=qBHY#TOkxvEU#5*RbRO literal 0 HcmV?d00001 diff --git a/src/accounts/accounts/__pycache__/forms.cpython-310.pyc b/src/accounts/accounts/__pycache__/forms.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ee6bd7d75dda023d91f39b5e7cece09929066e8 GIT binary patch literal 5259 zcmb_gTXWmi5e7g21Sv|QC|ka?$#9xB4ckVo-A*s*&53Mh+DSE@wrOWDolBs~C zdH_bYMwzih%aUtRa$?Ju#EvD`7s-w#yNPZ2R`++{H3CRJ^*s-9ch4aql9rl26dWwi zWzXWA-TihK$hEeHIk@&tN7Em@$Z?-zr~VS5vkPwPHV~cDd6UcVD$iqEFojG&4P-=B zr2T*?Wn@*(1l1tzi>8tZsUg~y%y6bfZJ}-1jAUBXHfRTR#ca<+)hKO;%#KVVi~Fw#*S*`mk(z$H2> zuxl>CJ5pbcWUPeXwDlXdmbVO|WpcVw+FQs;J@wCvvI;hZLRATe<#p7B`u{ z3@gHMxZLAKcjk8K^ny2ezjXeQ`((B}zu*>U|NVHX^ys>G{ORt2WVS!&9vd$`S$h7o zR7Pc@=pH-I)_K#{S0CN;=I*Ygm(8JCZh=0@FHXT7Dnr4NrX4Zul3VZJm+L1)S)GsYJY^s!e{xvWic=Az)V z44TQ5aWH4<263d6368c1o|v?4VmV69?|Ku8BU$-DgOFC(rlV+DI-9mNjVRa+{So}< zH`&3w?Z|y;!_+NDW)9}G?yI^kon8z$qGgzUj+i9qb}OLv%C0U*lDqllpmpoVar zUs`>7kv-9yz3mm3+>?{ur-0;1-&;RFGFlaTdUul95TKO@i+2 zglcHqG2Bj!K?w2j+Q{16&fY|bbm9605_DdQQL+_V+mLJr;)M60UwVD&T7z8$kbp6?2gP**fyfAndK>p1Z}SCiKo|&& zK-?_ybK*QlHrtXd>wyAa;K%q89`i@O2#frPR_~tP%DDI0Xw6AK^!|RtyL6Q?c;}Yg zlh@q)|0rL+$*ko|D4)0rm9l7OCaBY z9WCeUa&jYEJ^7RKMOv`L8YB0s#f!47q-Dd1|jFtzQ1+d@~V z#ULY4;8aLoDx@h;d0@DsLEi5SI|+NEZi+|mO~~Y%@K3&l%&d+5nK;)Tj(xk5mxN%S%`4BN#)5Uugh1+@+68(+z^5{BXw? zyt|{Ng%gaQK4du3YYh3JibgnaqZn+KbRfY%2i2@(CJz`UjZXOp46!}1jSokAa@oE# z$@pUOXh4QcN20Mz6?>pqZU{Q)j(GRLZM^{mgU%eU^Yi?BSejv5fHstHWc!E!H$Ni0 z6Cm4d!4A~Ik1ftgBf!N&kw*j3zd(>51laznd!DXj)yl;(an-{^u!o z{3=##HSM!t1&Ogd@tMDBXLp>bl0As=Kt&7LwOSjq`tR{W_u;kj^`j8VAmzH(#wxQ< zVFWFDrRZH8gLW<7Vh(d!v&N8COCLKLyE})wYBN?`&&9JjJDy3}se@2Di5SP60Jt6v zSLBDF41$HR(@}jlF?_L!01IT_MV{{=S%d9MxTKSXsQRt!9D)f8?H`dNd=%nZSb)g( z^7HQAz0TqW>kA;nw|*_<)U0nq!Wj^&DMy;;tj)G;1Kb-@&Sj9`TjDEIj&ISX-1B_m z@h;q8gKLSWG3B^3H@pkSAwMxTRzZ~uPZ)riEBG5@i2@H;%R+k2u&C8&Vu7&weV8P_ zM1qw&na2s^>jFzMO(r84hmXNc6Hi2n2@y2UiyNAUW~v6Wph?hS zC@Asy^`JzvtN|0u6DSG*Pl0T)#Sw_fel>6l7Wx`!3QkQ3EZ;7G0_h8_w=nNsJPV+E z*C6S>!`_((P@=Ncz6?;jH#=FM=%G|tn>{-RvkYS8rxV_VO8~Lem7+WK0A9KPVofIX z_vnZz2v5{jpk~aU!w_UjuYxRm?V4gb&jE&mWfz&ke}l;t037tEiU~tkzXSd&DguK zwT61yLgqA0_D1p(-n;xqY6F)VyVzmZ=FEA5d8m74;Q4HvrHF1W2Y zkoA*2I2x}8brB-G3a<6RfY%m+?n?)@X}P1N1si82D?b`61QWK?(3+a}4ZWztVuB!n_C z`#2n7xS?gI#UdQOjMKu;5z%tgE$FG%b4Ew`1^Wo_D=dpGbmj;b<)eJW|3#H(EcQy* G+y4Qq%HN>? literal 0 HcmV?d00001 diff --git a/src/accounts/accounts/__pycache__/models.cpython-310.pyc b/src/accounts/accounts/__pycache__/models.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9420b88f35a5a3da2fdfd04838d2408daace5977 GIT binary patch literal 2515 zcmZuz-A~(A6t^8GaY8}^rPDsN4fSDD9+tjN)iiZ$wY_XrwY4u6l5uhkggP<1w%3w^ z6h6YoMkv}s8PFD()|P%HW2GfP+dS;Q@YAHmcJi{m?_t`xc1Y8%7y0C#d(Ztizw
  • pUAqMi|@wXvX_fKTlKfha`FREp+ln)Df&NqKmW)Az`1%FBD5J}diD ze%|l&y)u^y@Ij~VliN}uK15SrQpk_ENeXciJ>lWQDDW*64c6fVDoPun&*G|v#e{ZF zl`!bOl!7E#g`OWv-%D7PpzBF17l>`Yq-oMcO_0URFz6A`g@lf=l-2|_sYwa4X>0R> zasobe9E3#iG@^J0(Y!}u2fdOHF%vZMAQpM^9^MaQ9E|xUXeogFd4>-*M!4okTVq|I zWnBmbQQIU1j)hPde%nzyIt0Im!0`y`0LMEf7(RkJ!R^Ba_qnX}6JPbe;7Y-6Xtn7( zW^tvuJ8Ks}>_B_xaEw7kAY#bQQ)wARET;9yDn8jc12mAK^8rZl;*~JZeVG34RTjOw1t5CA$#>~|x z_2ry7cLi3#U7d3>fpju)zB?_Zq&fnB&qf&|m=Z4vvXt)E1`Kap7Src5;RKdMEvclt zwPcC}+>fL_QJ1w$L>5)88#s~d8+Ilewo1GrvaU&jj^$qK%C7Zb5nQd!@0gdb zG*AFI_OF?x-P%scd^&mVo3EPNnz;wHvBE3Z9j!jOTFd5YrJL47uD4NU^`&C-glhRtb#Jd)-iHOv?K7QmV9{|=*9H_UAW0LGGT+uv3D^Lk z@B9T3_p4C`6G#kB2hNeH+qA)|lH7Nj!W}U2Pwdq3o-@O+QThy42Bn17@DH6^M?xFmGP|-DSS{)10+*!>laTp8jl3 z?^#Qa;0SZ(u6ug(EQo?vNB zHTnG>C>k{Z+#aP7J;;pEBh6F5Ba~=GvMAm0Lte{IBwX3k7i_Zk?uge6;D-LsZUWS^Vk z0C08xQ9Zk2uibzOW#7A3EziPz+=U0ViENwc8ql;M_4u)4mq=Y7y)-0h1EWwpSfUy% zQKKyOgL{S#DS&a>@bxJ;C2IIE3`RXT0zdc-V&H)ceieN3ln8ygER*z(ft5-K$ay;% zmd+e$MxoUz0R}HM60;NVsYgM)cwG9O=Y>2iyjSw_J_Jt-Vj&#fOt`u6*O0Rr&qqmQ zup}`JCW%f*IfD}flB0rP1O)+dPM1kPBnaQ>qTHC_6pSRCO!pf!Ou-WwS5(PaW*lzS zgdthSqHKhT6SAmDswTvTTYwS}%}VZ#V^>xC#Z{ z9r3x9-H9VU0!#2aM3Aez?(&$F9+F>b7YL4G=`M(3C)34n3>W15ok7RqbhO=g8#!-j zETN<|oQ%gv!Hl_;4U=FRfrbH!yPmZ*bl`~UmeC^zBU(~cW5k1ul9sUJ*_$uVVKJ=& w?C`2#!Yv0IBjVb`p}59$vWLTA=Z3Fy&fq?AS@0<-lxjPO5EY^i{<;GH0lDj-0ssI2 literal 0 HcmV?d00001 diff --git a/src/accounts/accounts/__pycache__/urls.cpython-310.pyc b/src/accounts/accounts/__pycache__/urls.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06f8bc051e5d5a8f8d7022ccc325e55177c22173 GIT binary patch literal 835 zcmZ8fO^?$s5RFqOO`G(CcA@OOmrAI;uh0s?vZocQK;jTpjiT73DM?(}4%OyARN~AD zj$HXUzH;hcSP8^9NfFdiX5PFviswzjdcCdz=ga3|qR~%X{Cl+e0v_=L0x<|mjSTq+ zX>0;EeG_nt;Ix%tA6M8)t<3gqz_#C}CTUZ=b+#Sikj?>-E^(=yAmSY`Xg$!nf3-dt z99qB*fpx!jz8U0E#UL^Q#v|iKzksS&>5m)c2{8SNnP?`q{Q-IU&X`>uVQyoF6yAk$ z1G&vSoS%Xp@oFp5}_OHn%ORI`ie16n|jD~c$hLIed*l~qY< z92RmzbD2b;OjsU#fH%xch4V0@%Ko?yc`QnI&1swnN%>-`oD-fQ?!gpgN8iE<37TD_ za@>rt;8YZ;oNr{7D!VSq(Hbs^sRScJ>=`GEsXsWQ%E@_C5HUib^Wv~eMblq>COzPne>^Nf)qrPulg5Z4eq!$Y3wA#7sHLng8j{#7IV8)F9;opF;- Ib)B8#AG>w=Q~&?~ literal 0 HcmV?d00001 diff --git a/src/accounts/accounts/__pycache__/user_login_backend.cpython-310.pyc b/src/accounts/accounts/__pycache__/user_login_backend.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..92350098cd9b6200793120664d6b7fa53c5549d4 GIT binary patch literal 1102 zcmaJ=OKTKC5bo}I>|~h)$-|2tf_QL1_6G!!5EDFzYamE)SR5wP?Y?GblI~gKhRs0} zA(w~;6}%~U&`ZFRf&~9ZUp0w22%d9^RkPW+NJ!0gZS||Zs;{ck;o%|yY5e|En=uIa zfs;WoVKNOvT?fSpr!i?!pHj40%vy$TP;#4agPWU#o5I*Ie3M%X#Iq0K2JxsKsfsLU zDODzaRxw+zBJbg z>+wB#A3&0@B?jHMOW>tA39|S6>93uyKX<<$y!r^g{jD#1FSqx%-W)#LIoSDp@b=r@ z?(6(uIl`>_VuRtpxC28y1jT8WJtlI9vo5RCbu%%!(X-YjGO|G$w86MpCtdRceL>f) zuJz978t{^Rtfh+=d3&bS(S+i531iuIQ>MHT9@)4cZmcW#>gS=r%pI}2WK-YzI zsUf0FX#;rGBT-F6&0slHYBiNyyR)fKa>&cI}+Xl?EL}n0sH)#$a#3$QyHQ zs84*Ca=J~C<`aUA&cuHNvof?GTW)%0PN*CLFA@WlE+~)5VYHE{e~idcm};lr$(WJC zD~yiP3y=_`a`n7a`WHP`E<;tEAi$yqfi4F4D%vsHZV;@r!}w&U5ClApf7*UeYUo~6flUbj+|CYh`+<&^f?=<*pmrIRu$iEl7>au%j` SZuDZlNf8(yh)Y~LZu|vC`5B=A literal 0 HcmV?d00001 diff --git a/src/accounts/accounts/__pycache__/utils.cpython-310.pyc b/src/accounts/accounts/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72874a878eff0e80342c106afba1799b30a71c55 GIT binary patch literal 3582 zcma)9?N1v=8sA+TV~k0FrfE)!?pBdbs-+aEJyn%Zb!vOLuUvJNPC6?RalI38vbNFg zx-Aiv%!^-04K|4h;U$6epryHDLmLr;4g4Se8qdCb%J+V__L-UW+9asDm3BR|JM+vl z^Zb6l=W)EJ$H&3-V4*h>>E*Z=q|<(NLgyp6)qQwKoW#pqko*cV9~FWEPueco6?F&Q zwC$EVqMo3KwmYER8T8V&NA8OHf=o6?M2+JbIAV(4!-qC{WI?1`R2EqZs`r_0=(&^Z(1YW(wovnc;6@SpK*b= zEbyHh3h?zVEfPf%lC=MpwD1t`k2+}n4l?F|}-5i!fcgCUbQ&CJBM$u42wmQHF z86Azp5*kwP`2U5J$jx!_@8oN}Q;QGNhC3XWP`y)4e0~dswXln*n;)%i0PqEEuaLeIo+{jMAPxz3^W)3yc^mGtIfP zbOqMK=@c#<=*6<0o5ZU})^ZBx9=xpOoO$}%tiG7l*VY@Ad7RqDdr4fWSeg8dVY2sO zO_OglP9DMRMkQ%xo`GlY<80cxxBjv=>!S;Otc<9Gv_&>%AKjp+&u{6)edYx?cgp*< zUHm8srtr>^@#A-m%1_2#6+ixl__daVH@J{x`_wmTxRxhN5!>V7?XlQ^5v=e}(&n9u+<|Bffbkb*=7sQ}zaY(&&X zkvJAo)i2|Ubd~nLAlQtALs}#rqx%;lDnR1TZ<3k(l_tXoLEy zI3}Zzip1LpjfoLW98=i$tJDgyvw8j5K@ zaLPG&`O7=k2CsiQu8qcHpDJ;}hwImFg<>P|PvrQ>^-wq*PsB9!dP0lH>cH4|y^qq3 z`aaHB*8^5mKRkqPffs)D2=4#&yA@C>uHHduS@$SNODM50u*A6t=}4av?J_hpZi1WU zzZQU6{B4)w*LXs_&p^k3Vy?szOkf0HgrZMI5F6{s?;)hG@Jru#0w4mU=-KDy@qL`y zBH(Jy1#TF%`*`PJqp|}}Jhcs9^@n9#0Pr$eoJj_{t3eSTr)*P{q=dgs27qN}6{EU< zODkX*7t3H4$ZnR?fFk2b2`K0cv+iYa`9LSTc%2q!`w#mCVx74Q$E;p1f>~mdp58xc zBwD;?33Ig<`CDWK-a)KGnum`RDjvj=4bA%;fRzLc3o$z0^1YmscHY?+u8pqo!bEzQeYx3wz^e z4Mc^3?KT<*<7BP;0|#}E5%>fFWmp9%GFG1v#kGT&#hc&b-8@b&>x+4PhKO~$Gk+Nn z&2P3GNb5@*W_gP#E?@v37TduOhlanK0uLccyhu*!YfoABp*C|kyUgU!KGG&-+R^bM zIYrOm&RbL?4sPsz=IsXw^MTVEzPc z)e8@AzgO_`?q8J)&^e_eL_V26iw<2AyMO-DS9`}j^uTY z;)8t_5S-n0PLV+WbkZj2fo2^W{rN)UcokBJJ_i6L8M(<|W=vn&H#1N1-EFgU3Ghk+ch5u>9PfnQigOwQ@4R!kRq6sW}HI`fjChkYd_*R>rnP${Fv-kPQe&s@kCG zh!n#pQz+mDpoctDKbw=q=H^;i8#JheWn1W;SAxSxwjc<2qX(MC`W*2K|L)G${P@zYjD$0`nqX9&|mjx<6}^3)x8p6 ziu{jINT}<55-n&3!f^!+P|aZrT(aM@+D21CF;%A37?mo8sER`BntCrA_&HQW19UL0 zuat}An@iOcR_u8HF-A(ckW592!izfA2F5l-ei)5Q2^syBfKDZryqur+3vk2JC;ZWM LxxfF?<(}UFL8`Mu literal 0 HcmV?d00001 diff --git a/src/accounts/accounts/__pycache__/views.cpython-310.pyc b/src/accounts/accounts/__pycache__/views.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d2002e2b54fc2960ed8b1a89fe3840b34682c3e5 GIT binary patch literal 10211 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 a;{OL)9bNbU literal 0 HcmV?d00001 diff --git a/src/accounts/accounts/admin.py b/src/accounts/accounts/admin.py new file mode 100644 index 00000000..1a6c0345 --- /dev/null +++ b/src/accounts/accounts/admin.py @@ -0,0 +1,101 @@ +from django import forms +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.forms import UserChangeForm +from django.contrib.auth.forms import UsernameField +from django.utils.translation import gettext_lazy as _ + +# 模块级注释——accounts应用的Admin后台表单配置文件, +# 自定义BlogUser(自定义用户模型)的创建表单、修改表单,以及Admin后台管理配置, +# 适配Django Admin的用户管理逻辑,支持自定义字段(如nickname、source)的后台操作 +from .models import BlogUser + + +class BlogUserCreationForm(forms.ModelForm): + """ + 自定义用户创建表单(用于Django Admin后台添加新用户) + 扩展默认表单,增加密码二次验证逻辑,适配BlogUser模型的字段要求 + """ + # 密码输入字段:label支持国际化,使用密码输入控件(隐藏输入内容) + password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) + # 密码二次确认字段:用于验证两次输入密码一致 + password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) + + class Meta: + model = BlogUser # 关联的模型:accounts应用的BlogUser(自定义用户模型) + fields = ('email',) # 后台创建用户时,必填的核心字段(仅邮箱,用户名可后续补充或自动生成) + + def clean_password2(self): + """ + 密码二次验证的清洁方法(Django表单验证机制) + 检查两次输入的密码是否一致,不一致则抛出验证错误 + """ + # 获取第一次和第二次输入的密码(已通过表单基础验证的清洁数据) + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") + # 若两次密码均存在且不一致,抛出验证错误(错误信息支持国际化) + if password1 and password2 and password1 != password2: + raise forms.ValidationError(_("passwords do not match")) + # 验证通过,返回第二次输入的密码(用于后续保存) + return password2 + + def save(self, commit=True): + """ + 重写保存方法,实现密码哈希存储和创建来源标记 + Django默认会对密码进行哈希处理,此处明确调用set_password确保安全性 + """ + # 调用父类save方法,先不提交到数据库(commit=False) + user = super().save(commit=False) + # 对密码进行哈希处理后存储(避免明文存储,符合Django安全规范) + user.set_password(self.cleaned_data["password1"]) + # 若需要提交到数据库(默认commit=True) + if commit: + user.source = 'adminsite' # 标记用户创建来源:Django Admin后台 + user.save() # 最终保存用户数据到数据库 + return user + + +class BlogUserChangeForm(UserChangeForm): + """ + 自定义用户修改表单(用于Django Admin后台编辑用户信息) + 继承Django内置UserChangeForm,适配BlogUser模型的所有字段 + """ + class Meta: + model = BlogUser # 关联的模型:accounts应用的BlogUser + fields = '__all__' # 后台可修改的字段:所有模型字段(支持自定义扩展字段) + field_classes = {'username': UsernameField} # 用户名字段的类:使用Django内置UsernameField(确保符合用户名验证规则) + + def __init__(self, *args, **kwargs): + """ + 初始化表单,调用父类构造方法保持默认逻辑 + 若后续需要扩展表单初始化行为(如隐藏字段、设置默认值),可在此方法中添加 + """ + super().__init__(*args, **kwargs) + + +class BlogUserAdmin(UserAdmin): + """ + 自定义UserAdmin配置(用于Django Admin后台管理BlogUser模型) + 配置后台显示字段、排序规则、搜索字段等,优化用户管理体验 + """ + form = BlogUserChangeForm # 关联用户修改表单:使用自定义的BlogUserChangeForm + add_form = BlogUserCreationForm # 关联用户创建表单:使用自定义的BlogUserCreationForm + + # 后台列表页显示的字段(按业务优先级排序) + list_display = ( + 'id', # 用户ID(唯一标识) + 'nickname', # 用户昵称(自定义扩展字段) + 'username', # 用户名(Django用户模型核心字段) + 'email', # 邮箱(用于登录和通知,核心字段) + 'last_login', # 最后登录时间(安全审计字段) + 'date_joined', # 注册时间(业务统计字段) + 'source' # 创建来源(区分后台创建/前台注册等,自定义扩展字段) + ) + + # 后台列表页可点击跳转的字段(用于快速进入编辑页) + list_display_links = ('id', 'username') + + # 后台列表页默认排序规则:按ID倒序(新创建的用户排在前面) + ordering = ('-id',) + + # 后台搜索支持的字段(支持模糊查询,提升管理效率) + search_fields = ('username', 'nickname', 'email') \ No newline at end of file diff --git a/src/accounts/accounts/apps.py b/src/accounts/accounts/apps.py new file mode 100644 index 00000000..92080f74 --- /dev/null +++ b/src/accounts/accounts/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig + +# 模块级注释——accounts应用的核心配置类文件, +# 继承Django内置的AppConfig,用于定义应用的基本元信息, +# 是Django识别和加载accounts应用的关键配置 +class AccountsConfig(AppConfig): + """ + accounts应用的配置类,用于注册应用的核心信息 + 遵循Django应用配置规范,定义应用的唯一标识名称 + """ + name = 'accounts' # 应用的唯一标识名称,与应用目录名一致, + # Django通过该名称识别并加载应用,关联应用内的模型、视图等组件 \ No newline at end of file diff --git a/src/accounts/accounts/forms.py b/src/accounts/accounts/forms.py new file mode 100644 index 00000000..62f5a88f --- /dev/null +++ b/src/accounts/accounts/forms.py @@ -0,0 +1,166 @@ +from django import forms +from django.contrib.auth import get_user_model, password_validation +from django.contrib.auth.forms import AuthenticationForm, UserCreationForm +from django.core.exceptions import ValidationError +from django.forms import widgets +from django.utils.translation import gettext_lazy as _ +from . import utils # 导入自定义工具模块(用于验证码验证等功能) +from .models import BlogUser # 导入自定义用户模型 + +# 模块级注释——accounts应用的前台用户交互表单配置文件, +# 包含登录、注册、忘记密码、验证码获取等核心业务表单, +# 负责用户输入数据的验证、前端样式适配(如表单控件class、占位符), +# 确保用户输入合法且符合业务规则(如邮箱唯一性、密码强度、验证码有效性) + + +class LoginForm(AuthenticationForm): + """ + 前台用户登录表单,继承Django内置AuthenticationForm + 重写表单控件样式和占位符,适配前端页面布局,提升用户体验 + """ + def __init__(self, *args, **kwargs): + """ + 初始化登录表单,重写用户名和密码字段的控件配置 + """ + super(LoginForm, self).__init__(*args, **kwargs) + # 用户名输入框:设置占位符和Bootstrap表单样式类,适配前端页面 + self.fields['username'].widget = widgets.TextInput( + attrs={'placeholder': "username", "class": "form-control"}) + # 密码输入框:设置占位符和Bootstrap表单样式类,使用密码隐藏控件 + self.fields['password'].widget = widgets.PasswordInput( + attrs={'placeholder': "password", "class": "form-control"}) + + +class RegisterForm(UserCreationForm): + """ + 前台用户注册表单,继承Django内置UserCreationForm + 扩展邮箱字段验证,重写表单控件样式,确保注册数据合法(用户名、邮箱唯一) + """ + def __init__(self, *args, **kwargs): + """ + 初始化注册表单,重写用户名、邮箱、密码字段的控件配置 + """ + super(RegisterForm, self).__init__(*args, **kwargs) + + # 用户名输入框:占位符+Bootstrap样式 + self.fields['username'].widget = widgets.TextInput( + attrs={'placeholder': "username", "class": "form-control"}) + # 邮箱输入框:使用EmailInput控件,占位符+Bootstrap样式 + self.fields['email'].widget = widgets.EmailInput( + attrs={'placeholder': "email", "class": "form-control"}) + # 密码输入框1:密码隐藏控件,占位符+Bootstrap样式 + self.fields['password1'].widget = widgets.PasswordInput( + attrs={'placeholder': "password", "class": "form-control"}) + # 密码确认框:密码隐藏控件,占位符+Bootstrap样式 + self.fields['password2'].widget = widgets.PasswordInput( + attrs={'placeholder': "repeat password", "class": "form-control"}) + + def clean_email(self): + """ + 邮箱字段清洁验证:检查邮箱是否已被注册 + 若已存在则抛出验证错误,确保邮箱唯一性 + """ + email = self.cleaned_data['email'] # 获取经过基础验证的邮箱数据 + # 查询数据库,判断该邮箱是否已关联用户 + if get_user_model().objects.filter(email=email).exists(): + raise ValidationError(_("email already exists")) # 抛出国际化的验证错误 + return email # 验证通过,返回邮箱数据 + + class Meta: + model = get_user_model() # 关联Django当前激活的用户模型(此处为BlogUser) + fields = ("username", "email") # 注册表单需填写的核心字段:用户名、邮箱(密码字段由父类提供) + + +class ForgetPasswordForm(forms.Form): + """ + 前台用户忘记密码重置表单 + 包含新密码、密码确认、邮箱、验证码字段,实现密码重置的全流程验证 + """ + new_password1 = forms.CharField( + label=_("New password"), # 字段标签(支持国际化) + widget=forms.PasswordInput( + attrs={ + "class": "form-control", # Bootstrap表单样式类 + 'placeholder': _("New password") # 占位符(支持国际化) + } + ), + ) + + new_password2 = forms.CharField( + label="确认密码", # 密码确认字段标签 + widget=forms.PasswordInput( + attrs={ + "class": "form-control", + 'placeholder': _("Confirm password") # 占位符(支持国际化) + } + ), + ) + + email = forms.EmailField( + label='邮箱', # 邮箱字段标签 + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + 'placeholder': _("Email") # 占位符(支持国际化) + } + ), + ) + + code = forms.CharField( + label=_('Code'), # 验证码字段标签(支持国际化) + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + 'placeholder': _("Code") # 占位符(支持国际化) + } + ), + ) + + def clean_new_password2(self): + """ + 密码确认字段清洁验证: + 1. 检查两次输入的新密码是否一致 + 2. 验证密码是否符合Django密码强度规则(如长度、复杂度) + """ + password1 = self.data.get("new_password1") # 获取第一次输入的新密码 + password2 = self.data.get("new_password2") # 获取第二次输入的确认密码 + # 验证两次密码是否一致 + if password1 and password2 and password1 != password2: + raise ValidationError(_("passwords do not match")) # 抛出密码不匹配错误 + # 调用Django内置密码验证器,检查密码强度 + password_validation.validate_password(password2) + return password2 # 验证通过,返回确认密码 + + def clean_email(self): + """ + 邮箱字段清洁验证:检查输入的邮箱是否已注册 + 若未注册则抛出错误,确保只有已注册用户能重置密码 + """ + user_email = self.cleaned_data.get("email") # 获取经过基础验证的邮箱 + # 查询数据库,判断邮箱是否关联BlogUser + if not BlogUser.objects.filter(email=user_email).exists(): + # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改(原代码注释保留,提示后续优化隐私保护) + raise ValidationError(_("email does not exist")) # 抛出邮箱未注册错误 + return user_email # 验证通过,返回邮箱 + + def clean_code(self): + """ + 验证码字段清洁验证:调用utils模块的verify方法验证验证码有效性 + 若验证码无效(如过期、不匹配)则抛出错误 + """ + code = self.cleaned_data.get("code") # 获取用户输入的验证码 + # 调用工具函数验证验证码(传入邮箱和验证码,返回错误信息或None) + error = utils.verify(email=self.cleaned_data.get("email"), code=code) + if error: # 若存在错误信息,抛出验证错误 + raise ValidationError(error) + return code # 验证通过,返回验证码 + + +class ForgetPasswordCodeForm(forms.Form): + """ + 前台用户获取忘记密码验证码的表单 + 仅包含邮箱字段,用于接收用户邮箱并发送验证码 + """ + email = forms.EmailField( + label=_('Email'), # 字段标签(支持国际化) + ) \ No newline at end of file diff --git a/src/accounts/accounts/migrations/0001_initial.py b/src/accounts/accounts/migrations/0001_initial.py new file mode 100644 index 00000000..23ba59f7 --- /dev/null +++ b/src/accounts/accounts/migrations/0001_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 4.1.7 on 2023-03-02 07:14 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + +#模块级注释——comments应用的初始数据库迁移文件,用于创建`Comment`模型,实现文章评论功能,支持评论层级、文章关联、用户关联等业务逻辑 +class Migration(migrations.Migration): + + initial = True # xxx: 标记该迁移为应用的初始迁移 + + dependencies = [ + ('blog', '0001_initial'), # xxx: 依赖`blog`应用的`0001_initial`迁移,确保`Article`模型已存在 + migrations.swappable_dependency(settings.AUTH_USER_MODEL), # xxx: 依赖Django可交换的用户模型,支持自定义用户模型场景 + ] + + operations = [ + migrations.CreateModel( + name='Comment', # xxx: 定义`Comment`模型,用于存储文章评论数据 + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # 主键字段,自增大整数类型 + ('body', models.TextField(max_length=300, verbose_name='正文')), # xxx: 评论正文字段,文本类型,最大长度300 + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # 评论创建时间字段,默认当前时间 + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # xxx: 评论修改时间字段,默认当前时间 + ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), # xxx: 控制评论是否显示的布尔字段,默认显示 + ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')), # xxx: 外键关联`blog`应用的`Article`模型,文章删除时评论级联删除 + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), # xxx: 外键关联用户模型,用户删除时评论级联删除 + ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')), # xxx: 自外键关联,支持评论层级结构,允许空值,上级评论删除时当前评论级联删除 + ], + options={ + 'verbose_name': '评论', # xxx: 模型单数显示名称 + 'verbose_name_plural': '评论', # xxx: 模型复数显示名称 + 'ordering': ['-id'], # xxx: 数据查询时按ID倒序排列 + 'get_latest_by': 'id', # xxx: 按ID字段获取最新记录 + }, + ), + ] \ No newline at end of file diff --git a/src/accounts/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/src/accounts/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py new file mode 100644 index 00000000..0c8bccc9 --- /dev/null +++ b/src/accounts/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py @@ -0,0 +1,55 @@ +# Generated by Django 4.2.5 on 2023-09-06 13:13 + +from django.db import migrations, models +import django.utils.timezone + +# 模块级注释——accounts应用的模型更新迁移文件,用于调整`BlogUser`模型的选项、字段名称及属性, +# 优化字段命名规范(如时间字段命名统一)、完善字段配置(如允许空值),确保模型设计更规范 +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), # 依赖`accounts`应用的初始迁移`0001_initial`,确保`BlogUser`模型已创建 + ] + + operations = [ + migrations.AlterModelOptions( + name='bloguser', # 目标模型:`accounts`应用的`BlogUser`(自定义用户模型) + options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'}, + # 调整模型选项: + # 1. get_latest_by: 按`id`字段获取最新记录 + # 2. ordering: 查询时按`id`倒序排列(新用户在前) + # 3. verbose_name/verbose_name_plural: 模型单复数显示名称均为"user" + ), + migrations.RemoveField( + model_name='bloguser', + name='created_time', # 删除原有的"创建时间"字段(字段名称规范调整,后续用`creation_time`替代) + ), + migrations.RemoveField( + model_name='bloguser', + name='last_mod_time', # 删除原有的"修改时间"字段(字段名称规范调整,后续用`last_modify_time`替代) + ), + migrations.AddField( + model_name='bloguser', + name='creation_time', # 新增标准化的"创建时间"字段(替代原`created_time`) + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), + # 字段配置:默认值为当前时间,后台显示名称为"creation time" + ), + migrations.AddField( + model_name='bloguser', + name='last_modify_time', # 新增标准化的"修改时间"字段(替代原`last_mod_time`) + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), + # 字段配置:默认值为当前时间,后台显示名称为"last modify time" + ), + migrations.AlterField( + model_name='bloguser', + name='nickname', # 调整`nickname`(昵称)字段属性 + field=models.CharField(blank=True, max_length=100, verbose_name='nick name'), + # 调整内容:允许空值(blank=True),最大长度100,后台显示名称为"nick name" + ), + migrations.AlterField( + model_name='bloguser', + name='source', # 调整`source`(创建来源)字段属性 + field=models.CharField(blank=True, max_length=100, verbose_name='create source'), + # 调整内容:允许空值(blank=True),最大长度100,后台显示名称为"create source" + ), + ] \ No newline at end of file diff --git a/src/accounts/accounts/migrations/__init__.py b/src/accounts/accounts/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/accounts/accounts/migrations/__pycache__/0001_initial.cpython-310.pyc b/src/accounts/accounts/migrations/__pycache__/0001_initial.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f3ec25bac25fb7e26a5b8db07457769a1fcc05e GIT binary patch literal 1484 zcmY*Z&2Jk;6yKR$uh;fEahk+wODP{WOVkP{5JJ!-P5?nlMbgX4(rWD)Z+APMF|%u0 z`-DVM5t1bisI5c~MIaDDv=@XR74Uz|g#(F`UJ(a`5b*XRO**3)&HJ49e(%kQYPGDv z_>IDYGvKWzmIhqIZ1E0dXEy z^apHIa}d!SOpv1!OpUt*YKFznk&hjV8exg(#GvJ{LJ-UXF-dV^^f0l2XOj{slgb1G zZ3A7UGvRDl3+G6c%%t4e$GEkeI|tko;O#_C&jURVwhLi7Tny`Efh>~xL2o9kV=5o*-u?g&y`T@+8c&J_78Xx?_;1C%TWECGrd)_pB;y9>TN@pcx$| zd}S{$FF)@FJct92sp6XG_bHDR4#~6?df-4OmE&u3*}vN8RdR_SZE>!qPfA`mW(@%fO^i?T{T*|!;%Y035 z-{1S>tLg1u_P^XwMNh^-m(j7EB;L%aEWVpJ_P@Q`&{aur7rKYWRN+`;cbbs@Wg%0= z-JkCqy#Jw^8F&&xaR6vz6xfHaKk>iOfKx&tNyO*}s-*GqJZmqwi>_-{_jZ|eV?Ul{s9m?eWD|g*Z$s%b(j=kBeW41&IAD>olBO67PL#8gUH2)U9(c}vdNxJ?0YU`!;#Ss*hUz8%G-MHOK?_GtyXnWnR81gu3 zAN?uq<8P!n7|8;-D#41Huvwacv0g^TzlMp?0#Z?xu5gVpuw;7@M(wu#OG{`oiTnBkjX_!gLv>D!DX$f)*+OmOgPd?C#y6Oo zv{*aYNIHoJ9q<+wY%Ua=Lbv`yyKCqsqCG^nU>kPe2J9~EdYwY{u92HdBx?FyxP=+t zmaXGus+hMMgke^uMJ_x0`}+@RoW?~QaOsYD7L_>@(u!ejKZrpR--{@R4$sM#Z__OE&VX6d|jR#QK$cGU!Q{ z>U=1bZK^UUIYYZc>H9L^idA{QrYr@PhB3>9r84gw@`8zDrN`@9t&KNql3lVFIurL0 zKwH9A)4e0q@g1sIp$$laG|Hq?7BSDItMr*?DU)`ZeUNRno>1=dg;e zR{#xCK#2q!PG>?rY#0Chl{PG!{3VWMZkpr|aV^tQBUvA}P4b&GRAgo0>^i0__q%2t yG-RvNX9#1hcBSEN9n_i)my(IQxW8VvlBBZkBOu);)tx>y)mag`kf~>b6GC=fW5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!Hui&acOWl2VU zUO-WPR%&udj7wHxUV6S$PJVh!VsdhRXn&Z literal 0 HcmV?d00001 diff --git a/src/accounts/accounts/models.py b/src/accounts/accounts/models.py new file mode 100644 index 00000000..d194f1b3 --- /dev/null +++ b/src/accounts/accounts/models.py @@ -0,0 +1,60 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models +from django.urls import reverse +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ +from djangoblog.utils import get_current_site # 导入项目公共工具函数(获取当前站点域名) + +# 模块级注释——accounts应用的核心数据模型文件, +# 定义自定义用户模型`BlogUser`,继承Django内置`AbstractUser`, +# 扩展业务所需的自定义字段(如昵称、创建时间、创建来源), +# 并重写核心方法以适配项目业务逻辑(如用户URL生成、字符串表示) +# Create your models here. + + +class BlogUser(AbstractUser): + """ + 自定义用户模型,继承Django内置`AbstractUser`(保留用户名、密码、邮箱等核心字段) + 扩展项目所需的业务字段,适配博客系统的用户管理需求,支持国际化配置 + """ + # 昵称字段:支持国际化标签,最大长度100,允许空值(用户可选择不设置昵称) + nickname = models.CharField(_('nick name'), max_length=100, blank=True) + # 创建时间字段:支持国际化标签,默认值为当前时间,记录用户注册时间 + creation_time = models.DateTimeField(_('creation time'), default=now) + # 最后修改时间字段:支持国际化标签,默认值为当前时间,记录用户信息最后更新时间 + last_modify_time = models.DateTimeField(_('last modify time'), default=now) + # 创建来源字段:支持国际化标签,最大长度100,允许空值,用于标记用户注册渠道(如"adminsite"/"frontend"/"oauth") + source = models.CharField(_('create source'), max_length=100, blank=True) + + def get_absolute_url(self): + """ + 重写Django模型的`get_absolute_url`方法,获取用户的绝对路径URL + 关联博客系统的"作者详情页"路由,用于直接访问用户的个人主页 + """ + return reverse( + 'blog:author_detail', # 路由名称(对应blog应用的作者详情页路由) + kwargs={'author_name': self.username} # 路由参数:用户名(作为作者标识) + ) + + def __str__(self): + """ + 重写模型的字符串表示方法,返回用户邮箱作为标识 + 相比默认的用户名,邮箱更具唯一性,便于后台管理和日志输出时识别用户 + """ + return self.email + + def get_full_url(self): + """ + 扩展方法:获取用户个人主页的完整URL(包含站点域名) + 用于需要分享用户主页的场景(如邮件通知、第三方分享) + """ + site = get_current_site().domain # 通过公共工具函数获取当前站点的域名(如"example.com") + # 拼接域名和用户绝对路径,生成完整URL(支持HTTPS协议) + url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) + return url + + class Meta: + ordering = ['-id'] # 数据查询时默认按ID倒序排列(新注册用户优先展示) + verbose_name = _('user') # 模型单数显示名称(支持国际化) + verbose_name_plural = verbose_name # 模型复数显示名称(与单数一致,简化管理) + get_latest_by = 'id' # 按ID字段获取最新创建的用户记录 \ No newline at end of file diff --git a/src/accounts/accounts/templatetags/__init__.py b/src/accounts/accounts/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/accounts/accounts/templatetags/__pycache__/__init__.cpython-310.pyc b/src/accounts/accounts/templatetags/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..56ee8decaa1577777cef804470e42dcbe8345548 GIT binary patch literal 154 zcmd1j<>g`kf~>b6GC=fW5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!Hmi&acOWl2VU zUO-WPR%&udj7wHxUV6S$PJVh!VsdhRXxYDr>xaZG%CW?p7Ve7s&k X typing.Optional[str]: + """ + 验证验证码的有效性(核心功能:校验用户输入的验证码与缓存中存储的是否一致) + + Args: + email: 待验证的邮箱地址(与验证码绑定的唯一标识,确保验证码针对性) + code: 用户输入的验证码(需与缓存中存储的验证码比对) + + Return: + 验证失败返回错误提示字符串(支持国际化),验证成功返回None + + Note: + 1. 原代码注释保留:当前错误处理逻辑不合理,建议改用raise抛出异常(而非返回错误字符串), + 便于调用方统一捕获和处理,减少错误处理冗余; + 2. 验证码校验逻辑:通过邮箱作为缓存key,获取存储的验证码,与输入值直接比对(大小写敏感); + 3. 若缓存中无该邮箱对应的验证码(如过期、未发送),则默认返回验证码错误提示。 + """ + # 从缓存中获取该邮箱对应的验证码(缓存key为邮箱地址,value为之前存储的验证码) + cache_code = get_code(email) + # 比对缓存中的验证码与用户输入的验证码,不一致则返回错误提示 + if cache_code != code: + return gettext("Verification code error") # 国际化的验证码错误提示 + + +def set_code(email: str, code: str): + """ + 将邮箱与验证码绑定并存储到缓存中(核心功能:为后续验证提供数据支持) + 存储时自动设置过期时间(与全局`_code_ttl`一致,5分钟),避免验证码永久有效 + + Args: + email: 验证码绑定的邮箱地址(作为缓存的唯一key,确保一对一关联) + code: 需存储的随机验证码(字符串类型,建议由`generate_code`等工具函数生成) + """ + # 缓存存储:key=邮箱地址,value=验证码,timeout=有效期(秒数,由_timedelta转换) + cache.set(email, code, _code_ttl.seconds) + + +def get_code(email: str) -> typing.Optional[str]: + """ + 从缓存中获取指定邮箱对应的验证码(核心功能:为验证码校验提供数据源) + + Args: + email: 目标邮箱地址(缓存key,用于精准获取绑定的验证码) + + Return: + 缓存中存在该邮箱对应的验证码则返回字符串类型的验证码,否则返回None(如过期、未存储) + """ + # 从缓存中获取值:key为邮箱地址,不存在或过期时返回None + return cache.get(email) \ No newline at end of file diff --git a/src/accounts/accounts/views.py b/src/accounts/accounts/views.py new file mode 100644 index 00000000..c4e956af --- /dev/null +++ b/src/accounts/accounts/views.py @@ -0,0 +1,306 @@ +import logging +from django.utils.translation import gettext_lazy as _ +from django.conf import settings +from django.contrib import auth +from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth import get_user_model +from django.contrib.auth import logout +from django.contrib.auth.forms import AuthenticationForm +from django.contrib.auth.hashers import make_password +from django.http import HttpResponseRedirect, HttpResponseForbidden +from django.http.request import HttpRequest +from django.http.response import HttpResponse +from django.shortcuts import get_object_or_404 +from django.shortcuts import render +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.http import url_has_allowed_host_and_scheme +from django.views import View +from django.views.decorators.cache import never_cache +from django.views.decorators.csrf import csrf_protect +from django.views.decorators.debug import sensitive_post_parameters +from django.views.generic import FormView, RedirectView + +# 导入项目公共工具函数:邮件发送、加密、站点信息、验证码生成、缓存删除 +from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache +from . import utils # 导入accounts应用自定义工具(验证码发送/存储) +from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm # 导入用户交互表单 +from .models import BlogUser # 导入自定义用户模型 + +logger = logging.getLogger(__name__) # 初始化日志对象,用于记录视图层操作日志 + +# 模块级注释——accounts应用的核心视图文件, +# 包含用户注册、登录、退出、邮箱验证、密码重置等核心业务的视图实现, +# 基于Django类视图(FormView/RedirectView/View)和函数视图, +# 整合表单验证、邮件发送、缓存操作、权限控制等逻辑,处理用户交互的全流程 +# Create your views here. + + +class RegisterView(FormView): + """ + 用户注册视图,继承Django FormView(专门处理表单提交的类视图) + 核心功能:接收注册表单数据、验证合法性、创建未激活用户、发送邮箱验证链接、重定向到结果页 + """ + form_class = RegisterForm # 关联注册表单类(处理用户名、邮箱、密码验证) + template_name = 'account/registration_form.html' # 注册页面模板路径 + + @method_decorator(csrf_protect) + def dispatch(self, *args, **kwargs): + """ + 重写分发方法,添加CSRF防护装饰器 + 防止跨站请求伪造攻击,确保注册请求来自本网站合法表单 + """ + return super(RegisterView, self).dispatch(*args, **kwargs) + + def form_valid(self, form): + """ + 表单验证通过后的核心处理逻辑(FormView的核心方法) + 流程:创建未激活用户 → 生成邮箱验证链接 → 发送验证邮件 → 重定向到注册结果页 + """ + if form.is_valid(): + # 1. 表单验证通过,先不提交到数据库(commit=False),后续补充字段 + user = form.save(False) + user.is_active = False # 默认设置用户为未激活状态(需邮箱验证后激活) + user.source = 'Register' # 标记用户创建来源:前台注册(区别于后台创建/第三方登录) + user.save(True) # 最终保存用户数据到数据库 + + # 2. 生成邮箱验证链接(包含站点域名、用户ID、加密签名,确保链接安全性) + site = get_current_site().domain # 获取当前站点域名(如"example.com") + # 双重SHA256加密:结合SECRET_KEY和用户ID,防止链接被篡改 + sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + + # 开发环境适配:DEBUG模式下使用本地测试域名(127.0.0.1:8000) + if settings.DEBUG: + site = '127.0.0.1:8000' + path = reverse('account:result') # 验证结果页路由 + # 拼接完整的验证链接(HTTP协议,开发环境可用;生产环境建议改为HTTPS) + url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( + site=site, path=path, id=user.id, sign=sign) + + # 3. 构建验证邮件内容(HTML格式,包含验证链接) + content = """ +

    请点击下面链接验证您的邮箱

    + + {url} + + 再次感谢您! +
    + 如果上面链接无法打开,请将此链接复制至浏览器。 + {url} + """.format(url=url) + # 发送验证邮件到用户注册邮箱 + send_email( + emailto=[user.email], # 收件人(注册时填写的邮箱) + title='验证您的电子邮箱', # 邮件标题 + content=content) # 邮件HTML内容 + + # 4. 重定向到注册结果页(告知用户验证邮件已发送) + url = reverse('accounts:result') + '?type=register&id=' + str(user.id) + return HttpResponseRedirect(url) + else: + # 表单验证失败(如用户名已存在、密码不匹配),重新渲染注册页面并显示错误 + return self.render_to_response({'form': form}) + + +class LogoutView(RedirectView): + """ + 用户退出登录视图,继承Django RedirectView(专门处理重定向的类视图) + 核心功能:执行退出登录逻辑、清理缓存、重定向到登录页 + """ + url = '/login/' # 退出后重定向的目标URL(登录页) + + @method_decorator(never_cache) + def dispatch(self, request, *args, **kwargs): + """ + 重写分发方法,添加never_cache装饰器 + 禁止浏览器缓存退出页,避免用户后退到已登录状态 + """ + return super(LogoutView, self).dispatch(request, *args, **kwargs) + + def get(self, request, *args, **kwargs): + """ + 处理GET请求(退出登录请求) + 流程:执行退出登录 → 清理侧边栏缓存 → 重定向到登录页 + """ + logout(request) # Django内置logout函数:清除用户会话,销毁登录状态 + delete_sidebar_cache() # 清除侧边栏缓存(避免缓存中保留用户相关信息) + return super(LogoutView, self).get(request, *args, **kwargs) + + +class LoginView(FormView): + """ + 用户登录视图,继承Django FormView + 核心功能:接收登录表单数据、验证合法性、执行登录逻辑、处理"记住我"、重定向到目标页面 + """ + form_class = LoginForm # 关联自定义登录表单(适配前端样式) + template_name = 'account/login.html' # 登录页面模板路径 + success_url = '/' # 登录成功后的默认重定向URL(网站根目录) + redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名(默认'redirect_to',用于跳转前的目标页面) + login_ttl = 2626560 # "记住我"的会话有效期:2626560秒 ≈ 1个月 + + @method_decorator(sensitive_post_parameters('password')) + @method_decorator(csrf_protect) + @method_decorator(never_cache) + def dispatch(self, request, *args, **kwargs): + """ + 重写分发方法,添加3个核心装饰器: + 1. sensitive_post_parameters('password'):保护密码字段,避免在错误报告中泄露 + 2. csrf_protect:防跨站请求伪造攻击 + 3. never_cache:禁止浏览器缓存登录页,确保每次请求都是最新状态 + """ + return super(LoginView, self).dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + """ + 补充模板上下文数据(传递重定向地址给模板) + 用于登录成功后跳转到登录前的目标页面(如访问需要登录的页面时,先跳转登录,登录后返回原页面) + """ + # 从GET请求中获取重定向地址(如?redirect_to=/article/1/) + redirect_to = self.request.GET.get(self.redirect_field_name) + if redirect_to is None: + redirect_to = '/' # 无重定向地址时,默认跳转到根目录 + kwargs['redirect_to'] = redirect_to # 将重定向地址添加到上下文 + return super(LoginView, self).get_context_data(** kwargs) + + def form_valid(self, form): + """ + 表单验证通过后的核心登录逻辑 + 流程:验证表单 → 清理缓存 → 执行登录 → 处理"记住我" → 重定向 + """ + # 重新初始化Django内置AuthenticationForm(确保使用正确的认证逻辑) + form = AuthenticationForm(data=self.request.POST, request=self.request) + + if form.is_valid(): + delete_sidebar_cache() # 清理侧边栏缓存(更新用户相关的缓存数据) + logger.info(self.redirect_field_name) # 记录重定向字段名到日志 + + # 执行登录:将用户信息存入会话,创建登录状态 + auth.login(self.request, form.get_user()) + # 处理"记住我"选项:若勾选,则设置会话有效期为1个月 + if self.request.POST.get("remember"): + self.request.session.set_expiry(self.login_ttl) + # 调用父类form_valid方法,执行重定向 + return super(LoginView, self).form_valid(form) + else: + # 表单验证失败(如用户名/密码错误),重新渲染登录页并显示错误 + return self.render_to_response({'form': form}) + + def get_success_url(self): + """ + 自定义登录成功后的重定向URL + 核心:校验重定向地址的合法性,避免恶意重定向攻击 + """ + # 从POST请求中获取目标重定向地址(登录表单中隐藏字段传递) + redirect_to = self.request.POST.get(self.redirect_field_name) + # 校验重定向地址是否合法(仅允许本网站域名下的地址) + if not url_has_allowed_host_and_scheme( + url=redirect_to, allowed_hosts=[self.request.get_host()]): + redirect_to = self.success_url # 非法地址则使用默认重定向地址(根目录) + return redirect_to + + +def account_result(request): + """ + 账号操作结果展示函数视图 + 处理两种场景:1. 注册成功后的提示 2. 邮箱验证后的激活与提示 + 核心:根据URL参数区分场景,验证链接合法性,激活用户,渲染结果页面 + """ + # 从GET请求中获取操作类型(register/validation)和用户ID + type = request.GET.get('type') + id = request.GET.get('id') + + # 根据用户ID查询用户,不存在则返回404页面 + user = get_object_or_404(get_user_model(), id=id) + logger.info(type) # 记录操作类型到日志 + + # 若用户已激活,直接重定向到首页(避免重复验证) + if user.is_active: + return HttpResponseRedirect('/') + + # 仅处理注册和验证两种合法操作类型 + if type and type in ['register', 'validation']: + if type == 'register': + # 场景1:注册成功提示(告知用户验证邮件已发送) + content = ''' + 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。 + ''' + title = '注册成功' + else: + # 场景2:邮箱验证(校验链接签名合法性,激活用户) + # 重新计算签名(与注册时的加密规则一致) + c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) + sign = request.GET.get('sign') # 从URL中获取传递的签名 + + # 签名不匹配则返回403禁止访问(防止恶意篡改链接) + if sign != c_sign: + return HttpResponseForbidden() + + # 签名匹配,激活用户(设置is_active为True) + user.is_active = True + user.save() + # 验证成功提示 + content = ''' + 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。 + ''' + title = '验证成功' + # 渲染结果页面,传递标题和内容 + return render(request, 'account/result.html', { + 'title': title, + 'content': content + }) + else: + # 非法操作类型,重定向到首页 + return HttpResponseRedirect('/') + + +class ForgetPasswordView(FormView): + """ + 忘记密码重置视图,继承Django FormView + 核心功能:接收密码重置表单(新密码、验证码、邮箱)、验证合法性、更新用户密码、重定向到登录页 + """ + form_class = ForgetPasswordForm # 关联忘记密码表单(处理密码强度、验证码、邮箱验证) + template_name = 'account/forget_password.html' # 密码重置页面模板路径 + + def form_valid(self, form): + """ + 表单验证通过后的密码重置逻辑 + 流程:查询用户 → 哈希新密码 → 保存更新 → 重定向到登录页 + """ + if form.is_valid(): + # 根据表单验证后的邮箱查询用户 + blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() + # 哈希处理新密码(make_password自动使用Django配置的哈希算法,避免明文存储) + blog_user.password = make_password(form.cleaned_data["new_password2"]) + blog_user.save() # 保存更新后的密码 + return HttpResponseRedirect('/login/') # 密码重置成功,重定向到登录页 + else: + # 表单验证失败(如验证码错误、密码不匹配),重新渲染页面并显示错误 + return self.render_to_response({'form': form}) + + +class ForgetPasswordEmailCode(View): + """ + 忘记密码验证码发送视图,继承Django View + 核心功能:接收邮箱 → 验证邮箱合法性 → 生成验证码 → 发送验证邮件 → 存储验证码到缓存 + """ + + def post(self, request: HttpRequest): + """ + 处理POST请求(接收邮箱,发送验证码) + """ + # 初始化忘记密码验证码表单,验证请求数据 + form = ForgetPasswordCodeForm(request.POST) + if not form.is_valid(): + return HttpResponse("错误的邮箱") # 表单验证失败(如邮箱格式错误),返回错误提示 + + # 表单验证通过,获取目标邮箱 + to_email = form.cleaned_data["email"] + + # 生成随机验证码(项目工具函数) + code = generate_code() + # 发送验证码邮件(调用accounts应用自定义工具函数) + utils.send_verify_email(to_email, code) + # 存储验证码到缓存(key=邮箱,value=验证码,有效期5分钟,由utils模块的_code_ttl控制) + utils.set_code(to_email, code) + + return HttpResponse("ok") # 验证码发送成功,返回"ok"提示n \ No newline at end of file