From 7f3152ac7bb9d0af8df49039db7f34c7b7587dcd Mon Sep 17 00:00:00 2001 From: zyl <3116318851@qq.com> Date: Thu, 27 Nov 2025 23:45:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A7=9C=E9=9B=A8=E8=8F=B2=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/djangoblog/djangoblog/__init__.py | 8 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 202 bytes .../__pycache__/admin_site.cpython-310.pyc | Bin 0 -> 2283 bytes .../__pycache__/apps.cpython-310.pyc | Bin 0 -> 1005 bytes .../__pycache__/blog_signals.cpython-310.pyc | Bin 0 -> 4185 bytes .../elasticsearch_backend.cpython-310.pyc | Bin 0 -> 6455 bytes .../__pycache__/feeds.cpython-310.pyc | Bin 0 -> 2677 bytes .../__pycache__/logentryadmin.cpython-310.pyc | Bin 0 -> 3714 bytes .../__pycache__/settings.cpython-310.pyc | Bin 0 -> 8383 bytes .../__pycache__/sitemap.cpython-310.pyc | Bin 0 -> 3213 bytes .../__pycache__/spider_notify.cpython-310.pyc | Bin 0 -> 1601 bytes .../__pycache__/urls.cpython-310.pyc | Bin 0 -> 2935 bytes .../__pycache__/utils.cpython-310.pyc | Bin 0 -> 11064 bytes .../whoosh_cn_backend.cpython-310.pyc | Bin 0 -> 19406 bytes .../__pycache__/wsgi.cpython-310.pyc | Bin 0 -> 554 bytes src/djangoblog/djangoblog/admin_site.py | 89 ++ src/djangoblog/djangoblog/apps.py | 29 + src/djangoblog/djangoblog/blog_signals.py | 177 +++ .../djangoblog/elasticsearch_backend.py | 183 +++ src/djangoblog/djangoblog/feeds.py | 72 ++ src/djangoblog/djangoblog/logentryadmin.py | 136 +++ .../__pycache__/base_plugin.cpython-310.pyc | Bin 0 -> 8872 bytes .../hook_constants.cpython-310.pyc | Bin 0 -> 733 bytes .../__pycache__/hooks.cpython-310.pyc | Bin 0 -> 2497 bytes .../__pycache__/loader.cpython-310.pyc | Bin 0 -> 3771 bytes .../djangoblog/plugin_manage/base_plugin.py | 247 ++++ .../plugin_manage/hook_constants.py | 35 + .../djangoblog/plugin_manage/hooks.py | 89 ++ .../djangoblog/plugin_manage/loader.py | 124 ++ src/djangoblog/djangoblog/settings.py | 384 ++++++ src/djangoblog/djangoblog/sitemap.py | 82 ++ src/djangoblog/djangoblog/spider_notify.py | 45 + src/djangoblog/djangoblog/tests.py | 51 + src/djangoblog/djangoblog/urls.py | 89 ++ src/djangoblog/djangoblog/utils.py | 372 ++++++ .../djangoblog/whoosh_cn_backend.py | 1044 +++++++++++++++++ src/djangoblog/djangoblog/wsgi.py | 21 + 37 files changed, 3277 insertions(+) create mode 100644 src/djangoblog/djangoblog/__init__.py create mode 100644 src/djangoblog/djangoblog/__pycache__/__init__.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/__pycache__/admin_site.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/__pycache__/apps.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/__pycache__/blog_signals.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/__pycache__/elasticsearch_backend.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/__pycache__/feeds.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/__pycache__/logentryadmin.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/__pycache__/settings.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/__pycache__/sitemap.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/__pycache__/spider_notify.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/__pycache__/urls.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/__pycache__/utils.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/__pycache__/whoosh_cn_backend.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/__pycache__/wsgi.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/admin_site.py create mode 100644 src/djangoblog/djangoblog/apps.py create mode 100644 src/djangoblog/djangoblog/blog_signals.py create mode 100644 src/djangoblog/djangoblog/elasticsearch_backend.py create mode 100644 src/djangoblog/djangoblog/feeds.py create mode 100644 src/djangoblog/djangoblog/logentryadmin.py create mode 100644 src/djangoblog/djangoblog/plugin_manage/__pycache__/base_plugin.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/plugin_manage/__pycache__/hook_constants.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/plugin_manage/__pycache__/hooks.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/plugin_manage/__pycache__/loader.cpython-310.pyc create mode 100644 src/djangoblog/djangoblog/plugin_manage/base_plugin.py create mode 100644 src/djangoblog/djangoblog/plugin_manage/hook_constants.py create mode 100644 src/djangoblog/djangoblog/plugin_manage/hooks.py create mode 100644 src/djangoblog/djangoblog/plugin_manage/loader.py create mode 100644 src/djangoblog/djangoblog/settings.py create mode 100644 src/djangoblog/djangoblog/sitemap.py create mode 100644 src/djangoblog/djangoblog/spider_notify.py create mode 100644 src/djangoblog/djangoblog/tests.py create mode 100644 src/djangoblog/djangoblog/urls.py create mode 100644 src/djangoblog/djangoblog/utils.py create mode 100644 src/djangoblog/djangoblog/whoosh_cn_backend.py create mode 100644 src/djangoblog/djangoblog/wsgi.py diff --git a/src/djangoblog/djangoblog/__init__.py b/src/djangoblog/djangoblog/__init__.py new file mode 100644 index 00000000..c4944bb9 --- /dev/null +++ b/src/djangoblog/djangoblog/__init__.py @@ -0,0 +1,8 @@ +# Django应用配置指定模块 +# 该模块的主要功能是定义当前Django应用的默认配置类, +# 当Django加载应用时,会根据此配置类进行应用的初始化设置, +# 包括应用名称、信号注册、权限配置等应用级别的配置项 + +# 指定当前Django应用的默认配置类为'djangoblog.apps.DjangoblogAppConfig' +# Django在启动时会自动加载该配置类,执行其中的初始化逻辑(如ready()方法) +default_app_config = 'djangoblog.apps.DjangoblogAppConfig' \ No newline at end of file diff --git a/src/djangoblog/djangoblog/__pycache__/__init__.cpython-310.pyc b/src/djangoblog/djangoblog/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..40906a498a168002251953c7ffd4934839683e2d GIT binary patch literal 202 zcmd1j<>g`kf~(yEnLJDk439w^WWWgIH~?`m2arf%h+;@#3}(<|s!~qLO3X{oPs+(p z*GnuYDAsd<^BoHcob&V2GSmGu8KZUr5^od^sVROm_~;(Ak-GW>x}e z3; z+L%ZR!asy*USVK9hu0ky1wj{d(G&_I5=DYTW~d+`iDJnN7i1(;95!1D5fq_VHkDyw zE1OYHW1Pl0O;FlmCJQN);y$fhm*%vM(^oic=d@#3_(ISlx-uu|3KnN2)QLOws2;l| zqGR~jgIDo!d}2K$3KVzYlL0=3UkmW-_>BN}<2M7`gWn49Y5aD8-@)$&_zcbjxEH?{ z;P>$d0nXwN1N;#_8{i0*11#Yb!Fqg1$RrNQ1Tvx@?b8c}HDq`g7-i7TU$8u9k|>Cb zDuD@e1F;vFWZjBtb0kpnBI6{>JqH^Wkf;kNq>D(>Lny3ENY8Op(zC~v>5cO8UFKI_T`Aw*f?$;&|E~VJTRK>; z%seT7w_LeDd$_h*S=lHrZd3266FSZ73gp$Tk+vDG=V+c`TT`7)Di<2Z^uA2Zav{!tyC!mRK>ydkcW6E8voLd9MXmlyAsH@<}|D|dG)PwzlcwXInwpt`sXHCBF_ zJDh${UihnWXX|1nA8C{_Lep1CLD_4QP8`xY4X;aTNQb6QHJpMzXX_dKuV}d3 z!T|ABV7noX3*)3%u{Z}T?JpVb;li!*%{AWoU#2TRKMcxelc;UYly5&N?LDk*9vuF# z**wA^NNI1rJU>(0d{*0@ukLO@(<+O99_=q0u1ecfbg^@^zr-R0UZiD2<0qQ!G9f1c z3HlKSUoT>qHyIh*+K4BcDI8J>yhgq%*2SCY>=hfPyeo3c)U z)*-1oWDZ^Mx}-ow2#aAk97=`KVX9$?i4Od+g^QVUeg``i*_;hO2;)kY9u0?_1pnmA zR9Pqjo32C7I_i4Cf~4&75afS>UJStucnFzTq_kYlE?ORULospMYq_iSB+?B5H))3x^c5Tj z&C>GN;ZZuC7cx`YWk1%qV!q-?R5=uwOI%0|l2?T9N2@aSzdZ`Z@PaRSLWU4sj2W)H9$fI-< zQ&&$|!(=WrlS>W8rTV7RPj4^1xXPz62DoLPk^hq04_iV`h~!ZuMPv`)rY9}MdJ+<| Np*z76eJD+f{{ctB;->%r literal 0 HcmV?d00001 diff --git a/src/djangoblog/djangoblog/__pycache__/apps.cpython-310.pyc b/src/djangoblog/djangoblog/__pycache__/apps.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4c01a7ea2a6af4ade586ab83bdef76f0ac08523 GIT binary patch literal 1005 zcmZWo%WD%s7@vKmF(x3j7Y{;@<`VHLB3g^yO2OL_7`K^COqpcY-Rz-7MNHbXnn>S5 zskD_!A7X3~YG1+sW3P60lT+`W{AM?#5eH_N`5wRdeZTp}NF-ti2CZMqKT{C;%@4gp zhQkAx_8=?-5rLwDpb7$df`~|@3L=s&lqFRp@>7&l+7N}3f_vqmX+AQF*?ca;ExkkP z9PC@L(UB?;1lfWr5mA+iq$*XE6^KlfN>q(hk&M(nK#sWAhsU*I&KRRct_$wG28`Gs zjL^yItE};vohv&! z_Lld(dV?+0{H1j^-(>SU-r9P5VV8ZI3-Ts!l7Lr=jHL<&(W#vp%I6+BCF5~kr^Jng zZcaNR^ynbMo=+}EAZ`hj(T1>z1Q3~sR4hoZK<1Z1nOh`~Kz6=DY;Y;BI3&sT5B$?T z@8h92yWQHXL0R6)vG-&7;%uf<=zp{?&Soz`nu{HrcN?u{lQnAI%p5yDWYuZ5Rb^kA zY^&nWO}D=P^bX#_Ev@}B2nBMs->kBuqfj(9^NDT0XSEe?v3}lM>w>p=z0eGCYG?$T z)XC+G_DE88727d&%T+90BNOUC=)nc8sO9t&57sR!!gEDewso2f4!h7*7-y(v+ZgYn zKevbPrbj1A&yC`!WsK{Y(w%fz@eoLzzEnM}nWmjGC#)zS_QSL}bSx5=V&deDFy)`7 zN(Ho%Q8$WlQ7h;eyD^O6Z5_(@ag1L$8tr)cFeXL@%>6{qY7Q-74W1rngSVG?P;jrC z=upq-lwxeffkL?O?`I|W!aIR;w7DsYWI7;+d|zAu literal 0 HcmV?d00001 diff --git a/src/djangoblog/djangoblog/__pycache__/blog_signals.cpython-310.pyc b/src/djangoblog/djangoblog/__pycache__/blog_signals.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c54f77ce3a14b13409b639c11d1645d38a0706a2 GIT binary patch literal 4185 zcmaJ^|8v~L71v5Sold?xe~Fzq1c-q^IhvX>?UYWYWe6oHw25Jw&@U=eRX(lHI_o5N zwU@-Mj!AH60wu9KA%Q|5XPQ9hv~HMY0!bY5f9O}Nv-4AC`XfV2-&@@|Zh+p6_RHJ1 zZ&$mY_jyl2p2Be=2kyhbP=hB*~E+;z?#&6O)L(Y${O8o?6S8 z8G>Vqr`57%RK`Wo?PoTyG2{~dTPC9uW0AI zzS=r-ooE|ge{H}V5bZ8+eQkrep*Cm^);5|OYj>D;2zeglo6Jq3UGVO#-DTcIBubih z)6Mh59HL6~p60zyu|lACU)rlkPWQXm-$J|TmKoyoIK79Z8DicK{rjNb=d2U`2cTbs ze!nvy`rm+lZiYZ97jqd)sLZ-?+PkAe9opya$Ky3 zlv!R-sZhsq{kVH=Kd5t1U%#VfyWZ})$KCB7r_8sxyN{xw+++}PE424fw906i!T^JI z_D4ajMtz*!7gFD`%4y%CXbPnHJ(I3O*+{_Mv8j?8XUcYYoW{NMz@*Elwa=ydtyI+S zP>*tIg`hubv$nLiExn}(MtdrhTje@qurla#+*R(G?R7r>9K2*KyG}`ovtkQ8h%?;f z9*wi*z~?Zb4A@%zAs+GhDLmmPP(;Kb8>NN}wG6cawF0%urAUcXPO5SwJL)mDk#RCn z#!?*3$s!sX8j&JxMrve1FKcO$3T4*On|dVA6KLnAyP{0fn3o!Qq&wOKj(7b+@`qH3 z=cj&2q(+WN)BU{AEIPSJeoJzUDEF51wmMG$UtOv8uOuhm$i0yBGikk_?3bc!vwL0w zojl;6P(OliGz7mJFD@)yy_C%U>BhlBOV{U<*^idrJe-_8ygc`ZZ{u%ZhJ%srJhI@4@8O>z8gWzPfVcugNRtQ(I5%-My>TTuc^c+bdkYw6bs^xpp!g zZoPgkx&FGa&DOzQSU(m6Pyh&xF&5NPY{W8LjXKV29>WVks0jG&0TcmjKO(I`TQV-M6t_(%o_vDV9+n?LD*V_5BXLegVc5i=rSKQsc zZ7Zyg@~KH`_5(5ivo+hdE0hh_0svOXHlmT$9mZ-kaDB1uSjA(ByTsuz8iG^Axg7_} zbdtM)A8W#qSe&7Z1q_tYHH=t8bwHptvEOEu@U{bpm0DO4OU4S!kF~a4LO+n$L$H(2 z9({7#^UqH4@xXtU1yx$+k39cm)%GjF6QKXRBbuYw#G-v$;qc^Cyix3P?Il{{w(oe9 z-3NBVZ$qJHb)pklk%>%Hc(l*8OkU9l{_{kZb-752WWC%+^n>!h^D5g4lm5q9@Wd5( zgtPnx8XafhQZ;o9`50*^T$z!oYD0~bYKA?8WfRR6F%f9j_PG=dIRV?%HZe> zi4+T3%^bp*8#4&z41igy{{H?{)(9TBrY(oW78}dMf$*8_A33Z!W%^T)x!$_$Bb2 zrBB{p`Q&n|IlH`g0!CAy_rX3tAPlPxntKoumLOSspUrJHQtD!e=r|jU;?;(+0yM^Y zP^38l@TDJTC*3lyGa9R7E}*aH){PI>xeIt5#ue~NSrLko#N8{o+G+eqp_<|$ALabJ42NOXCU2(T~UNL0WPK8O(J8=Y=`htSwGLn?3{AiJ}97r#3q zV+7wK%CdK0)@H%pcvA3`dg{w~ra<6Bsq44S&nNHvaq0Toh{d_%UkQ9b@M?qxu!`BM z{M|Zz<>um>OINQhT|1c?YrXvb%KWPc&<9JcQAdimnJ$>Pb@iQ_j8zPWtn+@}W*rM{3l2y|Eead>6nO0qEDYJLnl zE7y;JeTY6QhhBp)(mHW$>Dp2ufLRD>0PUbm!!Tge12*Gs z*`u9gCA5C;It}(MFdp~ZK2{ov4L5ZCklTKlGUUHlaa@*$p6uQod-gs(@?-XWkh3n_ zUVhYeow}920UJX3Jy_g}MGlHM=Y|&b?NLlh+RLRWjXfF$ULA5EI0``w>=B&w9V`l1 zN%mHo!Aft(iE;6UaYMm?$I>T zQxI1x(*vY75 zlDuNbhAaa^^*{g{#PIcn3UWiyK(3P_gtRp5Vd&F7H#CE&GK9S^3|W)=Nl{UO;nXkK z&%tCFFSzr=?-A^afcF42BAInZa#dozoHQX56y6Tu4%h&)@Xk|`>;D2}f$+64>tPIC zN3MLO2Y;2zuAK`WzBXZZeQ?j#(Nn3#+Xf+<$A~p|To}F0f74to&Cvho9K-K^|9*ufKU;v z)D&Y`Mt@hQA&*tmOk>w=0jf{DDn_!^J3j{p8tQneIfEEP{}1~$bV?=m6R6@WFapG} zO5B}B%wfk30cz!O@Dq_V#a-*kj718#phGlSr_@iEUrq^ppTB)F^AkY z9ExwM4#gnru+7k&-0Shp;^Fw07>Ouuz_d&KuZ zyy0sShPd}jX{X2mGd!v;S}#6nX{{R2g%P9(crGWFCW} YZ74HW7kOBC7ytkO literal 0 HcmV?d00001 diff --git a/src/djangoblog/djangoblog/__pycache__/elasticsearch_backend.cpython-310.pyc b/src/djangoblog/djangoblog/__pycache__/elasticsearch_backend.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ec4ebdf19bd0e7660ad4d55d43e5a161df5e9be4 GIT binary patch literal 6455 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 literal 0 HcmV?d00001 diff --git a/src/djangoblog/djangoblog/__pycache__/feeds.cpython-310.pyc b/src/djangoblog/djangoblog/__pycache__/feeds.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f231cbbaf6886a182edae566ae3958214893b07 GIT binary patch literal 2677 zcma)8+jA3D7~flxO`3*qkr5QAIx3mbav6Ql@q)srJlJ92ZKva!|BQXLn{A$W_sQ=&n}*Pa8FpvS_4l3g{l3dNYqqz? zIC%a#pVYsC*1yj{u!L z@LAtNp>YaNI7J}5B5INnB7zboq7osJB9jma4{=H~&{v6(D2YM8jkJ+C)N#^I5>U63 zH6#gjf~+O$pk71PlMPTO$xFa&qnp^*m(KT^pBrW`D(id<+j?V@-pn=s@mc@QMSpVW z;GsiHGuLVthO2Yq{-2MY%%AcfO;tx1mPT#|!hk_p@9G>(JDYKZ>r>u?sp|QX_u$4m zwUIGz^el906F+x4+hKaKGU1Kit^IM@8^7A|d~8QCyrTye-jU@&X!*f5T4^tQRssr} z8|3=_%m+&i;bi zmUaM6Ra|WgEMeh>39-2OsT^9YvkCTmj!BSx%CRJtImU7;47X$n?x_9xclGp*I#C)0 zB*kQMlNiA1jAa^*t*H(*XdLH|Wl*d^)Ux)5otdX)-v>+3fy=-vD=NVNU?{ClH~Rl5Q&wF=X~2&e?JG@4urwKf8R zx>9RhS!EZ4s=6^1|0EnkgLYh1zjM+CUK!Afs46kDs!H)xQliY!?Br@|igJoKohzXS zDBcou9Ts>2Q;cN#8WwM2@fH^7_NH)Sr#>F|6vG4MEhH#&5-*AH3X&iTatPk2EI}EL zOAOhSYIoNYcI$O!&Ad(Z47t(TH+D!%`Y&=*Aj~J$9E26IaQDE**F!b2ZL!6Hb K>=om}2JwGu)KjDY literal 0 HcmV?d00001 diff --git a/src/djangoblog/djangoblog/__pycache__/logentryadmin.cpython-310.pyc b/src/djangoblog/djangoblog/__pycache__/logentryadmin.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be411b5937443062c8eef7801f8ec437ff458ecf GIT binary patch literal 3714 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 E1LvW@1poj5 literal 0 HcmV?d00001 diff --git a/src/djangoblog/djangoblog/__pycache__/settings.cpython-310.pyc b/src/djangoblog/djangoblog/__pycache__/settings.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c9350d1ebbce9d56adab61f34f7b537547469ec GIT binary patch literal 8383 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 literal 0 HcmV?d00001 diff --git a/src/djangoblog/djangoblog/__pycache__/spider_notify.cpython-310.pyc b/src/djangoblog/djangoblog/__pycache__/spider_notify.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5e9f55b8ac1a605d3d933c56c7287cd1d243dbc GIT binary patch literal 1601 zcma)6&2Jk;6rY)0f8s__f(qPn;80(JxJ3~{v;`ID_K+kK-8jBtxczxPXy@s-ohn;kf@ZBnz^* zgk*7%!~-%OM^Rq~tWhkA&7h%Bs%BG0l1aR)7wHU06~w&9iHzfdBFaP-me3-O`+yf^ zpUeAYKX?w9k@0C&R`i%g)ybJ$35>%-uCkuYt^-mT?u$p&FjY z6sHLl(&8MJ$t^NZsITguBk4d5WAqiYbsUL^24{vbnunIpMnE5$6S(#==;wuR1$K&a z*YGtI6>|i_yLqs-K*78!Z`r&&D9qVj>fGD1^9y$6rTufcX9cZmzq-K(VKF{ojR)0#nb0{3C|pF4Yg?2;7w{LAy7UX~!N>Cb4Xl%kuW znw`{4kt)}z8T#aUQkkZzmNk8fu4{T!G)0!b?qg2;x?uAm=$8H|unnDmnY z(udzB!94kE_8spa40~-}10Vw81JeZWvu#SDHJrW+=TZ}aE_kZtBxG2tTpzUu!(q$KYiON>Vm4rAU$)mZT|7&Se-Mk)*4+M5e0=NRq52nb@ESswSrtK#1%n zpq~c!p70DsI?KV$k-Z^C1V|8xnE1k&4EAAH6AhXH23UAy^#B|&-{9aHPHNdn4)IV* op%+|C>u)iK%o(rT4e}3hOAda(lmC!G$6#lJC`ckWEDQ*L0}>fdH2?qr literal 0 HcmV?d00001 diff --git a/src/djangoblog/djangoblog/__pycache__/urls.cpython-310.pyc b/src/djangoblog/djangoblog/__pycache__/urls.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b2bb3d751d1f67361fdcca52d8aace201dbf173 GIT binary patch literal 2935 zcma(TS#R7#*mk^^_sAw`2yK(pZBvqku$w?R6ak{7DHKsaP0GV4ig7)&S*IR*nejqn z38|<&Ak>sYAdsklsw&3=@BjjYkoX^dh20hj0S`QZMEPdCn=A+7*%I>daMfqMJ{m%#>55miTQWXU&NWm)BJe6ulQw2QW4Nx8F64t#GO{27gQ(lH< zQC7lfFGmez&^*dZJOg+E6(pSXinN4E63%&pw2aCUHoPG;1p4#dFdac7l2-6`(NQ!i z;i7jF9YbRhE_pZ8-DtOj2faOXFWO7T(Kwwz6LbYv>R;M08TXd&uaiMn{sEu+m8=y^Mvd{JoPvH5WZy_pC(y6Lrho26|g7l+)8$zSESzC zwxL;4A|qrB-@c{cs_?7pMGenwYZ8{`@f`_{@5G0n_JVcu)XB%p#lWw->n&!7Zr~fnN`shB zx0u(o!;mnaKW%z04^0-dLc+xcZia#RtV^EbRr6TDOd2o<)2|1NN)=|)BQ_@{CuG4e z;c0|nlP}b2IB37{^+^ zBiu{uO0Dx%^9aVK?VB!b1}t1K%{mLHNjELK+3XVg8lE9@747BdxJ-@-&OnWXESag8^=>*Bl9VMifnxaes>poL zSAb`GyZ*n=qOod_$DX-qL6CZGg5MnAqC0=DZ}l1u2x%TdD>(pV zxEm5`H!E7KgFqwBbkzzTBVtnnz!6St<}_rEth()l0ow#};ZYv=Ckbx`J|~Y0!V+JW zl21W00XJ++UEdN$02^O|3WME5vMse&K5So3A=o&UN0q+PQeO{mKgxE`Hd#wAFt3a_8;O+OK}l`Q%Fb>#whW_v-b_7k>Hn!nIG&wYT29{@okx zciv6J5`EWR{<3rKh4$+gI&XdX<6B>JUiw^WMVSV%y|A$feIc8r#p7J)QUZ-bp;?%? z0b^ZIpl;A8&$NK`vBJiI{(J4{!s?ToVI%OLOwOVC)t$<&Cet8twJL_8+T4r_i8ae< z5a-N1sNh*xw3Mc%)$E^a5|Ah*nz$B?{&SeBf*y7Kt${bXI8>s{nryvVtVB6k7h#L- z0!<*26C%86J0!|cOt1_6T#Jf5P!3a6gu{~(KjadPGhH+)+KvM)(WRqI7mbX-ZiS6R zI3?kzw8oRBT&9$afJ&;_g_+%+wc9E$}(SecD8mIWlsit{^x)frrd3i|hou>rvw>ywrqR+E!S zjw71b;r>gKSPC?3+mq!@1BGS8BKilF8uljWbIv4LWx4(h;vST3%=QRbV`q3-UJ$x& zOY+Vs#tMRGCXPuSNzSw+>DFnmMtsbQLaf{;ChEvpNsx#8$lbcLL0~ZWm{i!1Sj2I_ z#CIf{f(023+s7VZhvyD&!*|WipaR3%PU7#jxSnpZN>R6 zeDaAWR^rsrrH4*E9Oq6f9X)hn~|_UCj;@3I$dFQ%@UmM;TVwscEWy!_W-1B%!iahJE@k RrQ!6?>Cr6gE8Ng;`~|EOqC5Zq literal 0 HcmV?d00001 diff --git a/src/djangoblog/djangoblog/__pycache__/utils.cpython-310.pyc b/src/djangoblog/djangoblog/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b6f1d15cba201f81d66bf569f9b2279087573597 GIT binary patch literal 11064 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 literal 0 HcmV?d00001 diff --git a/src/djangoblog/djangoblog/__pycache__/whoosh_cn_backend.cpython-310.pyc b/src/djangoblog/djangoblog/__pycache__/whoosh_cn_backend.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9589c62fa261ec5c2354977a82f33e9596c378e8 GIT binary patch literal 19406 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 literal 0 HcmV?d00001 diff --git a/src/djangoblog/djangoblog/admin_site.py b/src/djangoblog/djangoblog/admin_site.py new file mode 100644 index 00000000..1f0130db --- /dev/null +++ b/src/djangoblog/djangoblog/admin_site.py @@ -0,0 +1,89 @@ +# Django博客系统的Admin配置模块 +# 该模块用于自定义Django管理后台(Admin Site),包括管理员站点的属性设置、权限控制, +# 以及注册系统中各模型到管理后台,实现对数据的可视化管理 + +# 导入Django内置的AdminSite及相关模型、管理类 +from django.contrib.admin import AdminSite +from django.contrib.admin.models import LogEntry +from django.contrib.sites.admin import SiteAdmin +from django.contrib.sites.models import Site + +# 导入各应用的Admin配置和模型(账号、博客、评论、OAuth等模块) +from accounts.admin import * +from blog.admin import * +from blog.models import * +from comments.admin import * +from comments.models import * +from djangoblog.logentryadmin import LogEntryAdmin +from oauth.admin import * +from oauth.models import * +from owntracks.admin import * +from owntracks.models import * +from servermanager.admin import * +from servermanager.models import * + + +class DjangoBlogAdminSite(AdminSite): + """ + 自定义的Django管理站点类,继承自AdminSite + 用于个性化管理后台的显示信息和权限控制 + """ + # 管理后台页面顶部的标题 + site_header = 'djangoblog administration' + # 浏览器标签页的标题 + site_title = 'djangoblog site admin' + + def __init__(self, name='admin'): + """初始化方法,调用父类构造函数""" + super().__init__(name) + + def has_permission(self, request): + """ + 重写权限检查方法,控制谁可以访问管理后台 + 仅允许超级用户(is_superuser)访问 + """ + return request.user.is_superuser + + # 以下为注释掉的自定义URL示例(如需扩展管理后台URL可启用) + # def get_urls(self): + # urls = super().get_urls() + # from django.urls import path + # from blog.views import refresh_memcache + # + # my_urls = [ + # path('refresh/', self.admin_view(refresh_memcache), name="refresh"), + # ] + # return urls + my_urls + + +# 实例化自定义的管理站点 +admin_site = DjangoBlogAdminSite(name='admin') + +# 注册博客核心模型到管理站点,关联对应的Admin配置类 +admin_site.register(Article, ArticlelAdmin) # 文章模型 +admin_site.register(Category, CategoryAdmin) # 分类模型 +admin_site.register(Tag, TagAdmin) # 标签模型 +admin_site.register(Links, LinksAdmin) # 链接模型 +admin_site.register(SideBar, SideBarAdmin) # 侧边栏模型 +admin_site.register(BlogSettings, BlogSettingsAdmin) # 博客设置模型 + +# 注册服务器管理相关模型 +admin_site.register(commands, CommandsAdmin) # 命令模型 +admin_site.register(EmailSendLog, EmailSendLogAdmin) # 邮件发送日志模型 + +# 注册用户模型 +admin_site.register(BlogUser, BlogUserAdmin) # 自定义用户模型 + +# 注册评论模型 +admin_site.register(Comment, CommentAdmin) # 评论模型 + +# 注册OAuth相关模型 +admin_site.register(OAuthUser, OAuthUserAdmin) # OAuth用户模型 +admin_site.register(OAuthConfig, OAuthConfigAdmin) # OAuth配置模型 + +# 注册位置追踪相关模型 +admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) # 位置日志模型 + +# 注册站点和日志模型(Django内置) +admin_site.register(Site, SiteAdmin) # 站点模型 +admin_site.register(LogEntry, LogEntryAdmin) # 操作日志模型 \ No newline at end of file diff --git a/src/djangoblog/djangoblog/apps.py b/src/djangoblog/djangoblog/apps.py new file mode 100644 index 00000000..6316b6c8 --- /dev/null +++ b/src/djangoblog/djangoblog/apps.py @@ -0,0 +1,29 @@ +# Django博客应用配置类模块 +# 该模块定义了Django博客应用(djangoblog)的配置类,用于设置应用的核心属性和初始化逻辑 +# 主要功能包括:指定默认的自增字段类型、定义应用名称、以及在应用就绪时加载插件 + +from django.apps import AppConfig + + +class DjangoblogAppConfig(AppConfig): + """ + Django博客应用的配置类,继承自Django的AppConfig + 用于配置应用的元数据和生命周期钩子 + """ + # 指定模型默认的自增主键字段类型为BigAutoField(支持更大范围的整数) + default_auto_field = 'django.db.models.BigAutoField' + # 应用的名称,对应项目中的应用目录名 + name = 'djangoblog' + + def ready(self): + """ + 应用就绪时执行的方法(Django生命周期钩子) + 当应用加载完成并准备好处理请求时调用,通常用于初始化操作 + """ + # 调用父类的ready()方法,确保基类的初始化逻辑执行 + super().ready() + # 导入并加载插件:在应用就绪后加载所有激活的插件 + # 从当前应用的plugin_manage.loader模块导入load_plugins函数 + from .plugin_manage.loader import load_plugins + # 执行插件加载函数,完成插件的动态导入和初始化 + load_plugins() \ No newline at end of file diff --git a/src/djangoblog/djangoblog/blog_signals.py b/src/djangoblog/djangoblog/blog_signals.py new file mode 100644 index 00000000..065de3ae --- /dev/null +++ b/src/djangoblog/djangoblog/blog_signals.py @@ -0,0 +1,177 @@ +# Django博客系统信号处理模块 +# 该模块用于注册和处理Django内置信号及自定义信号,实现事件驱动的业务逻辑 +# 核心功能包括:邮件发送、OAuth用户登录处理、模型保存后缓存清理/搜索引擎通知、用户登录登出缓存处理等 +# 通过信号机制解耦业务逻辑,当特定事件触发时自动执行对应处理函数 + +import _thread +import logging + +import django.dispatch +from django.conf import settings +from django.contrib.admin.models import LogEntry +from django.contrib.auth.signals import user_logged_in, user_logged_out +from django.core.mail import EmailMultiAlternatives +from django.db.models.signals import post_save +from django.dispatch import receiver + +# 导入项目内部模块:评论相关、插件通知、缓存工具、站点工具、OAuth模型 +from comments.models import Comment +from comments.utils import send_comment_email +from djangoblog.spider_notify import SpiderNotify +from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache +from djangoblog.utils import get_current_site +from oauth.models import OAuthUser + +# 初始化日志记录器,记录信号处理过程中的信息和错误 +logger = logging.getLogger(__name__) + +# 自定义信号:OAuth用户登录信号,携带参数'id'(OAuthUser的主键) +oauth_user_login_signal = django.dispatch.Signal(['id']) +# 自定义信号:邮件发送信号,携带参数'emailto'(收件人列表)、'title'(邮件标题)、'content'(邮件内容) +send_email_signal = django.dispatch.Signal( + ['emailto', 'title', 'content']) + + +@receiver(send_email_signal) +def send_email_signal_handler(sender, **kwargs): + """ + 邮件发送信号的处理函数 + 当send_email_signal信号触发时,自动发送HTML格式邮件并记录发送日志 + """ + # 从信号参数中提取邮件相关信息 + emailto = kwargs['emailto'] + title = kwargs['title'] + content = kwargs['content'] + + # 构建HTML格式邮件(content_subtype设为html支持富文本) + msg = EmailMultiAlternatives( + title, + content, + from_email=settings.DEFAULT_FROM_EMAIL, # 发件人从Django配置中读取 + to=emailto) # 收件人列表 + msg.content_subtype = "html" # 指定邮件内容为HTML格式 + + # 初始化邮件发送日志模型,记录发送详情 + from servermanager.models import EmailSendLog + log = EmailSendLog() + log.title = title + log.content = content + log.emailto = ','.join(emailto) # 多个收件人用逗号拼接存储 + + try: + # 发送邮件,result为成功发送的邮件数量 + result = msg.send() + log.send_result = result > 0 # 发送数量>0表示发送成功 + except Exception as e: + # 捕获发送异常,记录错误日志 + logger.error(f"失败邮箱号: {emailto}, {e}") + log.send_result = False # 标记发送失败 + log.save() # 保存日志到数据库 + + +@receiver(oauth_user_login_signal) +def oauth_user_login_signal_handler(sender, **kwargs): + """ + OAuth用户登录信号的处理函数 + 当OAuth用户登录成功后,处理用户头像(如跨域头像本地化存储)并清理侧边栏缓存 + """ + # 从信号参数中提取OAuthUser的id,查询对应的用户实例 + id = kwargs['id'] + oauthuser = OAuthUser.objects.get(id=id) + # 获取当前站点域名(用于判断头像是否为本地地址) + site = get_current_site().domain + + # 若用户有头像且头像地址不包含当前站点域名(跨域头像),则本地化存储 + if oauthuser.picture and not oauthuser.picture.find(site) >= 0: + from djangoblog.utils import save_user_avatar + oauthuser.picture = save_user_avatar(oauthuser.picture) # 保存头像到本地并更新地址 + oauthuser.save() # 保存更新后的用户信息 + + # 清理侧边栏缓存(确保登录后侧边栏展示最新数据) + delete_sidebar_cache() + + +@receiver(post_save) +def model_post_save_callback( + sender, + instance, + created, + raw, + using, + update_fields, + **kwargs): + """ + Django模型保存后信号的处理函数(post_save) + 触发时机:任何模型执行save()方法后(新增/更新) + 主要处理:搜索引擎通知、缓存清理、评论审核通过后的联动操作 + """ + clearcache = False # 标记是否需要清理全局缓存 + + # 跳过Admin操作日志模型(LogEntry)的处理,无需触发后续逻辑 + if isinstance(instance, LogEntry): + return + + # 若模型实例有get_full_url方法(通常是博客文章等需要对外展示的模型) + if 'get_full_url' in dir(instance): + # 判断是否仅更新浏览量字段(views) + is_update_views = update_fields == {'views'} + # 非测试环境且不是仅更新浏览量时,通知搜索引擎(如百度)收录新链接 + if not settings.TESTING and not is_update_views: + try: + notify_url = instance.get_full_url() # 获取模型实例的完整访问链接 + SpiderNotify.baidu_notify([notify_url]) # 通知百度搜索引擎 + except Exception as ex: + logger.error("notify sipder", ex) # 记录搜索引擎通知失败的错误 + # 非浏览量更新时,标记需要清理缓存 + if not is_update_views: + clearcache = True + + # 若保存的是评论模型实例 + if isinstance(instance, Comment): + # 仅处理审核通过的评论(is_enable为True) + if instance.is_enable: + # 获取评论对应的文章绝对路径 + path = instance.article.get_absolute_url() + site = get_current_site().domain + # 处理带端口的域名(如localhost:8000),仅保留主域名部分 + if site.find(':') > 0: + site = site[0:site.find(':')] + + # 清理文章详情页的视图缓存(确保评论实时展示) + expire_view_cache( + path, + servername=site, + serverport=80, + key_prefix='blogdetail') + # 清理SEO相关缓存 + if cache.get('seo_processor'): + cache.delete('seo_processor') + # 清理该文章的评论列表缓存 + comment_cache_key = 'article_comments_{id}'.format( + id=instance.article.id) + cache.delete(comment_cache_key) + # 清理侧边栏缓存和评论相关视图缓存 + delete_sidebar_cache() + delete_view_cache('article_comments', [str(instance.article.pk)]) + + # 启动新线程发送评论通知邮件(避免阻塞主线程) + _thread.start_new_thread(send_comment_email, (instance,)) + + # 若标记需要清理缓存,则执行全局缓存清理 + if clearcache: + cache.clear() + + +@receiver(user_logged_in) +@receiver(user_logged_out) +def user_auth_callback(sender, request, user, **kwargs): + """ + 用户登录/登出信号的处理函数 + 触发时机:用户登录(user_logged_in)或登出(user_logged_out)后 + 主要处理:记录日志并清理侧边栏缓存(确保登录状态变化后展示最新数据) + """ + # 若用户存在且用户名有效 + if user and user.username: + logger.info(user) # 记录用户登录/登出日志 + delete_sidebar_cache() # 清理侧边栏缓存 + # cache.clear() # 全局缓存清理(按需启用) \ No newline at end of file diff --git a/src/djangoblog/djangoblog/elasticsearch_backend.py b/src/djangoblog/djangoblog/elasticsearch_backend.py new file mode 100644 index 00000000..2ccc3f80 --- /dev/null +++ b/src/djangoblog/djangoblog/elasticsearch_backend.py @@ -0,0 +1,183 @@ +from django.utils.encoding import force_str +from elasticsearch_dsl import Q +from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query +from haystack.forms import ModelSearchForm +from haystack.models import SearchResult +from haystack.utils import log as logging + +from blog.documents import ArticleDocument, ArticleDocumentManager +from blog.models import Article + +logger = logging.getLogger(__name__) + + +class ElasticSearchBackend(BaseSearchBackend): + def __init__(self, connection_alias, **connection_options): + super( + ElasticSearchBackend, + self).__init__( + connection_alias, + **connection_options) + self.manager = ArticleDocumentManager() + self.include_spelling = True + + def _get_models(self, iterable): + models = iterable if iterable and iterable[0] else Article.objects.all() + docs = self.manager.convert_to_doc(models) + return docs + + def _create(self, models): + self.manager.create_index() + docs = self._get_models(models) + self.manager.rebuild(docs) + + def _delete(self, models): + for m in models: + m.delete() + return True + + def _rebuild(self, models): + models = models if models else Article.objects.all() + docs = self.manager.convert_to_doc(models) + self.manager.update_docs(docs) + + def update(self, index, iterable, commit=True): + + models = self._get_models(iterable) + self.manager.update_docs(models) + + def remove(self, obj_or_string): + models = self._get_models([obj_or_string]) + self._delete(models) + + def clear(self, models=None, commit=True): + self.remove(None) + + @staticmethod + def get_suggestion(query: str) -> str: + """获取推荐词, 如果没有找到添加原搜索词""" + + search = ArticleDocument.search() \ + .query("match", body=query) \ + .suggest('suggest_search', query, term={'field': 'body'}) \ + .execute() + + keywords = [] + for suggest in search.suggest.suggest_search: + if suggest["options"]: + keywords.append(suggest["options"][0]["text"]) + else: + keywords.append(suggest["text"]) + + return ' '.join(keywords) + + @log_query + def search(self, query_string, **kwargs): + logger.info('search query_string:' + query_string) + + start_offset = kwargs.get('start_offset') + end_offset = kwargs.get('end_offset') + + # 推荐词搜索 + if getattr(self, "is_suggest", None): + suggestion = self.get_suggestion(query_string) + else: + suggestion = query_string + + q = Q('bool', + should=[Q('match', body=suggestion), Q('match', title=suggestion)], + minimum_should_match="70%") + + search = ArticleDocument.search() \ + .query('bool', filter=[q]) \ + .filter('term', status='p') \ + .filter('term', type='a') \ + .source(False)[start_offset: end_offset] + + results = search.execute() + hits = results['hits'].total + raw_results = [] + for raw_result in results['hits']['hits']: + app_label = 'blog' + model_name = 'Article' + additional_fields = {} + + result_class = SearchResult + + result = result_class( + app_label, + model_name, + raw_result['_id'], + raw_result['_score'], + **additional_fields) + raw_results.append(result) + facets = {} + spelling_suggestion = None if query_string == suggestion else suggestion + + return { + 'results': raw_results, + 'hits': hits, + 'facets': facets, + 'spelling_suggestion': spelling_suggestion, + } + + +class ElasticSearchQuery(BaseSearchQuery): + def _convert_datetime(self, date): + if hasattr(date, 'hour'): + return force_str(date.strftime('%Y%m%d%H%M%S')) + else: + return force_str(date.strftime('%Y%m%d000000')) + + def clean(self, query_fragment): + """ + Provides a mechanism for sanitizing user input before presenting the + value to the backend. + + Whoosh 1.X differs here in that you can no longer use a backslash + to escape reserved characters. Instead, the whole word should be + quoted. + """ + words = query_fragment.split() + cleaned_words = [] + + for word in words: + if word in self.backend.RESERVED_WORDS: + word = word.replace(word, word.lower()) + + for char in self.backend.RESERVED_CHARACTERS: + if char in word: + word = "'%s'" % word + break + + cleaned_words.append(word) + + return ' '.join(cleaned_words) + + def build_query_fragment(self, field, filter_type, value): + return value.query_string + + def get_count(self): + results = self.get_results() + return len(results) if results else 0 + + def get_spelling_suggestion(self, preferred_query=None): + return self._spelling_suggestion + + def build_params(self, spelling_query=None): + kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query) + return kwargs + + +class ElasticSearchModelSearchForm(ModelSearchForm): + + def search(self): + # 是否建议搜索 + self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" + sqs = super().search() + return sqs + + +class ElasticSearchEngine(BaseEngine): + backend = ElasticSearchBackend + query = ElasticSearchQuery \ No newline at end of file diff --git a/src/djangoblog/djangoblog/feeds.py b/src/djangoblog/djangoblog/feeds.py new file mode 100644 index 00000000..ee07797f --- /dev/null +++ b/src/djangoblog/djangoblog/feeds.py @@ -0,0 +1,72 @@ +#姜雨菲:导入Django用户模型获取工具 +from django.contrib.auth import get_user_model +#姜雨菲: 导入Django的Feed基类,用于创建RSS/Atom订阅源 +from django.contrib.syndication.views import Feed +#姜雨菲: 导入时区处理工具 +from django.utils import timezone +#姜雨菲: 导入RSS 2.0版本的生成器 +from django.utils.feedgenerator import Rss201rev2Feed + +#姜雨菲: 导入博客文章模型 +from blog.models import Article +#姜雨菲: 导入自定义的Markdown处理工具 +from djangoblog.utils import CommonMarkdown + + +class DjangoBlogFeed(Feed): + """博客网站的RSS订阅源类,继承自Django的Feed基类""" + + #姜雨菲: 指定订阅源类型为RSS 2.0版本 + feed_type = Rss201rev2Feed + + # 订阅源的描述信息 + description = '大巧无工,重剑无锋.' + # 订阅源的标题 + title = "且听风吟 大巧无工,重剑无锋. " + # 订阅源的链接(相对路径) + link = "/feed/" + + def author_name(self): + """返回订阅源作者名称""" + # 获取第一个用户的昵称作为作者名 + return get_user_model().objects.first().nickname + + def author_link(self): + """返回订阅源作者的链接""" + # 获取第一个用户的绝对URL + return get_user_model().objects.first().get_absolute_url() + + def items(self): + """ + 定义订阅源包含的项目列表 + 返回最新发布的5篇文章 + """ + # 筛选类型为'article'(a)且状态为'published'(p)的文章 + # 按发布时间倒序排列,取前5篇 + return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] + + def item_title(self, item): + """返回单个项目(文章)的标题""" + return item.title + + def item_description(self, item): + """返回单个项目(文章)的描述""" + # 将文章正文从Markdown格式转换为HTML + return CommonMarkdown.get_markdown(item.body) + + def feed_copyright(self): + """返回订阅源的版权信息""" + # 获取当前时间,并格式化版权信息 + now = timezone.now() + return "Copyright© {year} 且听风吟".format(year=now.year) + + def item_link(self, item): + """返回单个项目(文章)的链接""" + return item.get_absolute_url() + + def item_guid(self, item): + """ + 返回单个项目的唯一标识符(guid) + 此处返回空值,实际应用中通常应返回唯一标识如文章ID等 + """ + return \ No newline at end of file diff --git a/src/djangoblog/djangoblog/logentryadmin.py b/src/djangoblog/djangoblog/logentryadmin.py new file mode 100644 index 00000000..4ac7f9b8 --- /dev/null +++ b/src/djangoblog/djangoblog/logentryadmin.py @@ -0,0 +1,136 @@ +#姜雨菲: 导入Django管理后台核心模块 +from django.contrib import admin +#姜雨菲: 导入日志相关常量和模型 +from django.contrib.admin.models import DELETION # 表示"删除"操作的常量 +from django.contrib.contenttypes.models import ContentType # 内容类型模型,用于关联不同模型 +#姜雨菲: 导入URL反向解析和异常处理 +from django.urls import reverse, NoReverseMatch +# 导入字符串处理工具 +from django.utils.encoding import force_str +# 导入HTML转义工具 +from django.utils.html import escape +# 导入安全字符串标记工具(用于渲染HTML) +from django.utils.safestring import mark_safe +# 导入国际化翻译工具 +from django.utils.translation import gettext_lazy as _ + + +class LogEntryAdmin(admin.ModelAdmin): + """ + 自定义管理员日志(LogEntry)的管理类 + 用于在Django admin后台展示和管理系统操作日志 + """ + + # 列表页的筛选器:按内容类型筛选 + list_filter = [ + 'content_type' + ] + + # 搜索字段:支持按对象表示和变更消息搜索 + search_fields = [ + 'object_repr', # 对象的字符串表示 + 'change_message' # 操作变更的描述信息 + ] + + # 列表页中可点击的链接字段 + list_display_links = [ + 'action_time', # 操作时间 + 'get_change_message', # 变更消息 + ] + + # 列表页展示的字段 + list_display = [ + 'action_time', # 操作时间 + 'user_link', # 操作用户(带链接) + 'content_type', # 操作的内容类型(模型) + 'object_link', # 操作的对象(带链接) + 'get_change_message', # 变更消息 + ] + + def has_add_permission(self, request): + """禁用添加权限:不允许手动添加日志记录""" + return False + + def has_change_permission(self, request, obj=None): + """ + 限制修改权限: + - 仅超级用户或拥有change_logentry权限的用户可查看 + - 禁止POST请求(即不允许修改日志内容) + """ + return ( + request.user.is_superuser or + request.user.has_perm('admin.change_logentry') + ) and request.method != 'POST' + + def has_delete_permission(self, request, obj=None): + """禁用删除权限:不允许删除日志记录""" + return False + + def object_link(self, obj): + """ + 生成操作对象的链接(若对象存在) + 对于已删除的对象,仅显示文本;对于存在的对象,显示可点击的链接 + """ + # 先对对象的字符串表示进行HTML转义,防止XSS攻击 + object_link = escape(obj.object_repr) + # 获取操作对象的内容类型 + content_type = obj.content_type + + # 如果不是删除操作且内容类型存在,尝试生成编辑链接 + if obj.action_flag != DELETION and content_type is not None: + try: + # 反向解析对象的编辑页面URL + url = reverse( + # 生成admin的URL名称格式:app_label_model_change + 'admin:{}_{}_change'.format(content_type.app_label, + content_type.model), + args=[obj.object_id] # 传递对象ID作为参数 + ) + # 生成带链接的HTML + object_link = '{}'.format(url, object_link) + except NoReverseMatch: + # 若无法解析URL(如模型未注册到admin),则只显示文本 + pass + # 标记为安全字符串,允许Django渲染HTML + return mark_safe(object_link) + + # 配置列表页字段的排序和显示名称 + object_link.admin_order_field = 'object_repr' # 允许按对象表示排序 + object_link.short_description = _('object') # 列表页显示的列名(支持国际化) + + def user_link(self, obj): + """生成操作用户的链接(指向用户编辑页面)""" + # 获取用户模型的内容类型 + content_type = ContentType.objects.get_for_model(type(obj.user)) + # 对用户名进行HTML转义 + user_link = escape(force_str(obj.user)) + try: + # 反向解析用户编辑页面的URL + url = reverse( + 'admin:{}_{}_change'.format(content_type.app_label, + content_type.model), + args=[obj.user.pk] # 传递用户ID作为参数 + ) + # 生成带链接的HTML + user_link = '{}'.format(url, user_link) + except NoReverseMatch: + # 若无法解析URL,只显示用户名 + pass + # 标记为安全字符串,允许渲染HTML + return mark_safe(user_link) + + # 配置用户链接字段的排序和显示名称 + user_link.admin_order_field = 'user' # 允许按用户排序 + user_link.short_description = _('user') # 列表页显示的列名(支持国际化) + + def get_queryset(self, request): + """优化查询集:预加载content_type,减少数据库查询次数""" + queryset = super(LogEntryAdmin, self).get_queryset(request) + return queryset.prefetch_related('content_type') # 使用预加载优化性能 + + def get_actions(self, request): + """移除批量删除操作:不允许批量删除日志""" + actions = super(LogEntryAdmin, self).get_actions(request) + if 'delete_selected' in actions: + del actions['delete_selected'] + return actions \ No newline at end of file diff --git a/src/djangoblog/djangoblog/plugin_manage/__pycache__/base_plugin.cpython-310.pyc b/src/djangoblog/djangoblog/plugin_manage/__pycache__/base_plugin.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a7f82f33dd1c9a813c359244b713d8ed4b640c54 GIT binary patch literal 8872 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 TJP5xV_PZ)13c>^K&F;SgDvw!e literal 0 HcmV?d00001 diff --git a/src/djangoblog/djangoblog/plugin_manage/__pycache__/hook_constants.cpython-310.pyc b/src/djangoblog/djangoblog/plugin_manage/__pycache__/hook_constants.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..829013a76e3f8f8c3dbbfada0aeaad84949beb18 GIT binary patch literal 733 zcmZ9K(QcbC6ov^Uz?z0uidtzmyU)#}NxPb+X*5`Cv<#v^FLo5kG8~7{0gk}Ylo!|w z>~=5ESK_u$v0aUj5z^W6k-y*e`Hy9b;qY9k+s_|Y$)l+#PkT6RwWfTmNBOo6sDOe! z=%ETKp1~PvpkW_%G|yJhoGYc244Eg_zFg>kV%wsid?cJrKw;DRx3sY zXOefk-QY7up7MHqAlIBmB9}auT}CN3^m~FamnGb^8ABnZ$d3ADnII2YaTKgIXUI=@ zC4@ZTn@D6?Z9_x85(Ph=yKZ&1I@vxpV9+|(<#ZQ0lu;S~>?{3XTj>Wou-bG78yL3S zq=%+TTJTbAi-?zCg#vdSyJHhZ_9B>0W`sHB$czM} zdU@RMxdCwldgpncscV0!kIp@R8caR6(R>))5!;~);(Pat3Go{hMz@~x^*`}ZDf;z) zX~LgR&b#G&C)XmM7vh0O^3BqDV0kQVQxPw*nTGr6Cf&wKPBWIXm@n5teA_>GDOoPd k*PC5szRv`1Q~sg1|B$chpy`_0bWQE6y{_xOwF^`I3pe!Vl>h($ literal 0 HcmV?d00001 diff --git a/src/djangoblog/djangoblog/plugin_manage/__pycache__/hooks.cpython-310.pyc b/src/djangoblog/djangoblog/plugin_manage/__pycache__/hooks.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e716db3e35fce7f69269df1f69d36ebc50ea777 GIT binary patch literal 2497 zcmb`J+iw#`6o+T_;;an`Bq|_MRIU1AnTM+FV}vM_P*JH$5g=8KQdy37vvDxC8QX+d z8)Rsj3w18#s*;4pD3F9Umnu>fuv653V_)&E?I)i4&^(}?nQYr{IpC1EV1ghfzDy(G1pwEN&M=s9n+oUDib1 z)g@_?CJ#vMb@WY_pt-aB-^OV6GYO~zk(dcD=BydHylwcqn?hFJk z2L<}Aa?x@f+-Km9p8#W`1iFTaV!+cxCzw47;g~p!F-izCqJ~X0i8R5)JrX>{X_AUzfzS;nBA5k4D4824TU$EL_a(Zck@FDcyp26K6kRcN-pP$ zL25H3=)T{cFRV}T?SfrB9ZZCx5%nCbI&5bZ-~yuZu0I`t?~I(!95-zM!ZEWVU}oieOZMW!nx3gc7m|@kC}OAqzI2oy)k|plOTBxR zkSI~fZY?(Uduw?Yy)Ejc1#Bt$^`Ji#>58^3xCq6H&@4tMewO>}^`IW38)V5k^-SFG z+9xyiEr|wix%ecQ5-hAUDR4viv^)@tpGW#W)Z@p%DEpf5enG%ZLIakB!x&0$m9+3y z`b!eY5oqv_#Oi_Y8;R(Kg0C|xj-;b-vp8@3M9mq=+ zc zlaQXP=-aac#fia6;!EdwhOUC=?BJW~VG2_3a77`kyFD=mSQc}4oySxEivX?#!bu&{ z_VBbl_dQo3B<9kSpKF!CW91XrlEK~@C_kCDXCM4W0rAe;WyFT&tFd*66?5Zs8+I%o zkCv|&Uq_f7^N>1xIu;A}Q7LxuZLO8!F)l?oM|Nvc4;+&QFqg;yux+Fftk1=TC9}mO z@6a}fsG+9}IZO>bXlm3|&)oa@@a$hlyct?32e`nRw+pO}9EHz5k5xy{HHh4D8+zgk zcE&MnbMezOV2SYr;s134onf6FK}?*?MNJ?qB&K!c#FeO)4C^Q9NfoDOmLhvZNoo=s Inwpyb1|?+Y0ssI2 literal 0 HcmV?d00001 diff --git a/src/djangoblog/djangoblog/plugin_manage/__pycache__/loader.cpython-310.pyc b/src/djangoblog/djangoblog/plugin_manage/__pycache__/loader.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ea44af0a1cac7c8f0c7dd515a95944fd4eb51ea GIT binary patch literal 3771 zcmb_e-BTOa6~A}2LW{&#u$#7S+9+-^gz=PdUm1_v)Zmt>3ru7uQ;k!NtX)7}%4sws((!01%v8w|n<<@PYd=KYP7xWe^+Yi06){Rw8RebTvYW1% z5t5?p85J=45d9v>Wxe8`#BL|AFJstsr04pcUFEN|>uAsYJ-aHtd)H;qQZA$D$BzS~ zZZCYNUCPMY3_5+?cacedIv?AS3_jR%J$5UyIGf$PSv)^c z%Au)?^&})4rMiTxjyKH`UQ;J-)6}j=ST_=H_~N0EqU&w(NTkObQG>D~d-=M0Z^GgI znK$8TWq5lNUazbwx;LsC-j3kMiq|$ojTekz1fwA(@x!yha0CPxsyEyfQ#E5-6g^bA z5(vB+wf{Lc6iQT=C z>TNoi`k)nv3SWvF3Lh?Qyb@AkMp%v3I!zD1Kp-0IQvAgZRoP!|&vdDBJfiq3?QRip z6D#vbI|d5=>lY367h8Lb4mH}Ushxn_&o6R!{S|0@QD$y_i8hN8gVA8Sa*-jZfy0<( zv{{7mHOgZrp%5jvqe?g~2>5k}+fgF8@iC#=A*Lz>uOOu=@J=cGTS$pSdjmG@p+@K< zh+a;OA~XyHA&ukx(Bu(0_6-@oV-2UQyTjX2wH>$I!+Ee!Kj&MvCdu?{J!?}|psrl+ zB_o4m;;&qgH8eqP+~WbcPZo5!IJSKrYbBrw194aJBN#KQ!8!N&*eMv-sh^21?I%!$ z=+%lJ9jD%M2L69~p)e5ezo!=`aVJiqKMGoT5(|Ka4|NOdT@m80BC}cPRcmyS(*T(m z%{^KMR>&{iw?|l@0}}xrh0h216usDIOSVCEh*I)o*5-@xrF{QJc4f@wx`|P;@EOn@ zxwQeZ$>2I!T(yadp+Ls30wwMMYRjY!S?|q1?Atky(GEL{>3Fa`$jB2x zZ$7Z@KDLH8$K?LHhw9$)fN;``D^p>S5t?mO@YS z2FdNl5*Gd=3fgh#9%?7;O_;3};(v#VX|dZ5RDlDn0LXp|_T1A=cqaJOUjbVx`!H;+ z^!5DGwf`%&nwQ;Edkd;p0h6lt0bTo3TD%QK2>v}Eb$9COdIc0RmW2W4@clGla>V}u zgACC8LGf)rcT)q|t&G*TL>4|JgDX2x&YpTUlbd|9y_SZf4LhaZcWbRXtJasdt?`NM z<}|ohAvHx-RyYmt5PFg7v&P5sTg%YM4G&p&|CU=ACWHOp>=10>1IQuTNwzj-~zOfxD-SKw5hTw$4ND7PIfo z-G@4jRs%&6e&&P6P~yaHCjvSyZaLf)YpEFKg{;L~@%x8J=`NmB?a@v^yS74;pfX)F zA!At=&vEKPYP5}Kf~6qp98cGz0DBz*0gYxh{Kco43VXxZ1HmYc`8pGSV3标签内)的内容(钩子方法) + 子类重写此方法返回自定义HTML(如额外的CSS链接、meta标签等) + + Returns: + str: 要插入的HTML字符串 + """ + return "" + + def get_body_html(self, context=None): + """ + 获取需要插入到HTML body底部的内容(钩子方法) + 子类重写此方法返回自定义HTML(如额外的JS脚本) + + Returns: + str: 要插入底部的HTML字符串 + """ + return "" + + def get_plugin_info(self): + """ + 获取插件的详细信息(用于插件管理、展示等) + + Returns: + dict: 包含插件元数据和配置的字典 + """ + return { + 'name': self.PLUGIN_NAME, + 'description': self.PLUGIN_DESCRIPTION, + 'version': self.PLUGIN_VERSION, + 'author': self.PLUGIN_AUTHOR, + 'slug': self.plugin_slug, + 'directory': str(self.plugin_dir), + 'supported_positions': self.SUPPORTED_POSITIONS, + 'priorities': self.POSITION_PRIORITIES + } \ No newline at end of file diff --git a/src/djangoblog/djangoblog/plugin_manage/hook_constants.py b/src/djangoblog/djangoblog/plugin_manage/hook_constants.py new file mode 100644 index 00000000..56d4d858 --- /dev/null +++ b/src/djangoblog/djangoblog/plugin_manage/hook_constants.py @@ -0,0 +1,35 @@ +# 文章相关事件常量模块 +# 该模块定义了与文章操作相关的事件常量、内容钩子常量、位置钩子常量以及资源注入钩子常量 +# 这些常量用于在系统中统一标识不同的操作事件和钩子位置,便于模块间的交互和扩展 + +# 文章操作事件常量 +# 用于标识文章详情加载事件,当加载文章详情时触发相关处理逻辑 +ARTICLE_DETAIL_LOAD = 'article_detail_load' +# 用于标识文章创建事件,当创建新文章时触发相关处理逻辑 +ARTICLE_CREATE = 'article_create' +# 用于标识文章更新事件,当更新已有文章时触发相关处理逻辑 +ARTICLE_UPDATE = 'article_update' +# 用于标识文章删除事件,当删除文章时触发相关处理逻辑 +ARTICLE_DELETE = 'article_delete' + +# 文章内容钩子常量 +# 定义文章内容处理的钩子名称,用于在文章内容渲染前后插入自定义处理逻辑 +ARTICLE_CONTENT_HOOK_NAME = "the_content" + +# 位置钩子常量字典 +# 键为位置标识,值为对应的钩子名称,用于在页面不同位置挂载自定义组件或逻辑 +POSITION_HOOKS = { + 'article_top': 'article_top_widgets', # 文章顶部位置的钩子,用于挂载顶部组件 + 'article_bottom': 'article_bottom_widgets', # 文章底部位置的钩子,用于挂载底部组件 + 'sidebar': 'sidebar_widgets', # 侧边栏位置的钩子,用于挂载侧边栏组件 + 'header': 'header_widgets', # 页头位置的钩子,用于挂载页头组件 + 'footer': 'footer_widgets', # 页脚位置的钩子,用于挂载页脚组件 + 'comment_before': 'comment_before_widgets', # 评论区之前位置的钩子,用于在评论前插入内容 + 'comment_after': 'comment_after_widgets', # 评论区之后位置的钩子,用于在评论后插入内容 +} + +# 资源注入钩子常量 +# 用于标识在HTML头部注入资源(如CSS、JS)的钩子,可通过该钩子添加头部资源 +HEAD_RESOURCES_HOOK = 'head_resources' +# 用于标识在HTML body部分注入资源(如JS)的钩子,可通过该钩子添加body资源 +BODY_RESOURCES_HOOK = 'body_resources' diff --git a/src/djangoblog/djangoblog/plugin_manage/hooks.py b/src/djangoblog/djangoblog/plugin_manage/hooks.py new file mode 100644 index 00000000..f89071ba --- /dev/null +++ b/src/djangoblog/djangoblog/plugin_manage/hooks.py @@ -0,0 +1,89 @@ +# 钩子系统核心模块 +# 该模块实现了一个轻量级的钩子(Hook)机制,支持注册回调函数、执行动作钩子(Action Hook)和过滤钩子(Filter Hook) +# 主要功能包括:管理钩子与回调函数的映射关系、按顺序执行钩子回调、处理回调执行过程中的异常并记录日志 +# 适用于需要模块解耦、灵活扩展的场景,通过钩子机制实现不同组件间的间接交互 + +import logging + +# 初始化日志记录器,用于记录钩子注册、执行过程中的调试信息和错误信息 +logger = logging.getLogger(__name__) + +# 私有字典,用于存储钩子名称与回调函数列表的映射关系 +# 键为钩子名称(字符串),值为注册到该钩子的回调函数列表 +_hooks = {} + + +def register(hook_name: str, callback: callable): + """ + 注册一个回调函数到指定的钩子上。 + 同一钩子可以注册多个回调函数,执行时将按注册顺序依次调用。 + + 参数: + hook_name: 钩子名称,用于标识一组相关的回调函数 + callback: 可调用对象(函数、方法等),将在钩子触发时执行 + """ + # 如果钩子名称不在映射表中,初始化一个空列表用于存储回调 + if hook_name not in _hooks: + _hooks[hook_name] = [] + # 将回调函数添加到对应钩子的列表中 + _hooks[hook_name].append(callback) + # 记录调试日志,说明已成功注册回调 + logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'") + + +def run_action(hook_name: str, *args, **kwargs): + """ + 执行指定名称的动作钩子(Action Hook)。 + 动作钩子用于触发一系列操作,不关注返回值,按注册顺序依次执行所有回调函数。 + + 参数: + hook_name: 要执行的钩子名称 + *args: 传递给回调函数的位置参数 + **kwargs: 传递给回调函数的关键字参数 + """ + # 检查该钩子是否有注册的回调函数 + if hook_name in _hooks: + logger.debug(f"Running action hook '{hook_name}'") + # 遍历执行该钩子下的所有回调函数 + for callback in _hooks[hook_name]: + try: + # 调用回调函数并传递参数 + callback(*args, **kwargs) + except Exception as e: + # 捕获并记录回调执行过程中的异常,不中断后续回调 + logger.error( + f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", + exc_info=True # 记录完整的异常堆栈信息,便于调试 + ) + + +def apply_filters(hook_name: str, value, *args, **kwargs): + """ + 执行指定名称的过滤钩子(Filter Hook)。 + 过滤钩子用于对某个值进行一系列处理,将值依次传递给所有回调函数,最终返回处理后的结果。 + + 参数: + hook_name: 要执行的钩子名称 + value: 初始值,将被回调函数依次处理 + *args: 传递给回调函数的额外位置参数 + **kwargs: 传递给回调函数的额外关键字参数 + + 返回: + 经过所有回调函数处理后的最终值 + """ + # 检查该钩子是否有注册的回调函数 + if hook_name in _hooks: + logger.debug(f"Applying filter hook '{hook_name}'") + # 遍历执行该钩子下的所有回调函数,依次处理值 + for callback in _hooks[hook_name]: + try: + # 调用回调函数,传入当前值和其他参数,并用返回值更新当前值 + value = callback(value, *args, **kwargs) + except Exception as e: + # 捕获并记录回调执行过程中的异常,不中断后续回调 + logger.error( + f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", + exc_info=True # 记录完整的异常堆栈信息,便于调试 + ) + # 返回最终处理后的值 + return value \ No newline at end of file diff --git a/src/djangoblog/djangoblog/plugin_manage/loader.py b/src/djangoblog/djangoblog/plugin_manage/loader.py new file mode 100644 index 00000000..63814822 --- /dev/null +++ b/src/djangoblog/djangoblog/plugin_manage/loader.py @@ -0,0 +1,124 @@ +# 插件加载与管理模块 +# 该模块提供了Django应用中插件的动态加载、注册和查询功能 +# 主要功能包括:从指定目录加载激活的插件、维护插件注册表、提供多种插件查询接口 +# 插件需符合特定结构(包含plugin.py及插件实例),通过Django配置指定激活的插件和插件目录 + +import os +import logging +from django.conf import settings + +# 初始化日志记录器,用于记录插件加载过程中的信息和错误 +logger = logging.getLogger(__name__) + +# 全局插件注册表,存储所有已成功加载的插件实例 +_loaded_plugins = [] + + +def load_plugins(): + """ + 从'plugins'目录动态加载并初始化激活的插件。 + 该函数应在Django应用注册表就绪后调用(确保Django配置已加载)。 + 加载逻辑:遍历配置中激活的插件列表,检查插件目录结构完整性,导入并初始化插件实例。 + """ + global _loaded_plugins + # 重置插件注册表,避免重复加载 + _loaded_plugins = [] + + # 遍历配置中激活的插件名称列表(settings.ACTIVE_PLUGINS定义需加载的插件) + for plugin_name in settings.ACTIVE_PLUGINS: + # 构建插件目录的绝对路径(settings.PLUGINS_DIR为插件根目录) + plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name) + # 检查插件目录是否存在且包含必要的plugin.py文件(插件入口) + if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')): + try: + # 动态导入插件模块:从plugins.插件名.plugin导入模块 + # fromlist=['plugin']确保导入子模块而非父模块 + plugin_module = __import__(f'plugins.{plugin_name}.plugin', fromlist=['plugin']) + + # 检查导入的模块是否包含'plugin'属性(插件实例) + if hasattr(plugin_module, 'plugin'): + # 获取插件实例并添加到全局注册表 + plugin_instance = plugin_module.plugin + _loaded_plugins.append(plugin_instance) + # 记录成功加载日志,包含插件名称和插件定义的名称 + logger.info(f"Successfully loaded plugin: {plugin_name} - {plugin_instance.PLUGIN_NAME}") + else: + # 插件模块结构不完整(缺少plugin实例)时记录警告 + logger.warning(f"Plugin {plugin_name} does not have 'plugin' instance") + + except ImportError as e: + # 捕获导入错误(如模块不存在、依赖缺失等) + logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e) + except AttributeError as e: + # 捕获属性错误(如插件实例缺少必要属性) + logger.error(f"Failed to get plugin instance: {plugin_name}", exc_info=e) + except Exception as e: + # 捕获其他未预期的错误 + logger.error(f"Unexpected error loading plugin: {plugin_name}", exc_info=e) + + +def get_loaded_plugins(): + """ + 获取所有已加载的插件实例列表。 + + 返回: + list: 包含所有成功加载的插件实例的列表 + """ + return _loaded_plugins + + +def get_plugin_by_name(plugin_name): + """ + 根据插件名称查询插件实例(实际查询的是plugin_slug属性,可能与函数名存在命名兼容)。 + + 参数: + plugin_name: 要查询的插件slug名称 + + 返回: + 匹配的插件实例,若未找到则返回None + """ + for plugin in _loaded_plugins: + if plugin.plugin_slug == plugin_name: + return plugin + return None + + +def get_plugin_by_slug(plugin_slug): + """ + 根据插件slug查询插件实例(与plugin_slug属性精确匹配)。 + + 参数: + plugin_slug: 要查询的插件slug标识 + + 返回: + 匹配的插件实例,若未找到则返回None + """ + for plugin in _loaded_plugins: + if plugin.plugin_slug == plugin_slug: + return plugin + return None + + +def get_plugins_info(): + """ + 获取所有已加载插件的信息字典列表。 + 信息由插件的get_plugin_info()方法提供,通常包含名称、描述、版本等元数据。 + + 返回: + list: 每个元素为一个插件的信息字典 + """ + return [plugin.get_plugin_info() for plugin in _loaded_plugins] + + +def get_plugins_by_position(position): + """ + 获取支持指定位置的所有插件实例(基于插件的SUPPORTED_POSITIONS属性筛选)。 + 用于在页面特定位置渲染插件内容(如侧边栏、页头等)。 + + 参数: + position: 位置标识(如'sidebar'、'header'等,对应POSITION_HOOKS中的键) + + 返回: + list: 所有支持该位置的插件实例 + """ + return [plugin for plugin in _loaded_plugins if position in plugin.SUPPORTED_POSITIONS] \ No newline at end of file diff --git a/src/djangoblog/djangoblog/settings.py b/src/djangoblog/djangoblog/settings.py new file mode 100644 index 00000000..416148e3 --- /dev/null +++ b/src/djangoblog/djangoblog/settings.py @@ -0,0 +1,384 @@ +""" +Django settings for djangoblog project. + +Generated by 'django-admin startproject' using Django 1.10.2. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.10/ref/settings/ +""" +import os +import sys +from pathlib import Path + +from django.utils.translation import gettext_lazy as _ + + +def env_to_bool(env, default): + """将环境变量值转换为布尔值的工具函数""" + str_val = os.environ.get(env) + return default if str_val is None else str_val == 'True' + + +#姜雨菲: 构建项目路径,BASE_DIR为项目根目录 +BASE_DIR = Path(__file__).resolve().parent.parent + +#姜雨菲: 快速开发设置 - 不适用于生产环境 +# 安全警告:生产环境中请保持SECRET_KEY的机密性! +SECRET_KEY = os.environ.get( + 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' +# 安全警告:生产环境中请关闭DEBUG模式 +DEBUG = env_to_bool('DJANGO_DEBUG', True) +# 测试环境标识,当执行测试命令时为True +TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' + +# 允许访问的主机,生产环境需配置具体域名 +ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] +# Django 4.0新增配置,指定可信任的CSRF来源 +CSRF_TRUSTED_ORIGINS = ['http://example.com'] + +# 应用定义 +INSTALLED_APPS = [ + # 自定义的Admin配置(简化版) + 'django.contrib.admin.apps.SimpleAdminConfig', + 'django.contrib.auth', # 用户认证应用 + 'django.contrib.contenttypes', # 内容类型框架 + 'django.contrib.sessions', # 会话框架 + 'django.contrib.messages', # 消息框架 + 'django.contrib.staticfiles', # 静态文件管理 + 'django.contrib.sites', # 站点框架(用于多站点管理) + 'django.contrib.sitemaps', # 站点地图框架 + 'mdeditor', # Markdown编辑器应用 + 'haystack', # 搜索框架 + 'blog', # 博客应用 + 'accounts', # 用户账户应用 + 'comments', # 评论应用 + 'oauth', # 第三方登录应用 + 'servermanager', # 服务器管理应用 + 'owntracks', # 位置追踪应用 + 'compressor', # 静态文件压缩应用 + 'djangoblog' # 项目主应用 +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', # 安全中间件(处理HTTPS等安全相关) + 'django.contrib.sessions.middleware.SessionMiddleware', # 会话中间件 + 'django.middleware.locale.LocaleMiddleware', # 国际化中间件(语言切换) + 'django.middleware.gzip.GZipMiddleware', # GZip压缩中间件 + 'django.middleware.common.CommonMiddleware', # 通用中间件(处理请求/响应) + 'django.middleware.csrf.CsrfViewMiddleware', # CSRF保护中间件 + 'django.contrib.auth.middleware.AuthenticationMiddleware', # 认证中间件 + 'django.contrib.messages.middleware.MessageMiddleware', # 消息中间件 + 'django.middleware.clickjacking.XFrameOptionsMiddleware', # 点击劫持保护中间件 + 'django.middleware.http.ConditionalGetMiddleware', # 条件获取中间件(处理304响应) + 'blog.middleware.OnlineMiddleware' # 自定义在线用户统计中间件 +] + +ROOT_URLCONF = 'djangoblog.urls' # 项目URL配置入口 + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', # 模板引擎 + 'DIRS': [os.path.join(BASE_DIR, 'templates')], # 自定义模板目录 + 'APP_DIRS': True, # 是否自动搜索应用内的templates目录 + 'OPTIONS': { + 'context_processors': [ # 模板上下文处理器 + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'blog.context_processors.seo_processor' # 自定义SEO上下文处理器 + ], + }, + }, +] + +WSGI_APPLICATION = 'djangoblog.wsgi.application' # WSGI应用入口 + +# 数据库配置 +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', # 使用MySQL数据库引擎 + 'NAME': 'djangoblog', # 数据库名 + 'USER': 'root', # 数据库用户名 + 'PASSWORD': '050807', # 数据库密码 + 'HOST': '127.0.0.1', # 数据库主机 + 'PORT': 3306, # 数据库端口 + } +} + +# 密码验证配置 +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# 支持的语言列表(国际化配置) +LANGUAGES = ( + ('en', _('English')), + ('zh-hans', _('Simplified Chinese')), + ('zh-hant', _('Traditional Chinese')), +) +# 语言文件路径 +LOCALE_PATHS = ( + os.path.join(BASE_DIR, 'locale'), +) + +LANGUAGE_CODE = 'zh-hans' # 默认语言 +TIME_ZONE = 'Asia/Shanghai' # 时区 +USE_I18N = True # 启用国际化 +USE_L10N = True # 启用本地化格式 +USE_TZ = False # 不使用时区感知模型 + +# 搜索框架Haystack配置 +HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', # 使用Whoosh搜索引擎(中文适配版) + 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), # 索引存储路径 + }, +} +HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' # 实时信号处理器(自动更新索引) + +# 认证后端配置(支持用户名或邮箱登录) +AUTHENTICATION_BACKENDS = [ + 'accounts.user_login_backend.EmailOrUsernameModelBackend'] + +# 静态文件配置 +STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') # 静态文件收集目录(生产环境用) +STATIC_URL = '/static/' # 静态文件URL前缀 +STATICFILES = os.path.join(BASE_DIR, 'static') # 静态文件源目录 + +# 插件静态文件目录 +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, 'plugins'), # 插件静态文件目录 +] + +AUTH_USER_MODEL = 'accounts.BlogUser' # 自定义用户模型 +LOGIN_URL = '/login/' # 登录URL + +# 时间格式定义 +TIME_FORMAT = '%Y-%m-%d %H:%M:%S' +DATE_TIME_FORMAT = '%Y-%m-%d' + +# Bootstrap颜色样式 +BOOTSTRAP_COLOR_TYPES = [ + 'default', 'primary', 'success', 'info', 'warning', 'danger' +] + +# 分页配置 +PAGINATE_BY = 10 +# HTTP缓存超时时间(秒) +CACHE_CONTROL_MAX_AGE = 2592000 + +# 缓存配置 +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', # 本地内存缓存 + 'TIMEOUT': 10800, # 缓存超时时间(秒) + 'LOCATION': 'unique-snowflake', # 缓存位置标识 + } +} +# 若存在环境变量,则使用Redis作为缓存 +if os.environ.get("DJANGO_REDIS_URL"): + CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', + } + } + +SITE_ID = 1 # 站点ID(多站点时使用) +# 百度链接提交通知URL +BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \ + or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn' + +# 邮件配置 +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # SMTP邮件后端 +EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) # 是否使用TLS +EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) # 是否使用SSL +EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' # 邮件服务器 +EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) # 邮件端口 +EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') # 邮件用户名 +EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') # 邮件密码 +DEFAULT_FROM_EMAIL = EMAIL_HOST_USER # 默认发件人 +SERVER_EMAIL = EMAIL_HOST_USER # 服务器邮件发件人 +# 管理员邮件通知配置 +ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] +# 微信管理密码(两次MD5加密) +WXADMIN = os.environ.get( + 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7' + +# 日志配置 +LOG_PATH = os.path.join(BASE_DIR, 'logs') +if not os.path.exists(LOG_PATH): + os.makedirs(LOG_PATH, exist_ok=True) + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'root': { + 'level': 'INFO', + 'handlers': ['console', 'log_file'], + }, + 'formatters': { + 'verbose': { + 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s', + } + }, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + }, + 'handlers': { + 'log_file': { + 'level': 'INFO', + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': os.path.join(LOG_PATH, 'djangoblog.log'), + 'when': 'D', + 'formatter': 'verbose', + 'interval': 1, + 'delay': True, + 'backupCount': 5, + 'encoding': 'utf-8' + }, + 'console': { + 'level': 'DEBUG', + 'filters': ['require_debug_true'], + 'class': 'logging.StreamHandler', + 'formatter': 'verbose' + }, + 'null': { + 'class': 'logging.NullHandler', + }, + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'djangoblog': { + 'handlers': ['log_file', 'console'], + 'level': 'INFO', + 'propagate': True, + }, + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + } + } +} + +# 静态文件查找器(用于Compressor) +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'compressor.finders.CompressorFinder', +) +COMPRESS_ENABLED = True # 启用静态文件压缩 +# 根据环境变量决定是否启用离线压缩 +COMPRESS_OFFLINE = os.environ.get('COMPRESS_OFFLINE', 'False').lower() == 'true' + +COMPRESS_OUTPUT_DIR = 'compressed' # 压缩文件输出目录 +COMPRESS_CSS_HASHING_METHOD = 'mtime' # CSS哈希生成方式(基于修改时间) +COMPRESS_JS_HASHING_METHOD = 'mtime' # JS哈希生成方式(基于修改时间) + +# CSS压缩过滤器 +COMPRESS_CSS_FILTERS = [ + 'compressor.filters.css_default.CssAbsoluteFilter', # 处理CSS中的绝对URL + 'compressor.filters.cssmin.CSSCompressorFilter', # CSS压缩器 +] + +# JS压缩过滤器 +COMPRESS_JS_FILTERS = [ + 'compressor.filters.jsmin.SlimItFilter', # JS压缩器 +] + +COMPRESS_CACHE_BACKEND = 'default' # 压缩缓存后端 +COMPRESS_CACHE_KEY_FUNCTION = 'compressor.cache.simple_cachekey' # 缓存键生成函数 + +# 预编译器配置(支持SCSS/SASS) +COMPRESS_PRECOMPILERS = ( + ('text/x-scss', 'django_libsass.SassCompiler'), + ('text/x-sass', 'django_libsass.SassCompiler'), +) + +# 压缩性能优化配置 +COMPRESS_MINT_DELAY = 30 +COMPRESS_MTIME_DELAY = 10 +COMPRESS_REBUILD_TIMEOUT = 2592000 + +COMPRESS_CSS_COMPRESSOR = 'compressor.css.CssCompressor' +COMPRESS_JS_COMPRESSOR = 'compressor.js.JsCompressor' + +# 静态文件存储(带Manifest用于缓存破坏) +STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' + +COMPRESS_URL = STATIC_URL +COMPRESS_ROOT = STATIC_ROOT + +# 媒体文件(用户上传文件)配置 +MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') +MEDIA_URL = '/media/' + +# XFrameOptions配置(允许同域iframe) +X_FRAME_OPTIONS = 'SAMEORIGIN' + +# 安全头部配置 +SECURE_BROWSER_XSS_FILTER = True # 启用XSS过滤 +SECURE_CONTENT_TYPE_NOSNIFF = True # 禁止内容类型嗅探 +SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin' # Referrer策略 + +# 内容安全策略(CSP) +CSP_DEFAULT_SRC = ["'self'"] +CSP_SCRIPT_SRC = ["'self'", "'unsafe-inline'", "cdn.mathjax.org", "*.googleapis.com"] +CSP_STYLE_SRC = ["'self'", "'unsafe-inline'", "*.googleapis.com", "*.gstatic.com"] +CSP_IMG_SRC = ["'self'", "data:", "*.lylinux.net", "*.gravatar.com", "*.githubusercontent.com"] +CSP_FONT_SRC = ["'self'", "*.googleapis.com", "*.gstatic.com"] +CSP_CONNECT_SRC = ["'self'"] +CSP_FRAME_SRC = ["'none'"] +CSP_OBJECT_SRC = ["'none'"] + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # 默认自增字段类型 + +# 若存在环境变量,则使用Elasticsearch作为搜索后端 +if os.environ.get('DJANGO_ELASTICSEARCH_HOST'): + ELASTICSEARCH_DSL = { + 'default': { + 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') + }, + } + HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', + }, + } + +# 插件系统配置 +PLUGINS_DIR = BASE_DIR / 'plugins' +ACTIVE_PLUGINS = [ + 'article_copyright', # 文章版权插件 + 'reading_time', # 阅读时间插件 + 'external_links', # 外部链接插件 + 'view_count', # 阅读计数插件 + 'seo_optimizer', # SEO优化插件 + 'image_lazy_loading', # 图片懒加载插件 + 'article_recommendation', # 文章推荐插件 +] \ No newline at end of file diff --git a/src/djangoblog/djangoblog/sitemap.py b/src/djangoblog/djangoblog/sitemap.py new file mode 100644 index 00000000..75d22e09 --- /dev/null +++ b/src/djangoblog/djangoblog/sitemap.py @@ -0,0 +1,82 @@ +from django.contrib.sitemaps import Sitemap +from django.urls import reverse + +from blog.models import Article, Category, Tag + + +class StaticViewSitemap(Sitemap): + """ + 静态页面的站点地图类 + 用于生成网站中固定URL的页面(如首页)的站点地图条目 + """ + # 页面优先级(0.0-1.0,1.0表示最高优先级) + priority = 0.5 + # 页面内容更新频率(可选值:always, hourly, daily, weekly, monthly, yearly, never) + changefreq = 'daily' + + def items(self): + """返回需要包含在站点地图中的静态URL名称列表""" + # 这里仅包含博客首页的URL名称(对应urls.py中定义的name='blog:index') + return ['blog:index', ] + + def location(self, item): + """根据items返回的URL名称生成完整URL""" + return reverse(item) + + +class ArticleSiteMap(Sitemap): + """文章页面的站点地图类""" + changefreq = "monthly" # 文章内容更新频率为每月 + priority = "0.6" # 文章页面优先级 + + def items(self): + """返回需要包含的文章对象列表""" + # 仅包含状态为已发布(status='p')的文章 + return Article.objects.filter(status='p') + + def lastmod(self, obj): + """返回文章的最后修改时间(用于搜索引擎判断内容是否更新)""" + return obj.last_modify_time + + +class CategorySiteMap(Sitemap): + """分类页面的站点地图类""" + changefreq = "Weekly" # 分类页面更新频率为每周 + priority = "0.6" # 分类页面优先级 + + def items(self): + """返回所有分类对象""" + return Category.objects.all() + + def lastmod(self, obj): + """返回分类的最后修改时间""" + return obj.last_modify_time + + +class TagSiteMap(Sitemap): + """标签页面的站点地图类""" + changefreq = "Weekly" # 标签页面更新频率为每周 + priority = "0.3" # 标签页面优先级(低于文章和分类) + + def items(self): + """返回所有标签对象""" + return Tag.objects.all() + + def lastmod(self, obj): + """返回标签的最后修改时间""" + return obj.last_modify_time + + +class UserSiteMap(Sitemap): + """用户页面的站点地图类""" + changefreq = "Weekly" # 用户页面更新频率为每周 + priority = "0.3" # 用户页面优先级 + + def items(self): + """返回所有发布过文章的作者(去重处理)""" + # 通过文章作者去重,获取所有有发布文章的用户 + return list(set(map(lambda x: x.author, Article.objects.all()))) + + def lastmod(self, obj): + """返回用户的注册时间(作为最后修改时间的替代)""" + return obj.date_joined \ No newline at end of file diff --git a/src/djangoblog/djangoblog/spider_notify.py b/src/djangoblog/djangoblog/spider_notify.py new file mode 100644 index 00000000..c8f9f8c1 --- /dev/null +++ b/src/djangoblog/djangoblog/spider_notify.py @@ -0,0 +1,45 @@ +import logging + +import requests +from django.conf import settings + +# 创建当前模块的日志记录器,用于记录通知相关的日志信息 +logger = logging.getLogger(__name__) + + +class SpiderNotify(): + """ + 搜索引擎爬虫通知类 + 用于向搜索引擎(目前支持百度)提交网站URL,告知内容更新,便于爬虫抓取 + """ + + @staticmethod + def baidu_notify(urls): + """ + 向百度搜索引擎提交URL的静态方法 + 通过百度链接提交通知接口,告知百度新增/更新的页面URL + + Args: + urls (list): 需要提交的URL列表,每个元素为完整的页面URL字符串 + """ + try: + # 将URL列表用换行符拼接,符合百度接口的数据格式要求 + data = '\n'.join(urls) + # 向百度通知接口发送POST请求,提交URL数据 + result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) + # 记录接口返回的响应信息(日志级别:INFO) + logger.info(result.text) + except Exception as e: + # 捕获所有异常并记录错误信息(日志级别:ERROR) + logger.error(e) + + @staticmethod + def notify(url): + """ + 通用通知入口静态方法 + 统一调用百度通知方法,便于后续扩展支持其他搜索引擎 + + Args: + url (list): 需要提交的URL列表,与baidu_notify方法的urls参数格式一致 + """ + SpiderNotify.baidu_notify(url) \ No newline at end of file diff --git a/src/djangoblog/djangoblog/tests.py b/src/djangoblog/djangoblog/tests.py new file mode 100644 index 00000000..586b837c --- /dev/null +++ b/src/djangoblog/djangoblog/tests.py @@ -0,0 +1,51 @@ +from django.test import TestCase + +#姜雨菲: 导入项目工具模块中的所有工具函数/类 +from djangoblog.utils import * + + +class DjangoBlogTest(TestCase): + """ + 博客项目核心工具类的单元测试类 + 用于验证工具函数的功能正确性 + """ + + def setUp(self): + """ + 测试前置方法 + 在每个测试方法执行前调用,可用于初始化测试数据 + 此处暂无需初始化操作,保持空实现 + """ + pass + + def test_utils(self): + """ + 测试工具函数的功能 + 包括SHA256加密、Markdown解析和字典转URL参数功能 + """ + # 测试SHA256加密函数 + md5 = get_sha256('test') # 对字符串'test'进行SHA256加密 + self.assertIsNotNone(md5) # 断言加密结果不为空 + + # 测试Markdown解析功能 + # 定义一段包含标题、代码块、链接的Markdown文本 + c = CommonMarkdown.get_markdown(''' + # Title1 + + ```python + import os + ``` + + [url](https://www.lylinux.net/) + + [ddd](http://www.baidu.com) + + + ''') + self.assertIsNotNone(c) + d = { + 'd': 'key1', + 'd2': 'key2' + } + data = parse_dict_to_url(d) + self.assertIsNotNone(data) diff --git a/src/djangoblog/djangoblog/urls.py b/src/djangoblog/djangoblog/urls.py new file mode 100644 index 00000000..9e2274d4 --- /dev/null +++ b/src/djangoblog/djangoblog/urls.py @@ -0,0 +1,89 @@ +"""djangoblog URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf import settings +from django.conf.urls.i18n import i18n_patterns +from django.conf.urls.static import static +from django.contrib.sitemaps.views import sitemap +from django.urls import path, include +from django.urls import re_path +from haystack.views import search_view_factory +from django.http import JsonResponse +import time + +from blog.views import EsSearchView +from djangoblog.admin_site import admin_site # 自定义的admin站点 +from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm # ElasticSearch搜索表单 +from djangoblog.feeds import DjangoBlogFeed # RSS订阅源 +from djangoblog.sitemap import ( # 站点地图相关类 + ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap +) + +#姜雨菲: 站点地图配置:将不同类型的页面分别映射到对应的站点地图类 +sitemaps = { + 'blog': ArticleSiteMap, # 文章页面 + 'Category': CategorySiteMap, # 分类页面 + 'Tag': TagSiteMap, # 标签页面 + 'User': UserSiteMap, # 用户页面 + 'static': StaticViewSitemap # 静态页面 +} + +#姜雨菲: 自定义错误页面处理视图 +handler404 = 'blog.views.page_not_found_view' # 404页面未找到 +handler500 = 'blog.views.server_error_view' # 500服务器错误 +handle403 = 'blog.views.permission_denied_view' # 403权限拒绝 + + +def health_check(request): + """ + 健康检查接口 + 用于监控服务是否正常运行,简单返回服务健康状态和时间戳 + """ + return JsonResponse({ + 'status': 'healthy', # 健康状态标识 + 'timestamp': time.time() # 当前时间戳 + }) + +# 基础URL配置(不包含国际化前缀) +urlpatterns = [ + path('i18n/', include('django.conf.urls.i18n')), # 国际化配置入口 + path('health/', health_check, name='health_check'), # 健康检查接口 +] + +# 包含国际化前缀的URL配置(会自动添加语言代码前缀,如/en/、/zh-hans/) +urlpatterns += i18n_patterns( + re_path(r'^admin/', admin_site.urls), # 自定义admin后台URL + re_path(r'', include('blog.urls', namespace='blog')), # 博客应用URL,命名空间blog + re_path(r'mdeditor/', include('mdeditor.urls')), # Markdown编辑器URL + re_path(r'', include('comments.urls', namespace='comment')), # 评论应用URL,命名空间comment + re_path(r'', include('accounts.urls', namespace='account')), # 账户应用URL,命名空间account + re_path(r'', include('oauth.urls', namespace='oauth')), # 第三方登录应用URL,命名空间oauth + # 站点地图XML + re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, + name='django.contrib.sitemaps.views.sitemap'), + re_path(r'^feed/$', DjangoBlogFeed()), # RSS订阅源URL + re_path(r'^rss/$', DjangoBlogFeed()), # 另一个RSS订阅源URL(与feed功能相同) + # 搜索功能URL,使用自定义的EsSearchView和搜索表单 + re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), + name='search'), + re_path(r'', include('servermanager.urls', namespace='servermanager')), # 服务器管理应用URL + re_path(r'', include('owntracks.urls', namespace='owntracks')), # 位置追踪应用URL + prefix_default_language=False # 不为主语言添加前缀 +) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # 静态文件URL配置 + +# 开发环境下添加媒体文件(用户上传文件)的URL配置 +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/src/djangoblog/djangoblog/utils.py b/src/djangoblog/djangoblog/utils.py new file mode 100644 index 00000000..fb8b1f72 --- /dev/null +++ b/src/djangoblog/djangoblog/utils.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python +# encoding: utf-8 + + +import logging +import os +import random +import string +import uuid +from hashlib import sha256 + +import bleach +import markdown +import requests +from django.conf import settings +from django.contrib.sites.models import Site +from django.core.cache import cache +from django.templatetags.static import static + +#姜雨菲: 创建当前模块的日志记录器 +logger = logging.getLogger(__name__) + + +def get_max_articleid_commentid(): + """ + 获取最新文章和评论的ID + 用于获取当前系统中最新发布的文章ID和最新的评论ID + """ + from blog.models import Article + from comments.models import Comment + return (Article.objects.latest().pk, Comment.objects.latest().pk) + + +def get_sha256(str): + """ + 对字符串进行SHA256加密 + :param str: 需要加密的字符串 + :return: 加密后的十六进制字符串 + """ + m = sha256(str.encode('utf-8')) + return m.hexdigest() + + +def cache_decorator(expiration=3 * 60): + """ + 缓存装饰器 + 用于缓存函数返回结果,减少重复计算,默认缓存3分钟 + :param expiration: 缓存过期时间(秒) + :return: 装饰器函数 + """ + def wrapper(func): + def news(*args, **kwargs): + try: + # 尝试从第一个参数(通常是视图实例)获取缓存键 + view = args[0] + key = view.get_cache_key() + except: + # 获取失败时自动生成缓存键 + key = None + if not key: + # 根据函数和参数生成唯一字符串,用于创建缓存键 + unique_str = repr((func, args, kwargs)) + m = sha256(unique_str.encode('utf-8')) + key = m.hexdigest() + # 尝试从缓存获取数据 + value = cache.get(key) + if value is not None: + # 缓存命中时返回结果(过滤默认占位值) + if str(value) == '__default_cache_value__': + return None + else: + return value + else: + # 缓存未命中时执行原函数并缓存结果 + logger.debug( + 'cache_decorator set cache:%s key:%s' % + (func.__name__, key)) + value = func(*args, **kwargs) + if value is None: + cache.set(key, '__default_cache_value__', expiration) + else: + cache.set(key, value, expiration) + return value + + return news + + return wrapper + + +def expire_view_cache(path, servername, serverport, key_prefix=None): + ''' + 刷新视图缓存 + 手动删除指定URL路径的视图缓存 + :param path: URL路径 + :param servername: 主机名 + :param serverport: 端口号 + :param key_prefix: 缓存键前缀 + :return: 是否刷新成功(布尔值) + ''' + from django.http import HttpRequest + from django.utils.cache import get_cache_key + + # 构造模拟请求对象 + request = HttpRequest() + request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport} + request.path = path + + # 获取缓存键并删除缓存 + key = get_cache_key(request, key_prefix=key_prefix, cache=cache) + if key: + logger.info('expire_view_cache:get key:{path}'.format(path=path)) + if cache.get(key): + cache.delete(key) + return True + return False + + +@cache_decorator() +def get_current_site(): + """ + 获取当前站点信息(带缓存) + 从Django的Site模型获取当前站点配置,结果缓存3分钟 + :return: Site模型实例 + """ + site = Site.objects.get_current() + return site + + +class CommonMarkdown: + """ + Markdown解析工具类 + 提供Markdown文本转HTML的功能,支持代码高亮、目录生成等 + """ + @staticmethod + def _convert_markdown(value): + """ + 内部Markdown转换方法 + 配置Markdown解析器并转换文本 + :param value: Markdown格式文本 + :return: (转换后的HTML内容, 目录HTML) + """ + md = markdown.Markdown( + extensions=[ + 'extra', # 额外功能(表格、脚注等) + 'codehilite', # 代码高亮 + 'toc', # 目录生成 + 'tables', # 表格支持 + ] + ) + body = md.convert(value) + toc = md.toc + return body, toc + + @staticmethod + def get_markdown_with_toc(value): + """ + Markdown转HTML(带目录) + :param value: Markdown格式文本 + :return: (HTML内容, 目录HTML) + """ + body, toc = CommonMarkdown._convert_markdown(value) + return body, toc + + @staticmethod + def get_markdown(value): + """ + Markdown转HTML(仅内容,不含目录) + :param value: Markdown格式文本 + :return: 转换后的HTML内容 + """ + body, toc = CommonMarkdown._convert_markdown(value) + return body + + +def send_email(emailto, title, content): + """ + 发送邮件(通过信号机制) + 触发邮件发送信号,解耦邮件发送逻辑 + :param emailto: 收件人邮箱 + :param title: 邮件标题 + :param content: 邮件内容 + """ + from djangoblog.blog_signals import send_email_signal + send_email_signal.send( + send_email.__class__, + emailto=emailto, + title=title, + content=content) + + +def generate_code() -> str: + """生成6位随机数字验证码""" + return ''.join(random.sample(string.digits, 6)) + + +def parse_dict_to_url(dict): + """ + 将字典转换为URL查询字符串 + 对键值对进行URL编码,避免特殊字符问题 + :param dict: 待转换的字典 + :return: URL查询字符串(格式:key1=value1&key2=value2) + """ + from urllib.parse import quote + url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/')) + for k, v in dict.items()]) + return url + + +def get_blog_setting(): + """ + 获取博客系统配置(带缓存) + 从数据库获取博客全局配置,无配置时创建默认配置,结果缓存 + :return: BlogSettings模型实例 + """ + value = cache.get('get_blog_setting') + if value: + return value + else: + from blog.models import BlogSettings + # 无配置时初始化默认配置 + if not BlogSettings.objects.count(): + setting = BlogSettings() + setting.site_name = 'djangoblog' + setting.site_description = '基于Django的博客系统' + setting.site_seo_description = '基于Django的博客系统' + setting.site_keywords = 'Django,Python' + setting.article_sub_length = 300 + setting.sidebar_article_count = 10 + setting.sidebar_comment_count = 5 + setting.show_google_adsense = False + setting.open_site_comment = True + setting.analytics_code = '' + setting.beian_code = '' + setting.show_gongan_code = False + setting.comment_need_review = False + setting.save() + value = BlogSettings.objects.first() + logger.info('set cache get_blog_setting') + cache.set('get_blog_setting', value) + return value + + +def save_user_avatar(url): + ''' + 保存用户头像到本地静态文件目录 + 从URL下载头像并保存,支持常见图片格式,失败时返回默认头像 + :param url: 头像图片URL + :return: 本地头像的静态文件URL + ''' + logger.info(url) + + try: + # 定义头像保存目录 + basedir = os.path.join(settings.STATICFILES, 'avatar') + # 下载头像图片 + rsp = requests.get(url, timeout=2) + if rsp.status_code == 200: + # 目录不存在时创建 + if not os.path.exists(basedir): + os.makedirs(basedir) + + # 验证图片格式 + image_extensions = ['.jpg', '.png', 'jpeg', '.gif'] + isimage = len([i for i in image_extensions if url.endswith(i)]) > 0 + ext = os.path.splitext(url)[1] if isimage else '.jpg' + # 生成唯一文件名 + save_filename = str(uuid.uuid4().hex) + ext + logger.info('保存用户头像:' + basedir + save_filename) + # 写入文件 + with open(os.path.join(basedir, save_filename), 'wb+') as file: + file.write(rsp.content) + # 返回静态文件URL + return static('avatar/' + save_filename) + except Exception as e: + logger.error(e) + # 异常时返回默认头像 + return static('blog/img/avatar.png') + + +def delete_sidebar_cache(): + """ + 删除侧边栏缓存 + 根据LinkShowType的所有值生成缓存键并删除 + """ + from blog.models import LinkShowType + keys = ["sidebar" + x for x in LinkShowType.values] + for k in keys: + logger.info('delete sidebar key:' + k) + cache.delete(k) + + +def delete_view_cache(prefix, keys): + """ + 删除模板片段缓存 + :param prefix: 缓存前缀 + :param keys: 缓存键列表 + """ + from django.core.cache.utils import make_template_fragment_key + key = make_template_fragment_key(prefix, keys) + cache.delete(key) + + +def get_resource_url(): + """ + 获取静态资源基础URL + 优先使用settings中的STATIC_URL,无配置时使用站点域名拼接/static/ + :return: 静态资源基础URL + """ + if settings.STATIC_URL: + return settings.STATIC_URL + else: + site = get_current_site() + return 'http://' + site.domain + '/static/' + + +# HTML清理配置 - 防止XSS攻击 +# 允许的HTML标签白名单 +ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', + 'h2', 'p', 'span', 'div'] + +# 允许的CSS类白名单(主要用于代码高亮) +ALLOWED_CLASSES = [ + 'codehilite', 'highlight', 'hll', 'c', 'err', 'k', 'l', 'n', 'o', 'p', 'cm', 'cp', 'c1', 'cs', + 'gd', 'ge', 'gr', 'gh', 'gi', 'go', 'gp', 'gs', 'gu', 'gt', 'kc', 'kd', 'kn', 'kp', 'kr', 'kt', + 'ld', 'm', 'mf', 'mh', 'mi', 'mo', 'na', 'nb', 'nc', 'no', 'nd', 'ni', 'ne', 'nf', 'nl', 'nn', + 'nt', 'nv', 'ow', 'w', 'mb', 'mh', 'mi', 'mo', 'sb', 'sc', 'sd', 'se', 'sh', 'si', 'sx', 's2', + 's1', 'ss', 'bp', 'vc', 'vg', 'vi', 'il' +] + +def class_filter(tag, name, value): + """ + 自定义class属性过滤器 + 只保留ALLOWED_CLASSES中的CSS类,过滤危险或未授权的类名 + :param tag: HTML标签名 + :param name: 属性名 + :param value: 属性值 + :return: 过滤后的属性值,无合法类时返回False + """ + if name == 'class': + allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES] + return ' '.join(allowed_classes) if allowed_classes else False + return value + +# 允许的HTML属性白名单 +ALLOWED_ATTRIBUTES = { + 'a': ['href', 'title'], # 链接标签允许的属性 + 'abbr': ['title'], # 缩写标签允许的属性 + 'acronym': ['title'], # 首字母缩写标签允许的属性 + 'span': class_filter, # span标签的class属性使用自定义过滤器 + 'div': class_filter, # div标签的class属性使用自定义过滤器 + 'pre': class_filter, # pre标签的class属性使用自定义过滤器 + 'code': class_filter # code标签的class属性使用自定义过滤器 +} + +# 允许的URL协议白名单(防止javascript:等危险协议) +ALLOWED_PROTOCOLS = ['http', 'https', 'mailto'] + +def sanitize_html(html): + """ + 安全的HTML清理函数 + 使用bleach库过滤危险HTML内容,防止XSS攻击 + :param html: 需要清理的HTML字符串 + :return: 安全的HTML字符串 + """ + return bleach.clean( + html, + tags=ALLOWED_TAGS, # 只允许白名单中的标签 + attributes=ALLOWED_ATTRIBUTES, # 只允许白名单中的属性 + protocols=ALLOWED_PROTOCOLS, # 限制URL协议 + strip=True, # 移除不允许的标签(而非转义) + strip_comments=True # 移除HTML注释 + ) \ No newline at end of file diff --git a/src/djangoblog/djangoblog/whoosh_cn_backend.py b/src/djangoblog/djangoblog/whoosh_cn_backend.py new file mode 100644 index 00000000..b885613f --- /dev/null +++ b/src/djangoblog/djangoblog/whoosh_cn_backend.py @@ -0,0 +1,1044 @@ +# encoding: utf-8 + +from __future__ import absolute_import, division, print_function, unicode_literals + +import json +import os +import re +import shutil +import threading +import warnings + +import six +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from datetime import datetime +from django.utils.encoding import force_str +from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query +from haystack.constants import DJANGO_CT, DJANGO_ID, ID +from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument +from haystack.inputs import Clean, Exact, PythonData, Raw +from haystack.models import SearchResult +from haystack.utils import get_identifier, get_model_ct +from haystack.utils import log as logging +from haystack.utils.app_loading import haystack_get_model +from jieba.analyse import ChineseAnalyzer +from whoosh import index +from whoosh.analysis import StemmingAnalyzer +from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT +from whoosh.fields import ID as WHOOSH_ID +from whoosh.filedb.filestore import FileStorage, RamStorage +from whoosh.highlight import ContextFragmenter, HtmlFormatter +from whoosh.highlight import highlight as whoosh_highlight +from whoosh.qparser import QueryParser +from whoosh.searching import ResultsPage +from whoosh.writing import AsyncWriter + +try: + import whoosh +except ImportError: + raise MissingDependency( + "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.") + +#姜雨菲: 检查Whoosh版本,要求2.5.0及以上 +if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0): + raise MissingDependency( + "The 'whoosh' backend requires version 2.5.0 or greater.") + +#姜雨菲:日期时间格式正则表达式,用于解析Whoosh返回的日期时间字符串 +DATETIME_REGEX = re.compile( + '^(?P\d{4})-(?P\d{2})-(?P\d{2})T(?P\d{2}):(?P\d{2}):(?P\d{2})(\.\d{3,6}Z?)?$') +LOCALS = threading.local() +LOCALS.RAM_STORE = None # 线程本地存储,用于内存存储模式 + + + +class WhooshHtmlFormatter(HtmlFormatter): + """ + This is a HtmlFormatter simpler than the whoosh.HtmlFormatter. + We use it to have consistent results across backends. Specifically, + Solr, Xapian and Elasticsearch are using this formatting. + """ + template = '<%(tag)s>%(t)s' + + +class WhooshSearchBackend(BaseSearchBackend): + # Word reserved by Whoosh for special use. + RESERVED_WORDS = ( + 'AND', + 'NOT', + 'OR', + 'TO', + ) + + # Characters reserved by Whoosh for special use. + # The '\\' must come first, so as not to overwrite the other slash + # replacements. + RESERVED_CHARACTERS = ( + '\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}', + '[', ']', '^', '"', '~', '*', '?', ':', '.', + ) + + def __init__(self, connection_alias, **connection_options): + super( + WhooshSearchBackend, + self).__init__( + connection_alias, + **connection_options) + self.setup_complete = False + self.use_file_storage = True + self.post_limit = getattr( + connection_options, + 'POST_LIMIT', + 128 * 1024 * 1024) + self.path = connection_options.get('PATH') + + if connection_options.get('STORAGE', 'file') != 'file': + self.use_file_storage = False + + if self.use_file_storage and not self.path: + raise ImproperlyConfigured( + "You must specify a 'PATH' in your settings for connection '%s'." % + connection_alias) + + self.log = logging.getLogger('haystack') + + def setup(self): + """ + Defers loading until needed. + """ + from haystack import connections + new_index = False + + # Make sure the index is there. + if self.use_file_storage and not os.path.exists(self.path): + os.makedirs(self.path) + new_index = True + + if self.use_file_storage and not os.access(self.path, os.W_OK): + raise IOError( + "The path to your Whoosh index '%s' is not writable for the current user/group." % + self.path) + + if self.use_file_storage: + self.storage = FileStorage(self.path) + else: + global LOCALS + + if getattr(LOCALS, 'RAM_STORE', None) is None: + LOCALS.RAM_STORE = RamStorage() + + self.storage = LOCALS.RAM_STORE + + self.content_field_name, self.schema = self.build_schema( + connections[self.connection_alias].get_unified_index().all_searchfields()) + self.parser = QueryParser(self.content_field_name, schema=self.schema) + + if new_index is True: + self.index = self.storage.create_index(self.schema) + else: + try: + self.index = self.storage.open_index(schema=self.schema) + except index.EmptyIndexError: + self.index = self.storage.create_index(self.schema) + + self.setup_complete = True + + def build_schema(self, fields): + schema_fields = { + ID: WHOOSH_ID(stored=True, unique=True), + DJANGO_CT: WHOOSH_ID(stored=True), + DJANGO_ID: WHOOSH_ID(stored=True), + } + # Grab the number of keys that are hard-coded into Haystack. + # We'll use this to (possibly) fail slightly more gracefully later. + initial_key_count = len(schema_fields) + content_field_name = '' + + for field_name, field_class in fields.items(): + if field_class.is_multivalued: + if field_class.indexed is False: + schema_fields[field_class.index_fieldname] = IDLIST( + stored=True, field_boost=field_class.boost) + else: + schema_fields[field_class.index_fieldname] = KEYWORD( + stored=True, commas=True, scorable=True, field_boost=field_class.boost) + elif field_class.field_type in ['date', 'datetime']: + schema_fields[field_class.index_fieldname] = DATETIME( + stored=field_class.stored, sortable=True) + elif field_class.field_type == 'integer': + schema_fields[field_class.index_fieldname] = NUMERIC( + stored=field_class.stored, numtype=int, field_boost=field_class.boost) + elif field_class.field_type == 'float': + schema_fields[field_class.index_fieldname] = NUMERIC( + stored=field_class.stored, numtype=float, field_boost=field_class.boost) + elif field_class.field_type == 'boolean': + # Field boost isn't supported on BOOLEAN as of 1.8.2. + schema_fields[field_class.index_fieldname] = BOOLEAN( + stored=field_class.stored) + elif field_class.field_type == 'ngram': + schema_fields[field_class.index_fieldname] = NGRAM( + minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost) + elif field_class.field_type == 'edge_ngram': + schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start', + stored=field_class.stored, + field_boost=field_class.boost) + else: + # schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=StemmingAnalyzer(), field_boost=field_class.boost, sortable=True) + schema_fields[field_class.index_fieldname] = TEXT( + stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True) + if field_class.document is True: + content_field_name = field_class.index_fieldname + schema_fields[field_class.index_fieldname].spelling = True + + # Fail more gracefully than relying on the backend to die if no fields + # are found. + if len(schema_fields) <= initial_key_count: + raise SearchBackendError( + "No fields were found in any search_indexes. Please correct this before attempting to search.") + + return (content_field_name, Schema(**schema_fields)) + + def update(self, index, iterable, commit=True): + if not self.setup_complete: + self.setup() + + self.index = self.index.refresh() + writer = AsyncWriter(self.index) + + for obj in iterable: + try: + doc = index.full_prepare(obj) + except SkipDocument: + self.log.debug(u"Indexing for object `%s` skipped", obj) + else: + # Really make sure it's unicode, because Whoosh won't have it any + # other way. + for key in doc: + doc[key] = self._from_python(doc[key]) + + # Document boosts aren't supported in Whoosh 2.5.0+. + if 'boost' in doc: + del doc['boost'] + + try: + writer.update_document(**doc) + except Exception as e: + if not self.silently_fail: + raise + + # We'll log the object identifier but won't include the actual object + # to avoid the possibility of that generating encoding errors while + # processing the log message: + self.log.error( + u"%s while preparing object for update" % + e.__class__.__name__, + exc_info=True, + extra={ + "data": { + "index": index, + "object": get_identifier(obj)}}) + + if len(iterable) > 0: + # For now, commit no matter what, as we run into locking issues + # otherwise. + writer.commit() + + def remove(self, obj_or_string, commit=True): + if not self.setup_complete: + self.setup() + + self.index = self.index.refresh() + whoosh_id = get_identifier(obj_or_string) + + try: + self.index.delete_by_query( + q=self.parser.parse( + u'%s:"%s"' % + (ID, whoosh_id))) + except Exception as e: + if not self.silently_fail: + raise + + self.log.error( + "Failed to remove document '%s' from Whoosh: %s", + whoosh_id, + e, + exc_info=True) + + def clear(self, models=None, commit=True): + if not self.setup_complete: + self.setup() + + self.index = self.index.refresh() + + if models is not None: + assert isinstance(models, (list, tuple)) + + try: + if models is None: + self.delete_index() + else: + models_to_delete = [] + + for model in models: + models_to_delete.append( + u"%s:%s" % + (DJANGO_CT, get_model_ct(model))) + + self.index.delete_by_query( + q=self.parser.parse( + u" OR ".join(models_to_delete))) + except Exception as e: + if not self.silently_fail: + raise + + if models is not None: + self.log.error( + "Failed to clear Whoosh index of models '%s': %s", + ','.join(models_to_delete), + e, + exc_info=True) + else: + self.log.error( + "Failed to clear Whoosh index: %s", e, exc_info=True) + + def delete_index(self): + # Per the Whoosh mailing list, if wiping out everything from the index, + # it's much more efficient to simply delete the index files. + if self.use_file_storage and os.path.exists(self.path): + shutil.rmtree(self.path) + elif not self.use_file_storage: + self.storage.clean() + + # Recreate everything. + self.setup() + + def optimize(self): + if not self.setup_complete: + self.setup() + + self.index = self.index.refresh() + self.index.optimize() + + def calculate_page(self, start_offset=0, end_offset=None): + # Prevent against Whoosh throwing an error. Requires an end_offset + # greater than 0. + if end_offset is not None and end_offset <= 0: + end_offset = 1 + + # Determine the page. + page_num = 0 + + if end_offset is None: + end_offset = 1000000 + + if start_offset is None: + start_offset = 0 + + page_length = end_offset - start_offset + + if page_length and page_length > 0: + page_num = int(start_offset / page_length) + + # Increment because Whoosh uses 1-based page numbers. + page_num += 1 + return page_num, page_length + + @log_query + def search( + self, + query_string, + sort_by=None, + start_offset=0, + end_offset=None, + fields='', + highlight=False, + facets=None, + date_facets=None, + query_facets=None, + narrow_queries=None, + spelling_query=None, + within=None, + dwithin=None, + distance_point=None, + models=None, + limit_to_registered_models=None, + result_class=None, + **kwargs): + if not self.setup_complete: + self.setup() + + # A zero length query should return no results. + if len(query_string) == 0: + return { + 'results': [], + 'hits': 0, + } + + query_string = force_str(query_string) + + # A one-character query (non-wildcard) gets nabbed by a stopwords + # filter and should yield zero results. + if len(query_string) <= 1 and query_string != u'*': + return { + 'results': [], + 'hits': 0, + } + + reverse = False + + if sort_by is not None: + # Determine if we need to reverse the results and if Whoosh can + # handle what it's being asked to sort by. Reversing is an + # all-or-nothing action, unfortunately. + sort_by_list = [] + reverse_counter = 0 + + for order_by in sort_by: + if order_by.startswith('-'): + reverse_counter += 1 + + if reverse_counter and reverse_counter != len(sort_by): + raise SearchBackendError("Whoosh requires all order_by fields" + " to use the same sort direction") + + for order_by in sort_by: + if order_by.startswith('-'): + sort_by_list.append(order_by[1:]) + + if len(sort_by_list) == 1: + reverse = True + else: + sort_by_list.append(order_by) + + if len(sort_by_list) == 1: + reverse = False + + sort_by = sort_by_list[0] + + if facets is not None: + warnings.warn( + "Whoosh does not handle faceting.", + Warning, + stacklevel=2) + + if date_facets is not None: + warnings.warn( + "Whoosh does not handle date faceting.", + Warning, + stacklevel=2) + + if query_facets is not None: + warnings.warn( + "Whoosh does not handle query faceting.", + Warning, + stacklevel=2) + + narrowed_results = None + self.index = self.index.refresh() + + if limit_to_registered_models is None: + limit_to_registered_models = getattr( + settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) + + if models and len(models): + model_choices = sorted(get_model_ct(model) for model in models) + elif limit_to_registered_models: + # Using narrow queries, limit the results to only models handled + # with the current routers. + model_choices = self.build_models_list() + else: + model_choices = [] + + if len(model_choices) > 0: + if narrow_queries is None: + narrow_queries = set() + + narrow_queries.add(' OR '.join( + ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices])) + + narrow_searcher = None + + if narrow_queries is not None: + # Potentially expensive? I don't see another way to do it in + # Whoosh... + narrow_searcher = self.index.searcher() + + for nq in narrow_queries: + recent_narrowed_results = narrow_searcher.search( + self.parser.parse(force_str(nq)), limit=None) + + if len(recent_narrowed_results) <= 0: + return { + 'results': [], + 'hits': 0, + } + + if narrowed_results: + narrowed_results.filter(recent_narrowed_results) + else: + narrowed_results = recent_narrowed_results + + self.index = self.index.refresh() + + if self.index.doc_count(): + searcher = self.index.searcher() + parsed_query = self.parser.parse(query_string) + + # In the event of an invalid/stopworded query, recover gracefully. + if parsed_query is None: + return { + 'results': [], + 'hits': 0, + } + + page_num, page_length = self.calculate_page( + start_offset, end_offset) + + search_kwargs = { + 'pagelen': page_length, + 'sortedby': sort_by, + 'reverse': reverse, + } + + # Handle the case where the results have been narrowed. + if narrowed_results is not None: + search_kwargs['filter'] = narrowed_results + + try: + raw_page = searcher.search_page( + parsed_query, + page_num, + **search_kwargs + ) + except ValueError: + if not self.silently_fail: + raise + + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + # Because as of Whoosh 2.5.1, it will return the wrong page of + # results if you request something too high. :( + if raw_page.pagenum < page_num: + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + results = self._process_results( + raw_page, + highlight=highlight, + query_string=query_string, + spelling_query=spelling_query, + result_class=result_class) + searcher.close() + + if hasattr(narrow_searcher, 'close'): + narrow_searcher.close() + + return results + else: + if self.include_spelling: + if spelling_query: + spelling_suggestion = self.create_spelling_suggestion( + spelling_query) + else: + spelling_suggestion = self.create_spelling_suggestion( + query_string) + else: + spelling_suggestion = None + + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': spelling_suggestion, + } + + def more_like_this( + self, + model_instance, + additional_query_string=None, + start_offset=0, + end_offset=None, + models=None, + limit_to_registered_models=None, + result_class=None, + **kwargs): + if not self.setup_complete: + self.setup() + + # Deferred models will have a different class ("RealClass_Deferred_fieldname") + # which won't be in our registry: + model_klass = model_instance._meta.concrete_model + + field_name = self.content_field_name + narrow_queries = set() + narrowed_results = None + self.index = self.index.refresh() + + if limit_to_registered_models is None: + limit_to_registered_models = getattr( + settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) + + if models and len(models): + model_choices = sorted(get_model_ct(model) for model in models) + elif limit_to_registered_models: + # Using narrow queries, limit the results to only models handled + # with the current routers. + model_choices = self.build_models_list() + else: + model_choices = [] + + if len(model_choices) > 0: + if narrow_queries is None: + narrow_queries = set() + + narrow_queries.add(' OR '.join( + ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices])) + + if additional_query_string and additional_query_string != '*': + narrow_queries.add(additional_query_string) + + narrow_searcher = None + + if narrow_queries is not None: + # Potentially expensive? I don't see another way to do it in + # Whoosh... + narrow_searcher = self.index.searcher() + + for nq in narrow_queries: + recent_narrowed_results = narrow_searcher.search( + self.parser.parse(force_str(nq)), limit=None) + + if len(recent_narrowed_results) <= 0: + return { + 'results': [], + 'hits': 0, + } + + if narrowed_results: + narrowed_results.filter(recent_narrowed_results) + else: + narrowed_results = recent_narrowed_results + + page_num, page_length = self.calculate_page(start_offset, end_offset) + + self.index = self.index.refresh() + raw_results = EmptyResults() + + if self.index.doc_count(): + query = "%s:%s" % (ID, get_identifier(model_instance)) + searcher = self.index.searcher() + parsed_query = self.parser.parse(query) + results = searcher.search(parsed_query) + + if len(results): + raw_results = results[0].more_like_this( + field_name, top=end_offset) + + # Handle the case where the results have been narrowed. + if narrowed_results is not None and hasattr(raw_results, 'filter'): + raw_results.filter(narrowed_results) + + try: + raw_page = ResultsPage(raw_results, page_num, page_length) + except ValueError: + if not self.silently_fail: + raise + + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + # Because as of Whoosh 2.5.1, it will return the wrong page of + # results if you request something too high. :( + if raw_page.pagenum < page_num: + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + results = self._process_results(raw_page, result_class=result_class) + searcher.close() + + if hasattr(narrow_searcher, 'close'): + narrow_searcher.close() + + return results + + def _process_results( + self, + raw_page, + highlight=False, + query_string='', + spelling_query=None, + result_class=None): + from haystack import connections + results = [] + + # It's important to grab the hits first before slicing. Otherwise, this + # can cause pagination failures. + hits = len(raw_page) + + if result_class is None: + result_class = SearchResult + + facets = {} + spelling_suggestion = None + unified_index = connections[self.connection_alias].get_unified_index() + indexed_models = unified_index.get_indexed_models() + + for doc_offset, raw_result in enumerate(raw_page): + score = raw_page.score(doc_offset) or 0 + app_label, model_name = raw_result[DJANGO_CT].split('.') + additional_fields = {} + model = haystack_get_model(app_label, model_name) + + if model and model in indexed_models: + for key, value in raw_result.items(): + index = unified_index.get_index(model) + string_key = str(key) + + if string_key in index.fields and hasattr( + index.fields[string_key], 'convert'): + # Special-cased due to the nature of KEYWORD fields. + if index.fields[string_key].is_multivalued: + if value is None or len(value) == 0: + additional_fields[string_key] = [] + else: + additional_fields[string_key] = value.split( + ',') + else: + additional_fields[string_key] = index.fields[string_key].convert( + value) + else: + additional_fields[string_key] = self._to_python(value) + + del (additional_fields[DJANGO_CT]) + del (additional_fields[DJANGO_ID]) + + if highlight: + sa = StemmingAnalyzer() + formatter = WhooshHtmlFormatter('em') + terms = [token.text for token in sa(query_string)] + + whoosh_result = whoosh_highlight( + additional_fields.get(self.content_field_name), + terms, + sa, + ContextFragmenter(), + formatter + ) + additional_fields['highlighted'] = { + self.content_field_name: [whoosh_result], + } + + result = result_class( + app_label, + model_name, + raw_result[DJANGO_ID], + score, + **additional_fields) + results.append(result) + else: + hits -= 1 + + if self.include_spelling: + if spelling_query: + spelling_suggestion = self.create_spelling_suggestion( + spelling_query) + else: + spelling_suggestion = self.create_spelling_suggestion( + query_string) + + return { + 'results': results, + 'hits': hits, + 'facets': facets, + 'spelling_suggestion': spelling_suggestion, + } + + def create_spelling_suggestion(self, query_string): + spelling_suggestion = None + reader = self.index.reader() + corrector = reader.corrector(self.content_field_name) + cleaned_query = force_str(query_string) + + if not query_string: + return spelling_suggestion + + # Clean the string. + for rev_word in self.RESERVED_WORDS: + cleaned_query = cleaned_query.replace(rev_word, '') + + for rev_char in self.RESERVED_CHARACTERS: + cleaned_query = cleaned_query.replace(rev_char, '') + + # Break it down. + query_words = cleaned_query.split() + suggested_words = [] + + for word in query_words: + suggestions = corrector.suggest(word, limit=1) + + if len(suggestions) > 0: + suggested_words.append(suggestions[0]) + + spelling_suggestion = ' '.join(suggested_words) + return spelling_suggestion + + def _from_python(self, value): + """ + Converts Python values to a string for Whoosh. + + Code courtesy of pysolr. + """ + if hasattr(value, 'strftime'): + if not hasattr(value, 'hour'): + value = datetime(value.year, value.month, value.day, 0, 0, 0) + elif isinstance(value, bool): + if value: + value = 'true' + else: + value = 'false' + elif isinstance(value, (list, tuple)): + value = u','.join([force_str(v) for v in value]) + elif isinstance(value, (six.integer_types, float)): + # Leave it alone. + pass + else: + value = force_str(value) + return value + + def _to_python(self, value): + """ + Converts values from Whoosh to native Python values. + + A port of the same method in pysolr, as they deal with data the same way. + """ + if value == 'true': + return True + elif value == 'false': + return False + + if value and isinstance(value, six.string_types): + possible_datetime = DATETIME_REGEX.search(value) + + if possible_datetime: + date_values = possible_datetime.groupdict() + + for dk, dv in date_values.items(): + date_values[dk] = int(dv) + + return datetime( + date_values['year'], + date_values['month'], + date_values['day'], + date_values['hour'], + date_values['minute'], + date_values['second']) + + try: + # Attempt to use json to load the values. + converted_value = json.loads(value) + + # Try to handle most built-in types. + if isinstance( + converted_value, + (list, + tuple, + set, + dict, + six.integer_types, + float, + complex)): + return converted_value + except BaseException: + # If it fails (SyntaxError or its ilk) or we don't trust it, + # continue on. + pass + + return value + + +class WhooshSearchQuery(BaseSearchQuery): + def _convert_datetime(self, date): + if hasattr(date, 'hour'): + return force_str(date.strftime('%Y%m%d%H%M%S')) + else: + return force_str(date.strftime('%Y%m%d000000')) + + def clean(self, query_fragment): + """ + Provides a mechanism for sanitizing user input before presenting the + value to the backend. + + Whoosh 1.X differs here in that you can no longer use a backslash + to escape reserved characters. Instead, the whole word should be + quoted. + """ + words = query_fragment.split() + cleaned_words = [] + + for word in words: + if word in self.backend.RESERVED_WORDS: + word = word.replace(word, word.lower()) + + for char in self.backend.RESERVED_CHARACTERS: + if char in word: + word = "'%s'" % word + break + + cleaned_words.append(word) + + return ' '.join(cleaned_words) + + def build_query_fragment(self, field, filter_type, value): + from haystack import connections + query_frag = '' + is_datetime = False + + if not hasattr(value, 'input_type_name'): + # Handle when we've got a ``ValuesListQuerySet``... + if hasattr(value, 'values_list'): + value = list(value) + + if hasattr(value, 'strftime'): + is_datetime = True + + if isinstance(value, six.string_types) and value != ' ': + # It's not an ``InputType``. Assume ``Clean``. + value = Clean(value) + else: + value = PythonData(value) + + # Prepare the query using the InputType. + prepared_value = value.prepare(self) + + if not isinstance(prepared_value, (set, list, tuple)): + # Then convert whatever we get back to what pysolr wants if needed. + prepared_value = self.backend._from_python(prepared_value) + + # 'content' is a special reserved word, much like 'pk' in + # Django's ORM layer. It indicates 'no special field'. + if field == 'content': + index_fieldname = '' + else: + index_fieldname = u'%s:' % connections[self._using].get_unified_index( + ).get_index_fieldname(field) + + filter_types = { + 'content': '%s', + 'contains': '*%s*', + 'endswith': "*%s", + 'startswith': "%s*", + 'exact': '%s', + 'gt': "{%s to}", + 'gte': "[%s to]", + 'lt': "{to %s}", + 'lte': "[to %s]", + 'fuzzy': u'%s~', + } + + if value.post_process is False: + query_frag = prepared_value + else: + if filter_type in [ + 'content', + 'contains', + 'startswith', + 'endswith', + 'fuzzy']: + if value.input_type_name == 'exact': + query_frag = prepared_value + else: + # Iterate over terms & incorportate the converted form of + # each into the query. + terms = [] + + if isinstance(prepared_value, six.string_types): + possible_values = prepared_value.split(' ') + else: + if is_datetime is True: + prepared_value = self._convert_datetime( + prepared_value) + + possible_values = [prepared_value] + + for possible_value in possible_values: + terms.append( + filter_types[filter_type] % + self.backend._from_python(possible_value)) + + if len(terms) == 1: + query_frag = terms[0] + else: + query_frag = u"(%s)" % " AND ".join(terms) + elif filter_type == 'in': + in_options = [] + + for possible_value in prepared_value: + is_datetime = False + + if hasattr(possible_value, 'strftime'): + is_datetime = True + + pv = self.backend._from_python(possible_value) + + if is_datetime is True: + pv = self._convert_datetime(pv) + + if isinstance(pv, six.string_types) and not is_datetime: + in_options.append('"%s"' % pv) + else: + in_options.append('%s' % pv) + + query_frag = "(%s)" % " OR ".join(in_options) + elif filter_type == 'range': + start = self.backend._from_python(prepared_value[0]) + end = self.backend._from_python(prepared_value[1]) + + if hasattr(prepared_value[0], 'strftime'): + start = self._convert_datetime(start) + + if hasattr(prepared_value[1], 'strftime'): + end = self._convert_datetime(end) + + query_frag = u"[%s to %s]" % (start, end) + elif filter_type == 'exact': + if value.input_type_name == 'exact': + query_frag = prepared_value + else: + prepared_value = Exact(prepared_value).prepare(self) + query_frag = filter_types[filter_type] % prepared_value + else: + if is_datetime is True: + prepared_value = self._convert_datetime(prepared_value) + + query_frag = filter_types[filter_type] % prepared_value + + if len(query_frag) and not isinstance(value, Raw): + if not query_frag.startswith('(') and not query_frag.endswith(')'): + query_frag = "(%s)" % query_frag + + return u"%s%s" % (index_fieldname, query_frag) + + # if not filter_type in ('in', 'range'): + # # 'in' is a bit of a special case, as we don't want to + # # convert a valid list/tuple to string. Defer handling it + # # until later... + # value = self.backend._from_python(value) + + +class WhooshEngine(BaseEngine): + backend = WhooshSearchBackend + query = WhooshSearchQuery diff --git a/src/djangoblog/djangoblog/wsgi.py b/src/djangoblog/djangoblog/wsgi.py new file mode 100644 index 00000000..89d4b9ec --- /dev/null +++ b/src/djangoblog/djangoblog/wsgi.py @@ -0,0 +1,21 @@ +""" +WSGI config for djangoblog project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ +""" + +import os + +# 导入Django的WSGI应用获取函数 +from django.core.wsgi import get_wsgi_application + +#姜雨菲: 设置Django默认的配置模块环境变量 +#姜雨菲: 告诉Django使用哪个settings.py文件(这里指定为项目的djangoblog.settings) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") + +#姜雨菲: 创建WSGI应用实例 +#姜雨菲: 这个application变量会被WSGI服务器(如Gunicorn、uWSGI)调用,处理HTTP请求 +application = get_wsgi_application() \ No newline at end of file