From a40a2b80c438cc17e3316dff5f960c7ff8f38469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=85=A7?= <2965967549@qq.com> Date: Sun, 9 Nov 2025 00:59:53 +0800 Subject: [PATCH] =?UTF-8?q?sh=5Fdjangoblog=20APP=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/djangoblog/__init__.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 215 bytes .../__pycache__/admin_site.cpython-312.pyc | Bin 0 -> 2711 bytes .../__pycache__/apps.cpython-312.pyc | Bin 0 -> 775 bytes .../__pycache__/blog_signals.cpython-312.pyc | Bin 0 -> 5545 bytes .../elasticsearch_backend.cpython-312.pyc | Bin 0 -> 9411 bytes .../__pycache__/feeds.cpython-312.pyc | Bin 0 -> 2761 bytes .../__pycache__/logentryadmin.cpython-312.pyc | Bin 0 -> 4284 bytes .../__pycache__/settings.cpython-312.pyc | Bin 0 -> 9340 bytes .../__pycache__/sitemap.cpython-312.pyc | Bin 0 -> 3261 bytes .../__pycache__/spider_notify.cpython-312.pyc | Bin 0 -> 1337 bytes .../__pycache__/urls.cpython-312.pyc | Bin 0 -> 3217 bytes .../__pycache__/utils.cpython-312.pyc | Bin 0 -> 10888 bytes .../whoosh_cn_backend.cpython-312.pyc | Bin 0 -> 33882 bytes .../__pycache__/wsgi.cpython-312.pyc | Bin 0 -> 646 bytes src/djangoblog/admin_site.py | 63 ++ src/djangoblog/apps.py | 22 + src/djangoblog/blog_signals.py | 142 +++ src/djangoblog/elasticsearch_backend.py | 209 ++++ src/djangoblog/feeds.py | 55 + src/djangoblog/logentryadmin.py | 112 +++ .../__pycache__/base_plugin.cpython-312.pyc | Bin 0 -> 1968 bytes .../hook_constants.cpython-312.pyc | Bin 0 -> 393 bytes .../__pycache__/hooks.cpython-312.pyc | Bin 0 -> 2358 bytes .../__pycache__/loader.cpython-312.pyc | Bin 0 -> 1536 bytes src/djangoblog/plugin_manage/base_plugin.py | 42 + .../plugin_manage/hook_constants.py | 11 + src/djangoblog/plugin_manage/hooks.py | 45 + src/djangoblog/plugin_manage/loader.py | 27 + src/djangoblog/settings.py | 317 ++++++ src/djangoblog/sitemap.py | 84 ++ src/djangoblog/spider_notify.py | 35 + src/djangoblog/tests.py | 41 + src/djangoblog/urls.py | 53 + src/djangoblog/utils.py | 271 +++++ src/djangoblog/whoosh_cn_backend.py | 944 ++++++++++++++++++ src/djangoblog/wsgi.py | 13 + 37 files changed, 2488 insertions(+) create mode 100644 src/djangoblog/__init__.py create mode 100644 src/djangoblog/__pycache__/__init__.cpython-312.pyc create mode 100644 src/djangoblog/__pycache__/admin_site.cpython-312.pyc create mode 100644 src/djangoblog/__pycache__/apps.cpython-312.pyc create mode 100644 src/djangoblog/__pycache__/blog_signals.cpython-312.pyc create mode 100644 src/djangoblog/__pycache__/elasticsearch_backend.cpython-312.pyc create mode 100644 src/djangoblog/__pycache__/feeds.cpython-312.pyc create mode 100644 src/djangoblog/__pycache__/logentryadmin.cpython-312.pyc create mode 100644 src/djangoblog/__pycache__/settings.cpython-312.pyc create mode 100644 src/djangoblog/__pycache__/sitemap.cpython-312.pyc create mode 100644 src/djangoblog/__pycache__/spider_notify.cpython-312.pyc create mode 100644 src/djangoblog/__pycache__/urls.cpython-312.pyc create mode 100644 src/djangoblog/__pycache__/utils.cpython-312.pyc create mode 100644 src/djangoblog/__pycache__/whoosh_cn_backend.cpython-312.pyc create mode 100644 src/djangoblog/__pycache__/wsgi.cpython-312.pyc create mode 100644 src/djangoblog/admin_site.py create mode 100644 src/djangoblog/apps.py create mode 100644 src/djangoblog/blog_signals.py create mode 100644 src/djangoblog/elasticsearch_backend.py create mode 100644 src/djangoblog/feeds.py create mode 100644 src/djangoblog/logentryadmin.py create mode 100644 src/djangoblog/plugin_manage/__pycache__/base_plugin.cpython-312.pyc create mode 100644 src/djangoblog/plugin_manage/__pycache__/hook_constants.cpython-312.pyc create mode 100644 src/djangoblog/plugin_manage/__pycache__/hooks.cpython-312.pyc create mode 100644 src/djangoblog/plugin_manage/__pycache__/loader.cpython-312.pyc create mode 100644 src/djangoblog/plugin_manage/base_plugin.py create mode 100644 src/djangoblog/plugin_manage/hook_constants.py create mode 100644 src/djangoblog/plugin_manage/hooks.py create mode 100644 src/djangoblog/plugin_manage/loader.py create mode 100644 src/djangoblog/settings.py create mode 100644 src/djangoblog/sitemap.py create mode 100644 src/djangoblog/spider_notify.py create mode 100644 src/djangoblog/tests.py create mode 100644 src/djangoblog/urls.py create mode 100644 src/djangoblog/utils.py create mode 100644 src/djangoblog/whoosh_cn_backend.py create mode 100644 src/djangoblog/wsgi.py diff --git a/src/djangoblog/__init__.py b/src/djangoblog/__init__.py new file mode 100644 index 0000000..2ab0c39 --- /dev/null +++ b/src/djangoblog/__init__.py @@ -0,0 +1,2 @@ +"""sh:该配置类通常位于djangoblog/apps.py文件中,可用于定义应用的名称、信号注册、启动时执行的操作等""" +default_app_config = 'djangoblog.apps.DjangoblogAppConfig' diff --git a/src/djangoblog/__pycache__/__init__.cpython-312.pyc b/src/djangoblog/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1cf126d953c22dbdc65ff83ca0062496b097eb7c GIT binary patch literal 215 zcmX@j%ge<81Xn|vGOU60V-N=h7@>^M96-i&h7^Vti-(Z{G^=xbiKra zf?_=vIN!0Lz&SrJEi>Iulkt{NN@`kSX--KzP*Hp`Sa>DFXON-4%v`Nv!c*N7b5nsj zien%KH~|gR%}p#WNiB*&SP>H+pP83g5+AQuP4 RBO~JtQLBp#LPab#h0fd#ONTVSa~d6>*an9|c~+IY17kat@KhshlI^Xewu%Or&xq$(yO1 zW8|$=&T;Z~DrbslshnwYB9${kPNs6|G&z&XDUy6DCrhdcXT4u5|IUwqrm(&_ z%eB2FJ0vKmi+*$73xjoT;5uPMAtwpSGzyg-OIRB`Q16ik5_amS`5G@40bQ{`&sd6{ zwK96n%HM?>ik02!)ALqt3-rQW_(IW(OQ03l@WoZrYx?IMzZqwBXUr1H>!=(zd9F4M z12eRJ@6NtmCW_fc_J%y)A}5{@|KB-=%lFEC2!)z2_c9ys4SokUl-{jTU>zPPm$eMb z)7BaZm}(ffUuYOwmgOjMZm^tZx`g!^M#C{FH4MswkB@Ku`|kX?LA=aL2Q7pY|Q$sX?OIjwVp^+5>a;rsXNO^$q z@nDE6t1Biouv@N8DWA&Zt4i6+If#CgJ8!~vZU46$_cor^rXJU(9;s8oFskv83YI!B?5;SPpS$kmHO4FEMx(REFTcR{skT5oCx0XH?Bb!*6+^ zbYmrcX`&FUU*L$%stwksv*5{%s_i zjhnqB1`>s&4C9IV9M6mCnN1SNC?k875mj`VmBh~CzA!Z)P5c1hn2U2kXg3_fDiSzz z{OiEf3uZ`~ez49eJro(TSIs6oZo?^>KF0_-@9Qq*}yKLXTI%S<#Q34TuB5f><=J1*5XF1?_vA#p1mJ@g45=A zgwEmmM^;gkXE6N?PK&>p-{4ReM!PW9<>2r$m=*W{;w~KN!bBG)pBGd`YirK|Uw=+R zb>fF+r&8Z6{Q{+@F#H&XzguX3)`1h-vHW4%?Z8YTUuchZU^0>C+vhrPJdqdL%N>|b zS@#00Oi=ML47;=FE}^@Yk^B^V#(^@5-=>#;}#I#(!F)}?mtq(XK~ zke4ZKOO~dTrh34YmC@hZk5*gH#1@MM>P}mQ4I3!wR?O0YP^}A*EruCEf2gWhyW1>M z|8p!bj-Bf_~z9nrAh7G(ogTsSFe7sIK^}DOSwmB4=<%OuTYBaW>W7LD1DhR zIb$w!(P5b!P==nNYx7Ei?yOuhO-q$ww%%bnDz(D`vj0ni%Rkjp9?A5b_;v P{Bk_f_~8Er{F$nM!&t(? literal 0 HcmV?d00001 diff --git a/src/djangoblog/__pycache__/blog_signals.cpython-312.pyc b/src/djangoblog/__pycache__/blog_signals.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea0aceac833496f7fd6720e536dc88294d03a48e GIT binary patch literal 5545 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 Sv+gx<+vW%Vhrkcp$o~Pcz)-XR literal 0 HcmV?d00001 diff --git a/src/djangoblog/__pycache__/feeds.cpython-312.pyc b/src/djangoblog/__pycache__/feeds.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d4ee8cd8f6a64fbdc742666729041c8fb9db95c GIT binary patch literal 2761 zcmb_eU2Gdg5Z<$W_W38?&{Ty+YGa^jaND?l^bZh9+q4w|BGIH!9g!^Cdr2<6^O?PK zD;ugts1igKv`CfmSR_DgluGc{@&w|Q7h4u`bRrTG7%6Wkfft^b*|Sq8aRd*np=Wzv4q{$W3K63(6c}gh>W5B~_A8_ZdFZulj}VHv*=t z%0dqqK{KR=gf1J38CJtW4;m3OszxQ^BNqq@-6TxW{0n~1hZ-w|laXb7NJ?&eQghNp zTjOcdVwwRnf%BTi;9qfcQ!86}4Od;X?IVW{bM5MprZse$J9^F##))i*$yBN|a9MpU2XR|8B^W#&_Z%&&%6;3iQOCW8#KAjk+ttwxywGRDFn zyI2HdoJB#>QY_i!#)qdf`AO@HVNGIC#a?jH`0{?^(f!}&K5snw>cH|xpEPDagNNn2 zw^GFy!2H|eyNx>!m%sS5apzuQ>$Qn;@Zbb&c`(4R-LCg_n&9(r~IHqyM zhB4i$Y}@?W`e55+QynGq(nvDE(UBXnveR15v0ZsW=eFZ2c|CU}pE0$0D3h%j6E?<> z`1|O&m&V`GE`Z^5pnLsKFNR4A&cB)WvqnV;JW$`rbSMGiC4q};V zvpoea>;#Az@-y94qkVNcP^ANPI#{KHHF~g4Pgd#4igI!l){xGKd93~)5DTG?^`nL6 zDB&@%G_m7dpz}Bgw;L&*$=a4tbhLDl8@wB)J?Qv0T+xEU(DPTef&y*Z@xA9kYzf6s zl@8VDnTm3TGX)Cp2c$RtD{q*@d=LE#O_oh}z=8!9nyiGbA@`C|k_?Wx zQpS}ENuL{VN(Idg+D^tP+U4+Ip_onMAzwz00&cjWz|9R#=mzk{RV>b+HDpVhAd_`R zGwt&B4ZIPvPs2X;ArJ`Dj@vh8Z`5gjmG(b4`i)(o{Wbb>oeo#&@b~Z5=v$SGW0ldd z3cXZOE_s->pCZggoH;z)bL0bQ8vZwqR^=sEc67(k=6xJV>jp$LxLqRQ6*_`5kTWbQ z@9Yq|XF^{#a%qMv(w^Jp*>XkMxj8nBOT;z=N_#)(R8NQ;>%U>Ns@2NC&lOnV#*ndQ zv!A+wti?(kX=k_R(k*gfnw{f%!O^XJxvx`3Z3}kc7FZlX-#Xn_rTZSd`1PszQ#E?5 zq8!^y*|ik2@YcVj*gA;UB_?%hdjLU)hWF6-1cao2q}T_Z&RT^M*C(f(ALbM9m$VGO zo@npfcuC-``A zulSDJ#;kEG|FM6O9;(w5ReIv_=y#XDy?XIlq}8SS>-2=RgtUUAr=1qB&Zq zwJ0F*9bnm9?yxAg=uppWh{J6oH=Ew;* zERYxdDcZ(Zj>x$I%$oOr%8|W1fdcEolPFRsu-)*ZD2|}O<|~do$8RnnP_a*gxJeef zch9``%eK_a>x;4DGv^kgJu|~A0ZBf!5|re#63VxQ>_zqXO3)`?kX95)9$1BE>g|sE zjlgF}4-=PbDre;#u4l#7>0RH64HmnUXL>G!XEk+I*RI(dv3Fz5Wkp9fY>&~s?Ncel zW>U**JmXm0!z+q+c}je;*`jZ|-QFu=-rEUViI@I*NL=L=uN1HHAy|T(6qni`eo2y+ gNNR~3ULwyfkz)EP9&w{qkHGX zQY1hv3`7Z9#4b?C0SrWKe$P{>|re6KMaq_LJ1Qhme0^$7;4Jv-5Lc)`&zTMj;s{&M?Tc3Y+2L zoWXMnpRvVl2G1+@j3e&IIO9&EZ&O?ucie68cEywN#=QpbP<$DG+;8wsC6EcmLkwZb zn?!QmCX!ob=6bD-?0z3{CLeEF@Wj0T!A^|PZK9M(XMu5ExHxt3+U2*Vf%lxxWi>gg zU0cY@F;;g_`F)wHvhJVGecxoSidrHCny%R#O~`_(QRvuZH6g+b5NAX>FR0=ycDzYh z)8t#4poohLiQNqmQr!{R*#PNBU?-Wx8HvPMiAgeXPGTi4$;Nq!mu&F0C5dGJ6%)5h zj@u;ekb;u)HWPQk*9BjfA}V$t@-xnjx`}?wiUbFJ?8mFoDL@y$RENf(G`}J-HimazYcR zoTs`!krJ~>S;)w$Dkfo2Olav`R?yNJSr3DugnEUTjfP;$y6dJYQ$a~*=cyn2)}n?a z43Y$Eovivr+wSl{*2n_$ke$BtkbohsA0mS;7;!V0U(mxTQ58f<67n+5q*WD;v)EN< zkiq>Lui)+wBrBvGi0lAE5>_-|2YKTJp8&H)mY6w|Hnh3MGsE~TMm@y5Oy5G~o^>R~ z^7k!<28@~ZJ;PO2V&~r=1bod+FE*PeulmU{ht9wK*37jSuk+|B-J4bg^(I)v=sD2< zXdd03k+oD#ddSgMSemv0(K!eZ6%X6h)kWJ;(v14mAJ}8np8Zzcr-Z;)&jMK?PlM4l z?_F;x7%K#0+k^?7EnAYlLa=Z1=$T@0eA~ta`zv-5X!$hzadzEa@E@%>I9ESC3~SW6 z1p1~+te~PTAfZtt#;@)_f+Q=lChrZ51CFpy={T;~fyCIuUMHh#jIfvkNUssC3T%Q7 z->U~|*eusBvoMa^ZfMIK$PaM0=Cl;i?ezK*_v5&WXtlH=F>|#iJ7yixo%bwlN7x{(e%0lPBC zft)@R;4#d#xtml`5#E5IDvFHJ#kP9PR9D2bIRM(~Z-K0kCoLWKWT50sBToXY)M5{sM8_wkbVi+w( z1Bl_p*U*;OgWv+vT0rkkumE)Mug_ufYH0glchC;_CC*xTnbQtgOMtWnI{+BQsL^Tw zJ5bXC7{#0y^HX$z?l3*6>OQOo03Ct}2Oi4_+65{mvd|v}geZnG9Yit&L}&B!F98hV zkTxNC6-Xmsa9}Z7513lrF@Y0)2L#{}?Jh-+7ox{Y(UC%QCsY2_iFK0Gl#nua}mrdlf6$3q+ z&K|@|uoLj&3YEP<#7lJm@Zt)+Q1MiM2~V_6zD^#$?mF+`{_19c8iuSx3tlY7f5M!r}ECtnp%_s^eLc^$w1*pU5 zf+}lzFb@e-hN>D0MMczPDaP-q5u7zWmmnk*QB?&&#T*LRZpA1X=qU6S+v~lD0c6db zfpHbn=5z31Q|tZ15BNgU=*Anxrt_=Lav)j?94iElJ?Jb3MmC)z`|sYGdB$LkZ*Crm zSz_NOlv{$L{tQ`Sp@?Fq^+5I3hjJ8ZV>m2FPs6gNVLZ5P^RiIutl+2$=@~pr{qfI% zs4?5`S?3*`fhfb%hokp>>slc^vJowYCsrM0Psh5W;OU3@XtOo889ZJNw5<9K6U3P5 zSXg%oLKdoHLD1a-qVcAJyjK9@ib{3Fqoh@UYg&PVS@)^3NWqu0X<3m}J#36gX*I8i z3q~2Q>fXIFis^wKMRE)Y<}*EHT)9GyN>KY7E9s1&htyP#YJvohEt<~b>%l}c`k$V| zdA@3hBxFSqgcxI*3)9m%GgL7A3{zu8qkaVB`EByl*;3)yCi}`2fSjwP>};*rxMusx z)r!~Xc0jk?zR9<2JM9ts%G7p{u{H15d0YRE7r1SEz;?9KP5NG2x$+hN%GbSp>!~lU z82mQxv-MUwNl$F$@>l$kue*EJFMVP1&v@2$`hQN|7OfBPS#P$)T3S&D6zMHQdjCtXXPEf^7pKrL literal 0 HcmV?d00001 diff --git a/src/djangoblog/__pycache__/settings.cpython-312.pyc b/src/djangoblog/__pycache__/settings.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82dede5802426d711191fa9a1050ae0c06de7ac8 GIT binary patch literal 9340 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 JaI5M1e*q|1xt{<4 literal 0 HcmV?d00001 diff --git a/src/djangoblog/__pycache__/sitemap.cpython-312.pyc b/src/djangoblog/__pycache__/sitemap.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17a2630c439183b45da037f3fe4d470112560846 GIT binary patch literal 3261 zcmcIm&2Jl35P#2WdpCB{%@=VR66`8Tiz%3dQa++8s0}5dQgd-3k`>9?csI_Pb?m;k zf!Hmog>tDlG(t$>fDqyWQMe!>@kelRWT8Z>Rzl){xFH-aJu&lkoo*axC8EB=?Ck8k zd9yRWnR!2VbwvoQUv7M|@>!6O-|(fiLLSrn3YaaT6P;S5NHdfoAFu*NDI*D9vVz4> zCR7Y(!bLeFQxYJvL=SEdJ!H@w$+eSFgft9N*(dD~QU#*!8#2^`}lT1LT8A%Uhg1WRpG9mn_Ot=zChMk_Yt%1?^P2(f)%)D#i*SDOIu9;RP z*D{H6aGAUCXKj_Qer7q3n&E*P^~(YRbL z^ToFd+G@UZ1@vDkYTP#1qAnz8VUfE(o+?+I5VE`)JLX|Ze29W=j-*ERyNBy)yspHX z@Jez4q~1bjg~NluY!UnLXKvFSk57ZY^9*nUKf+XaI6-j$nn@&BH%b(%l!G{xR;xqD z-D!P01)97QNR8ZA2R7|bKdvjotQQ3HN!d}d*;NQZHtR&P*)n-&0xi3|wPxdFn+Rr`;+?V<;R z#fN~1T;02K_s*U=zN?OZm#(Yh4Rx}vOp2%-0TWslV8V@CjxRsK(__( z##Ecjm8@+RjcpHK68O#i@RCsz6srSmzIyW0V6cfl2Uu5o_teo{b+oRGy4c#5Sqwfg zL^wlD_AC zFg~yh$Cs1_3t2grp-YYC1+q0$KWDL zVtZZySW&C6iVQ0~?=kW|57#OCj8f*8*L&6wtIgdp274{;sZMjXM3<;*~ zFt?|DxjpO4Eh?TUw-J~@URv49U<}*Ag{PfUoIfI`Y!W%F4R_l33cvf2V{SSzSiBDi zrnC3nt-H6d@EdBPt|XpbDtmoycv-|SJt>Q?q7D)P5U;uS__KJ%mqk=OQ5K``afA8O z$S$Hue`$7rK$PY>F#cI;qf{*Wu$Sv}gV@-m#VTOF+ExLFciU9xFPXpsBy#WVu z{G}TCTKjh8n-zf`xK0HvliQBO4IprT6JzR0Fzq>Z;Ol7qoG^c1?fZP@OKEpF^`rcg z`h&VRF|#`{)0nv07<{MEKig2}>dM^7unVz>>TsdV7E0!-q5nri>hpW)!FUS&bT2$G zmavxU%5#rtOmBidLvjH(Vqb;($`MGvOA*s?VMA!nC%c@Odp}I&;JU%gr4+~OnO5cu zVF$j8+hb~tS)3vKoG9KTQ!q{qkRSxz2_RZV1YgDQ6J1ginQ-f94+T89SF~w13p)^G z{5p_7B}(Z7a{d8{|4vf7By|v0=*SoH0fEQCfI`PM7my!w`Mi&h9rE$bbAN-5`OZ2qBaT|QV9tz+yXv;;LLr))tS3r~%|g^kkfQEJ@oAgrZGnwz3wkem-DrvH-)votLA;d_<|dFZlhlH zg6)pin=)}bUve9fQ;bn>PCJ zYc!?hI5k{@A5Q6%AuyeJi_Ox*F@pbdTs(|-p(gd*r7>`=97 z?=xPyI-1GQ7->JcR)@F8?et+#Dg#;pj^^_AiXQu!lBI a?%PAV_R!Y(zwNO#^PQO>XWjy`pw@qFEgPZ$ literal 0 HcmV?d00001 diff --git a/src/djangoblog/__pycache__/urls.cpython-312.pyc b/src/djangoblog/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..61f9da3152c1ac741536d0fcdf41b3931a8a1f45 GIT binary patch literal 3217 zcma)8%WoUU8Q-OrujNamB~zkatJp3{TV6_z+qz1vK-MFnYsYXb_puektU1Gnyboe_ z8CwKWxVbdF6!57wP@p{~kPbTbf9OR56_{N>2#TVI-XtWw^we)=cO}M-iY&m{?=`>g zIkWs*A|Z0{e7W{%;}1anlQRA@>=9qb2RQDJ9O4uX@hG72dO!*AlpfFqbU_gqEoebK zq=Xn9)WUj1i7+~(MRidT^_UW4d>Ht+5@&QoOXx`@$>^w-(g&46MvGcn&nOv2$Fy_$ zkTS&RxHhbxSI#p!p=I?EWkerUM)jPM)5nxCeOwvmIXr?#@faS*Ih6cUP?xz|1Q?HxnoXY6&cC(z{o)lVVm4E;2YpttZulSdbJE^Z5M2F&v%SkG^JDSa#6 zADpSlgC>aw@z74e-+_vN3zI<~hU-Rr8C^LeB3D0yuD+>9@=j?xae@`Xfz4Hy>t8|o zen0QhJF|X79T0Q?1^O7+y#1ynd5iUzqThK_3(^nXl=h)Bw8g@B02R)4?i19esCsDqy(zVp04z9n~W5 zL@aFE5Vr+%qB(cNsQ3d1f{Wd?yF-DElfNH-~?J3C*p%R z17PY1)P@A1N}WkoIW|6WOReJyw760;+XiBZcE=18T7@)DEI^|6<0^n_o95V9?TYt5%n{F>Ux; zfam$+K#~{!Gs*GOhg|89yL`yaeZ^fmko<)yCTrm1s_IZ9UH`mQv>k4ll4Fpnw zXR)IY$H#yF$=`+8XSZL7e@?wjb%g@umY!!{jO~UF(m(DBH>lvw%luzT-5lNP>!)_h z2kC{b@a~y{@!jk}`X^ms9xV3a6EB{8p4)ZnG#Nv3KHk5N5AwBR(AN25F2&b*sDZ=@@!u_cy3i9+ r&wuoy@G3X6pPT6wW?vQF*)O~UZij9jbA0gTZ+~_)zzG?06I%8^>TI)K literal 0 HcmV?d00001 diff --git a/src/djangoblog/__pycache__/utils.cpython-312.pyc b/src/djangoblog/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93e3ca8f413bce72cacc62cbb35a348a7474a80d GIT binary patch literal 10888 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 SROw@LO!2CjWglVb!T2AZSDb7B literal 0 HcmV?d00001 diff --git a/src/djangoblog/__pycache__/whoosh_cn_backend.cpython-312.pyc b/src/djangoblog/__pycache__/whoosh_cn_backend.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e61cfe16f20a3439f3d49ae4f43dd239c4413ac5 GIT binary patch literal 33882 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 literal 0 HcmV?d00001 diff --git a/src/djangoblog/__pycache__/wsgi.cpython-312.pyc b/src/djangoblog/__pycache__/wsgi.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09d2d05d9fa72184edb75e7e85debef1fb4a22ba GIT binary patch literal 646 zcmYjPL2DC16rS0QP1)EW+V(t%mw??(JeDF8)P8 zdG$9)e~Ooiim+Z3@!(C#&6Bf9L;4Q$-kUe?d*Az*4_jLfGWMnWdi)k6^fOOZR=5e~ zLleA14D}Gh%;LC*jkmcK+dUg2z?02Y%i2#$>v$Eh0=si%Ei;TP1oqWUskbq+DjOHy zoRdzoMFJ^CQAkEIC2T@PD2H5zBuV820_{3ZOA|Oxqyk0s7|61Ra!!XF2vvlVSh7zy9@k&JXk0v4E8r^7IufC(6tkl58ydc!SyE;9mT%m^?BM3e zscmLuMG(^{l_GOYBZCn= 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_user_id = kwargs['id'] + """ + OAuth用户登录信号的接收器:处理用户头像本地化存储 + """ + oauthuser = OAuthUser.objects.get(id=oauth_user_id) + site = get_current_site().domain + if oauthuser.picture and oauthuser.picture.find(site) == -1: + from djangoblog.utils import save_user_avatar + oauthuser.picture = save_user_avatar(oauthuser.picture) + oauthuser.save() + + delete_sidebar_cache() + + +@receiver(post_save) +def _notify_baidu_spider(instance, is_update_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 spider: %s", ex) + return not is_update_views + + +def _handle_comment_cache(instance): + path = instance.article.get_absolute_url() + site = get_current_site().domain + if site.find(':') > 0: + site = site[:site.find(':')] + + expire_view_cache( + path, + servername=site, + serverport=80, + key_prefix='blogdetail' + ) + + 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(prefix='article_comments', keys=[str(instance.article.pk)]) + + _thread.start_new_thread(send_comment_email, args=(instance,)) + +def model_post_save_callback( + instance, + update_fields, + **kwargs +): + clearcache = False + + """跳过LogEntry类型的实例""" + if isinstance(instance, LogEntry): + return + + """处理百度蜘蛛通知逻辑""" + if 'get_full_url' in dir(instance): + is_update_views = update_fields == {'views'} + clearcache = _notify_baidu_spider(instance, is_update_views) or clearcache + + """处理评论相关逻辑""" + if isinstance(instance, Comment) and instance.is_enable: + _handle_comment_cache(instance) + clearcache = True + + """最终清理缓存""" + if clearcache: + cache.clear() + + +@receiver(user_logged_in) +@receiver(user_logged_out) +def user_auth_callback(sender, request, user, **kwargs): + if user and user.username: + logger.info(user) + delete_sidebar_cache() + # cache.clear() diff --git a/src/djangoblog/elasticsearch_backend.py b/src/djangoblog/elasticsearch_backend.py new file mode 100644 index 0000000..c20a3a8 --- /dev/null +++ b/src/djangoblog/elasticsearch_backend.py @@ -0,0 +1,209 @@ +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): + """ + sh: + 自定义Elasticsearch搜索后端,继承Haystack的BaseSearchBackend + 实现与Elasticsearch的交互逻辑(数据同步、搜索查询、拼写建议等) + """ + 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): + """ + 将模型实例列表转换为Elasticsearch文档对象 + 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): + """创建Elasticsearch索引并批量写入文档""" + self.manager.create_index() + docs = self._get_models(models) + self.manager.rebuild(docs) + + def _delete(self, models): + """从Elasticsearch中删除指定模型对应的文档""" + for m in models: + m.delete() + return True + + def _rebuild(self, models): + """更新Elasticsearch索引(增量同步文档)""" + 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): + """Haystack规范方法:更新搜索索引(接收Haystack的索引更新请求)""" + models = self._get_models(iterable) + self.manager.update_docs(models) + + def remove(self, obj_or_string): + """Haystack规范方法:移除索引中指定对象""" + models = self._get_models([obj_or_string]) + self._delete(models) + + def clear(self, models=None, commit=True): + """Haystack规范方法:清空索引""" + 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): + """ + 核心搜索方法:执行Elasticsearch查询并封装结果 + 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): + """ + 自定义搜索查询类,继承Haystack的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): + """ + 清洗用户输入的搜索关键词 + 处理保留词、特殊字符转义,避免查询语法错误 + """ + 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): + """构建查询片段(适配Haystack查询构建逻辑)""" + 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): + """ + 自定义搜索表单,继承Haystack的ModelSearchForm + 控制是否启用关键词推荐功能 + """ + def search(self): + self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" + sqs = super().search() + return sqs + + +class ElasticSearchEngine(BaseEngine): + """ + Haystack搜索引擎入口类,关联自定义的Backend和Query + 供Django项目配置使用(在settings中指定该引擎) + """ + backend = ElasticSearchBackend + query = ElasticSearchQuery diff --git a/src/djangoblog/feeds.py b/src/djangoblog/feeds.py new file mode 100644 index 0000000..9fd3d19 --- /dev/null +++ b/src/djangoblog/feeds.py @@ -0,0 +1,55 @@ +from django.contrib.auth import get_user_model +from django.contrib.syndication.views import Feed +from django.utils import timezone +from django.utils.feedgenerator import Rss201rev2Feed + +from blog.models import Article +from djangoblog.utils import CommonMarkdown + +class DjangoBlogFeed(Feed): + """ + sh: + 自定义博客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): + """返回作者的个人主页链接""" + return get_user_model().objects.first().get_absolute_url() + + def items(self): + """ + 定义RSS源中包含的内容项 + 返回最新发布的5篇文章(类型为'article',状态为'published') + """ + 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格式用于RSS展示 + """ + return CommonMarkdown.get_markdown(item.body) + + def feed_copyright(self): + """返回RSS源的版权信息,包含当前年份""" + 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): + return diff --git a/src/djangoblog/logentryadmin.py b/src/djangoblog/logentryadmin.py new file mode 100644 index 0000000..6fb93f3 --- /dev/null +++ b/src/djangoblog/logentryadmin.py @@ -0,0 +1,112 @@ +from django.contrib import admin +from django.contrib.admin.models import DELETION +from django.contrib.contenttypes.models import ContentType +from django.urls import reverse, NoReverseMatch +from django.utils.encoding import force_str +from django.utils.html import escape +from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ + +class LogEntryAdmin(admin.ModelAdmin): + """ + sh: + 自定义Admin操作日志管理类,继承自ModelAdmin + 用于在Django Admin后台展示和管理系统操作日志(LogEntry模型) + 优化日志展示格式,提供关联对象/用户的快速链接,限制操作权限 + """ + 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): + """ + 自定义列表字段:操作对象的名称(带跳转链接) + 非删除操作时,生成对象的Admin编辑页链接;删除操作仅显示对象名称 + """ + object_link = escape(obj.object_repr) + content_type = obj.content_type + + if obj.action_flag != DELETION and content_type is not None: + # try returning an actual link instead of object repr string + try: + url = reverse( + 'admin:{}_{}_change'.format(content_type.app_label, + content_type.model), + args=[obj.object_id] + ) + object_link = '{}'.format(url, object_link) + except NoReverseMatch: + pass + return mark_safe(object_link) + + object_link.admin_order_field = 'object_repr' + object_link.short_description = _('object') + + def user_link(self, obj): + """ + 自定义列表字段:操作用户的名称(带跳转链接) + 生成用户的Admin编辑页链接,方便快速查看用户信息 + """ + content_type = ContentType.objects.get_for_model(type(obj.user)) + user_link = escape(force_str(obj.user)) + try: + # try returning an actual link instead of object repr string + url = reverse( + 'admin:{}_{}_change'.format(content_type.app_label, + content_type.model), + args=[obj.user.pk] + ) + user_link = '{}'.format(url, user_link) + except NoReverseMatch: + pass + 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 diff --git a/src/djangoblog/plugin_manage/__pycache__/base_plugin.cpython-312.pyc b/src/djangoblog/plugin_manage/__pycache__/base_plugin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f789faf2f10f17bfaceb42b9ea370155d12aac91 GIT binary patch literal 1968 zcmaJ>|4$rM5Py5S_x%p)LAk<}x<*2KjS|xzv?11}6@q9z2_PmHl67I1>*C%W-Q6?c z;tywq+D0f?Q_j|6Y|_RO0?!KtNT5!R z0_x)IK;7Wcp?XfUGT$YpVnQM^Xgb3e*kO_|w|)eYMT({SWY6$HY9e5fz#aZq9FSLo7^~}<z_|*7p@f-ZWU)97Vlpz&feA*zg@e0Z|%}UZSnfr^)bsy@vF(zUv5}V(taA( zQD8+O4S0xzN!0eCOb+Bd!wYdwpj%2Y)gFztQBkV{vGYYgi)0B zD|3*)rTYI`7zeFdRGf{>?M@H_C|zHc;7(N6gIdOw$)F9$(Z4$x3m~cqJtzT4`-!T$`RRVQKuc z;?;Y_$wdr_g{(IIz_N#zrxMVZs+Z&CK}i@e#}cUFk#X_f(ve!@E=!+3AH-L zo1Xx*=1?--P*%PQ9SsiTFc7Gp5AClv`g0}pd?9o!A3C-Y3>AV$^TDHkIjMb~4Me#; zdWFLt-1j(0hEY8X6Hjs}5%WHVIhSHZLsJ>(J{&DZtb|`x3Sm-*2}YoSXZ%_cCir)k zn}Lj>r-8c6J1g~#IjU}De|;vhNm2Ggn?Ae!_-2*e?%%3%+l{F7gJHYs#C;4e=e4Tqbt8i}q!+*h|B!MMkPVU`h(A!(Q+LC~V8PvxcQ<@}{E@pU Qu0ND@#arXZ&@Ju@#pH!&|UJvAmHKR+Af;Np_Rypm$Qg34PQHo5sJr8%i~ rMf^acL7`Wy03<#zGcq#X5YxWEAbdkmv4IakJdn}6$RJt73seLEod9}^ literal 0 HcmV?d00001 diff --git a/src/djangoblog/plugin_manage/__pycache__/hooks.cpython-312.pyc b/src/djangoblog/plugin_manage/__pycache__/hooks.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96eabf6cdfd954ebd088d08885dfcc2ad3f2e1fb GIT binary patch literal 2358 zcmbW3Z%k8H6u|H8`_tE2T)Tl*1osk|q;76mG!emUc2UP7VOe2wAWKvF9;KzU?6nBR zSJSCcFl3_HB8n_%Tn1Ao$-Xq|rhML)*NI`i@xerj@U0tWF)Uej?t86N#C_OJde6Q0 zoOjQ?=l;&U{iUG5jG)}BKhk=MLFggr%uy&S}n$(Prc(N0h->1DJI`T_FwvLfT$!ZC{GHjM(r z*RmpD84V%9AmaeHOa)!gP{Yj9Trt|gbiM`(GY@6<$If%MQf zbK^%JCXQ(O9-B+_`6E$L5(S>bfiLf|mGfQxXe*zK1z)b>@>&O@vTkA#QCUw%hZqu6 zHW>E#L`h`?vANTya&9e%ZnwiAkqt{^R}m6Oo?mqCschOWz7Iimc63CVb_YBmU-&(k zU`xA)xNj1)k!Bca>IioF{2_O{C*<*oO`6X)J7Ov)WtS=yz?>u$ky@w{D8nJnc7aV9 zH~dzzp`Y!IrMPv|w7GXjiY}U>OHy>n;HQ)HCKx(V^F@vFO4&%m$Ufz*U3ctLPFKq5 zQg+oTb@htv<9qZc3os+{W|1OoL{NPRehYs=we0kf^mgHcb6cHu76O*Nn{>@al(;X|FMzVGHPW{KjR z-l#tu;;R9R98u{Xj?UaVKil6ofAL28#;IA&e6}w!d!bKGHDcUi~ zRZiPC48|tyj^65wfhv&keXD)2OtCsfHr=&4{$S9$%^3!9>z_O}qr%cZ5eAcK>zcEc z(-!5`%H)pZ>vx(ISL2k+n{s)Tj|4^VDXo6R7f?#ul!HA3mMQC@l=aZx8H#+l7%759 zUFWI{H%-n8W`f@A+{#RBWuX1cmG>0-f57}X=(pwJ_y3`Xq>9rT^t6n9STs+ zPq0(uXMP@@y?Sxx*2sL~%%hv<^3z}$_j9+0Ag$7uk3AYV{xk>Hz+D-PktAr*edQ-X ziJk;-7q4Vug(XWisOlxcVy#pNwS(1>X0+8wYl)4WROF_X$V(L=`K5841-n3-->v(P zNTozg?9M^8BHLR={i6qxSShNU>UUNKtutG!};P0C(zngO~2aWk^m z5;adAGg-*vOORJ3KS);H2`Y65rs{-LouIfxMQm040j0G~DGe%JhX~|y$|`G+$8(Td z-Zk7dId?J>+-B!?W@0-7ZHHMkl2;32s+xRav_^Zuw0zeu1adW~R3s|t{{=kFK~Z~d zw5-;)KZc~$V5H|gx3fJgbOyzDq>V5}o{h+dP(iX`{5x9xz*OA3cFI(oG8GS2+%=UY t*axOn2{ywT@qRo=We|}g-3yw`6t2QD9^ANqh|H8&ux+sJF#@4E{1;p9a%uno literal 0 HcmV?d00001 diff --git a/src/djangoblog/plugin_manage/__pycache__/loader.cpython-312.pyc b/src/djangoblog/plugin_manage/__pycache__/loader.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4002f1950659d737d91b67b0ee1e064e75107528 GIT binary patch literal 1536 zcmah}O=ufO6rNfAu2*u^kV=lMG~3#(P$Anx4tAZUkl0SD5=;}RKM_i|8|`?t%4%24 zuCOKK5=svV7+jxZD1ktGF$pB6qCg>~KK3Hx7{kVe(w=%t>rhI1>CCQHQfa|+*!R78 z?|tvhdpq-EG&%yv+RcAn=6nGBN)>Mr-B}0k5zz)Rkf8<&u7m|hbVg&UzJd<|1|V1o z_xy(h3fzURS_sO%FF+yGfEoX9bZa34?XZa~OVt&#G!z3uM4`D4{v@$&VD)VkrVsmI z4qW{ffE8f*y{$^nLjzf12iQYHyiO0ieOI=?-Cnj8cnEj&-;q>nLGRoqT)lKi{0sk$ zKP2LRPSb)i^eP_8y~YCTMUP)*THq@n`)>O@4+yLyp0)ClhXzvi(}>&5z<;&l2aV7K zu;QNAB*2p^%n;8p&oYp5z~yN%_kv!P5L^LrXodNVS^dS8OK=G+`>uc`m%O$5)kfNkK(ANQ6n#>WUP;e#%ofMaP6xJ*CTC9twoU~I&V(^NpdF$^ zAP=9-xlIyEhQ8>|nN^T=(H&7(bS5B*c$5S=?b!qe1OiO&svUm2YRGjBy@QVvl|HoQ r>*PZUf$#}npMcSy0RJ 1 and sys.argv[1] == 'test' + +ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] + +CSRF_TRUSTED_ORIGINS = ['http://example.com'] + + +INSTALLED_APPS = [ + """Django内置Admin(使用简化配置SimpleAdminConfig)""" + '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', + 'haystack', + 'blog', + 'accounts', + 'comments', + 'oauth', + 'servermanager', + 'owntracks', + 'compressor', + 'djangoblog' +] + +"""中间件配置(请求/响应处理流水线)""" +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.gzip.GZipMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.http.ConditionalGetMiddleware', + 'blog.middleware.OnlineMiddleware' +] + +ROOT_URLCONF = 'djangoblog.urls' + +"""模板配置""" +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'APP_DIRS': True, + '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' + ], + }, + }, +] + +WSGI_APPLICATION = 'djangoblog.wsgi.application' + +"""数据库配置(MySQL)""" +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'djangoblog', + 'USER': 'root', + 'PASSWORD': '2315304313', + 'HOST': '127.0.0.1', + 'PORT': int(3306), + 'OPTIONS': { + 'charset': 'utf8mb4'}, + }} + +"""密码验证规则""" +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_CONNECTIONS = { + 'default': { + 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', + '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/' +STATICFILES = os.path.join(BASE_DIR, 'static') + +AUTH_USER_MODEL = 'accounts.BlogUser' +LOGIN_URL = '/login/' + +TIME_FORMAT = '%Y-%m-%d %H:%M:%S' +DATE_TIME_FORMAT = '%Y-%m-%d' + + +BOOTSTRAP_COLOR_TYPES = [ + 'default', 'primary', 'success', 'info', 'warning', 'danger' +] + + +PAGINATE_BY = 10 + +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 +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' +EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) +EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) +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')] + +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, + } + } +} + +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + # other + 'compressor.finders.CompressorFinder', +) +COMPRESS_ENABLED = True + + +COMPRESS_CSS_FILTERS = [ + 'compressor.filters.css_default.CssAbsoluteFilter', + 'compressor.filters.cssmin.CSSMinFilter' +] +COMPRESS_JS_FILTERS = [ + 'compressor.filters.jsmin.JSMinFilter' +] + +MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') +MEDIA_URL = '/media/' +X_FRAME_OPTIONS = 'SAMEORIGIN' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +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' +] \ No newline at end of file diff --git a/src/djangoblog/sitemap.py b/src/djangoblog/sitemap.py new file mode 100644 index 0000000..03ef14d --- /dev/null +++ b/src/djangoblog/sitemap.py @@ -0,0 +1,84 @@ +from django.contrib.sitemaps import Sitemap +from django.urls import reverse + +from blog.models import Article, Category, Tag + +class StaticViewSitemap(Sitemap): + """ + sh: + 静态页面站点地图类:用于收录网站静态页面(无动态数据的页面) + """ + priority = 0.5 + changefreq = 'daily' + + def items(self): + """返回需要收录的静态页面URL名称(对应urls.py中的name属性)""" + return ['blog:index', ] + + def location(self, item): + """根据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({x.author for x in Article.objects.all()}) + + def lastmod(self, obj): + """返回作者的注册时间(作为页面更新时间)""" + return obj.date_joined diff --git a/src/djangoblog/spider_notify.py b/src/djangoblog/spider_notify.py new file mode 100644 index 0000000..5ae67ec --- /dev/null +++ b/src/djangoblog/spider_notify.py @@ -0,0 +1,35 @@ +import logging + +import requests +from django.conf import settings + +logger = logging.getLogger(__name__) + +class SpiderNotify(): + """ + sh: + 搜索引擎推送工具类:用于主动向搜索引擎(目前支持百度)提交网站URL + 实现新内容发布后快速通知搜索引擎抓取,提升收录效率 + """ + @staticmethod + def baidu_notify(urls): + """ + 向百度搜索引擎推送URL(批量) + 依赖settings中配置的BAIDU_NOTIFY_URL(百度站长平台的推送接口地址) + urls: 待推送的URL列表(如['https://example.com/article/1/', ...]) + """ + try: + data = '\n'.join(urls) + result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) + logger.info(result.text) + except Exception as e: + logger.error(e) + + @staticmethod + def notify(url): + """ + 通用推送方法(兼容单URL推送场景) + 内部调用百度推送方法,可扩展支持其他搜索引擎 + url: 待推送的单个URL或URL列表 + """ + SpiderNotify.baidu_notify(url) diff --git a/src/djangoblog/tests.py b/src/djangoblog/tests.py new file mode 100644 index 0000000..733a3be --- /dev/null +++ b/src/djangoblog/tests.py @@ -0,0 +1,41 @@ +from django.test import TestCase + +from djangoblog.utils import get_sha256, CommonMarkdown + + +class DjangoBlogTest(TestCase): + """ + sh: + 项目核心工具类单元测试类:验证工具函数的功能正确性 + 继承Django的TestCase,提供测试环境和断言方法 + """ + def setUp(self): + """ + 测试前置准备方法:在每个测试方法执行前运行 + 此处原代码抛出未实现异常,实际使用时可添加初始化逻辑(如创建测试数据) + """ + raise NotImplementedError("setUp method is not supported yet") + + def test_utils(self): + md5 = get_sha256('test') + self.assertIsNotNone(md5) + 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/urls.py b/src/djangoblog/urls.py new file mode 100644 index 0000000..83d8346 --- /dev/null +++ b/src/djangoblog/urls.py @@ -0,0 +1,53 @@ +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 blog.views import EsSearchView +from djangoblog.admin_site import admin_site +from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm +from djangoblog.feeds import DjangoBlogFeed +from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap + +"""sh:站点地图集合:关联各类型页面的站点地图类""" +sitemaps = { + 'blog': ArticleSiteMap, + 'Category': CategorySiteMap, + 'Tag': TagSiteMap, + 'User': UserSiteMap, + 'static': StaticViewSitemap +} + +"""自定义错误页面视图(覆盖Django默认错误页)""" +handler404 = 'blog.views.page_not_found_view' +handler500 = 'blog.views.server_error_view' +handle403 = 'blog.views.permission_denied_view' + +"""基础URL配置(无语言前缀)""" +urlpatterns = [ + path('i18n/', include('django.conf.urls.i18n')), +] + +"""多语言URL配置(自动添加语言前缀,如/en/、/zh-hans/)""" +urlpatterns += i18n_patterns( + re_path(r'^admin/', admin_site.urls), + re_path(r'', include('blog.urls', namespace='blog')), + re_path(r'mdeditor/', include('mdeditor.urls')), + re_path(r'', include('comments.urls', namespace='comment')), + re_path(r'', include('accounts.urls', namespace='account')), + re_path(r'', include('oauth.urls', namespace='oauth')), + re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, + name='django.contrib.sitemaps.views.sitemap'), + re_path(r'^feed/$', DjangoBlogFeed()), + re_path(r'^rss/$', DjangoBlogFeed()), + re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), + name='search'), + re_path(r'', include('servermanager.urls', namespace='servermanager')), + re_path(r'', include('owntracks.urls', namespace='owntracks')) + , prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT) diff --git a/src/djangoblog/utils.py b/src/djangoblog/utils.py new file mode 100644 index 0000000..78e1cf5 --- /dev/null +++ b/src/djangoblog/utils.py @@ -0,0 +1,271 @@ +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(): + """ + sh: + 获取当前最大的文章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: 加密后的十六进制字符串(64位) + """ + m = sha256(str.encode('utf-8')) + return m.hexdigest() + + +def cache_decorator(expiration=3 * 60): + """ + 缓存装饰器:为函数添加缓存功能,减少重复计算/数据库查询 + :param expiration: 缓存过期时间(秒),默认3分钟 + :return: 装饰器函数 + """ + def wrapper(func): + def news(*args, **kwargs): + try: + view = args[0] + key = view.get_cache_key() + except Exception: + 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): + ''' + 刷新视图缓存 + :param path:url路径 + :param servername:host + :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模型)""" + site = Site.objects.get_current() + return site + + +class CommonMarkdown: + """Markdown解析工具类:将Markdown文本转为HTML,并支持生成目录""" + @staticmethod + def _convert_markdown(value): + """ + 内部转换方法:使用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): + body, toc = CommonMarkdown._convert_markdown(value) + return body, toc + + @staticmethod + def get_markdown(value): + body, _ = CommonMarkdown._convert_markdown(value) + return body + + +def send_email(emailto, title, content): + """ + 发送邮件(通过信号机制解耦,避免直接依赖邮件发送逻辑) + :param emailto: 收件人列表 + :param title: 邮件标题 + :param content: 邮件内容(HTML格式) + """ + 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: + """生成随机数验证码""" + return ''.join(random.sample(string.digits, 6)) + + +def parse_dict_to_url(dict): + """ + 将字典转换为URL查询参数字符串 + :param dict: 键值对字典(如{'name': 'test', 'age': 18}) + :return: URL编码后的参数串(如'name=test&age=18') + """ + 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(): + """ + 获取博客系统设置(带缓存) + 若未初始化设置,自动创建默认配置 + """ + 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): + """ + 保存用户头像 + :param url:头像url + :return: 本地路径 + """ + 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) + return static('avatar/' + save_filename) + except Exception as e: + logger.error(e) + return static('blog/img/avatar.png') + + +def delete_sidebar_cache(): + """删除侧边栏相关缓存(当数据更新时调用,确保显示最新内容)""" + 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: 缓存前缀(与模板中cache标签的前缀一致) + :param keys: 缓存键参数(与模板中cache标签的参数一致) + """ + 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(优先使用配置,否则自动生成)""" + if settings.STATIC_URL: + return settings.STATIC_URL + else: + site = get_current_site() + return 'http://' + site.domain + '/static/' + + +ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', + 'h2', 'p'] +ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']} + + +def sanitize_html(html): + return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) diff --git a/src/djangoblog/whoosh_cn_backend.py b/src/djangoblog/whoosh_cn_backend.py new file mode 100644 index 0000000..55171e0 --- /dev/null +++ b/src/djangoblog/whoosh_cn_backend.py @@ -0,0 +1,944 @@ +# 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.") + +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?)?$') +"""线程本地存储(用于RAM缓存)""" +LOCALS = threading.local() +LOCALS.RAM_STORE = None + + +class WhooshHtmlFormatter(HtmlFormatter): + """ + 自定义HTML高亮格式化器:简化Whoosh默认格式化器 + 确保与其他搜索后端(Solr、Elasticsearch)的高亮结果格式一致 + """ + template = '<%(tag)s>%(t)s' + + +class WhooshSearchBackend(BaseSearchBackend): + + RESERVED_WORDS = ( + 'AND', + 'NOT', + 'OR', + 'TO', + ) + + 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') + + """若配置为非文件存储,则使用RAM存储""" + 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): + """ + 延迟初始化:在首次使用时创建索引存储和Schema + 避免项目启动时过早加载资源 + """ + from haystack import connections + new_index = False + + 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 _create_field(self, field_class): + """根据字段类型创建对应的Whoosh字段""" + if field_class.is_multivalued: + if field_class.indexed is False: + return IDLIST(stored=True, field_boost=field_class.boost) + else: + return KEYWORD(stored=True, commas=True, scorable=True, field_boost=field_class.boost) + elif field_class.field_type in ['date', 'datetime']: + return DATETIME(stored=field_class.stored, sortable=True) + elif field_class.field_type == 'integer': + return NUMERIC(stored=field_class.stored, numtype=int, field_boost=field_class.boost) + elif field_class.field_type == 'float': + return NUMERIC(stored=field_class.stored, numtype=float, field_boost=field_class.boost) + elif field_class.field_type == 'boolean': + return BOOLEAN(stored=field_class.stored) + elif field_class.field_type == 'ngram': + return NGRAM(minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost) + elif field_class.field_type == 'edge_ngram': + return NGRAMWORDS(minsize=2, maxsize=15, at='start', stored=field_class.stored, + field_boost=field_class.boost) + else: + return TEXT(stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=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), + } + initial_key_count = len(schema_fields) + content_field_name = '' + + """遍历并创建动态字段""" + for field_name, field_class in fields.items(): + field = _create_field(field_class) + schema_fields[field_class.index_fieldname] = field + if field_class.document is True: + content_field_name = field_class.index_fieldname + field.spelling = True + + """校验字段数量""" + 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 _process_doc(self, doc): + """处理文档字段的编码和boost字段清理""" + for key in doc: + doc[key] = self._from_python(doc[key]) + if 'boost' in doc: + del doc['boost'] + return doc + + def _handle_update_error(self, e, obj, index): + """处理更新文档时的异常""" + if not self.silently_fail: + raise + self.log.error( + u"%s while preparing object for update" % e.__class__.__name__, + exc_info=True, + extra={ + "data": { + "index": index, + "object": get_identifier(obj)}}) + + 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) + continue # 跳过当前对象,处理下一个 + # 处理文档格式 + processed_doc = self._process_doc(doc) + # 尝试更新文档 + try: + writer.update_document(**processed_doc) + except Exception as e: + self._handle_update_error(e, obj, index) + + if len(iterable) > 0: + 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): + if self.use_file_storage and os.path.exists(self.path): + shutil.rmtree(self.path) + elif not self.use_file_storage: + self.storage.clean() + + 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): + if end_offset is not None and end_offset <= 0: + end_offset = 1 + + 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) + + page_num += 1 + return page_num, page_length + + @log_query + def _handle_sorting(self, sort_by): + """处理排序逻辑,返回排序字段和是否逆序""" + if sort_by is None: + return None, False + + reverse_counter = sum(1 for order_by in sort_by if order_by.startswith('-')) + if reverse_counter and reverse_counter != len(sort_by): + raise SearchBackendError("Whoosh requires all order_by fields to use the same sort direction") + + sort_by_list = [order_by[1:] if order_by.startswith('-') else order_by for order_by in sort_by] + reverse = sort_by[0].startswith('-') + return sort_by_list[0], reverse + + def _handle_model_filters(self, models, limit_to_registered_models): + """处理模型过滤逻辑,返回窄查询集合""" + if limit_to_registered_models is None: + limit_to_registered_models = getattr(settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) + + model_choices = [] + if models and len(models): + model_choices = sorted(get_model_ct(model) for model in models) + elif limit_to_registered_models: + model_choices = self.build_models_list() + + narrow_queries = set() + if len(model_choices) > 0: + narrow_queries.add(' OR '.join(['%s:%s' % (DJANGO_CT, rm) for rm in model_choices])) + return narrow_queries + + def _process_narrow_queries(self, narrow_queries): + """处理窄查询,返回过滤后的结果集""" + if not narrow_queries: + return None + + narrow_searcher = self.index.searcher() + narrowed_results = None + 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: + narrow_searcher.close() + return { + 'results': [], + 'hits': 0, + } + if narrowed_results: + narrowed_results.filter(recent_narrowed_results) + else: + narrowed_results = recent_narrowed_results + return narrowed_results, narrow_searcher + + def _execute_search(self, parsed_query, page_num, page_length, sort_by, reverse, narrowed_results): + """执行搜索并处理原始结果""" + searcher = self.index.searcher() + search_kwargs = { + 'pagelen': page_length, + 'sortedby': sort_by, + 'reverse': reverse, + } + 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 + searcher.close() + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + if raw_page.pagenum < page_num: + searcher.close() + 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() + return results + + def _get_spelling_suggestion(self, query_string): + """获取拼写建议""" + if not self.include_spelling: + return None + return self.create_spelling_suggestion(query_string) if query_string else None + + def _return_empty_results(self, query_string): + """返回空结果集及拼写建议""" + spelling_suggestion = self._get_spelling_suggestion(query_string) + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': spelling_suggestion, + } + + 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() + + """处理空查询和短查询""" + if len(query_string) == 0 or (len(query_string) <= 1 and query_string != u'*'): + return self._return_empty_results(query_string) + query_string = force_str(query_string) + + """处理排序""" + sort_field, reverse = self._handle_sorting(sort_by) + + """处理分面警告""" + for facet_type in [facets, date_facets, query_facets]: + if facet_type is not None: + warnings.warn(f"Whoosh does not handle {facet_type.__class__.__name__} faceting.", Warning, + stacklevel=2) + + """处理模型过滤和窄查询""" + model_narrow_queries = self._handle_model_filters(models, limit_to_registered_models) + if narrow_queries is None: + narrow_queries = set() + narrow_queries.update(model_narrow_queries) + narrowed_results, narrow_searcher = self._process_narrow_queries(narrow_queries) + if isinstance(narrowed_results, dict): + return narrowed_results + + """执行搜索前的空索引校验""" + self.index = self.index.refresh() + if not self.index.doc_count(): + return self._return_empty_results(query_string) + + """执行搜索""" + searcher = self.index.searcher() + parsed_query = self.parser.parse(query_string) + if parsed_query is None: + searcher.close() + return self._return_empty_results(query_string) + + page_num, page_length = self.calculate_page(start_offset, end_offset) + results = self._execute_search(parsed_query, page_num, page_length, sort_field, reverse, narrowed_results) + + """关闭窄查询搜索器""" + if hasattr(narrow_searcher, 'close'): + narrow_searcher.close() + + return results + + + 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() + + 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: + 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, + } + + 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 = [] + + 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): + 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): + 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: + converted_value = json.loads(value) + + if isinstance( + converted_value, + (list, + tuple, + set, + dict, + six.integer_types, + float, + complex)): + return converted_value + except (SyntaxError, ValueError): + pass + except BaseException as e: + """对SystemExit、KeyboardInterrupt等系统级异常重新抛出""" + if isinstance(e, (SystemExit, KeyboardInterrupt)): + raise + 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): + 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'): + if hasattr(value, 'values_list'): + value = list(value) + + if hasattr(value, 'strftime'): + is_datetime = True + + if isinstance(value, six.string_types) and value != ' ': + value = Clean(value) + else: + value = PythonData(value) + + prepared_value = value.prepare(self) + + if not isinstance(prepared_value, (set, list, tuple)): + + prepared_value = self.backend._from_python(prepared_value) + + 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: + + 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) + +class WhooshEngine(BaseEngine): + backend = WhooshSearchBackend + query = WhooshSearchQuery diff --git a/src/djangoblog/wsgi.py b/src/djangoblog/wsgi.py new file mode 100644 index 0000000..3e9f0f1 --- /dev/null +++ b/src/djangoblog/wsgi.py @@ -0,0 +1,13 @@ +import os + +from django.core.wsgi import get_wsgi_application +""" +#设置环境变量:指定Django项目的配置文件路径 +#告诉WSGI服务器使用哪个settings.py文件(此处为djangoblog项目的settings) +""" +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") +""" +# 生成WSGI应用实例:Web服务器通过该实例与Django项目交互 +# 该实例封装了Django的请求处理流程,供WSGI服务器调用 +""" +application = get_wsgi_application() -- 2.34.1