From fba1ac0efffa3b493a91c155c6e923237fc096f4 Mon Sep 17 00:00:00 2001 From: ZXY <1908008916@qq.com> Date: Sat, 8 Nov 2025 18:23:15 +0800 Subject: [PATCH] Initial commit: djangoblog core --- __init__.py | 2 + __pycache__/__init__.cpython-312.pyc | Bin 0 -> 229 bytes __pycache__/admin_site.cpython-312.pyc | Bin 0 -> 2725 bytes __pycache__/apps.cpython-312.pyc | Bin 0 -> 789 bytes __pycache__/blog_signals.cpython-312.pyc | Bin 0 -> 5559 bytes .../elasticsearch_backend.cpython-312.pyc | Bin 0 -> 9425 bytes __pycache__/feeds.cpython-312.pyc | Bin 0 -> 2775 bytes __pycache__/logentryadmin.cpython-312.pyc | Bin 0 -> 4298 bytes __pycache__/settings.cpython-312.pyc | Bin 0 -> 9295 bytes __pycache__/sitemap.cpython-312.pyc | Bin 0 -> 3275 bytes __pycache__/spider_notify.cpython-312.pyc | Bin 0 -> 1351 bytes __pycache__/urls.cpython-312.pyc | Bin 0 -> 3231 bytes __pycache__/utils.cpython-312.pyc | Bin 0 -> 10893 bytes __pycache__/whoosh_cn_backend.cpython-312.pyc | Bin 0 -> 33880 bytes __pycache__/wsgi.cpython-312.pyc | Bin 0 -> 660 bytes admin_site.py | 60 + apps.py | 16 + blog_signals.py | 133 ++ elasticsearch_backend.py | 196 +++ feeds.py | 54 + logentryadmin.py | 88 ++ .../__pycache__/base_plugin.cpython-312.pyc | Bin 0 -> 1982 bytes .../hook_constants.cpython-312.pyc | Bin 0 -> 407 bytes .../__pycache__/hooks.cpython-312.pyc | Bin 0 -> 2372 bytes .../__pycache__/loader.cpython-312.pyc | Bin 0 -> 1550 bytes plugin_manage/base_plugin.py | 48 + plugin_manage/hook_constants.py | 8 + plugin_manage/hooks.py | 47 + plugin_manage/loader.py | 24 + settings.py | 344 +++++ sitemap.py | 70 + spider_notify.py | 26 + tests.py | 38 + urls.py | 68 + utils.py | 198 +++ whoosh_cn_backend.py | 1256 +++++++++++++++++ whoosh_index/MAIN_5bjud9prw1dxe6io.seg | Bin 0 -> 18085 bytes whoosh_index/MAIN_7s7twfilcbt07zs1.seg | Bin 0 -> 5352 bytes whoosh_index/MAIN_WRITELOCK | 0 whoosh_index/MAIN_hnkxp6bdzv6onzg5.seg | Bin 0 -> 6327 bytes whoosh_index/MAIN_hqwvke8n7syrdbus.seg | Bin 0 -> 6327 bytes whoosh_index/_MAIN_39.toc | Bin 0 -> 2426 bytes wsgi.py | 20 + 43 files changed, 2696 insertions(+) create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 __pycache__/admin_site.cpython-312.pyc create mode 100644 __pycache__/apps.cpython-312.pyc create mode 100644 __pycache__/blog_signals.cpython-312.pyc create mode 100644 __pycache__/elasticsearch_backend.cpython-312.pyc create mode 100644 __pycache__/feeds.cpython-312.pyc create mode 100644 __pycache__/logentryadmin.cpython-312.pyc create mode 100644 __pycache__/settings.cpython-312.pyc create mode 100644 __pycache__/sitemap.cpython-312.pyc create mode 100644 __pycache__/spider_notify.cpython-312.pyc create mode 100644 __pycache__/urls.cpython-312.pyc create mode 100644 __pycache__/utils.cpython-312.pyc create mode 100644 __pycache__/whoosh_cn_backend.cpython-312.pyc create mode 100644 __pycache__/wsgi.cpython-312.pyc create mode 100644 admin_site.py create mode 100644 apps.py create mode 100644 blog_signals.py create mode 100644 elasticsearch_backend.py create mode 100644 feeds.py create mode 100644 logentryadmin.py create mode 100644 plugin_manage/__pycache__/base_plugin.cpython-312.pyc create mode 100644 plugin_manage/__pycache__/hook_constants.cpython-312.pyc create mode 100644 plugin_manage/__pycache__/hooks.cpython-312.pyc create mode 100644 plugin_manage/__pycache__/loader.cpython-312.pyc create mode 100644 plugin_manage/base_plugin.py create mode 100644 plugin_manage/hook_constants.py create mode 100644 plugin_manage/hooks.py create mode 100644 plugin_manage/loader.py create mode 100644 settings.py create mode 100644 sitemap.py create mode 100644 spider_notify.py create mode 100644 tests.py create mode 100644 urls.py create mode 100644 utils.py create mode 100644 whoosh_cn_backend.py create mode 100644 whoosh_index/MAIN_5bjud9prw1dxe6io.seg create mode 100644 whoosh_index/MAIN_7s7twfilcbt07zs1.seg create mode 100644 whoosh_index/MAIN_WRITELOCK create mode 100644 whoosh_index/MAIN_hnkxp6bdzv6onzg5.seg create mode 100644 whoosh_index/MAIN_hqwvke8n7syrdbus.seg create mode 100644 whoosh_index/_MAIN_39.toc create mode 100644 wsgi.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..3279070 --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +# Zxy指定默认的应用配置类 +default_app_config = 'djangoblog.apps.DjangoblogAppConfig' diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58530898f8957e063b0cc9d32024b397c38d0a35 GIT binary patch literal 229 zcmX@j%ge<81VRnBGHihKV-N=h7@>^M96-i&h7^Vti-(Z{G^=xbiKra zf?_=vIN!0Lz&SrJEi>Iulkt{NN@`kSX--KzP*Hp`Sa>DFXON*^9f5?4Rm`)6Jul}j zih<~N0-C3rn^;_uTBM+1sEH&RgRn3rK0Y%qvm`!Vub}c5hfQvNN@-52T@f45JdksX Wxq-w7W=2NF8=_Vh8H9>hfP4Tz`a<>q literal 0 HcmV?d00001 diff --git a/__pycache__/admin_site.cpython-312.pyc b/__pycache__/admin_site.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0fa2546677058d391d09b56dba06d36c1160b36e GIT binary patch literal 2725 zcmah~-ES0C6hE_{)9rrri`w$Jt%#k{E>L_T##l-NN`;5w%cN#9>|C}}<^yJC#gfMC zLlYs<=!@yTn5b{0`j_|u$r75IB{9*M@TL|F@yT=V+?^>1nMr%+ch32pbI-@Uv%i!| z1;A&hMOU}-0KYMz_hbdw{W}G)1qK+B1-7J062mDgWvA6N$7w5LXVomn87pV!)jY>p zOa3_}P+x=!5$canQJ|bvvdd~&_*5eMK!gS(G!&uX2#qk5g7aYHH^GpJw4DyCQ1_*P z>@)g)Dr`$aGfGC+PV|b6-(-x8Z)3&mk>E9){p3I*=O8(h$T>`oByy@`B9Svm-b~~i zC2u8ij*+($Ia5SQ*9R#oLEBafA=UZ-%48$a5Y=*rZ(U!{0?kLy;~!}I^2^kDJh!u zn>UC@Wlh8V0!>rWG~*NN8qGMmO=zE{)h*rkHO*(itJT)C1$>`7v+~CeUq1Qn$CbE{ zSzGr5;#H?kDSNalapo($D2?S`yRq(-G0yFSv47NvS}&I|WcUNPJa0i)F5S&PkjL61 z59PzVsDl@(XI}$eJsZasR=UP0FFk8}`z374;1v;KCo)YR8&NcUjkknJO3JICk>)*e zvq}8GX922JZ-^<&tGcgY?`+fe*^DM%RmzK;1L)^7=N9Z#M!(*;yYZ+p^`J6!U!L-Y zQH@6~%bTRgY$9v|nnnwnX1hkyVz{VjH=DX8Ed8vP+A7fv%)G#bzzi(Hd+V|G(UKTo zomdu5SMml~4y;}8^D;d4%fXisW_Saa=bNxIG~2qkGdR`y@L5L69{DFHWfh)bNFPTQ zhGL_+q|DKgaK^Q|i#N)=TH_^nY#j8!t4uX4hgiOsVT}DRF<2G82*G~4{ud%7h!8o( zDC_mQ+jIiqMhol;g&}A^$03`R>#l8+m{^HqK^O~@;XW7xsor{6jm$RcclBnlDk2v{ zL3mc&TS(UH*Lz6}#0o(f#uN1^o)_KG8^jY)MqX5gRNiG;;5+mCLS>IM@N~#lR_dY9kY|Cg z?X)OBsP4NwkB#RJw9>erwILZHJeusly80*4#mw`i%VUFYdh`VsO3lm+Ke43Lb zrL8;#eEl^I<%#bbol>n;_!SC|VE6$He{;V5Ne7PaMDm5U-GP}{UTBYXU^12$+h;m( zESAr=S2{2q%S-nsPj}#*c!QS%zJ?Gxgb`!;gZAegsKq;6ygzZO19LF}6>}Xp9V^bY Ql@1(@WlZ5A%XHm z*H~46PdNAnC%(xI-`ax?;pW)!?LBx&{7X;4wbCoi=d2%u+cFFmWp}Rg%<1uMuNQJ5 zV{fAytS5u;NmodomU3F>v{Bh&>2CEmTSIt54q>pMCdMutpz5Q!hoE9^NTX-I$~5ni z3uXwexi!^=QS6co8K;pjoQ*(dh%+oA+Bk-|tV~eQ4!V*g!?Mdn_IZ)wp%X{m0w7klbQX6noK7 zm9fH3J3NTHfzRR_y7O^ literal 0 HcmV?d00001 diff --git a/__pycache__/blog_signals.cpython-312.pyc b/__pycache__/blog_signals.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3632114f313d33c9e3b2db22a560efa304f61a74 GIT binary patch literal 5559 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 literal 0 HcmV?d00001 diff --git a/__pycache__/elasticsearch_backend.cpython-312.pyc b/__pycache__/elasticsearch_backend.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..87a8e0727f2bf36965a7e83475c60fc02a702fc1 GIT binary patch literal 9425 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 QsM_9Wjc)BDr&6H*7x6c&=l}o! literal 0 HcmV?d00001 diff --git a/__pycache__/sitemap.cpython-312.pyc b/__pycache__/sitemap.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36414fc3ee4c2368424c7c3b7b4e41ec78f35e50 GIT binary patch literal 3275 zcmcIm&2Jl35P#2WdpFL<=8Gf^33ipFttps<@)=bDnnFUQ<`STi70KFoH_n=MoW8e# z*e$9Lxl|xE2c&QS1ie5ME=WlH5eUha4~bT-goFg@4dL|C6Eknu?#6*u!sd1_}8cU+OF5F}1IO*&#a7sYP-$Nh$IHE0B|tlHesPmd{78P~#ZZ#W@cGp$0p zZW86lVklX3H0Lh(jVV`cJHg( zOBb`+S|)!1)Su63+%{P3%v9VbUDAaD+FRmo7!vCRCxk374j%J2AwIOj85~K89CVMA z)o59X*5H+-14zAvt_$~vfY~8-l0G!2M;5=2uSIz7XsO9yIyX&#~TB0^`}g$^pV1Rb^B zzv|ctUongitpbape>^0N+L+8Vh%;mhqQUv5#crQAkfB6(?LFMu&@2j`x2PH`HEoU$q8ur}P;<175`hmN`F z#9;A0AeheHTi0%0!@{qsv9c0-a;fa~x#49I!}Pc;zJxkR1VFsz-s8{WNnaLGv861w zzt0WkPa}I7P5Mi-4FsYzH-YibQX8dW*^OSV(=B3Smli9F`D$8O9NtZdGU9tYE&V+X#9=1&XrchtVmXTOvx zBZ;5npVc4L{mI$Nb0Oa847&HjL>jIe%v?@z zyq;<6oFVMMcX4}6Y%q&6grC!nH^~HyQv)OjL3aX(mJz{MG5kcA6h$W7I@&`45AGFh zhRwka1R1{!>{!wMb!LOvw$I2=&u`1Td#$6Y?-;|Cjjbo=yu I&~g9rA1sV`W&i*H literal 0 HcmV?d00001 diff --git a/__pycache__/spider_notify.cpython-312.pyc b/__pycache__/spider_notify.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..200e720bdee87046ba961e568daa52dbd24bf615 GIT binary patch literal 1351 zcmZux-D?zA6hC+F%s7*5G$vh*X^IIOs#DrPp$~q9N&bD%@P^bott%+-JN*n zF3Cz#g5)6qLBxkmA4)&yOG*ET=DANnC32fWK`Gd$=u%qjOV6DTcZD99^L@{pd(Lm> zm!6(3K=eH znHqcw9$g1d-_~N^p$2h`=gP=BL|Oq8qEsseg(w{HI2T&)qus>!s5j>{Mm@3)+i_-2 zM_cQ#jRtjenfiROki74nmo+qz?iO$bjfg{G-56uF{T*{Yj)P zS7`h69PV$Gen}g#Tp~-bOy^*UI5aCs7w$p#@*?kaNxkfsYSQBDR-FYRkouw@2(G3e zdnYF@PF;1gvsW%o&$(AG&q%Z47mJKby;NK9r7qaAkoG6b1-2+keoY#TbD!f%^g6Eu zTtP*eoCWoYkc0(lpu|MfKK-H7;hp!w zgd3PJAB3xRYvHcP78^2kBVY3BvE%lMPAH6ZhSV~GYsl8%KsW56C;i8pXYZfgy7J({ z#?*fLo#yrX*MB&(ml@y9jQ{-6qx5GR)BCBudzUsZZJFP6f7SgsHTuu~k%5;$>>(AH z*__!rdT(}f_TK}Fu2;bT*1j{5o*00g0eceY&PaOVJl#2Op#BCZ)o4Rdj-k>7fjpw} zYv?5&fHpK0Bpi};G;Uy+A47&)q-xL#(!)HBT$<5jZuh@2h{&lh6DUZn!87ZvJ!@pw z8u>Q!*m}2xKKwRKX}NAKUuCW)AzzQ_z67z zBk(~KVHZ({s!bc8;54#zcw!uVp_#@|i-OsAptOTt)Lv-BI2HOrEh?gpI+i@7(1yPX zu9hdeBN&Vo{Mtf9?7>}oaO=!NdvwkG+f0#DF9BIt>p!1PAmac4 literal 0 HcmV?d00001 diff --git a/__pycache__/urls.cpython-312.pyc b/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..513c11770460e2e054c3b401e5302f1e5bdb5287 GIT binary patch literal 3231 zcma)8*>4-i8Q-Or*YXl+$&{#T7273g%S+L5TUV(S$T}Ljb_~aI(QXAXYtHbH_aJ7M zu|*(-+Lxw4U;GpsXwl;-f%Krq{tpET2~=Tr0U;=g7RZ}~BrkpHH?v%cv7;gjaQ3_A z_nkA#zs6!B2Ty9tss9bAe^JJJhI_;p(E*P8m_wYxAs+cOUiT?Jp3(!_fG#KkqXo^c z2b2J#{aR2DDIrD&w6HEJq8?Eqj1K}IRicazX)!&n#2Fpd68fMr$Y@bZ>M13~=!kYs zA5w-G9o2^Q^U8Tf$F#IQqKxRH%BY@EGWwV@rjIM*Jcmc{C?3P(ID_K9^D7f5p-iGd zWeOz~$;V+4rC^PqbKAU|-?q&$= zA8BSydidTQY0)&QjarMScEdD8akY-6_gh43sOd{zf!BWGhnnY(1Qd7gKg(VB)IZ=YAZre?3u24W`#gbj;O=51~iY-@6y?~8E zQ7+CFY_r*@SOscb0RL7&Tz;zR0A|gJD=ni!!*l&I)S@gcAS9`V)XW+#aa7SEKYo0pt(88q00GvQ8jqucRy%Pr)Z{8ZO2!rZv5)xLBZ zwM`n2WKZFqH;;ur3N%#6rV33gTx(K$%qMdT9 ztE&cdfw=L-z9wtO%_oLU)XL*tuHh4Y1wu$S39dGtLh-9=OS8)w+`uis0#yvI#TG46 znV6>iZa(J(%Vm(2%TDAp!z!U)Ea#g$Md(-3N%vW^5M}oiu~~8^PPN}qEfSyzoZfu`+<4^|geON(W=Mx98He(&DBRmZ=yeCy$lobcV{rP2aZi!MpEpK@P- zOYWW=Y&UR_{C-EcK?QeSWdB&` zWawT`KebyrNX~bJx6c%e@1_ruKj;W^V6hjSc>d&*%5ehF17h17z517_gJh{A-0nHw8_nrov>M!MmeR*N;{?A_C$NTs3LAH7f+8Te%CHNW-HIO(V y`s?{$&3A>wv+q66z0Az)XJ)#&*_XMu_H%E6+o7As9PhvR%O4*Na6*dE5%?cjs<_Gk literal 0 HcmV?d00001 diff --git a/__pycache__/utils.cpython-312.pyc b/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8205f44a44ffd8a3b093f6c374fdd9576616641a GIT binary patch literal 10893 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 N@rs#cA7Sai_#dcKoiYFb literal 0 HcmV?d00001 diff --git a/__pycache__/whoosh_cn_backend.cpython-312.pyc b/__pycache__/whoosh_cn_backend.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9c86766335cc142e44a23d4765a2025597901d1 GIT binary patch literal 33880 zcmdVD3v?UTnI>3;7x5w<1VDlWNP=&HlqgY8lX{XO^%mtwbR5I78JGYiQUEDefs#mr zj+}9KQ$5O>O1sxo;B$Kb?d*+y7#~T>;Cn(#l=PrSIO1P`N5Yt?nAnfFKsq* zXGO(vuW%tQ#7DR&AK;^^fGVmEsH2*IhNpB@L>thuUtK`Qev1M{>{lPqvtL8NzkaH@acAT}v@_5d?Fw|UdspOOv^&rp?Fsa-`%>H=3LIwfvdEF>(ZEp_cSnvz zj|YykczNVR^km>9&l#k*)D~z{ap6XaYq)KAmPOYDA@2o12ztO*g(~0T zrH5bSabGn2r)xmjO{hwS37iIuVffGPhj}ZT6zmJBVsrBI{>S8)iaX4Os$b+nzHrZ5 zYAH4FS(aLZ)Y`C1NqwB9)*-b%T&kpgj-@srwK42eQlA)C)p6n8dQLF#9P3Yo+}1Dh zm$@tIC%DVJzv&lLpf}BV(~}|4{Ves1VEOLqnH_#G%oVjOC&*G!p9{92*&k z(YRUJhN5A_4TGb?K)7Fw34U$H*cBARJtOCaM#338#m|I;!oc~i;J}6ONGM~?C4FWr zER1K&J<*G?@zY^(ED{qlhREo-{%7fdU!5^@AM5Nre5(IopAtRVolza_#xwiLAyGsd zyTcbzYajrL$99307Ld#(frVj1J3r9!0k_*EIpxp1t12o=PJ28Y5ZknW=xu}J?w4Df0+7(XTFgX3Zh zee74BBh`BFJcdCGca8)j;}c;4N!lT__6j1lGqG?qiXJEr4Vj{@Q>RY!boOTS-JN|s zeMe9BWOPTnPaHkdmnk~l^Tgw)PIqUty@yYCp3E5852c*R6!kuKvgh>CgBjhKf%D;L zFr(?~`COl$7fNV8s4}KQLy_>A*r*UZ7tR<@2cy|I9@+6yW8o{YLwH6*gdP?jiA5ua zMuliFhFi4MaDM3A`3U|~e@*PIJsK25_RN%hagU;CdfF+Dj|@C6&}5o;y6=40*L3;( z=%{$U$@h#j0enLE*|8xZEc#-2Iy8boi$sDn7k#6HzNW_|=vLpOcu!&&(1T&Y7aL{J zq3nBP;H?=`fB&VhK-0Rv->?1!p@Ux##uq3yvFqWrXLI_tw(e-%;u{rw=Y%k3w9q;s zKD}Z8qkG0NYxkWEJ-2<*zlrXn7^m};ur-?y3XW%Q`{?=k(J=;(#~loQXaox|3&B#v z@WALuD4Vk3Y%9vzwsGfVV84HV{Qz35Tv<%T-Eku66)wyL_z)LRh4_FvqzY)p)tJW) zX`t4;s)-*PeYpD04~ahf1$}Ft`a~du2o2`>;0PN|>CLpRfof$%JRbIqVep4yH1g+z zm%=_YT^tf)m?3y2Y^nN!1Hz~%%I{Td^_{sG9>5|QzzZGU$jUi08WA@7J{P<=gerq0 zAzx1*)yUbJUpOM{lL6Me>Ff@{-T7-g{(}PVdWsw5L+TgxpiML{a-dAK2#Z1` zA>E67K#$)d{2D@zkRHFrkO5CkAtOR_$Qd#rtvGBkVDEg`zmO|revy}VwtzJsHdGoa zMkzKqEmRh=;NBjxqK=Y~4WT1sN9dg5uJ8fZ8P4yP=jIx0j47+`?Cs8|dr$RcRHsg7 zRDGxXj(hysdwffV-!!3WY@ATVBHLjWBwH`Ca)nbrohFyFM(1taB+T+AL8PwH~9a7 z2aUnUxqkUQfDXi+MlLo+WwI--3aLXHc|Qh>F85^ED#vrbA+5YW16D7m$5jPkb)h1e zmI)PIz*G{7SgVwF#JORm47n^oRUqA#yO(34CFYRhA$^>mKwIV3e36g2df=lsUrLw(pTPo3%OKXLTr(Y_%((`Ph~cJ>{~6rJfib-MF#PewC{eP&|+ z6Qg6k=$IJui3}l(0~0k7U=z^2Zyb2iC({?S>_(MgvS259SI?*CZ`A^&}S9urjd$mA!;xF=a^=n(V{MQj~LWT;XLQ zPhhboRwTm?D^poc`E9w;R?P$3dMK}C!u9K}!tKiIUx7s{BLLBzw|KRMOIo|2A26Pt z+(Iv0iabtO=*rlRw8zTj&crI@Scni*7DZll#At=E>#gi>4zgcCg}Johsw`OEPkxdP*$0oicLc^#tB=IrvR!QZj<41zlBSe+l{i7w-eIf&MW5}FT%D~xRxC< zOzyXEE$Bmb6+f(=Ixe z5Pgx+V2B9IG4O2>-$*zd3Wst)j6--6>`D%si--CqdWmJA?SaT%woysA8~YY`Hnuwv zVLdV$^IZmu6MQBTW;+;hG6Q3RK-4jIB4P76VRY9^$HXB4f_{mBLw! zmk_1k^9VAkQBep{L>ImS8e7chqrnT|(2yWzbisjvuqb9UkN2NCo+&zdigC3<3qcoU z_c|fS@}4+#u=B(jVIyvY5lS&XGd2_n^-Hx1=PAif%qcj2aIzunrRY?~25P-uqQ{XM z2}xTV5&GCranFcsVU$YULPh8@KJD!j<%PCQ_GlQ;;@2@(uX4ZCa|VCfSw3Z0)p2H< zw1>T2^3Ae0%f4ClX4NgtcMRV)Ecv^Z&6byrUpqdddChRsuw<@HyS-CKl}!&B%HJ%X z-~N~F-)jH2&wS_Hx6dv44=tCJr){54+nlc$ZWw4Ete}0+L;Iiy`>R~pvZrx2_Koq^ z$CIA6sqQbGz!qsMd->ALb93EEYukdgeYtMa*Y~`vTeA6XdumdimV~Ees{8thmCf5y zn-3&5A4r?aW;7}HrUd@XttsweemD_0x zKOX`W5;MrP#8q;>Ns#!HnmAvG){ys?Zf;uj)NYQWJ!Dclsf+6h(H6p4q!*wJLR#zx zjWN4iH^_Y5Fb&bV>usIf*SHE0%1)I7+k@ydKk|c^vfI!YJ+Qo@x3XjQTgv+yYIEmm zkz*nKC2F@vz6X@D_sD7Vmfkje%61PupdRA|?76~@ShZYg;hJK;yyu0+&m_|;zoowo z4`_#(wPX8lZHGdkpq&5Wc8vVv16oncT2UWU=$*o&k(>Ds>+Q%lA5e>h)zTJID5sD$ zWGh?~QVaFc{%K~{$mj#=FJbj>jrrww3UP)lR`iI=Da_PC>5I@F=s%h1AFP;}z$CDbY*$1p~BKG=~)rd0oX}CgoE3Uo( zT3s;3w#oH|%4D4OMLw>HDKucnEek#dRS?yb_x3>qtRp%i9LnfG7(nt4vGKn^IT`Md zUnRv%5+U#mLCm! z8*~7xqG)6+8XE_HPkA%iK@e@ROcCmlWb`BFgkba+vXXPJ z-{ml*l!K#VkX}N35gZwZ^nw%`673R(+%qex#y|*=V@R^hXTlI|gncAJhkApFI7hP? zTZO%-!tY_y`HbQ5Bd1QCInsZ$TOhVjpuGV~g)np!5TZxLjAcmdk3u&xbSW4a!)wAI zf~X3IgnEM1vbU14vP6kuBiW}wC?jK(UK|_ZIx3t-T_CO|qsFTczC^c#!h}W&2muJh z-3Ww!q2bAj+wqbwh9ePlLZBHe&_eYWF;Xt0lSV01ERmT~3&f0_1T{m!NdJZKIK(ud z!iDFk%xsIOiJ9!QVxkkV2v0!*(k01Gt{`E;SBMDACu`}gic{E&uX4A|mX{5)&biQ% zZQHWh_44?fW~ppbF1A{vDK}0P-?ea+zS-iWw{<~Zp02E3&{wQ9ZTd$1_4qfQPnheb zj?I|T){2zXzi9QR9aSkuW5Urm*N|{*UhX`cuCAN+q}sX?ZC!WOd>7x%PoJD^N!9L6 z)b33>_ubWTUHnlVP;=e4w!ime>g3ajlTS0q&##4Cv3pbYri8s|p=E!)7!@hmf#WNRDRqctY_N1*t0iSDMv~OSD+pR#qBfe)!9eE;g#YH zyB6(R*JK-euO)Tt$;7cIDce(d*%sH=b~~>&0^XdfVhyZRMOWHeIV;S!q_%b^wstR^ zekyf(AaQztGKL>i%brF1uI0Ul7asl0!jn%eJTsI&6-b>5CQb#@ef_DvaH21KSF750 zZf*9vMs8mhpY~M0y*b&u`#n$UXdrPkun-=isxIWgCeR^zp>+vs-E1srZAmw5Ni`fu zG#ps4)us;}%XJ5kKd~KLg>rhs&budg&TO5UOqwbeYPK$#wl0^{r)?vk#vK>=RS>}l zb)3EKjYEn0-E&VNSg`I{(C?ANVG4nY>1o1$!eMvjFil?}))+VIFVTi{f?9XUTN7ic z^GnUb4b!?=cgcCcETcy0%G)*yir?hz0_3l%n|tac5w`|tnn4U3UVr@* zHbFvW?8*`EA^nAJ;3hjtP{<`23bkjWJU5Pi5Mn4II0!5PlX|qh0AVBYU!?a{VA~#6 z$nIPn&>iRt5zoVlAT0+=B0SP4#QB^!EjOwm?W?LG9!x-XM(`YjAKaw=dHwYtOTU*D ztv~vr2-whhrA{Aq=@HI4dqP{`&T|N)Vy6>V82J25d z+2}LFw9k?3e(WM`_kC>F4`RdjAx}mp0e~3Lhp!A^pBx;`XrW^kf)hR_5uidScebF< zH;AA2iO|zu#I8ookv2$&Kr#d21qyr!GDT1ZL-}}K*h+Wi!7)fKq*jHY_Zt*Oqx~0| zK0jlX+C|DgrufeodaewFnYbQ16?!ugi;VXV28SX{-jCLiv4B7;Qy>kVK%9<1LcxqS z6n-!XIE>gj{n zu2R)o64hJg9rI_tUA zhJ>?i?o86zwpznEy?0^g1FU)FU+$f0TQqrrA&VU^oxFDPOQ%+gICJGEcWXFz&BvUD zittopzgzTdO1mqe)<92=+&J<|?~UHHdw<$p{+j8gDdpanaBqa{rF_#$O~Z<}Htla+ zZhqvJBiXzcI+BL{t7>)i0e;oMx$1t!6}dY3k68GdRV&r-o4b{q(? zvD~mMRCm35@FzBOj~?6;TQI`kEi38vtG;I~IjB~BUv21a)PCRGY3r`h{#A_@H-F`C z=&@^mVB--EQGotLrfq?!qY3+)&t^mp9_99KX=Qb<>s!<%0vVS^f> z>T4Rg)*K^BcovvR!KuJZJF&ME#HCp*TS!Ds8^^i}I8z`3@GfDwteWvD}vkm{s{7e*V>vtLoR{~5tm?o*--XYDLV z*fvN~HN^(fLK2EenHv-4#&k*PD=jx#QYCeXlDdC#m3^fgYsXbj^3Vn#iQ=YTa_Zv7 zPe}Ax{xPRVZbwrLAZq-~I@+99FDqAm9PiqjNVe?-A= zRt?B^Pb6;lyZZfIdhUCA!@(l;_v{-Fs?^_CX%R2H^4|DwNL9u87uEYl>Ku=dL$th@L5g6$;QZfK-GJxjKgxAz&^oq#9O~g1OYVQu71xLq6qlVUpCH zzFdAe#_$9G!e%_JmD6&+g{Zv{@I#>%eL-naTpQB8tq4VVZlvyaC|#e;y_P!DKCc~@ z=rk4sCc@(eZs#(bYgQ37Y#tp4=pMhZ zPk5Jd`W}{(K>h2fCx)*wMp%K8$=%36IHQR`*_6>j*#gmwIg7)X906)4&|)JYNg_d# zUg5hG5MC3$hajUF9vvDfh}Lvjxq#Gk37^@doBr5nzf?^|%Tgrl22xnUZn6T1jYVMv z5PJk`Yffzc9cZEzlYPNdx$LN%8GIx5_G7crCCBy!^Y#VD_Ek-V;TWH`x~857iIB3@ zBy2VB+v-;88WwE64=d}Yd(zJOl(Qw_Z2711+BZ(jyO!#A0sED=|5B}TbpZQS)q{Gm zyIwKgFs5wH30rfzwBnTuH!h?~n-Zl>X?OK&=9}h)h8;=wPB4#Uk06V^<(Hhw-u?;1 z1%xoo)Pj7BX>~1FYw{?Tv=gl;Bl>8ea#PaTns&ONxd8^XSO1dJ*y~o!)PO8`lXmXB z#lPE4)l~nAl6j6A$7?8hNHBoK%L?6Z+M9)DF_ub~sE%^Dq$IxR{f4{!d+hbJ! zz^KK|8lw)H>-7Il#X~3^;b_j;vdkE>8*i#_4Z7{`%wJc>49H=ez&% z=(mn8HSAk9mk=#JQ~#R(rvH6&ecJ9y*&7$_jkMXHxOO6Cu1c7zmdrjBV<>*fc+L1F z^ZnL6#GkQ=K-9&5E0VYHq#0JG3YwO^@?VayghbF?YuQQ(G3v>fD@qdOG=TEoLI(GR zjkx42d+7}j+xTgTt+d`u!<*835_-@3`bue(v)c*E;W%0nY?Mb3!6>U0$iVl^|ldjII)Ak^_0BlZb7uD=K?@O}vOGbR=T zpM5TNem(hWfr0SA7#ZXC6Vya)A?|lAOgD`%tz&79moAuUW{2lH|8z86UNLnH^VR8^ z(yy3Y!1C#G-_)@cQ`v&4I_>dJomeUL&F)E*Zk{sz+*CTVd(Qrq{mW$)GkfNsiQkbd z+c|ZFrn`N*^IGwgK5Z?V@hn*D7xeYgJ5d-I)J?=4#*VuKUiP0c^J7HKvn#H!Yj{(n z_tc()NGzm!6}p+6g;@@2k}sH7(*79Zd`QELNApSvX(t_+JAVpd(uo=3V!}NV)|JLf z3$2!rPDZ>Uf87;#h16_rxJXzRcM0WjS6&Mgy*aDZlrxrvwHVDET+lwLK*B!xX=)T2 z#xC%vp$a8sZaLP4>)HH%<^{$Y9(s8}xL1oGCzonD_jokI8&})nvaiLgc zMK5?`3ejH3@HS+~F}xw;+lmevB02qU&1-&OUc+zATl}EBAVSN4D+`GO3lRJ=u7u#2 z<~qCLwyXAnHi4FqUKUSI+TvEBD(|gW3%gNnvLbGL;e`i`J1NiBT}n%l53A)F z)ar>>47aQU!!63(cz^2f=KIs~@P|ikoBSK%-Y${FzLVa(5%nq%(oRz(u2aLd{oq2`l?Y8^-DvzJ=qX*RZuraUv^kbTbiL2sO zSM%^J%Ghy#yJ$z1$C=p+RZsfjKJ3ztkmFVLWKF#61#P@6UK4VXLWHS5Dht_E#;fAh zgDR*%YDfh#SsSk%rVQ(@c=ZL)W`d5rGcM$MRgE{s4ewtEhTC{8lrVhU6t}~&A-fw* z*2OD_JMV{;#}@eVp!_sm0s8`?191s9%+!bGi`TJsbW8C0AuzrPGm0BNoS!Pi<0g47 zPS%e&9{{85pMX(e-PKdyFYdrTsra5-*>A2bkAX&MeI4<)7^ja zRCmvbGi!K6B3fyA-r0)aIL9+0=_zcPAY8Hp0VbdF(*8&O6&~oqu7RxhU#Tzij64S$ zz?!~Xg_j)nA40`1Q1%?mN{@(@n5&;~F32TIjn4hkcfATSxG zSf=Q4>0a;=2$NX{DFzajcv4I|n3UKhlZvdAhKYsLMBA%_p^)%n0w;AT9SbJI7ihP1 zQY#$bZ%G_e$9%xfERR56zN7!Nbq{i9E5Uf zb)FEKLBkkNn!L08qN!%ZQweS1M_RShvI;GP1!f`cnptnMtaZu^g+xB+Y-B#P*x0c? z2gG*{_f*k}sbs-a1$C#lYU)I~s(R|whu*qrecFKoPhAkhY{Oh);{$57WD3P0cG=w zsbRsiY1zMJ+2&rT*#E8*BF^T+{DQghBfO@Pmj`AVU%s&Htp$#K_s~-9@uc_owUa+B zZUW0)S(EZ^O?bB^z1y#yT*089ylt{DdmHv%)7HubYr}F){nuU7mKAe_B%GSL^xAVb zpG)uPf}PHs@Fz!K7Z&Z!i?)NP*4M+|g{t=undRUVoOQ)izF=y=O?BOC!#9UhRc(o? zwq(^dyw}qDX;r$pJ=J_5(R?87biZ=^#_^Q1A>nLzV`yIW^=Q%wV|WNqTRQ1Qwsi3e zwkAwFXW2qoYtpfKzHQO59TMWSuLkqZTlz7lc9ni)qwcZ6Q=4VQ%$7DoyzEmJ3P7Qp6PfHhQe2P~k@Cu~it8cT6W+Ov7)k(6h1!m}B3 zH0|_$q}6y!P-R&?GI~p<^{cg<&7HDVC#==88y2n2QZv#moAI1lqCrc{T$v|T+L9=3 zN!#4%in?^AKV7{E;i`!%+jxi5mbFYB22`<&^ny!f?`>=8^cUuemaJRSw(69vAz^D+ zvNf$ZDyDi@$n2qh(OUmgM@_o9HPw7H(R?)RZbbK5HedyzZ7rMSR466+mPRSHsb#@d zho{~uV2E^62iiwSLQ`z{^~XIBk9YH*+`Y(i&MN3E(Q&l6%)eMh$4QXMSBtTgJM}T^ zlmSqZhVqA7N>16$KWcWKD&_vJ)O%`&`tP>$2nX~)h4Q6{i#nj-_-6=8`9kvXVN#l` zyCw}Flk;1UeLiW-iDqPSeiFWGCe48V9cADU4Jb9obr22MG zlU#t3(-b!q)Mpmd9>$9y8nEPv1}r(tD~JYkamyOffJJVD{0p{2C6q#pE1mJS7p^;J z(Z)mx*0_m@5(>Si8o58YU!`ZrmlY*II8#Xcfp=e@%O}SQ9iPk@=ErSvU*uni5|pEl zllHtfU{@eCOlVD_1Wm~Bswyi=FoW?k7ZhCZSV3$DL`Oliu zF`^9FrHmvopM^%hO@_?<%6WvZ>)?a;sDktzo{pLmFCq!@F|*lw)yYT=TY376nW?89_g|gW7Jh+Zh5rLVK8r2fZb<`BpUoh?jZv$hvy;H+^#ViYG&B$i9I|H?6{ z#6=1&D(Ke~gVYKPwN%Un@-`v-8712ZfOPVLhXOc_gN+#lRTRJ)m=ki$oQb$MnQzoG zL4-6EK7zYV<<%l4lELzWSviS~+W^4As=Kv=v<8$uw2pE4hw7R)_RY60)pag7 zyQX@68z1vn(z6Xrc4^fsqc=upBgxWjQ-^O`OQv^SkEg2|W`o48?MhVbN;|7&kAV@Z zegu42^_FQ9W(#9l-YL3ediQfnTVPV?N!e->w%UcdZA&(o^B4`5<+9q@-ShfeEy=P& z(C$`nW@pOeNtiq{=NCD{+YCDZ$74=+*;*3gSB|gt{whA(TwB#G}b%xH~`55Gz4CV&+jr z$3tE{S(K;2QG}~F(xAPniz%f{YKOQ<1BiYjWd8-(1h&bWP|LbY-V`T^ST0`o5Vy*6 zb51*&BXe+Gnty6BdtSPD(t-me3d@iyE3OdiS9lS)8oFw0+>&D{SlblxB$qFw81ymy zifz)4UY6t-i?}Urmp5;ahK^}o`2DyeZoY(82`|Jg!&LCPOYRTI=kI0P?F6A#kWm1M z0yIe1WNF?(4n->svWo~W#mrHHr4YwnzL3*Z7KVwJQS;z-iZ)VQ!r;T;hHE9L{7`6# zyI!#5%@2hj%vu~kTLeO@jHs0BSK_YWLSw8P_E8|e^w4&YJaFBWBSe)lzKL1bv$MJV)Z5xI3nh(Q-@U<%$l@TR4B*dyXIC z<8byy?+HE7(!9p{P)32~N!&W9fsU|*=?E+1MZ@j)wA{{B`T)Ilg1I!;TM8YRw=MgQCuH4#Nn#$RgFR_S6n6sV-md> zK^?+p5oC%$@Il8C6W*gcRTx%9iX%!K6k0|qlq!sBA+3vGAtHt((*Ei^4=Xq65fL=Z z1Bx(?Sf+&BpfF2UMJAsygrSOn2UK_y)6$vdu%z?QiWlJ-RiG6te1-Cot1u~VMh%;N zNuR(7!LJg`W;6m02_g2Ejv}%1yh05kh9HHbNo}qZBJJ5}D$)&R#6*I&CC{jcmx3BM!-h4|)nC;QmPP znQ$ayl;}=&Y}cMgx=M$CnVL@c*JwjV%MK|sikuWwbfkff0B2NUka>xfe2T*8cofcH z@e>ws!nrs*8WLp+3rR`8N@MF%8>PM0^P z%G(p=?aA`pQ@GUD&DVe|s&9RJ9Hpe{_b2N2 zzpGBxcawPb==4O|SG!=YTy{3jj?XtIoqMNx{?Y23am}_amTyVdZcNo~P1J7vN$vI< z$8a>umG=5m-fao*w)vikBS0@R_k-F#Ro;hQ}vn}Q6TJ&_KtLtYb-%-!^{DtvW@XxL316>Qgw)y5o z-`^ba_si)hWp5Y%ofF?a@m@t@PhV=!*~Ff+X$tidtBqV~(;MRpuC}zRk^TPM;ax8GC(B!= zyOu$Rb|-!9(?@^0W7jQna!1ckyocs`W*&Pj@YTSA_t10?g4^}m({8`yHfJuBYVAq1 z_9WehaF#6Xu2^>alkS$ZyXKBb*HAira@EF_SEk&X6YkCPo93%m)v8BI7n}{V(S&n5 zR5%rlX=f!!P5iw&o^X-?_#=b1!Vh_R*@llyh)wIr#M(`#$7U}5#MZcCbI$NL3}4o) zUf{W=j@1#IddoT=`l+=t-PoLJ>`64jXuB38(o~;dV(6w8In_t0wIrO@bM9&=AkwAY zblpx=YBR2KT7&V|AMfWX5Az~XZ-4D~9BtICahRh;G9w570W+VxnlndrNL+tC>_jmc zd=67d>n@;hg=ow@lDDu)P0k)WN95t;2FSKJ%odTG_~>;PNGWKjaaLGch=8QizcPt4 zsl8sxwjpA_*In6CaAvh&De#%Ot`uz{Qd8b`C-oqm4RJk6g>eWrCe5&8U4^*CMy0~3 zwk!cQY08t4DF!N%j2Whf4RJ%EbHQJ!#&WvnOwZ}h!dxTkC>m#YBT#f_ z9Lj@(M>Nqar;yt5+ZtcR{{wlCGNC(*EnP_SVcisX$+ z_vWeMpIbd?MQSF6^(RXG$NxSnyOYqj)g-NT zOhML|urFjRHzYv*sCpZ!9zY8P!=R%~9ik{NS${$ddFn8|qz zX1=Ff7!WHfNZ6Bau>*Zhwgj;?=TZx-oQ9RSCO~Qd{DD+rV%J?T?8U#gvhXlq=VAD4 zl7VnGUXi=Y^$Rd_7Bq0v#b9Bidw(7UbL7sd=FZ>=%^_XEb&ivEFF`k19R=Q&7 zteAZ&DFx~;mD^wF^jyv@W?q}IZUT>U2v0*gYUs{;oq}l^5!G_4Wh!|Qt)F6 z7;aLFLsv4zS&u;dEO5uTY` zD&9m!1xIfjoqy~nRlDc;SI6h%Ovu@y6kj%HGuCsoQxB4Am|WEL(s15xBjILo4CoUl5i%lKZGN!ghGU8=otI*?OwpeFiIiQ+aS>+ecnSji5b z;^Gi|e)N;+Aj6ZA5ut>EGpf)9{J)g3W+fOPbfhO_b(zK95<&9_>Yb=6M9}0!2s;Uz zktPQQalmHw=|g|GA0p;-amn<##p24e!~JuU?d8C%_6;%Bv@6lHYsvM*uDA&#JzB zilpvaVf5sN4Ni+`^TLjxT`qd_!V48C2dd4m>~ z+>xg{wvF}rZIYx(BCHSf<}M<(X`xbzUvr2N-iVb-E@9efI(1OcOOxeM3ZACh7bv(w z!M~;82NXc!%?UrEAVa~=DX>z&l#Zl3lXQec(6UWUpqY><&U%s`fV(w((}ZUpY1m=J z1JZoyq%tU={~}fJ$&1{oMq8@8+Pmshdvpt$vb)AoZQI9NnzVHvpXPUH_ud)dTeRLg z0p6*tzO%7TdyN117;n(JJ}%X2tL|7zwGFFVI305uUyk#1P~M~+u5>BFGWf}buLh{! z=nuDGRi#K%3(`Ms=u>MgchrOY?lgn!E@eFqWHkK6INY!p%hIa zLPf4sCTy5bnnJbFSg*U#LJ%#Gb!g+7OLWvwkHaP!^5iv%RXxH(Zc&h|!6Be*|H1j= z3lZ>^L8xHel~YQ9>sJTe$YPWU{YQn-OZWs8ebOfy20uMABt{u&FCrQnnt+s*`5lMj zh>K$}U)J{|Tw{qa_@;;0!j;0g$YZvbEO85xC-^$|GpWCvM-tRxY33FnoHP(@fA2wn`+7lUx%9Zq*h7J%<=;ow9o zJb}R|J{a1_+Hv_je17^!4B!*NaYRDsgi_G6W1~2wkRv1}H1wSi;W&oKug^*?vK^6d z^C>)`?H;Z>B|M!IV`SY^SR{GGVD_t58LcFUP$U`bR(PDs(NLoq)h)da2@$98`Xb1|ADPww4bN=<@}9J(X14q0-s$e2!d$h8 zD{qIivKUswF)g3J2$^SS^KY`=u+_uFMoCt&I!%MrpBX^5&iiakf|wU>W7< zmNBJqwj7@wn$+d#4?$teOGkWiO}gT-8h&V%1(}=Flj=3ldDJb=>+14^it3oM^a`2X z$+g3vOdT??lgIgbX9dGfsz*%uoWWaom2@$&r$XyXX%xm4EgkgfL^QH1UKE4iQo3>+ zP>$*!R78WK2WU&-0pvU|oAc!2axAW#kA~AkJ|y|Ph9G28Ke9PwVhB*-9OOJ4M;yz; zPRI_|ZEV<=V0X6n(-|<$CKp(koQ)+eKXDix5#k0UuYy%j5GZ7?@%#Zq>+_p7ZCh*aHN~$ zlH*4CNyyDcrw9UN={&g>-rHwG<)Ml~r+0Jt*n1<`Kg8&JSQ%f($QH+o(4#+~9<5uU zW}LpyZfJsv*4FPZFO@#QPUGPttQa>>TJqkp{#wLDZ=a6}ag6KO=@b6pjxV&nPyzrzSY$-pEBGB&aPPI=vH^4jt+Vz%78clV&V zt(>i&w64j|%KjAj6%+1BTc|p2g?X|sZbMk}HSMH!{}`slOzjZ zNBtN%-^WN9X751XU71ifh#eEE25~~u+#ohjs1Z_H^F+~e@WvV&ot!9ol0}|E3aG3G zky2RXDFS@nZxmi6(nSw$AQpttDIBqmgruXQzd@tO8!LVORCSKNi3W*_DuM`(u|m2# zHZd^{PcM{T_&o|JgYf$lyH3F$P_T@&3C#w$8Jp1h@L4}!Mm01tp{Z>UYnj1sMl0Zh zieaIb!4a`3oT6fmQE(iA-&WbYcvth~L_iriv zIf5X0a7Ne9J|ZdEb~qIKjGQ-goNlC#z-3RF3SXq0r0f#LDPY9mO)8QJr^_ft3pLks zzn$@07#;c|4mmETBfspGGJ%lvK^tR^rp%b&v|W)6;hVeUI#-qg34cP3Hs=*03it>z zm$52hrK}vuvNi$U+eZ53kItzsUK0M83R2UnB^jv9EhxW?Qq4G}!@ar2(Pv)eL*SAO zopH&nh2X9cCHp3VtK0|r4QW@|lo67FDHEO78e6E^j00ONIul*8Zd`VIXCC{iX<92K z4KKlidR6T-fXinZ-f*O9+7dNwOV+LPWACc}Jbv3`G)qkyLJ}t(fp96>$r;Q z*D7yT!cTYkMwnC}W9!@7-qE}hUTEI4WZkc?< z%pG2IY)jW{{_-)DURgcea~n1pt?$&Owss}9b}f}1OgBH0YTlP<-giq(HXpocUGX%| z)h9fy=}m2^O$QU34yJvrun%b6GSiFU*x0eq+%aSQu(^H4f@TsUkoInv^CY}G(q12# zcci>Ka5F!W^!A`zm4|pZrYk?pLw?uNaTkYSJu^H0Xkzv=U;aY6p?R+D_0N69xYDq3 z?(#R|Z^m!+BsX^@8oIt>OxwM)nnim({NYaV)1MCG>mu4Lt|85MQA_Kks5_8glx4zl(zI*42l2Sik@}~cN_x9CFu4%`r4^H)F2fuOQ^$ReDY22Hr+F%D^lH0w=jYz6=Gn_rmTW&p@*wUFS?V48KhSq1_ zTn(L{9A0v?W7wpFr}I0K-gdm7b&o5mQWaYg6>x@}tk^Mq@RNUR-S+2a7Ao8Gn&5Ct z-$Ih#^^IeF-W7GU_ zYUk0!&ZF&&TF6ZPT5t2UOZ!5;y@3;JI?)o?Yv{RvcCbG+}K( zH_pQkxLIP9rLThbLYDprvIy-IFo|j>#TY_o4cUY;*SBz%(kLha_sYxzUgXl2nyZKL z;ma3#q_)vxe}KLRJUAA^cgp+w1#|~OR|-fV2l*s|3se-j=fh$7u<$-b3~-_VVFY9) z!v9Smv51D|XRA$zejNk|Km#;oDqtDK4_FiAt&lR~CYN{fVvkEcqxalz5+v zUH-0VYgSqz3dDU2q~#VMTFM?eA>mN&d*W0(9U6hB5pu#H*%Q(W$~ctb@>)Wyt>g~E zMKD$chEXA+w575wj`mfy&*cFzGe41;OJ*!eR@N8EB|7n?k`LoB6<@Iyrj=O?N?+b% zCX<=s;i2#|!B+N#707_9DU%Mu9()m8@X?J`y2(NeiOf_|wg_y^+7}XMD%4Rxd?)+1 ziO@(fViei;OxQO`q;HqZAT3jtEf$+B+;yOw2)uBRQ718f>>GKq4Q6Z329ww}p_PKo z6m-xWuw)B*R`TX7eFHCRzS$}{ETBo#h71ZUxqzocFr1h~N28=jll)#VN@p7-Gc^-h zB@9t5kX6B7iEx>!9idPheEB<@V_V=9iL(cdC=lg)$^#ND;0oU>~F8n?h{u#F`!R`7PcP7D| z`5D)q&i?_$5xPI%HvWtoOmTznbAw3v8Mpgq+};nkjt{s-03%%0XdvnR zdyV}gGpDysU7DU;Dr%T3P5E~x{JU=rE&31t)aXg;JF&Z&4w7>OEkYw#;+-xbdz`dp z98XG~b&zxw%g!`4J=v2N z@17L&CjC>qR1}2uqKHCoO7P;z*=$4d4)5o@_kHg%A2v2Dq-FDYI(mZ<`jG_-DO@_U zfp%UYA9azBeS_gH*4AW3WOhxA0FT!fElWEtE&YY(7yK({Mjl~gATZA^OWn1pQCU0r zYCY{VTf`H57zSi065@|355$0pfW(OygQslEYAFKGVj)2iH3E|NP{!zh0ilvm5()o^ z!4887FfyS@I4^M;flvB<8pka3s0sz|_w~Ge4HSt4NXRv2G%par_3%)VVaVVPkpR|6 zsaWnhjxRiE=foDAJP|p-otj;%IwNtcgyTcZ#54k~oMRb;j&+f(+OFXAMgYpqa_$O` zbi4lWe!F?#b{dC=t#-5HK0c^FdDJ+$zPx}fL8*`jvQkWouB&OdZfXdbUd!Lup5DRE zCMTxGN{hfJVIp{H=|&%h^oXgnv;z7b%`#Gd?`l=w?frWD^5XToUVVP@2m0hY5tRxO za=TVp$@Z3vdKs7 0 # Zxy记录发送结果 + except Exception as e: + logger.error(f"失败邮箱号: {emailto}, {e}") # Zxy记录错误信息 + log.send_result = False + log.save() # Zxy保存日志 + +# Zxy定义OAuth用户登录信号的处理函数 +@receiver(oauth_user_login_signal) +def oauth_user_login_signal_handler(sender, **kwargs): + id = kwargs['id'] # Zxy获取用户ID + oauthuser = OAuthUser.objects.get(id=id) # Zxy获取OAuth用户 + site = get_current_site().domain # Zxy获取当前站点域名 + if oauthuser.picture and not oauthuser.picture.find(site) >= 0: # Zxy检查头像URL是否包含当前站点域名 + from djangoblog.utils import save_user_avatar # Zxy导入保存头像的工具函数 + oauthuser.picture = save_user_avatar(oauthuser.picture) # Zxy保存用户头像 + oauthuser.save() # Zxy保存用户信息 + + delete_sidebar_cache() # Zxy删除侧边栏缓存 + +# Zxy定义模型保存后的回调函数 +@receiver(post_save) +def model_post_save_callback(sender, instance, created, raw, using, update_fields, **kwargs): + clearcache = False # Zxy标记是否需要清理缓存 + if isinstance(instance, LogEntry): # Zxy如果是LogEntry模型,直接返回 + return + if 'get_full_url' in dir(instance): # Zxy检查是否有获取完整URL的方法 + is_update_views = update_fields == {'views'} # Zxy检查是否是更新浏览次数 + if not settings.TESTING and not is_update_views: # Zxy如果不是测试环境且不是更新浏览次数 + try: + notify_url = instance.get_full_url() # Zxy获取完整URL + SpiderNotify.baidu_notify([notify_url]) # Zxy通知百度爬虫 + except Exception as ex: + logger.error("notify spider", ex) # Zxy记录错误信息 + if not is_update_views: + clearcache = True # Zxy标记清理缓存 + + if isinstance(instance, Comment): # Zxy如果是评论模型 + if instance.is_enable: # Zxy检查评论是否启用 + path = instance.article.get_absolute_url() # Zxy获取文章的绝对URL + site = get_current_site().domain # Zxy获取当前站点域名 + if site.find(':') > 0: # Zxy去除端口号 + site = site[0:site.find(':')] + + expire_view_cache( # Zxy清理视图缓存 + path, + servername=site, + serverport=80, + key_prefix='blogdetail' + ) + if cache.get('seo_processor'): # Zxy清理SEO处理器缓存 + cache.delete('seo_processor') + comment_cache_key = 'article_comments_{id}'.format(id=instance.article.id) # Zxy生成评论缓存键 + cache.delete(comment_cache_key) # Zxy清理评论缓存 + delete_sidebar_cache() # Zxy清理侧边栏缓存 + delete_view_cache('article_comments', [str(instance.article.pk)]) # Zxy清理文章评论缓存 + + _thread.start_new_thread(send_comment_email, (instance,)) # Zxy异步发送评论邮件 + + if clearcache: # Zxy清理缓存 + cache.clear() + +# Zxy定义用户登录和登出的回调函数 +@receiver(user_logged_in) +@receiver(user_logged_out) +def user_auth_callback(sender, request, user, **kwargs): + if user and user.username: # Zxy检查用户是否登录 + logger.info(user) # Zxy记录用户信息 + delete_sidebar_cache() # Zxy清理侧边栏缓存 + # cache.clear() # Zxy清理所有缓存(暂时注释) \ No newline at end of file diff --git a/elasticsearch_backend.py b/elasticsearch_backend.py new file mode 100644 index 0000000..55a9b89 --- /dev/null +++ b/elasticsearch_backend.py @@ -0,0 +1,196 @@ +# Zxy导入Django的编码工具 +from django.utils.encoding import force_str +# Zxy导入Elasticsearch DSL的查询构造器 +from elasticsearch_dsl import Q +# Zxy导入Haystack的搜索后端模块 +from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query +# Zxy导入Haystack的表单模块 +from haystack.forms import ModelSearchForm +# Zxy导入Haystack的搜索结果模型 +from haystack.models import SearchResult +# Zxy导入Haystack的日志工具 +from haystack.utils import log as logging + +# Zxy导入项目中的Elasticsearch文档和管理器 +from blog.documents import ArticleDocument, ArticleDocumentManager +# Zxy导入项目中的文章模型 +from blog.models import Article + +# Zxy获取日志记录器 +logger = logging.getLogger(__name__) + +# Zxy定义Elasticsearch搜索后端类 +class ElasticSearchBackend(BaseSearchBackend): + # Zxy初始化方法 + def __init__(self, connection_alias, **connection_options): + super(ElasticSearchBackend, self).__init__(connection_alias, **connection_options) + self.manager = ArticleDocumentManager() # Zxy初始化文档管理器 + self.include_spelling = True # Zxy启用拼写建议 + + # Zxy获取模型数据并转换为文档 + def _get_models(self, iterable): + models = iterable if iterable and iterable[0] else Article.objects.all() # Zxy获取模型数据 + docs = self.manager.convert_to_doc(models) # Zxy转换为Elasticsearch文档 + return docs + + # Zxy创建索引并重建数据 + def _create(self, models): + self.manager.create_index() # Zxy创建索引 + docs = self._get_models(models) # Zxy获取文档 + self.manager.rebuild(docs) # Zxy重建索引数据 + + # Zxy删除模型数据 + def _delete(self, models): + for m in models: + m.delete() # Zxy删除模型实例 + return True + + # Zxy重建索引数据 + def _rebuild(self, models): + models = models if models else Article.objects.all() # Zxy获取模型数据 + docs = self.manager.convert_to_doc(models) # Zxy转换为文档 + self.manager.update_docs(docs) # Zxy更新索引数据 + + # Zxy更新索引 + def update(self, index, iterable, commit=True): + models = self._get_models(iterable) # Zxy获取文档 + self.manager.update_docs(models) # Zxy更新索引 + + # Zxy删除文档 + def remove(self, obj_or_string): + models = self._get_models([obj_or_string]) # Zxy获取文档 + self._delete(models) # Zxy删除文档 + + # Zxy清空索引 + def clear(self, models=None, commit=True): + self.remove(None) # Zxy删除所有文档 + + # Zxy获取拼写建议 + @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"]: # Zxy检查是否有建议选项 + keywords.append(suggest["options"][0]["text"]) # Zxy添加建议词 + else: + keywords.append(suggest["text"]) # Zxy添加原搜索词 + + return ' '.join(keywords) # Zxy返回拼写建议 + + # Zxy执行搜索查询 + @log_query + def search(self, query_string, **kwargs): + logger.info('search query_string:' + query_string) # Zxy记录搜索查询 + + start_offset = kwargs.get('start_offset') # Zxy获取起始偏移量 + end_offset = kwargs.get('end_offset') # Zxy获取结束偏移量 + + # Zxy检查是否启用拼写建议 + if getattr(self, "is_suggest", None): + suggestion = self.get_suggestion(query_string) # Zxy获取拼写建议 + else: + suggestion = query_string # Zxy使用原搜索词 + + q = Q('bool', # Zxy构造布尔查询 + should=[Q('match', body=suggestion), Q('match', title=suggestion)], # Zxy匹配标题或正文 + minimum_should_match="70%") # Zxy至少匹配70% + + search = ArticleDocument.search() \ + .query('bool', filter=[q]) \ + .filter('term', status='p') \ + .filter('term', type='a') \ + .source(False)[start_offset: end_offset] # Zxy执行搜索 + + results = search.execute() # Zxy执行搜索查询 + hits = results['hits'].total # Zxy获取总匹配数 + raw_results = [] # Zxy初始化结果列表 + + for raw_result in results['hits']['hits']: # Zxy遍历搜索结果 + app_label = 'blog' # Zxy应用标签 + model_name = 'Article' # Zxy模型名称 + additional_fields = {} # Zxy额外字段 + + result_class = SearchResult # Zxy搜索结果类 + + result = result_class( # Zxy创建搜索结果对象 + app_label, + model_name, + raw_result['_id'], + raw_result['_score'], + **additional_fields + ) + raw_results.append(result) # Zxy添加到结果列表 + + facets = {} # Zxy初始化分面信息 + spelling_suggestion = None if query_string == suggestion else suggestion # Zxy拼写建议 + + return { + 'results': raw_results, # Zxy返回搜索结果 + 'hits': hits, # Zxy返回匹配数 + 'facets': facets, # Zxy返回分面信息 + 'spelling_suggestion': spelling_suggestion, # Zxy返回拼写建议 + } + +# Zxy定义Elasticsearch搜索查询类 +class ElasticSearchQuery(BaseSearchQuery): + # Zxy转换日期时间格式 + 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')) + + # Zxy清理用户输入 + 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) + + # Zxy构建查询片段 + def build_query_fragment(self, field, filter_type, value): + return value.query_string + + # Zxy获取结果数量 + def get_count(self): + results = self.get_results() + return len(results) if results else 0 + + # Zxy获取拼写建议 + def get_spelling_suggestion(self, preferred_query=None): + return self._spelling_suggestion + + # Zxy构建查询参数 + def build_params(self, spelling_query=None): + kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query) + return kwargs + +# Zxy定义Elasticsearch模型搜索表单 +class ElasticSearchModelSearchForm(ModelSearchForm): + # Zxy执行搜索 + def search(self): + self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" # Zxy检查是否启用拼写建议 + sqs = super().search() # Zxy调用父类搜索方法 + return sqs + +# Zxy定义Elasticsearch搜索引擎 +class ElasticSearchEngine(BaseEngine): + backend = ElasticSearchBackend # Zxy后端类 + query = ElasticSearchQuery # Zxy查询类 \ No newline at end of file diff --git a/feeds.py b/feeds.py new file mode 100644 index 0000000..c7afbe3 --- /dev/null +++ b/feeds.py @@ -0,0 +1,54 @@ +# Zxy导入Django的用户模型 +from django.contrib.auth import get_user_model +# Zxy导入Django的Feed视图 +from django.contrib.syndication.views import Feed +# Zxy导入Django的时区工具 +from django.utils import timezone +# Zxy导入RSS 2.0 Feed生成器 +from django.utils.feedgenerator import Rss201rev2Feed + +# Zxy导入项目中的文章模型 +from blog.models import Article +# Zxy导入Markdown工具 +from djangoblog.utils import CommonMarkdown + +# Zxy定义Django博客Feed +class DjangoBlogFeed(Feed): + feed_type = Rss201rev2Feed # Zxy使用RSS 2.0格式 + + description = '大巧无工,重剑无锋.' # ZxyFeed描述 + title = "且听风吟 大巧无工,重剑无锋." # ZxyFeed标题 + link = "/feed/" # ZxyFeed链接 + + # Zxy获取作者名称 + def author_name(self): + return get_user_model().objects.first().nickname + + # Zxy获取作者链接 + def author_link(self): + return get_user_model().objects.first().get_absolute_url() + + # Zxy获取Feed项 + def items(self): + return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] # Zxy获取最近5篇已发布的文章 + + # Zxy获取Feed项标题 + def item_title(self, item): + return item.title + + # Zxy获取Feed项描述 + def item_description(self, item): + return CommonMarkdown.get_markdown(item.body) # Zxy将文章内容转换为Markdown格式 + + # Zxy获取Feed版权信息 + def feed_copyright(self): + now = timezone.now() + return "Copyright© {year} 且听风吟".format(year=now.year) # Zxy动态生成版权年份 + + # Zxy获取Feed项链接 + def item_link(self, item): + return item.get_absolute_url() + + # Zxy获取Feed项GUID + def item_guid(self, item): + return \ No newline at end of file diff --git a/logentryadmin.py b/logentryadmin.py new file mode 100644 index 0000000..e1b4bd3 --- /dev/null +++ b/logentryadmin.py @@ -0,0 +1,88 @@ +# Zxy导入Django的admin模块 +from django.contrib import admin +# Zxy导入Django的LogEntry模型 +from django.contrib.admin.models import DELETION +# Zxy导入Django的内容类型模型 +from django.contrib.contenttypes.models import ContentType +# Zxy导入Django的URL工具 +from django.urls import reverse, NoReverseMatch +# Zxy导入Django的编码工具 +from django.utils.encoding import force_str +# Zxy导入Django的HTML工具 +from django.utils.html import escape +# Zxy导入Django的安全字符串工具 +from django.utils.safestring import mark_safe +# Zxy导入Django的翻译工具 +from django.utils.translation import gettext_lazy as _ + +# Zxy定义LogEntryAdmin类 +class LogEntryAdmin(admin.ModelAdmin): + list_filter = ['content_type'] # Zxy按内容类型过滤 + search_fields = ['object_repr', 'change_message'] # Zxy搜索字段 + + list_display_links = ['action_time', 'get_change_message'] # Zxy显示链接的字段 + list_display = ['action_time', 'user_link', 'content_type', 'object_link', 'get_change_message'] # Zxy显示字段 + + # Zxy检查是否有添加权限 + def has_add_permission(self, request): + return False + + # Zxy检查是否有修改权限 + def has_change_permission(self, request, obj=None): + return ( + request.user.is_superuser or + request.user.has_perm('admin.change_logentry') + ) and request.method != 'POST' + + # Zxy检查是否有删除权限 + def has_delete_permission(self, request, obj=None): + return False + + # Zxy获取对象链接 + def object_link(self, obj): + object_link = escape(obj.object_repr) # Zxy转义对象表示 + content_type = obj.content_type + + if obj.action_flag != DELETION and content_type is not None: # Zxy检查是否为删除操作 + try: + url = reverse( # Zxy生成反向URL + 'admin:{}_{}_change'.format(content_type.app_label, content_type.model), + args=[obj.object_id] + ) + object_link = '{}'.format(url, object_link) # Zxy生成链接 + except NoReverseMatch: + pass + return mark_safe(object_link) # Zxy标记为安全字符串 + + object_link.admin_order_field = 'object_repr' # Zxy排序字段 + object_link.short_description = _('object') # Zxy字段描述 + + # Zxy获取用户链接 + def user_link(self, obj): + content_type = ContentType.objects.get_for_model(type(obj.user)) # Zxy获取用户的内容类型 + user_link = escape(force_str(obj.user)) # Zxy转义用户表示 + + try: + url = reverse( # Zxy生成反向URL + 'admin:{}_{}_change'.format(content_type.app_label, content_type.model), + args=[obj.user.pk] + ) + user_link = '{}'.format(url, user_link) # Zxy生成链接 + except NoReverseMatch: + pass + return mark_safe(user_link) # Zxy标记为安全字符串 + + user_link.admin_order_field = 'user' # Zxy排序字段 + user_link.short_description = _('user') # Zxy字段描述 + + # Zxy获取查询集 + def get_queryset(self, request): + queryset = super(LogEntryAdmin, self).get_queryset(request) + return queryset.prefetch_related('content_type') # Zxy预加载内容类型 + + # Zxy获取操作 + def get_actions(self, request): + actions = super(LogEntryAdmin, self).get_actions(request) + if 'delete_selected' in actions: # Zxy移除删除操作 + del actions['delete_selected'] + return actions \ No newline at end of file diff --git a/plugin_manage/__pycache__/base_plugin.cpython-312.pyc b/plugin_manage/__pycache__/base_plugin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8039e14c5a2ee548fa83dc0b5f3001c9eaefdb9c GIT binary patch literal 1982 zcmaJ>|4$rM5Py5S_x%p)LAk<}x<*3TMv3+ZZHTq8La;P-Q6?c zV&bV#+Xw|~%GFwoH8z$IXtjy9A?hFCFL!C1BWr(XB72nI5Hu$KB-rK&V;d!H zs2lONF~pPeWy>fvN-)$KHV6+^oI>R@X!ALc=D>H9;8E1Z6HyWd1;dU#MIAf|+{shG zUA!H*8$3Ex&v8x`dgAGrm<$h^&hQ0x7$l6%AAseMV(H%BHGGho34^^!Aa$7QtvN=T zsmfl(ZOVDVlzv3OIjU-oM=x`ruNz8(0=StPMGXxs7F=RRpM?G|zN>dkCmVVUE&1&C$s!hzSWtSfR@|mU87N%Ff zU(%*-t}I<&`(#o(`$cKt+tSS4((Oy7+3VWkSF0Cpt)9QDEnZo@GG;j`eLlJJ^Hs}9 z#!urqid( zERBC$x^$~FxriaLkkiKRSoZMpRFX>ws+Sk!K}j4@#8gssz9&dBEEv*2f(ve!@Q+uG z3AHvRm|p_4=1?lrR8hVv9Ssg-1{l=OhxRra{ka@^vKTsC2pwJyhKj*Mh2Wt-ozx!B zI-=Yjy~2zK_dO1hWmON$CQ^Jlj@LexJ(K3*hNe2uJvds7SP8$X6vC_lGmJn3&-k?@ z%!XJ!%V+o`?deNUNSOxS$Yag@P8?{09z*sg7_WPJaRXk3l`l?1$Wcs UBlp}bS;s$)rwPvn0;UK5A8uI#sQ>@~ literal 0 HcmV?d00001 diff --git a/plugin_manage/__pycache__/hook_constants.cpython-312.pyc b/plugin_manage/__pycache__/hook_constants.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fac950773da4c6c0b2fc9b9e09b3f216e1b1859b GIT binary patch literal 407 zcmX@j%ge<81VRnBGL8c2#~=<2FhUuhC4h|S3@Hpz3@MCJj44b}OexG!%qc8UES0R9 zY`26Hi%K$+b5i3|QcDsubK-OI6H{*S!32|wQWHy35&Y7E6cm0+YECLpp1UL?H9k2% zuOv0E#7~p;mat<`h^MoUYrKnVh@+=ZypO-5%Pl^bpmUI`V~8t)9~$6-!gq1?aRtgt z!qxlxg}C~K#C!Prd&m1Z`ns-U_zd#h*H9qgViogjVb9CCi(*`|67$mYopSQib#oJo zOHzvzGz>M7L}OCG3X*^dVhVCf(=+qpa})Ct(^F$I^7FGn&Mz)W%quC@E2#X%VUwGm vQks)$SHusr0~D6U3P9omGb1D84KeKt48k`A6&v^v!~+@4iwu%Qyg)?&kNSpqY%49$N9iUb>9-mL2HIS%%|hE0#SY^fnOLuy+CrgrSJ2%ds+`9i z2(-98?VdtYMo^>RnZF3tStO$fNWC1ylF3`pm(flbSR9wpTIl=9>?K9UdB-t|OY}!60Klw`2&qFhUJ6i)%$`2h;T;448Q+neRr^#}YHi_)PMfxie$wtEbX~ zU(SvnxgY;j8}GF_MXxU$5hYRJNgVjfURx#K?TfVW`B?Ck3%Ii0!Kkc@XoOYP-PtJy z1(gkiyk1dK89{96@~WIm3!=;AFi2#>64_Ol1d`|ftOK#Tx;ZnFm>as-yxZ>%dP8r* z{LLM1Vx50!i=*Iev!E%ofI@R;pv&tEx;orJw^wY|Jb1M;s&Z1!aj6KFCV>gpLls9^ z4so^%Y|6O)*Ru5qwlA9E)=tyrz8xvLWQs0J(PaZ4PSP9U)$zJd>Xhdzh8u_XDX;Ci zZJ(-jrfQwat_G!HzhZmu7yA8qSP^-0fRQ#L;64Y>{9jNlVF5{7B9FQ81K3MT{!0!~ zi{EQ0E4$H0D-eoNCO}@MWb8w-;S&A~W0WYOQIv~v8&HJJuUvtoZ85sQswqFRCLsuM%0ZRrmg@?)cNZF zdUhno7hmg%_(DN`FF=y#EB)<}nVaV`iT=5f>*?z!Gn#s)Kc2bJucMjn8=t-Qm5%02 z^2CGlM|D8c$G^{fF`i$hx5U{M4Elmz-mTlvQI$v>de`ldq>#i*%V{iFDhoNLDy#YZ zV_4I+bedav($a5H*1R&>IJ$5A zW2I!rBv(CcUq29?v^)CtW(`!4jPF|Q0~LzZF}(4P)$u!n)^5r&h+FsQp&1pI|A8>5 zOj}oydoSu}^qr599f>&wtDPF%)-mV<%J!P4)9!gmc{hg)A z%!P0XICYJyG2AfKRxuOwrrNE{#8w8{PaJ@cq5lWepMri{0e=45-brhSj-6EG+?L3_6(;$mah(IZ zNL$~f+Yd|SWSHokgKAl}H;?*84$qd}#@5lR0u zaJK|R?YhyjTHF2@l2(F}miG(X&W@1K6%b#SHo!Y_ZG_)}3X%=u-_Xi?rqaGOQ>N0C zsdS*~j;SKf-ZQO;vsu=NKfnW277;nzGq1^P@d_;CferJB$ZVMf+XfmQA`qIve*wa5 Bcj5p5 literal 0 HcmV?d00001 diff --git a/plugin_manage/__pycache__/loader.cpython-312.pyc b/plugin_manage/__pycache__/loader.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9bbb7c260212e49a0e850c4b167a89eca936ed92 GIT binary patch literal 1550 zcmah}O>7%Q6rNfCZq^PaDt78PbRhv_q#B_f5>ko;NeV_KqIE#ja=UE3ll8jmU2Ar& z#Fm3{;Lu8uLQal^B9)&@)Jo-qTnVX&9Jx43qe$BzA&`)IE2LDYT$tJQ+D0H{4*R}0 z@4fH6d2eTa42K5*SwkyUanu9AuT*gt(d~8kE)h*20ST%g=SY}?L}yf{?8$i`U;u)p zV8_2tAm2^ss5!sn`4r>=HJI`~pj&e(Xax;qnu;bHg}xXNAPUWW_?W~tf!Vc{m@e#s z8F1-a0G5I2b+<}>7xiR`?O_-7@!CCb_Z`^)H#^yu;St>7e@9ZS0o`*OaP|BV@lX9X z{)mYGIZXpf(5<*H_Zkb#r(J%DX@Jjx(uhr_=fB$Wf?8k< zm{Hej9N_U~rjO^iYw1bJ=kPR`TYk4n2rdFCu*`hItp4K2MYsr-JQu+tOa<#-4e(^m z)HJcI6hu|6@v1IL1}|z7uV{*?h^kUY247LDvZ5LM0@lmCSwwuYvylq)(myC!-w&{9EA zv;}=G6~^>3Sc7j)&u2cI72ZGh!P}X+ti|Yt<*kTj(ejpbMYFh$L72&0uzZR^4sZF< z5-HoT1_Z%LAqbXV)nyrB%S+iUE@P*j#aPFd4^ffwSe#R&Kzfh5ik%B0gH}T(^XGd+ z&P?Th`(pLM=9hV=S#J^YlV#B`5$0cdHPyG9m+Wp{ArAQtds);(8Rh9%5hg9PLKF&) z)xh)zqfdd+BA*rTXaEfJ`?2)BSo#`w5FKvH>vB_FSHGUzADh@2n`n*fMrVnI3$IOF zpIAF{{mjP59{0kn=Wi#slDE@a>Fysj5j(Pn5p z)IKrU8fk^wqi5UaFMJl-k7Rcu*}orpsLK;$m=yoRbIECr+2+{kN#FLd`1C2?_9-91 zsUW5Yv;uS_ 1 and sys.argv[1] == 'test' + +# Zxy允许的主机名:允许访问的域名列表 +ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] +# ZxyCSRF 可信来源:允许的 CSRF 来源 +CSRF_TRUSTED_ORIGINS = ['http://example.com'] +# Application definition + +# Zxy应用程序定义:定义项目中使用的 Django 应用程序 +INSTALLED_APPS = [ + # 默认的 Django 应用程序 + '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' +] +# Zxy中间件配置:定义请求处理的中间件 +MIDDLEWARE = [ + + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.gzip.GZipMiddleware', + # 'django.middleware.cache.UpdateCacheMiddleware', + 'django.middleware.common.CommonMiddleware', + # 'django.middleware.cache.FetchFromCacheMiddleware', + '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' +] +# ZxyURL 配置:定义项目的 URL 配置文件 +ROOT_URLCONF = 'djangoblog.urls' +# Zxy模板配置:配置 Django 模板引擎 +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' + ], + }, + }, +] +# ZxyWSGI 应用程序:定义 WSGI 应用程序入口 +WSGI_APPLICATION = 'djangoblog.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases + +# Zxy数据库配置:配置数据库连接 +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'djangoblog', + 'USER': 'root', + 'PASSWORD': '123456', + 'HOST': '127.0.0.1', + 'PORT': 3306, + } +} + +# Password validation +# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators +# Zxy密码验证器:配置密码验证规则 +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', + }, +] +# Zxy语言和时区配置:配置语言和时区 +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 + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.10/howto/static-files/ + +# ZxyHaystack 搜索配置:配置 Haystack 搜索引擎 +HAYSTACK_CONNECTIONS = { + 'default': { + 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', + 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), + }, +} +# Automatically update searching index +HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' +# Allow user login with username and password +AUTHENTICATION_BACKENDS = [ + 'accounts.user_login_backend.EmailOrUsernameModelBackend'] +# Zxy静态文件配置:配置静态文件路径 +STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') + +STATIC_URL = '/static/' +STATICFILES = os.path.join(BASE_DIR, 'static') +# Zxy用户模型配置:定义自定义用户模型和登录 URL +AUTH_USER_MODEL = 'accounts.BlogUser' +LOGIN_URL = '/login/' + +TIME_FORMAT = '%Y-%m-%d %H:%M:%S' +DATE_TIME_FORMAT = '%Y-%m-%d' + +# bootstrap color styles +BOOTSTRAP_COLOR_TYPES = [ + 'default', 'primary', 'success', 'info', 'warning', 'danger' +] + +# paginate +PAGINATE_BY = 10 +# http cache timeout +CACHE_CONTROL_MAX_AGE = 2592000 +# cache setting +# Zxy缓存配置:配置缓存后端 +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")}', + } + } +# Zxy站点 ID:定义当前站点的 ID +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: +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 +# Setting debug=false did NOT handle except email notifications +ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] +# WX ADMIN password(Two times 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) +# Zxy日志配置:配置日志记录 +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, + } + } +} +# Zxy压缩配置:配置静态文件压缩 +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + # other + 'compressor.finders.CompressorFinder', +) +COMPRESS_ENABLED = True +# COMPRESS_OFFLINE = True + + +COMPRESS_CSS_FILTERS = [ + # creates absolute urls from relative ones + 'compressor.filters.css_default.CssAbsoluteFilter', + # css minimizer + 'compressor.filters.cssmin.CSSMinFilter' +] +COMPRESS_JS_FILTERS = [ + 'compressor.filters.jsmin.JSMinFilter' +] +# Zxy媒体文件配置:配置媒体文件路径 +MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') +MEDIA_URL = '/media/' +X_FRAME_OPTIONS = 'SAMEORIGIN' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +# ZxyElasticsearch 配置:配置 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', + }, + } + +# Zxy插件系统配置:定义插件目录和激活的插件 +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/sitemap.py b/sitemap.py new file mode 100644 index 0000000..986f1ef --- /dev/null +++ b/sitemap.py @@ -0,0 +1,70 @@ +# Zxy导入 Django 的 Sitemap 模块和其他相关模块 +from django.contrib.sitemaps import Sitemap +from django.urls import reverse + +from blog.models import Article, Category, Tag + +# Zxy定义静态页面的 Sitemap +class StaticViewSitemap(Sitemap): + priority = 0.5 # Zxy设置优先级 + changefreq = 'daily' # Zxy设置更新频率 + + def items(self): + # Zxy返回静态页面的 URL 名称 + return ['blog:index', ] + + def location(self, item): + # Zxy通过 URL 名称生成完整的 URL + return reverse(item) + +# Zxy定义文章的 Sitemap +class ArticleSiteMap(Sitemap): + changefreq = "monthly" # Zxy文章更新频率为每月 + priority = "0.6" # Zxy文章优先级 + + def items(self): + # Zxy返回所有已发布的文章 + return Article.objects.filter(status='p') + + def lastmod(self, obj): + # Zxy返回文章的最后修改时间 + return obj.last_modify_time + +# Zxy定义分类的 Sitemap +class CategorySiteMap(Sitemap): + changefreq = "Weekly" # Zxy分类更新频率为每周 + priority = "0.6" # Zxy分类优先级 + + def items(self): + # Zxy返回所有分类 + return Category.objects.all() + + def lastmod(self, obj): + # Zxy返回分类的最后修改时间 + return obj.last_modify_time + +# Zxy定义标签的 Sitemap +class TagSiteMap(Sitemap): + changefreq = "Weekly" # Zxy标签更新频率为每周 + priority = "0.3" # Zxy标签优先级 + + def items(self): + # Zxy返回所有标签 + return Tag.objects.all() + + def lastmod(self, obj): + # Zxy返回标签的最后修改时间 + return obj.last_modify_time + +# Zxy定义用户的 Sitemap +class UserSiteMap(Sitemap): + changefreq = "Weekly" # Zxy用户更新频率为每周 + priority = "0.3" # Zxy用户优先级 + + def items(self): + # Zxy返回所有用户的作者列表(去重) + return list(set(map(lambda x: x.author, Article.objects.all()))) + + def lastmod(self, obj): + # Zxy返回用户的注册时间 + return obj.date_joined \ No newline at end of file diff --git a/spider_notify.py b/spider_notify.py new file mode 100644 index 0000000..20fed14 --- /dev/null +++ b/spider_notify.py @@ -0,0 +1,26 @@ +# Zxy导入日志模块 +import logging + +# Zxy导入 requests 模块用于发送 HTTP 请求 +import requests +from django.conf import settings + +# Zxy获取日志记录器 +logger = logging.getLogger(__name__) + +# Zxy定义爬虫通知类 +class SpiderNotify(): + @staticmethod + def baidu_notify(urls): + # Zxy向百度站长平台发送 URL 提交请求 + try: + data = '\n'.join(urls) # Zxy将 URL 列表拼接为字符串 + result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) # Zxy发送 POST 请求 + logger.info(result.text) # Zxy记录响应内容 + except Exception as e: + logger.error(e) # Zxy记录异常信息 + + @staticmethod + def notify(url): + # Zxy调用百度通知方法 + SpiderNotify.baidu_notify(url) \ No newline at end of file diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..83af4c1 --- /dev/null +++ b/tests.py @@ -0,0 +1,38 @@ +# Zxy导入 Django 的测试模块 +from django.test import TestCase + +# Zxy导入项目中的工具函数 +from djangoblog.utils import * + +# Zxy定义测试类 +class DjangoBlogTest(TestCase): + def setUp(self): + # Zxy测试初始化方法(暂无内容) + pass + + def test_utils(self): + # Zxy测试工具函数 + md5 = get_sha256('test') # Zxy测试 SHA256 函数 + self.assertIsNotNone(md5) # Zxy断言返回值不为空 + + c = CommonMarkdown.get_markdown(''' # Zxy测试 Markdown 转换 + # Title1 + + ```python + import os + ``` + + [url](https://www.lylinux.net/) + + [ddd](http://www.baidu.com) + + + ''') + self.assertIsNotNone(c) # Zxy断言返回值不为空 + + d = { + 'd': 'key1', + 'd2': 'key2' + } + data = parse_dict_to_url(d) # Zxy测试字典转 URL 函数 + self.assertIsNotNone(data) # Zxy断言返回值不为空 \ No newline at end of file diff --git a/urls.py b/urls.py new file mode 100644 index 0000000..f158317 --- /dev/null +++ b/urls.py @@ -0,0 +1,68 @@ +# Zxy定义项目的 URL 配置 +"""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 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 + +# Zxy定义站点地图 +sitemaps = { + 'blog': ArticleSiteMap, + 'Category': CategorySiteMap, + 'Tag': TagSiteMap, + 'User': UserSiteMap, + 'static': StaticViewSitemap +} + +# Zxy定义 404、500 和 403 错误页面的视图 +handler404 = 'blog.views.page_not_found_view' +handler500 = 'blog.views.server_error_view' +handle403 = 'blog.views.permission_denied_view' + +# Zxy定义 URL 模式 +urlpatterns = [ + path('i18n/', include('django.conf.urls.i18n')), # Zxy国际化语言切换 +] +urlpatterns += i18n_patterns( + re_path(r'^admin/', admin_site.urls), # Zxy自定义 Admin 站点 + re_path(r'', include('blog.urls', namespace='blog')), # Zxy博客应用的 URL + re_path(r'mdeditor/', include('mdeditor.urls')), # Zxy Markdown 编辑器的 URL + re_path(r'', include('comments.urls', namespace='comment')), # Zxy评论应用的 URL + re_path(r'', include('accounts.urls', namespace='account')), # Zxy用户账户的 URL + re_path(r'', include('oauth.urls', namespace='oauth')), # Zxy OAuth 应用的 URL + re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, # Zxy站点地图 + name='django.contrib.sitemaps.views.sitemap'), + re_path(r'^feed/$', DjangoBlogFeed()), # Zxy RSS 订阅 + re_path(r'^rss/$', DjangoBlogFeed()), # Zxy RSS 订阅 + re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), # Zxy搜索功能 + name='search'), + re_path(r'', include('servermanager.urls', namespace='servermanager')), # Zxy服务器管理应用的 URL + re_path(r'', include('owntracks.urls', namespace='owntracks')), # Zxy位置跟踪应用的 URL + prefix_default_language=False +) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # Zxy静态文件的 URL +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, # Zxy媒体文件的 URL + document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..90cfa42 --- /dev/null +++ b/utils.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python +# encoding: utf-8 + +# Zxy导入日志模块 +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 + +# Zxy获取日志记录器 +logger = logging.getLogger(__name__) + +# Zxy获取文章和评论的最大 ID +def get_max_articleid_commentid(): + from blog.models import Article + from comments.models import Comment + return (Article.objects.latest().pk, Comment.objects.latest().pk) + +# Zxy计算字符串的 SHA256 值 +def get_sha256(str): + m = sha256(str.encode('utf-8')) + return m.hexdigest() + +# Zxy缓存装饰器,用于缓存函数的返回值 +def cache_decorator(expiration=3 * 60): + 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 + +# Zxy刷新视图缓存 +def expire_view_cache(path, servername, serverport, key_prefix=None): + 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 + +# Zxy获取当前站点 +@cache_decorator() +def get_current_site(): + site = Site.objects.get_current() + return site + +# Zxy定义 CommonMarkdown 类,用于处理 Markdown 转换 +class CommonMarkdown: + @staticmethod + def _convert_markdown(value): + 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, toc = CommonMarkdown._convert_markdown(value) + return body + +# Zxy发送邮件 +def send_email(emailto, title, content): + from djangoblog.blog_signals import send_email_signal + send_email_signal.send(send_email.__class__, emailto=emailto, title=title, content=content) + +# Zxy生成随机验证码 +def generate_code() -> str: + return ''.join(random.sample(string.digits, 6)) + +# Zxy将字典转换为 URL 参数 +def parse_dict_to_url(dict): + from urllib.parse import quote + url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/')) for k, v in dict.items()]) + return url + +# Zxy获取博客设置 +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 + +# Zxy保存用户头像 +def save_user_avatar(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) + return static('avatar/' + save_filename) + except Exception as e: + logger.error(e) + return static('blog/img/avatar.png') + +# Zxy删除侧边栏缓存 +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) + +# Zxy删除视图缓存 +def delete_view_cache(prefix, keys): + from django.core.cache.utils import make_template_fragment_key + key = make_template_fragment_key(prefix, keys) + cache.delete(key) + +# Zxy获取资源 URL +def get_resource_url(): + if settings.STATIC_URL: + return settings.STATIC_URL + else: + site = get_current_site() + return 'http://' + site.domain + '/static/' + +# Zxy定义允许的 HTML 标签和属性 +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']} + +# Zxy清理 HTML 内容 +def sanitize_html(html): + return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) \ No newline at end of file diff --git a/whoosh_cn_backend.py b/whoosh_cn_backend.py new file mode 100644 index 0000000..994247c --- /dev/null +++ b/whoosh_cn_backend.py @@ -0,0 +1,1256 @@ +# Zxy 导入未来支持模块,确保代码在 Python 2 和 3 中的兼容性 +# encoding: utf-8 +from __future__ import absolute_import, division, print_function, unicode_literals + +# Zxy 导入标准库模块 +import json +import os +import re +import shutil +import threading +import warnings + +# Zxy 导入第三方库 +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 + +# Zxy 尝试导入 whoosh 库,如果失败则抛出依赖缺失异常 +try: + import whoosh +except ImportError: + raise MissingDependency( + "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.") + +# Zxy 检查 whoosh 版本是否满足最低要求 +if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0): + raise MissingDependency( + "The 'whoosh' backend requires version 2.5.0 or greater.") + +# Zxy 定义一个正则表达式,用于匹配 ISO 8601 格式的日期时间字符串 +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?)?$') +# Zxy 创建一个线程局部存储对象,用于在每个线程中存储独立的数据 +LOCALS = threading.local() +# Zxy 初始化线程局部存储中的 RAM_STORE 为 None,用于存储内存索引 +LOCALS.RAM_STORE = None + + +# Zxy 定义一个自定义的 HTML 格式化器,继承自 Whoosh 的 HtmlFormatter +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. + """ + # Zxy 定义高亮显示的 HTML 模板 + template = '<%(tag)s>%(t)s' + + +# Zxy 定义 Whoosh 搜索后端类,继承自 Haystack 的 BaseSearchBackend +class WhooshSearchBackend(BaseSearchBackend): + # Zxy 定义 Whoosh 保留的关键字列表,这些关键字在查询中有特殊含义 + RESERVED_WORDS = ( + 'AND', + 'NOT', + 'OR', + 'TO', + ) + + # Zxy 定义 Whoosh 保留的字符列表,这些字符在查询中有特殊含义 + # The '\\' must come first, so as not to overwrite the other slash + # replacements. + RESERVED_CHARACTERS = ( + '\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}', + '[', ']', '^', '"', '~', '*', '?', ':', '.', + ) + + # Zxy 初始化方法,设置连接别名和连接选项 + def __init__(self, connection_alias, **connection_options): + # Zxy 调用父类的初始化方法 + super( + WhooshSearchBackend, + self).__init__( + connection_alias, + **connection_options) + # Zxy 标记后端设置尚未完成 + self.setup_complete = False + # Zxy 默认使用文件存储 + self.use_file_storage = True + # Zxy 设置 POST 请求的大小限制,默认为 128MB + self.post_limit = getattr( + connection_options, + 'POST_LIMIT', + 128 * 1024 * 1024) + # Zxy 从连接选项中获取索引文件存储路径 + self.path = connection_options.get('PATH') + + # Zxy 如果配置的存储类型不是 'file',则使用内存存储 + if connection_options.get('STORAGE', 'file') != 'file': + self.use_file_storage = False + + # Zxy 如果使用文件存储但未提供路径,则抛出配置错误异常 + if self.use_file_storage and not self.path: + raise ImproperlyConfigured( + "You must specify a 'PATH' in your settings for connection '%s'." % + connection_alias) + + # Zxy 获取 Haystack 的日志记录器 + self.log = logging.getLogger('haystack') + + + # Zxy 设置搜索引擎,延迟加载直到需要时才执行 + def setup(self): + """ + Defers loading until needed. + """ + # Zxy 导入 haystack 的连接管理器 + from haystack import connections + # Zxy 标记是否为新创建的索引 + new_index = False + + # Zxy 确保索引目录存在 + if self.use_file_storage and not os.path.exists(self.path): + # Zxy 如果目录不存在,则创建它 + os.makedirs(self.path) + # Zxy 标记这是一个新索引 + new_index = True + + # Zxy 检查索引目录是否可写 + if self.use_file_storage and not os.access(self.path, os.W_OK): + # Zxy 如果不可写,则抛出 IO 错误 + raise IOError( + "The path to your Whoosh index '%s' is not writable for the current user/group." % + self.path) + + # Zxy 根据配置选择存储方式:文件存储或内存存储 + if self.use_file_storage: + # Zxy 使用文件存储 + self.storage = FileStorage(self.path) + else: + # Zxy 声明使用全局的线程局部存储 + global LOCALS + + # Zxy 如果内存存储尚未初始化,则创建一个 + if getattr(LOCALS, 'RAM_STORE', None) is None: + LOCALS.RAM_STORE = RamStorage() + + # Zxy 使用内存存储 + self.storage = LOCALS.RAM_STORE + + # Zxy 构建索引模式,并获取主内容字段名 + self.content_field_name, self.schema = self.build_schema( + connections[self.connection_alias].get_unified_index().all_searchfields()) + # Zxy 创建查询解析器,用于解析用户查询 + self.parser = QueryParser(self.content_field_name, schema=self.schema) + + # Zxy 根据是否为新索引来创建或打开索引 + if new_index is True: + # Zxy 如果是新索引,则根据 schema 创建 + self.index = self.storage.create_index(self.schema) + else: + # Zxy 否则尝试打开现有索引 + try: + self.index = self.storage.open_index(schema=self.schema) + except index.EmptyIndexError: + # Zxy 如果索引为空(可能刚创建目录但无文件),则创建新索引 + self.index = self.storage.create_index(self.schema) + + # Zxy 标记后端设置已完成 + self.setup_complete = True + + # Zxy 根据 Haystack 的搜索字段构建 Whoosh 的索引模式 + def build_schema(self, fields): + # Zxy 初始化模式字段,包含 Haystack 内置的 ID、类型和模型 ID + schema_fields = { + ID: WHOOSH_ID(stored=True, unique=True), + DJANGO_CT: WHOOSH_ID(stored=True), + DJANGO_ID: WHOOSH_ID(stored=True), + } + # Zxy 获取 Haystack 内置字段的数量,用于后续检查 + initial_key_count = len(schema_fields) + # Zxy 初始化主内容字段名 + content_field_name = '' + + # Zxy 遍历所有搜索字段,根据字段类型转换为 Whoosh 字段 + for field_name, field_class in fields.items(): + # Zxy 如果字段是多值字段 + if field_class.is_multivalued: + if field_class.indexed is False: + # Zxy 如果多值字段不被索引,使用 IDLIST 类型 + schema_fields[field_class.index_fieldname] = IDLIST( + stored=True, field_boost=field_class.boost) + else: + # Zxy 如果多值字段被索引,使用 KEYWORD 类型 + schema_fields[field_class.index_fieldname] = KEYWORD( + stored=True, commas=True, scorable=True, field_boost=field_class.boost) + # Zxy 如果字段类型是日期或日期时间 + elif field_class.field_type in ['date', 'datetime']: + # Zxy 使用 DATETIME 类型,并设置为可排序 + schema_fields[field_class.index_fieldname] = DATETIME( + stored=field_class.stored, sortable=True) + # Zxy 如果字段类型是整数 + elif field_class.field_type == 'integer': + # Zxy 使用 NUMERIC 类型,并指定数字类型为整数 + schema_fields[field_class.index_fieldname] = NUMERIC( + stored=field_class.stored, numtype=int, field_boost=field_class.boost) + # Zxy 如果字段类型是浮点数 + elif field_class.field_type == 'float': + # Zxy 使用 NUMERIC 类型,并指定数字类型为浮点数 + schema_fields[field_class.index_fieldname] = NUMERIC( + stored=field_class.stored, numtype=float, field_boost=field_class.boost) + # Zxy 如果字段类型是布尔值 + elif field_class.field_type == 'boolean': + # Zxy 使用 BOOLEAN 类型 + # Field boost isn't supported on BOOLEAN as of 1.8.2. + schema_fields[field_class.index_fieldname] = BOOLEAN( + stored=field_class.stored) + # Zxy 如果字段类型是 N-gram + elif field_class.field_type == 'ngram': + # Zxy 使用 NGRAM 类型 + schema_fields[field_class.index_fieldname] = NGRAM( + minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost) + # Zxy 如果字段类型是边 N-gram + elif field_class.field_type == 'edge_ngram': + # Zxy 使用 NGRAMWORDS 类型,并设置为从词首开始 + schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start', + stored=field_class.stored, + field_boost=field_class.boost) + else: + # Zxy 默认情况下,使用 TEXT 类型,并配置中文分词器 + # 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) + # Zxy 如果字段被标记为文档主内容 + if field_class.document is True: + # Zxy 记录主内容字段名 + content_field_name = field_class.index_fieldname + # Zxy 启用拼写建议功能 + schema_fields[field_class.index_fieldname].spelling = True + + # Zxy 如果除了内置字段外没有找到任何其他字段,则优雅地报错 + if len(schema_fields) <= initial_key_count: + raise SearchBackendError( + "No fields were found in any search_indexes. Please correct this before attempting to search.") + + # Zxy 返回主内容字段名和构建好的 Schema 对象 + return (content_field_name, Schema(**schema_fields)) + + # Zxy 更新索引,将可迭代对象中的每个对象添加或更新到 Whoosh 索引中 + def update(self, index, iterable, commit=True): + # Zxy 如果后端尚未初始化,则先进行设置 + if not self.setup_complete: + self.setup() + + # Zxy 刷新索引以获取最新状态 + self.index = self.index.refresh() + # Zxy 创建一个异步写入器,用于高效地写入索引 + writer = AsyncWriter(self.index) + + # Zxy 遍历所有需要更新的对象 + for obj in iterable: + try: + # Zxy 准备要索引的文档数据 + doc = index.full_prepare(obj) + except SkipDocument: + # Zxy 如果对象被标记为跳过,则记录调试信息 + self.log.debug(u"Indexing for object `%s` skipped", obj) + else: + # Zxy 确保所有字段值都是 Whoosh 可以处理的格式 + for key in doc: + doc[key] = self._from_python(doc[key]) + + # Zxy 文档级别的权重提升在 Whoosh 2.5.0+ 版本中不再支持 + if 'boost' in doc: + del doc['boost'] + + try: + # Zxy 使用写入器更新文档(如果存在则更新,否则创建) + writer.update_document(**doc) + except Exception as e: + # Zxy 如果未配置为静默失败,则重新抛出异常 + if not self.silently_fail: + raise + + # Zxy 记录错误信息,包含对象标识符,但不包含对象本身以避免编码问题 + self.log.error( + u"%s while preparing object for update" % + e.__class__.__name__, + exc_info=True, + extra={ + "data": { + "index": index, + "object": get_identifier(obj)}}) + + # Zxy 如果可迭代对象不为空,则提交更改 + if len(iterable) > 0: + # For now, commit no matter what, as we run into locking issues + # otherwise. + writer.commit() + + # Zxy 从索引中移除一个对象 + def remove(self, obj_or_string, commit=True): + # Zxy 如果后端尚未初始化,则先进行设置 + if not self.setup_complete: + self.setup() + + # Zxy 刷新索引以获取最新状态 + self.index = self.index.refresh() + # Zxy 获取对象的唯一标识符 + whoosh_id = get_identifier(obj_or_string) + + try: + # Zxy 构建一个查询,根据 ID 查找文档并删除 + self.index.delete_by_query( + q=self.parser.parse( + u'%s:"%s"' % + (ID, whoosh_id))) + except Exception as e: + # Zxy 如果未配置为静默失败,则重新抛出异常 + if not self.silently_fail: + raise + + # Zxy 记录删除失败的错误 + self.log.error( + "Failed to remove document '%s' from Whoosh: %s", + whoosh_id, + e, + exc_info=True) + + # Zxy 清空索引,可以清空所有内容或指定模型的内容 + def clear(self, models=None, commit=True): + # Zxy 如果后端尚未初始化,则先进行设置 + if not self.setup_complete: + self.setup() + + # Zxy 刷新索引以获取最新状态 + self.index = self.index.refresh() + + # Zxy 如果提供了模型列表,则检查其类型 + if models is not None: + assert isinstance(models, (list, tuple)) + + try: + if models is None: + # Zxy 如果没有指定模型,则删除整个索引 + self.delete_index() + else: + # Zxy 准备要删除的模型列表 + models_to_delete = [] + + for model in models: + # Zxy 为每个模型构建查询字符串 + models_to_delete.append( + u"%s:%s" % + (DJANGO_CT, get_model_ct(model))) + + # Zxy 使用 OR 连接多个模型查询,并删除匹配的文档 + self.index.delete_by_query( + q=self.parser.parse( + u" OR ".join(models_to_delete))) + except Exception as e: + # Zxy 如果未配置为静默失败,则重新抛出异常 + if not self.silently_fail: + raise + + # Zxy 根据是否指定模型,记录不同的错误信息 + 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) + + # Zxy 物理删除整个索引 + 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. + # Zxy 如果使用文件存储,则直接删除索引目录 + if self.use_file_storage and os.path.exists(self.path): + shutil.rmtree(self.path) + # Zxy 如果使用内存存储,则清空存储 + elif not self.use_file_storage: + self.storage.clean() + + # Zxy 重新初始化后端,创建新的空索引 + self.setup() + + # Zxy 优化索引,合并索引段以提高搜索性能 + def optimize(self): + # Zxy 如果后端尚未初始化,则先进行设置 + if not self.setup_complete: + self.setup() + + # Zxy 刷新索引以获取最新状态 + self.index = self.index.refresh() + # Zxy 执行 Whoosh 的优化操作 + self.index.optimize() + + # Zxy 根据偏移量计算 Whoosh 分页所需的页码和每页大小 + def calculate_page(self, start_offset=0, end_offset=None): + # Zxy 防止 Whoosh 因 end_offset 小于等于 0 而抛出错误 + if end_offset is not None and end_offset <= 0: + end_offset = 1 + + # Zxy 初始化页码 + page_num = 0 + + # Zxy 如果未指定结束偏移量,则设置一个很大的默认值 + if end_offset is None: + end_offset = 1000000 + + # Zxy 如果未指定起始偏移量,则默认为 0 + if start_offset is None: + start_offset = 0 + + # Zxy 计算每页的长度 + page_length = end_offset - start_offset + + # Zxy 如果页长度有效,则计算页码 + if page_length and page_length > 0: + page_num = int(start_offset / page_length) + + # Zxy Whoosh 使用 1-based 页码,所以需要加 1 + page_num += 1 + return page_num, page_length + + # Zxy 执行搜索查询的核心方法 + @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): + # Zxy 如果后端尚未初始化,则先进行设置 + if not self.setup_complete: + self.setup() + + # Zxy 空查询字符串应返回无结果 + if len(query_string) == 0: + return { + 'results': [], + 'hits': 0, + } + + # Zxy 确保查询字符串为正确的字符串类型 + query_string = force_str(query_string) + + # Zxy 单个字符(非通配符)查询会被停用词过滤器拦截,应返回无结果 + if len(query_string) <= 1 and query_string != u'*': + return { + 'results': [], + 'hits': 0, + } + + # Zxy 初始化排序方向为非逆序 + reverse = False + + # Zxy 如果提供了排序字段 + if sort_by is not None: + # Zxy 确定是否需要反转结果,以及 Whoosh 是否能处理排序 + sort_by_list = [] + reverse_counter = 0 + + # Zxy 统计逆序排序字段的数量 + for order_by in sort_by: + if order_by.startswith('-'): + reverse_counter += 1 + + # Zxy Whoosh 要求所有排序字段的排序方向必须一致 + if reverse_counter and reverse_counter != len(sort_by): + raise SearchBackendError("Whoosh requires all order_by fields" + " to use the same sort direction") + + # Zxy 处理排序字段列表,去除 '-' 前缀并确定最终排序方向 + 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 + + # Zxy Whoosh 的 search_page 方法只接受单个排序字段 + sort_by = sort_by_list[0] + + # Zxy Whoosh 后端不支持分面搜索,发出警告 + if facets is not None: + warnings.warn( + "Whoosh does not handle faceting.", + Warning, + stacklevel=2) + + # Zxy Whoosh 后端不支持日期分面,发出警告 + if date_facets is not None: + warnings.warn( + "Whoosh does not handle date faceting.", + Warning, + stacklevel=2) + + # Zxy Whoosh 后端不支持查询分面,发出警告 + if query_facets is not None: + warnings.warn( + "Whoosh does not handle query faceting.", + Warning, + stacklevel=2) + + # Zxy 初始化用于存储缩小范围后的结果 + narrowed_results = None + # Zxy 刷新索引以获取最新状态 + self.index = self.index.refresh() + + # Zxy 确定是否限制搜索到已注册的模型 + if limit_to_registered_models is None: + limit_to_registered_models = getattr( + settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) + + # Zxy 根据传入的模型或配置构建模型选择列表 + if models and len(models): + model_choices = sorted(get_model_ct(model) for model in models) + elif limit_to_registered_models: + # Zxy 使用缩小查询的方式,将结果限制在当前路由器处理的模型中 + model_choices = self.build_models_list() + else: + model_choices = [] + + # Zxy 如果存在模型选择,则将其添加到缩小查询中 + if len(model_choices) > 0: + if narrow_queries is None: + narrow_queries = set() + + # Zxy 构建一个 OR 查询来限制模型类型 + narrow_queries.add(' OR '.join( + ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices])) + + # Zxy 初始化缩小查询的搜索器 + narrow_searcher = None + + # Zxy 如果存在缩小查询,则执行它们以获取一个结果集过滤器 + if narrow_queries is not None: + # Zxy 这个操作可能很昂贵,但 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) + + # Zxy 如果任何一个缩小查询返回空结果,则直接返回空 + if len(recent_narrowed_results) <= 0: + return { + 'results': [], + 'hits': 0, + } + + # Zxy 将多个缩小查询的结果集进行交集过滤 + if narrowed_results: + narrowed_results.filter(recent_narrowed_results) + else: + narrowed_results = recent_narrowed_results + + # Zxy 再次刷新索引以确保所有写入都可见 + self.index = self.index.refresh() + + # Zxy 如果索引中有文档,则执行搜索 + if self.index.doc_count(): + searcher = self.index.searcher() + # Zxy 解析查询字符串 + parsed_query = self.parser.parse(query_string) + + # Zxy 如果查询无效或被停用词过滤,则优雅地恢复 + if parsed_query is None: + return { + 'results': [], + 'hits': 0, + } + + # Zxy 计算分页参数 + page_num, page_length = self.calculate_page( + start_offset, end_offset) + + # Zxy 准备搜索参数 + search_kwargs = { + 'pagelen': page_length, + 'sortedby': sort_by, + 'reverse': reverse, + } + + # Zxy 如果存在缩小范围的结果,则将其作为过滤器 + if narrowed_results is not None: + search_kwargs['filter'] = narrowed_results + + try: + # Zxy 执行分页搜索 + raw_page = searcher.search_page( + parsed_query, + page_num, + **search_kwargs + ) + except ValueError: + # Zxy 如果页码无效,则返回空结果 + if not self.silently_fail: + raise + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + # Zxy 兼容 Whoosh 2.5.1 的 bug:请求过高的页码会返回错误的页 + if raw_page.pagenum < page_num: + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + # Zxy 处理原始搜索结果,转换为 Haystack 的 SearchResult 对象 + results = self._process_results( + raw_page, + highlight=highlight, + query_string=query_string, + spelling_query=spelling_query, + result_class=result_class) + # Zxy 关闭主搜索器 + searcher.close() + + # Zxy 关闭缩小查询的搜索器 + if hasattr(narrow_searcher, 'close'): + narrow_searcher.close() + + return results + else: + # Zxy 如果索引为空,但仍需处理拼写建议 + spelling_suggestion = None + 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': [], + 'hits': 0, + 'spelling_suggestion': spelling_suggestion, + } + + # Zxy 实现“更多类似于此”功能,根据给定模型实例查找相似文档 + 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): + # Zxy 如果后端尚未初始化,则先进行设置 + if not self.setup_complete: + self.setup() + + # Zxy 获取模型的真实类,避免使用延迟加载的模型类 + # Deferred models will have a different class ("RealClass_Deferred_fieldname") + # which won't be in our registry: + model_klass = model_instance._meta.concrete_model + + # Zxy 获取主内容字段名,用于相似性分析 + field_name = self.content_field_name + # Zxy 初始化缩小查询集合 + narrow_queries = set() + # Zxy 初始化缩小范围后的结果集 + narrowed_results = None + # Zxy 刷新索引以获取最新状态 + self.index = self.index.refresh() + + # Zxy 确定是否限制搜索到已注册的模型 + if limit_to_registered_models is None: + limit_to_registered_models = getattr( + settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) + + # Zxy 根据传入的模型或配置构建模型选择列表 + if models and len(models): + model_choices = sorted(get_model_ct(model) for model in models) + elif limit_to_registered_models: + # Zxy 使用缩小查询的方式,将结果限制在当前路由器处理的模型中 + model_choices = self.build_models_list() + else: + model_choices = [] + + # Zxy 如果存在模型选择,则将其添加到缩小查询中 + if len(model_choices) > 0: + if narrow_queries is None: + narrow_queries = set() + + # Zxy 构建一个 OR 查询来限制模型类型 + narrow_queries.add(' OR '.join( + ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices])) + + # Zxy 如果提供了额外的查询字符串,则也添加到缩小查询中 + if additional_query_string and additional_query_string != '*': + narrow_queries.add(additional_query_string) + + # Zxy 初始化缩小查询的搜索器 + narrow_searcher = None + + # Zxy 如果存在缩小查询,则执行它们以获取一个结果集过滤器 + if narrow_queries is not None: + # Zxy 这个操作可能很昂贵,但 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) + + # Zxy 如果任何一个缩小查询返回空结果,则直接返回空 + if len(recent_narrowed_results) <= 0: + return { + 'results': [], + 'hits': 0, + } + + # Zxy 将多个缩小查询的结果集进行交集过滤 + if narrowed_results: + narrowed_results.filter(recent_narrowed_results) + else: + narrowed_results = recent_narrowed_results + + # Zxy 计算分页参数 + page_num, page_length = self.calculate_page(start_offset, end_offset) + + # Zxy 再次刷新索引以确保所有写入都可见 + self.index = self.index.refresh() + # Zxy 初始化原始结果为空 + raw_results = EmptyResults() + + # Zxy 如果索引中有文档,则执行“更多类似于此”查询 + if self.index.doc_count(): + # Zxy 构建一个查询以找到当前模型实例对应的索引文档 + query = "%s:%s" % (ID, get_identifier(model_instance)) + searcher = self.index.searcher() + parsed_query = self.parser.parse(query) + # Zxy 搜索当前文档 + results = searcher.search(parsed_query) + + # Zxy 如果找到了当前文档,则调用其 more_like_this 方法 + if len(results): + # Zxy 获取与当前文档相似的其他文档 + raw_results = results[0].more_like_this( + field_name, top=end_offset) + + # Zxy 如果存在缩小范围的结果,则将其作为过滤器应用于相似结果 + if narrowed_results is not None and hasattr(raw_results, 'filter'): + raw_results.filter(narrowed_results) + + try: + # Zxy 将原始结果集包装成分页对象 + raw_page = ResultsPage(raw_results, page_num, page_length) + except ValueError: + # Zxy 如果页码无效,则返回空结果 + if not self.silently_fail: + raise + + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + # Zxy 兼容 Whoosh 2.5.1 的 bug:请求过高的页码会返回错误的页 + if raw_page.pagenum < page_num: + return { + 'results': [], + 'hits': 0, + 'spelling_suggestion': None, + } + + # Zxy 处理原始搜索结果,转换为 Haystack 的 SearchResult 对象 + results = self._process_results(raw_page, result_class=result_class) + # Zxy 关闭主搜索器 + searcher.close() + + # Zxy 关闭缩小查询的搜索器 + if hasattr(narrow_searcher, 'close'): + narrow_searcher.close() + + return results + + + # Zxy 处理 Whoosh 返回的原始搜索结果,转换为 Haystack 标准格式 + def _process_results( + self, + raw_page, + highlight=False, + query_string='', + spelling_query=None, + result_class=None): + # Zxy 导入 haystack 连接管理器 + from haystack import connections + # Zxy 初始化结果列表 + results = [] + + # Zxy 在切片之前获取总命中数,这对于分页至关重要 + hits = len(raw_page) + + # Zxy 如果未指定结果类,则使用默认的 SearchResult + if result_class is None: + result_class = SearchResult + + # Zxy 初始化分面和拼写建议 + facets = {} + spelling_suggestion = None + # Zxy 获取统一索引对象和已注册的模型列表 + unified_index = connections[self.connection_alias].get_unified_index() + indexed_models = unified_index.get_indexed_models() + + # Zxy 遍历原始结果页中的每个文档 + for doc_offset, raw_result in enumerate(raw_page): + # Zxy 获取文档的得分 + score = raw_page.score(doc_offset) or 0 + # Zxy 从文档中解析出应用标签和模型名 + app_label, model_name = raw_result[DJANGO_CT].split('.') + # Zxy 初始化额外字段字典 + additional_fields = {} + # Zxy 根据应用标签和模型名获取模型类 + model = haystack_get_model(app_label, model_name) + + # Zxy 确保模型存在且已注册到索引 + if model and model in indexed_models: + # Zxy 遍历文档中的所有字段 + for key, value in raw_result.items(): + # Zxy 获取该模型对应的索引 + index = unified_index.get_index(model) + string_key = str(key) + + # Zxy 如果字段在索引定义中,并且有转换方法 + if string_key in index.fields and hasattr( + index.fields[string_key], 'convert'): + # Zxy 特殊处理多值字段 + 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: + # Zxy 使用索引字段定义的转换方法 + additional_fields[string_key] = index.fields[string_key].convert( + value) + else: + # Zxy 否则使用通用的 Python 类型转换 + additional_fields[string_key] = self._to_python(value) + + # Zxy 删除 Haystack 内部字段,不返回给用户 + del (additional_fields[DJANGO_CT]) + del (additional_fields[DJANGO_ID]) + + # Zxy 如果需要高亮显示 + if highlight: + # Zxy 创建词干分析器和 HTML 格式化器 + sa = StemmingAnalyzer() + formatter = WhooshHtmlFormatter('em') + # Zxy 从查询字符串中提取词条 + terms = [token.text for token in sa(query_string)] + + # Zxy 调用 Whoosh 的高亮方法 + whoosh_result = whoosh_highlight( + additional_fields.get(self.content_field_name), + terms, + sa, + ContextFragmenter(), + formatter + ) + # Zxy 将高亮结果添加到额外字段中 + additional_fields['highlighted'] = { + self.content_field_name: [whoosh_result], + } + + # Zxy 创建 SearchResult 对象并添加到结果列表 + result = result_class( + app_label, + model_name, + raw_result[DJANGO_ID], + score, + **additional_fields) + results.append(result) + else: + # Zxy 如果模型未注册,则减少总命中数 + hits -= 1 + + # Zxy 如果启用了拼写建议 + if self.include_spelling: + if spelling_query: + spelling_suggestion = self.create_spelling_suggestion( + spelling_query) + else: + spelling_suggestion = self.create_spelling_suggestion( + query_string) + + # Zxy 返回包含结果、命中数、分面和拼写建议的字典 + return { + 'results': results, + 'hits': hits, + 'facets': facets, + 'spelling_suggestion': spelling_suggestion, + } + + # Zxy 根据查询字符串创建拼写建议 + def create_spelling_suggestion(self, query_string): + # Zxy 初始化拼写建议 + spelling_suggestion = None + # Zxy 获取索引的读取器和校正器 + reader = self.index.reader() + corrector = reader.corrector(self.content_field_name) + # Zxy 清理查询字符串 + cleaned_query = force_str(query_string) + + # Zxy 如果查询字符串为空,直接返回 + if not query_string: + return spelling_suggestion + + # Zxy 移除查询中的保留字 + for rev_word in self.RESERVED_WORDS: + cleaned_query = cleaned_query.replace(rev_word, '') + + # Zxy 移除查询中的保留字符 + for rev_char in self.RESERVED_CHARACTERS: + cleaned_query = cleaned_query.replace(rev_char, '') + + # Zxy 将清理后的查询拆分为单词列表 + query_words = cleaned_query.split() + suggested_words = [] + + # Zxy 为每个单词查找拼写建议 + for word in query_words: + suggestions = corrector.suggest(word, limit=1) + + if len(suggestions) > 0: + suggested_words.append(suggestions[0]) + + # Zxy 将建议的单词重新组合成字符串 + spelling_suggestion = ' '.join(suggested_words) + return spelling_suggestion + + # Zxy 将 Python 值转换为 Whoosh 可用的字符串格式 + def _from_python(self, value): + """ + Converts Python values to a string for Whoosh. + + Code courtesy of pysolr. + """ + # Zxy 处理日期时间对象 + if hasattr(value, 'strftime'): + # Zxy 如果只有日期没有时间,则将时间部分设为 0 + if not hasattr(value, 'hour'): + value = datetime(value.year, value.month, value.day, 0, 0, 0) + # Zxy 处理布尔值 + elif isinstance(value, bool): + if value: + value = 'true' + else: + value = 'false' + # Zxy 处理列表或元组,用逗号连接 + elif isinstance(value, (list, tuple)): + value = u','.join([force_str(v) for v in value]) + # Zxy 处理数字,保持原样 + elif isinstance(value, (six.integer_types, float)): + # Leave it alone. + pass + else: + # Zxy 其他类型强制转换为字符串 + value = force_str(value) + return value + + # Zxy 将 Whoosh 的值转换为原生 Python 值 + 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. + """ + # Zxy 处理布尔字符串 + if value == 'true': + return True + elif value == 'false': + return False + + # Zxy 尝试解析日期时间字符串 + 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']) + + # Zxy 尝试使用 json 解复杂数据类型 + 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 + + # Zxy 如果都无法转换,则返回原始值 + return value + + # Zxy 定义 Whoosh 搜索查询类,继承自 Haystack 的 BaseSearchQuery +class WhooshSearchQuery(BaseSearchQuery): + # Zxy 将日期时间对象转换为 Whoosh 查询所需的字符串格式 + def _convert_datetime(self, date): + # Zxy 如果包含时间,则转换为完整格式 + if hasattr(date, 'hour'): + return force_str(date.strftime('%Y%m%d%H%M%S')) + else: + # Zxy 如果只有日期,则补充零时间 + return force_str(date.strftime('%Y%m%d000000')) + + # Zxy 清理查询片段,转义 Whoosh 的保留字符 + 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. + """ + # Zxy 将查询片段按空格分割成单词 + words = query_fragment.split() + cleaned_words = [] + + # Zxy 遍历每个单词进行清理 + for word in words: + # Zxy 如果是保留字,则转为小写 + if word in self.backend.RESERVED_WORDS: + word = word.replace(word, word.lower()) + + # Zxy 如果包含保留字符,则用单引号将整个单词括起来 + for char in self.backend.RESERVED_CHARACTERS: + if char in word: + word = "'%s'" % word + break + + cleaned_words.append(word) + + # Zxy 将清理后的单词重新组合 + return ' '.join(cleaned_words) + + # Zxy 构建查询片段,根据字段、过滤类型和值生成 Whoosh 查询语法 + def build_query_fragment(self, field, filter_type, value): + # Zxy 导入 haystack 连接管理器 + from haystack import connections + query_frag = '' + is_datetime = False + + # Zxy 如果值没有 input_type_name 属性,则进行类型推断 + 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) + + # Zxy 使用 InputType 准备查询值 + prepared_value = value.prepare(self) + + # Zxy 如果准备好的值不是集合类型,则转换为 Whoosh 可用的格式 + 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'. + # Zxy 'content' 是特殊字段,代表所有可搜索内容 + if field == 'content': + index_fieldname = '' + else: + # Zxy 获取字段在索引中的真实名称 + index_fieldname = u'%s:' % connections[self._using].get_unified_index( + ).get_index_fieldname(field) + + # Zxy 定义不同过滤类型对应的 Whoosh 查询模板 + 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~', + } + # Zxy 如果值不需要后处理,则直接使用 + if value.post_process is False: + query_frag = prepared_value + else: + # Zxy 根据不同的过滤类型构建查询片段 + if filter_type in [ + 'content', + 'contains', + 'startswith', + 'endswith', + 'fuzzy']: + # Zxy 如果输入类型是精确匹配,则直接使用值 + 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) + # Zxy 处理 'in' 过滤类型 + 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) + # Zxy 处理 'range' 过滤类型 + 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) + # Zxy 处理 'exact' 过滤类型 + 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: + # Zxy 处理其他类型(如 gt, gte, lt, lte) + if is_datetime is True: + prepared_value = self._convert_datetime(prepared_value) + + query_frag = filter_types[filter_type] % prepared_value + + # Zxy 如果查询片段不为空且不是原始查询,则用括号括起来 + 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) + +# Zxy 定义 Whoosh 引擎类,继承自 Haystack 的 BaseEngine +class WhooshEngine(BaseEngine): + # Zxy 指定后端和查询类 + backend = WhooshSearchBackend + query = WhooshSearchQuery diff --git a/whoosh_index/MAIN_5bjud9prw1dxe6io.seg b/whoosh_index/MAIN_5bjud9prw1dxe6io.seg new file mode 100644 index 0000000000000000000000000000000000000000..d37c524e4e09bff98d256dd700734174ffc09114 GIT binary patch literal 18085 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 GX7fMS9I(Ow literal 0 HcmV?d00001 diff --git a/whoosh_index/MAIN_7s7twfilcbt07zs1.seg b/whoosh_index/MAIN_7s7twfilcbt07zs1.seg new file mode 100644 index 0000000000000000000000000000000000000000..36d441277d6278b76a04e749132162261efad3d8 GIT binary patch literal 5352 zcmZQz00H3z3=E7O;l@rm|NsBL*ubRW&FIbGZvO!+$>7cG&EU;YQpliJ5tp6tDJ?@c z<-_M}{gf-r43a#|?E|818Pt(LhhlM^cc1?`e@|b%XI^KS4R>%!D*uo;(C{&UIXFkL z*}Q&HoMJGGbbyNbMit}p8z$^rJ3)2Dgspog`0_JMyDB6B_0MpyVGfq4WcFr8jY_c9rAz*ZDSKK|?s;ii(Kjg7Zja zcsO|kdl*Bq5`^RfQpyYrNjdrHdWl6PnaMe+4E7CRIYtH;3!)T3ngf~4K&;HbXb4k- zT{TE02n&E!Ga*}}M?h;HR4WfyD>K5Y_-$MYRGtLH*67+01`x%@{O&md_-7(f&YuPscV2m;vx@eB*HXRx`7 zX)#nikb;0~;_Eq~OkS86h<^2IGbnul0h1(zhLCwN$zl)|V-2$SGl%IZQ2sMyaot4q z=}`W9Wbv&X>vuxtCYOrzm28V-Xc4Z!m0N~C-`%7&!R(Qu$nIKa}_ zL8LS`%7%pPK!yV>UTz}A%P1QXFC#G=VCmr_QhJ~_8|nsTPJ|lauW-tfOwoKS&4b+`SHmmQ@k1Ayv&p--i$p=Ad0DntGFOFCnvrnwF0Py zxrYV9U@7h4%*;yx3RV`RPU&Ivn9{=m=I5m5P4VU|)nnjgaAPQ8SkLf`(TA~yaWlBX z)Cn5oTBVzEq2obHLW0LhuQT4~`~CGaQ+ggSewtkL>GD^hZZD(B#X;ONCrnLQCSv4n zz%*4tY?F%tW99;eY!>-ha<+$^wsD9pNbt;Z^;E1jI#RRu(xfwUlJ-72vgOL7NsG?x zDOs1GU@UP+=Sjy021#!Am_WgaF&uiGYn;wqb6R)I>0OX5!$mP`)-ESzV8~c7B0@&e z*U{52-n`hnq&zJ%CpoFaz`Uy1P_Ll41RPXs`DtmzslYJc>0twUGQA{YiZ4HN4@Yi( zN+vK&Q>M5(-m>ZxzMgZrv_}rRQJ|pHOU};$nU}%rE6tq2;_a2e3e_kQP}+mT4JAdn zP(7(YJ-+Ms;hI3UDPp%RzPKd6C^ZGiLSbQ`g*|Y+APbdo=mj}79_URZ-Sa`Z8Ig6D L>NT*|PSFDZBXb2{ literal 0 HcmV?d00001 diff --git a/whoosh_index/MAIN_WRITELOCK b/whoosh_index/MAIN_WRITELOCK new file mode 100644 index 0000000..e69de29 diff --git a/whoosh_index/MAIN_hnkxp6bdzv6onzg5.seg b/whoosh_index/MAIN_hnkxp6bdzv6onzg5.seg new file mode 100644 index 0000000000000000000000000000000000000000..8bb7a70ea81132dd5daef1460037f3aeb3af50e9 GIT binary patch literal 6327 zcmeHL4@?_X7=L$H*uj|mDS=g!g$|5hN*yq_5sfRvZ9wfHPGs34f3MKO(ROfkh(YTX zvn4xS#}3^lMny%20z=29nI!7|STe>RWoVc)WH!v0=>Dk5bh`Q8y}NqDu1f})O!<;~ z@AuyOe((Ff-+c|V03eIC13+XIq-C;KuU;K?k%>ma2v#jWFCGFTWdugRZ2w6 z%?BTDDb%h}-IbScPj^Dv;6C?99qzl^+&#T+Q#g2eI7%zkGaKRmqS8xp!y9nFqlTxA z^fe9d*jVgxc`teY5JtSEog)uMY<}0Bk{Pk^FxU26(gAnI;a5(rU!33NPTDjtao5hZ zjk_z>dcJww6ZM#9Y@KJc(9<~wBrxhRskfTth>DNcep%q^79LWcn6B!UR{8&k%GC|@tLIKztfIiN2ES8BPgksihQP6gwCWg}wagsH zSvl4mmy#Yj(X;LQuBNj>+r-v`q2f`Q-Nu=196Y=?Giyy&n$)u5bO#*c0Vrkd75b7I z&RWKr!E%>)L;x9yOU1AhVikPi0g;j}JNb7Ium?Lq5F$c*Nm|&F%h!_tjQT4!#;}V0$4E%ZmQpn_MDfnFmrfioeSSuCO z$^dtu5j1TOfpQ=(z*P-=uvV(cTG6!B0}2vV1{W-RuvRKjD+6}$8-yB!hbV|dKw9~V z@E1Xq0i96N2cJzupOW^$%Ivp513n;5`y3{LH~;)<58fg- zxYFxTze6Kd;2DqF%7;F{h&-v>SK^Lu&wVbfyU!kK{95u4Q~Lb2X=Gel%d@y-w^ja# z_lb7t{2$X#`Zs>Tn`8@~DfC7qyCs;!!KgSnDgd*1R+B0BfZSKh6n!;|MKP$`NeB5o z<|*FebYl5_i{k#9!RXI9G-?-~XDpWM-KWTN7K>s~Q`JG%)}zq&R%1DbaYYU@5zBdA zQsntx#!|hih`;N-ZLwTSqN0}R#Pa9YFkd~&e})k?shjZt1P(3>J8Qdbj<6P?8+6{+NV>G1owW?*xy{*d*;agku!}g{U39k#c^r#Kht@) z4w03f1sP;h1xU1s*JJJ*t&BQFF5%H{;xt3>&xscy3azHb_7{eMx7-{ zA|8F#$zLZ2uBKYz^g@m)B+^Gh(X{@f71eU~8Z)B^cq|46Z1Zt@?sW3;*Wsd6i(r`w Twk;O22_GA#>(sk~8U+1coRtkD literal 0 HcmV?d00001 diff --git a/whoosh_index/MAIN_hqwvke8n7syrdbus.seg b/whoosh_index/MAIN_hqwvke8n7syrdbus.seg new file mode 100644 index 0000000000000000000000000000000000000000..14ffa8b648aba1f243d63a1e8d566a26bec99fdd GIT binary patch literal 6327 zcmeHL4@?_X7=L$H*uj|m4XiO)=)lMf)B#Ii(MUnJ0kwlT=g1cMdxaK`7U)%A22;0~ zk#M>S#~c%*qE0qt3>~0O5_NNyjPXYqnoSIu4KpU{hMJ($&G+uz;elP341$~TCHLO% zz4!gz_kF+n8fXDP7GVW|NZ*#2#$LO2ZNx#w=m|a8lyXTt1bRvj^nj}b+M3+5{HbD7 z9CPXCvU{0P3aA69J^u>rpp+y`)I)aq=duQ~G7Q?u%rjK{d;aRE>6ITjE@x9)Sk3O< zhuU&ATU2*uN8jBQoj7#V`F@AT z@DE<7@Nf6(r?obhQvt50z4Pp|25rk;55{El^7JDqEGt%%v*l+)N! zlH>aNF;~c=u5;U6W4W#)%Rn>}dQR%CRym^LBQ`hUrJHHuZf8fI^YK71q7Gdp#afu^ zKHib+y}H%ML(eQ!bxCEOKcaGVg9D+<7c5qu_gF*UhK4OvtX`e>Si_poaErOf7|EGA z))*PT-hZ;Y;k(Y}i$eQkeXqZGRAjYqMhgcI?@3GFlAb8FtT^2P$0z^_S!;>5po%jW zu||;M5RV8TBXOx1k|0*WCkhbp>t!d;?#LBA!J<2vAH=BarrFY6+iGsCKL9KLf z2O2@sIuR%b@-kdi!3S%lnyVE}OFf_7@u*N177>{%L90etaPle zLH!Pm*nnp|YDyn?7bEhla$ku%zWM1hY29`4c;i=+e}K~Gw_T$X(psLyCEHN?1KuYN zOXvTXe$>AD3*IE#@JyjMD%ql75(lFq<){EG;U!Jx-2-x8B`f-B35#M-x04R?d(2k6 z$A!f5{U*izH-piib7<6lJm0cduJ@=S&m}C1LCseOSzDJv+r`Fm4ikzTZbdBTIjzX^ zzl^1NRY8B(d)s2UmKa4X3yJ0Xb&C6M6)fkLqsVQ(SiZADap$6Ahfg#5Mu)6Uh~epC zvyo-W3n~P(@?%O1EG5>wB2LhQnU$Fl^aM|$iRArl6-JiL|N44Ma6@m>4GmNF7;8wuxVin$hplhvS&x z`UxgKKfOP5KtHrMOS^{QCy1%0s;Mu24Qa?sXtF)Byv5nf91Kg?w3cXIxjpWt48n&^(!N(zoyP#(~ztz6&Rhjb3PM8~Bhrk{_A4@{?G1{V-YT zI{onGtug(v{TW-=9&_*P>Av#m?}vuoe7k0>>!szf&8;tQYdo-x6k^?MoVzsEtvySD zt5;J_9ZaL4g30i=6i85odCCOG>#T43Y+6o&dO6qGZF18t=Rsp;( z6r+SQ35I8Uc)Hxmn4$KJuqpLiLPYI~4<5Aht2|$YB-R#LS#+O?GCc2NQt4w&J`(Ye z+sLic PWD`oZUF-0v6SRK;*0%|n literal 0 HcmV?d00001 diff --git a/whoosh_index/_MAIN_39.toc b/whoosh_index/_MAIN_39.toc new file mode 100644 index 0000000000000000000000000000000000000000..a71fc80e255d534f91d19231f541bada61cc8195 GIT binary patch literal 2426 zcmbVNTT>iG6b6>dGLVD>f<_as(F|VJL<}U}VnRYB8!!SU<7FM^(n}A_Oz)ZQVRtbT zK|@@0QbrwL{3*Wr;G=&+%UAyap6S_1D5#XBTSamD^f}+T_BY+d?(Oa0f7{;P{=TcP z`|kerE~`!%a!kiG}n8bJ@f+ur~r`i}#NAS#UjAz>ztqh|ZF$8G{ zIDWr5*$@{mX^RRxhnYMd!3jjp@9+0L9|>2cm{()m5#)S(N(!jxI2OSRT2Gp!ARdPo zEAWzPEjz?CN*M^r{^RJBDeCxylO5xG84*f*f>s?@q?5kN>LjoXPNtJ8xF6=;WbOw%@Cm+U)Pmtz}AS*iBRBl1twH2?XPA>{C8rU~FJ>DXi;yq&3hI41>@z&n}Hg!2-r?g$DOGJ7Im zN=kqId>bxG$?$EsBqhu6VOsh+K3vW&0|M_#m0&O<;i$1n;EEKnDi6xi&}Ra!O35>t zFxw97x-ch2Of-P^Bv75eH7Q`U_p@jM<}(LMd|1#r)84?MR0)dD>;nn>=MOPl&m1`1 z$RGr0^6rmc?!bpq!j}jiNlBPCd`vdslX<8}g@1E+09V5#k=O1!EMcJAmR1C?qlz@1W%3OogfEF{%Kbx!&?OFq7D zSfaQKbQ7F;SeCEYGLYvu5)Z7*Ui&pP2{-8;in*4)iwrTunYR@YgziaW}Ix3m#d)&qQJxl*@6f%hP0JJykP zQVQiY;<^sj6}GekJI;D7AXTRUt67IqD;?_E5Sn-)z6i;$I&xajgoflmn^XpeeRNm&Dbl|+q4+FwYE*~BVkhw&utzcIc*mOSE%!KWw8)bDj$lAx=manh@E_wsO6!1aWO@}TRUmR z`?A_y;x7rxlXw7^*Tu=F>yD@Pme`J!Ru>oB{EAlvM;5%9%wvh^RwU(HI0IV9T0nc- zh0|g@ME#XUZNjwIRwtOhwsbn73LR|Q9VoBUp>ly9EMw;MP`Q&1r)hTFqla5`lpdi+ xcZMiFGz8RFSI8NEk~ag}4Eeu=nEP+&lYCOt@v~)_;^gESKlQ)R3z$18{R3JO62AZd literal 0 HcmV?d00001 diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..730430e --- /dev/null +++ b/wsgi.py @@ -0,0 +1,20 @@ +""" +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/ +""" + +# Zxy导入os模块,用于操作环境变量 +import os + +# Zxy从Django中导入get_wsgi_application函数,用于获取WSGI应用 +from django.core.wsgi import get_wsgi_application + +# Zxy设置环境变量DJANGO_SETTINGS_MODULE,指定Django项目的配置文件 +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") + +# Zxy获取WSGI应用实例,用于部署 +application = get_wsgi_application() \ No newline at end of file