codex 1.6.19__py3-none-any.whl → 1.7.0a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of codex might be problematic. Click here for more details.
- codex/db.py +119 -0
- codex/librarian/covers/purge.py +1 -1
- codex/librarian/importer/const.py +7 -1
- codex/librarian/importer/create_fks.py +5 -2
- codex/librarian/importer/importerd.py +17 -12
- codex/librarian/importer/moved.py +44 -13
- codex/librarian/importer/tasks.py +2 -0
- codex/librarian/janitor/cleanup.py +21 -8
- codex/librarian/janitor/integrity.py +369 -0
- codex/librarian/janitor/janitor.py +50 -15
- codex/librarian/janitor/latest_version.py +2 -1
- codex/librarian/janitor/status.py +5 -0
- codex/librarian/janitor/tasks.py +37 -0
- codex/librarian/janitor/update.py +1 -1
- codex/librarian/librariand.py +32 -24
- codex/librarian/search/optimize.py +43 -0
- codex/librarian/search/remove.py +18 -46
- codex/librarian/search/searchd.py +14 -17
- codex/librarian/search/status.py +2 -1
- codex/librarian/search/tasks.py +4 -9
- codex/librarian/search/update.py +223 -301
- codex/librarian/watchdog/observers.py +2 -1
- codex/migrations/0029_comicfts.py +58 -0
- codex/migrations/0030_nocase_collation_day_month_indexes_status_types.py +198 -0
- codex/models/admin.py +15 -6
- codex/models/base.py +9 -0
- codex/models/bookmark.py +3 -3
- codex/models/comic.py +68 -16
- codex/models/functions.py +59 -2
- codex/models/groups.py +6 -1
- codex/models/library.py +1 -3
- codex/models/paths.py +9 -3
- codex/serializers/browser/page.py +2 -0
- codex/settings/settings.py +16 -17
- codex/signals/django_signals.py +4 -10
- codex/startup.py +1 -35
- codex/static_root/assets/{VCheckbox-Cko1sQK-.170726b3b8a1.js → VCheckbox-CWiqcZ7a.3feebd47daab.js} +1 -1
- codex/static_root/assets/VCheckbox-CWiqcZ7a.3feebd47daab.js.br +0 -0
- codex/static_root/assets/VCheckbox-CWiqcZ7a.3feebd47daab.js.gz +0 -0
- codex/static_root/assets/{VCheckbox-Cko1sQK-.js → VCheckbox-CWiqcZ7a.js} +1 -1
- codex/static_root/assets/VCheckbox-CWiqcZ7a.js.br +0 -0
- codex/static_root/assets/VCheckbox-CWiqcZ7a.js.gz +0 -0
- codex/static_root/assets/{VCheckboxBtn-CO3-aqQL.c9b8062fd66f.js → VCheckboxBtn-Dk3iB62X.bc66d5351baa.js} +1 -1
- codex/static_root/assets/VCheckboxBtn-Dk3iB62X.bc66d5351baa.js.br +0 -0
- codex/static_root/assets/VCheckboxBtn-Dk3iB62X.bc66d5351baa.js.gz +0 -0
- codex/static_root/assets/{VCheckboxBtn-CO3-aqQL.js → VCheckboxBtn-Dk3iB62X.js} +1 -1
- codex/static_root/assets/VCheckboxBtn-Dk3iB62X.js.br +0 -0
- codex/static_root/assets/VCheckboxBtn-Dk3iB62X.js.gz +0 -0
- codex/static_root/assets/{VCombobox-Bjp6lffE.js → VCombobox-D1_KU27A.b1d30587a973.js} +1 -1
- codex/static_root/assets/VCombobox-D1_KU27A.b1d30587a973.js.br +0 -0
- codex/static_root/assets/VCombobox-D1_KU27A.b1d30587a973.js.gz +0 -0
- codex/static_root/assets/{VCombobox-Bjp6lffE.caf59fba486e.js → VCombobox-D1_KU27A.js} +1 -1
- codex/static_root/assets/VCombobox-D1_KU27A.js.br +0 -0
- codex/static_root/assets/VCombobox-D1_KU27A.js.gz +0 -0
- codex/static_root/assets/{VDialog-BkpVGB70.6eea3ca762a5.js → VDialog-GbxvFMMN.594fb3a39ced.js} +1 -1
- codex/static_root/assets/VDialog-GbxvFMMN.594fb3a39ced.js.br +0 -0
- codex/static_root/assets/VDialog-GbxvFMMN.594fb3a39ced.js.gz +0 -0
- codex/static_root/assets/{VDialog-BkpVGB70.js → VDialog-GbxvFMMN.js} +1 -1
- codex/static_root/assets/VDialog-GbxvFMMN.js.br +0 -0
- codex/static_root/assets/VDialog-GbxvFMMN.js.gz +0 -0
- codex/static_root/assets/{VExpansionPanels-Ci2-j8XX.9c55db4ef49e.js → VExpansionPanels-Cywv_bEj.a762e63858e5.js} +1 -1
- codex/static_root/assets/VExpansionPanels-Cywv_bEj.a762e63858e5.js.br +0 -0
- codex/static_root/assets/VExpansionPanels-Cywv_bEj.a762e63858e5.js.gz +0 -0
- codex/static_root/assets/{VExpansionPanels-Ci2-j8XX.js → VExpansionPanels-Cywv_bEj.js} +1 -1
- codex/static_root/assets/VExpansionPanels-Cywv_bEj.js.br +0 -0
- codex/static_root/assets/VExpansionPanels-Cywv_bEj.js.gz +0 -0
- codex/static_root/assets/{VRadioGroup-D3Py5BfQ.a4f25edb86d3.js → VRadioGroup-C0etmxpZ.0eb3009a6300.js} +1 -1
- codex/static_root/assets/VRadioGroup-C0etmxpZ.0eb3009a6300.js.br +0 -0
- codex/static_root/assets/VRadioGroup-C0etmxpZ.0eb3009a6300.js.gz +0 -0
- codex/static_root/assets/{VRadioGroup-D3Py5BfQ.js → VRadioGroup-C0etmxpZ.js} +1 -1
- codex/static_root/assets/VRadioGroup-C0etmxpZ.js.br +0 -0
- codex/static_root/assets/VRadioGroup-C0etmxpZ.js.gz +0 -0
- codex/static_root/assets/{VSelect-DtvZsYZz.e98745d858eb.js → VSelect-txrnRiNJ.a5625760018c.js} +1 -1
- codex/static_root/assets/VSelect-txrnRiNJ.a5625760018c.js.br +0 -0
- codex/static_root/assets/VSelect-txrnRiNJ.a5625760018c.js.gz +0 -0
- codex/static_root/assets/{VSelect-DtvZsYZz.js → VSelect-txrnRiNJ.js} +1 -1
- codex/static_root/assets/VSelect-txrnRiNJ.js.br +0 -0
- codex/static_root/assets/VSelect-txrnRiNJ.js.gz +0 -0
- codex/static_root/assets/{VSelectionControl-D6kmykQW.1e1fda62ceba.js → VSelectionControl-CY6RmcvW.7e8824224f6c.js} +1 -1
- codex/static_root/assets/VSelectionControl-CY6RmcvW.7e8824224f6c.js.br +0 -0
- codex/static_root/assets/VSelectionControl-CY6RmcvW.7e8824224f6c.js.gz +0 -0
- codex/static_root/assets/{VSelectionControl-D6kmykQW.js → VSelectionControl-CY6RmcvW.js} +1 -1
- codex/static_root/assets/VSelectionControl-CY6RmcvW.js.br +0 -0
- codex/static_root/assets/VSelectionControl-CY6RmcvW.js.gz +0 -0
- codex/static_root/assets/{VSlideGroup-UGZWxpF5.360bdefcd215.js → VSlideGroup-DtEQMwY1.6c72325635ed.js} +1 -1
- codex/static_root/assets/VSlideGroup-DtEQMwY1.6c72325635ed.js.br +0 -0
- codex/static_root/assets/VSlideGroup-DtEQMwY1.6c72325635ed.js.gz +0 -0
- codex/static_root/assets/{VSlideGroup-UGZWxpF5.js → VSlideGroup-DtEQMwY1.js} +1 -1
- codex/static_root/assets/VSlideGroup-DtEQMwY1.js.br +0 -0
- codex/static_root/assets/VSlideGroup-DtEQMwY1.js.gz +0 -0
- codex/static_root/assets/{VTable-DXH_yQ0o.1149f56ad566.js → VTable-2af995xo.6c5bf36631b2.js} +1 -1
- codex/static_root/assets/VTable-2af995xo.6c5bf36631b2.js.br +0 -0
- codex/static_root/assets/VTable-2af995xo.6c5bf36631b2.js.gz +0 -0
- codex/static_root/assets/{VTable-DXH_yQ0o.js → VTable-2af995xo.js} +1 -1
- codex/static_root/assets/VTable-2af995xo.js.br +0 -0
- codex/static_root/assets/VTable-2af995xo.js.gz +0 -0
- codex/static_root/assets/{VTextField-Blqy56_L.26d8761e0155.js → VTextField-G6CMj7yO.657a130dd302.js} +1 -1
- codex/static_root/assets/VTextField-G6CMj7yO.657a130dd302.js.br +0 -0
- codex/static_root/assets/VTextField-G6CMj7yO.657a130dd302.js.gz +0 -0
- codex/static_root/assets/{VTextField-Blqy56_L.js → VTextField-G6CMj7yO.js} +1 -1
- codex/static_root/assets/VTextField-G6CMj7yO.js.br +0 -0
- codex/static_root/assets/VTextField-G6CMj7yO.js.gz +0 -0
- codex/static_root/assets/{VWindowItem-C8_ovV7e.afbc08414765.js → VWindowItem-CzoGd6Ul.721e5da11ad4.js} +1 -1
- codex/static_root/assets/VWindowItem-CzoGd6Ul.721e5da11ad4.js.br +0 -0
- codex/static_root/assets/VWindowItem-CzoGd6Ul.721e5da11ad4.js.gz +0 -0
- codex/static_root/assets/{VWindowItem-C8_ovV7e.js → VWindowItem-CzoGd6Ul.js} +1 -1
- codex/static_root/assets/VWindowItem-CzoGd6Ul.js.br +0 -0
- codex/static_root/assets/VWindowItem-CzoGd6Ul.js.gz +0 -0
- codex/static_root/assets/{admin-DTRn8bBs.d031ea42d8d0.js → admin-CsPAIQeG.5175467148a3.js} +1 -1
- codex/static_root/assets/admin-CsPAIQeG.5175467148a3.js.br +0 -0
- codex/static_root/assets/admin-CsPAIQeG.5175467148a3.js.gz +0 -0
- codex/static_root/assets/{admin-DTRn8bBs.js → admin-CsPAIQeG.js} +1 -1
- codex/static_root/assets/admin-CsPAIQeG.js.br +0 -0
- codex/static_root/assets/admin-CsPAIQeG.js.gz +0 -0
- codex/static_root/assets/{admin-drawer-panel-BaXFOcru.b1a44b05f482.js → admin-drawer-panel-BAT7GA64.73a064628c19.js} +11 -11
- codex/static_root/assets/admin-drawer-panel-BAT7GA64.73a064628c19.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-BAT7GA64.73a064628c19.js.gz +0 -0
- codex/static_root/assets/{admin-drawer-panel-BaXFOcru.js → admin-drawer-panel-BAT7GA64.js} +11 -11
- codex/static_root/assets/admin-drawer-panel-BAT7GA64.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-BAT7GA64.js.gz +0 -0
- codex/static_root/assets/browser-C18l93yc.923860009ef4.js +1 -0
- codex/static_root/assets/browser-C18l93yc.923860009ef4.js.br +0 -0
- codex/static_root/assets/browser-C18l93yc.923860009ef4.js.gz +0 -0
- codex/static_root/assets/browser-C18l93yc.js +1 -0
- codex/static_root/assets/browser-C18l93yc.js.br +0 -0
- codex/static_root/assets/browser-C18l93yc.js.gz +0 -0
- codex/static_root/assets/browser-DaAqlBYO.708ecf0904bb.css +1 -0
- codex/static_root/assets/browser-DaAqlBYO.708ecf0904bb.css.br +0 -0
- codex/static_root/assets/browser-DaAqlBYO.708ecf0904bb.css.gz +0 -0
- codex/static_root/assets/browser-DaAqlBYO.css +1 -0
- codex/static_root/assets/browser-DaAqlBYO.css.br +0 -0
- codex/static_root/assets/browser-DaAqlBYO.css.gz +0 -0
- codex/static_root/assets/change-password-dialog-BazJdNs9.0a4e02656a35.js +1 -0
- codex/static_root/assets/change-password-dialog-BazJdNs9.0a4e02656a35.js.br +0 -0
- codex/static_root/assets/change-password-dialog-BazJdNs9.0a4e02656a35.js.gz +0 -0
- codex/static_root/assets/change-password-dialog-BazJdNs9.js +1 -0
- codex/static_root/assets/change-password-dialog-BazJdNs9.js.br +0 -0
- codex/static_root/assets/change-password-dialog-BazJdNs9.js.gz +0 -0
- codex/static_root/assets/change-password-dialog-Mlmvz8_4.111421f51e79.css +1 -0
- codex/static_root/assets/change-password-dialog-Mlmvz8_4.111421f51e79.css.br +0 -0
- codex/static_root/assets/change-password-dialog-Mlmvz8_4.111421f51e79.css.gz +0 -0
- codex/static_root/assets/change-password-dialog-Mlmvz8_4.css +1 -0
- codex/static_root/assets/change-password-dialog-Mlmvz8_4.css.br +0 -0
- codex/static_root/assets/change-password-dialog-Mlmvz8_4.css.gz +0 -0
- codex/static_root/assets/{confirm-dialog-D3_s2DQy.9dde66036c72.js → confirm-dialog-CbW3hlOp.9dd39b443c31.js} +1 -1
- codex/static_root/assets/confirm-dialog-CbW3hlOp.9dd39b443c31.js.br +0 -0
- codex/static_root/assets/confirm-dialog-CbW3hlOp.9dd39b443c31.js.gz +0 -0
- codex/static_root/assets/{confirm-dialog-D3_s2DQy.js → confirm-dialog-CbW3hlOp.js} +1 -1
- codex/static_root/assets/confirm-dialog-CbW3hlOp.js.br +0 -0
- codex/static_root/assets/confirm-dialog-CbW3hlOp.js.gz +0 -0
- codex/static_root/assets/{datetime-column-BxC1Li9e.3dc314b3295d.js → datetime-column-uSqKZU37.b157b0743db5.js} +1 -1
- codex/static_root/assets/datetime-column-uSqKZU37.b157b0743db5.js.br +0 -0
- codex/static_root/assets/datetime-column-uSqKZU37.b157b0743db5.js.gz +0 -0
- codex/static_root/assets/{datetime-column-BxC1Li9e.js → datetime-column-uSqKZU37.js} +1 -1
- codex/static_root/assets/datetime-column-uSqKZU37.js.br +0 -0
- codex/static_root/assets/datetime-column-uSqKZU37.js.gz +0 -0
- codex/static_root/assets/{filter-Bzk7x841.d0c3b602e62d.js → filter-CBl5KEsB.845a81573c2e.js} +1 -1
- codex/static_root/assets/filter-CBl5KEsB.845a81573c2e.js.br +0 -0
- codex/static_root/assets/filter-CBl5KEsB.845a81573c2e.js.gz +0 -0
- codex/static_root/assets/{filter-Bzk7x841.js → filter-CBl5KEsB.js} +1 -1
- codex/static_root/assets/filter-CBl5KEsB.js.br +0 -0
- codex/static_root/assets/filter-CBl5KEsB.js.gz +0 -0
- codex/static_root/assets/flag-tab-CT8pqM4r.ecb6eee2e7a0.js +1 -0
- codex/static_root/assets/flag-tab-CT8pqM4r.ecb6eee2e7a0.js.br +0 -0
- codex/static_root/assets/flag-tab-CT8pqM4r.ecb6eee2e7a0.js.gz +0 -0
- codex/static_root/assets/flag-tab-CT8pqM4r.js +1 -0
- codex/static_root/assets/flag-tab-CT8pqM4r.js.br +0 -0
- codex/static_root/assets/flag-tab-CT8pqM4r.js.gz +0 -0
- codex/static_root/assets/{group-tab-5esH4lwX.a81fdd258117.js → group-tab-BOW9kigw.7e5007596367.js} +1 -1
- codex/static_root/assets/group-tab-BOW9kigw.7e5007596367.js.br +0 -0
- codex/static_root/assets/group-tab-BOW9kigw.7e5007596367.js.gz +0 -0
- codex/static_root/assets/{group-tab-5esH4lwX.js → group-tab-BOW9kigw.js} +1 -1
- codex/static_root/assets/group-tab-BOW9kigw.js.br +0 -0
- codex/static_root/assets/group-tab-BOW9kigw.js.gz +0 -0
- codex/static_root/assets/{http-error-BTOBfcGY.52ced6f28420.js → http-error-C6yuLApo.45cf99eb7a70.js} +1 -1
- codex/static_root/assets/http-error-C6yuLApo.45cf99eb7a70.js.br +4 -0
- codex/static_root/assets/http-error-C6yuLApo.45cf99eb7a70.js.gz +0 -0
- codex/static_root/assets/{http-error-BTOBfcGY.js → http-error-C6yuLApo.js} +1 -1
- codex/static_root/assets/http-error-C6yuLApo.js.br +4 -0
- codex/static_root/assets/http-error-C6yuLApo.js.gz +0 -0
- codex/static_root/assets/{library-tab-DiPEdOF7.ece2d642a9dc.js → library-tab-D3WOtpE0.9af34e6e7235.js} +1 -1
- codex/static_root/assets/library-tab-D3WOtpE0.9af34e6e7235.js.br +0 -0
- codex/static_root/assets/library-tab-D3WOtpE0.9af34e6e7235.js.gz +0 -0
- codex/static_root/assets/{library-tab-DiPEdOF7.js → library-tab-D3WOtpE0.js} +1 -1
- codex/static_root/assets/library-tab-D3WOtpE0.js.br +0 -0
- codex/static_root/assets/library-tab-D3WOtpE0.js.gz +0 -0
- codex/static_root/assets/main-CmjU2oTp.6a471f6900e5.js +6 -0
- codex/static_root/assets/main-CmjU2oTp.6a471f6900e5.js.br +0 -0
- codex/static_root/assets/main-CmjU2oTp.6a471f6900e5.js.gz +0 -0
- codex/static_root/assets/main-CmjU2oTp.js +6 -0
- codex/static_root/assets/main-CmjU2oTp.js.br +0 -0
- codex/static_root/assets/main-CmjU2oTp.js.gz +0 -0
- codex/static_root/assets/pager-full-pdf-BH54_5f6.6e92d0555cd7.js +1 -0
- codex/static_root/assets/pager-full-pdf-BH54_5f6.6e92d0555cd7.js.br +0 -0
- codex/static_root/assets/pager-full-pdf-BH54_5f6.6e92d0555cd7.js.gz +0 -0
- codex/static_root/assets/pager-full-pdf-BH54_5f6.js +1 -0
- codex/static_root/assets/pager-full-pdf-BH54_5f6.js.br +0 -0
- codex/static_root/assets/pager-full-pdf-BH54_5f6.js.gz +0 -0
- codex/static_root/assets/{pagination-toolbar-C45-onwe.4722195bb60b.css → pagination-toolbar-DGrxp2OD.css} +1 -1
- codex/static_root/assets/pagination-toolbar-DGrxp2OD.css.br +0 -0
- codex/static_root/assets/pagination-toolbar-DGrxp2OD.css.gz +0 -0
- codex/static_root/assets/{pagination-toolbar-C45-onwe.css → pagination-toolbar-DGrxp2OD.f960a624f04f.css} +1 -1
- codex/static_root/assets/pagination-toolbar-DGrxp2OD.f960a624f04f.css.br +0 -0
- codex/static_root/assets/pagination-toolbar-DGrxp2OD.f960a624f04f.css.gz +0 -0
- codex/static_root/assets/{pagination-toolbar-CkIzuyx1.00c354a82e6e.js → pagination-toolbar-JH3Kbffr.451171f7ac33.js} +1 -1
- codex/static_root/assets/pagination-toolbar-JH3Kbffr.451171f7ac33.js.br +0 -0
- codex/static_root/assets/pagination-toolbar-JH3Kbffr.451171f7ac33.js.gz +0 -0
- codex/static_root/assets/{pagination-toolbar-CkIzuyx1.js → pagination-toolbar-JH3Kbffr.js} +1 -1
- codex/static_root/assets/pagination-toolbar-JH3Kbffr.js.br +0 -0
- codex/static_root/assets/pagination-toolbar-JH3Kbffr.js.gz +0 -0
- codex/static_root/assets/{pdf-doc-BjlRPuf4.0f056fa321b6.js → pdf-doc-BcRNpoaW.882f6e967869.js} +1 -1
- codex/static_root/assets/pdf-doc-BcRNpoaW.882f6e967869.js.br +0 -0
- codex/static_root/assets/pdf-doc-BcRNpoaW.882f6e967869.js.gz +0 -0
- codex/static_root/assets/{pdf-doc-BjlRPuf4.js → pdf-doc-BcRNpoaW.js} +1 -1
- codex/static_root/assets/pdf-doc-BcRNpoaW.js.br +0 -0
- codex/static_root/assets/pdf-doc-BcRNpoaW.js.gz +0 -0
- codex/static_root/assets/reader-DIPRg2VB.900370e097aa.js +2 -0
- codex/static_root/assets/reader-DIPRg2VB.900370e097aa.js.br +0 -0
- codex/static_root/assets/reader-DIPRg2VB.900370e097aa.js.gz +0 -0
- codex/static_root/assets/reader-DIPRg2VB.js +2 -0
- codex/static_root/assets/reader-DIPRg2VB.js.br +0 -0
- codex/static_root/assets/reader-DIPRg2VB.js.gz +0 -0
- codex/static_root/assets/reader-DveZZr9x.7b21b9f8f628.css +1 -0
- codex/static_root/assets/reader-DveZZr9x.7b21b9f8f628.css.br +0 -0
- codex/static_root/assets/reader-DveZZr9x.7b21b9f8f628.css.gz +0 -0
- codex/static_root/assets/reader-DveZZr9x.css +1 -0
- codex/static_root/assets/reader-DveZZr9x.css.br +0 -0
- codex/static_root/assets/reader-DveZZr9x.css.gz +0 -0
- codex/static_root/assets/{relation-chips-CUSatZET.js → relation-chips-3tTlR5QH.71f18bc92adc.js} +1 -1
- codex/static_root/assets/relation-chips-3tTlR5QH.71f18bc92adc.js.br +0 -0
- codex/static_root/assets/relation-chips-3tTlR5QH.71f18bc92adc.js.gz +0 -0
- codex/static_root/assets/{relation-chips-CUSatZET.a58031ff8141.js → relation-chips-3tTlR5QH.js} +1 -1
- codex/static_root/assets/relation-chips-3tTlR5QH.js.br +0 -0
- codex/static_root/assets/relation-chips-3tTlR5QH.js.gz +0 -0
- codex/static_root/assets/{settings-drawer-BSHTAsJ9.js → settings-drawer-BdyFhA_r.151f4c3061ed.js} +2 -2
- codex/static_root/assets/settings-drawer-BdyFhA_r.151f4c3061ed.js.br +0 -0
- codex/static_root/assets/settings-drawer-BdyFhA_r.151f4c3061ed.js.gz +0 -0
- codex/static_root/assets/{settings-drawer-BSHTAsJ9.1d07ce084fc9.js → settings-drawer-BdyFhA_r.js} +2 -2
- codex/static_root/assets/settings-drawer-BdyFhA_r.js.br +0 -0
- codex/static_root/assets/settings-drawer-BdyFhA_r.js.gz +0 -0
- codex/static_root/assets/stats-tab-DeKjwvmh.7557c71b4cfa.js +1 -0
- codex/static_root/assets/stats-tab-DeKjwvmh.7557c71b4cfa.js.br +0 -0
- codex/static_root/assets/stats-tab-DeKjwvmh.7557c71b4cfa.js.gz +0 -0
- codex/static_root/assets/stats-tab-DeKjwvmh.js +1 -0
- codex/static_root/assets/stats-tab-DeKjwvmh.js.br +0 -0
- codex/static_root/assets/stats-tab-DeKjwvmh.js.gz +0 -0
- codex/static_root/assets/{stats-tab-BXGisn5N.9b7a71e7fe28.css → stats-tab-DpfGsVX3.1cb66f2649a9.css} +1 -1
- codex/static_root/assets/stats-tab-DpfGsVX3.1cb66f2649a9.css.br +0 -0
- codex/static_root/assets/stats-tab-DpfGsVX3.1cb66f2649a9.css.gz +0 -0
- codex/static_root/assets/{stats-tab-BXGisn5N.css → stats-tab-DpfGsVX3.css} +1 -1
- codex/static_root/assets/stats-tab-DpfGsVX3.css.br +0 -0
- codex/static_root/assets/stats-tab-DpfGsVX3.css.gz +0 -0
- codex/static_root/assets/{task-tab-NORFAa9p.97e07e366ddd.js → task-tab-C8D0qRP_.d4ab174c69a3.js} +1 -1
- codex/static_root/assets/task-tab-C8D0qRP_.d4ab174c69a3.js.br +0 -0
- codex/static_root/assets/task-tab-C8D0qRP_.d4ab174c69a3.js.gz +0 -0
- codex/static_root/assets/{task-tab-NORFAa9p.js → task-tab-C8D0qRP_.js} +1 -1
- codex/static_root/assets/task-tab-C8D0qRP_.js.br +0 -0
- codex/static_root/assets/task-tab-C8D0qRP_.js.gz +0 -0
- codex/static_root/assets/unauthorized-BHgBRF4z.8e5c1b9e52b0.js +1 -0
- codex/static_root/assets/unauthorized-BHgBRF4z.8e5c1b9e52b0.js.br +0 -0
- codex/static_root/assets/unauthorized-BHgBRF4z.8e5c1b9e52b0.js.gz +0 -0
- codex/static_root/assets/unauthorized-BHgBRF4z.js +1 -0
- codex/static_root/assets/unauthorized-BHgBRF4z.js.br +0 -0
- codex/static_root/assets/unauthorized-BHgBRF4z.js.gz +0 -0
- codex/static_root/assets/{unauthorized-muDsv-rC.css → unauthorized-BjhIc97q.728511c92cd0.css} +1 -1
- codex/static_root/assets/unauthorized-BjhIc97q.728511c92cd0.css.br +0 -0
- codex/static_root/assets/unauthorized-BjhIc97q.728511c92cd0.css.gz +0 -0
- codex/static_root/assets/{unauthorized-muDsv-rC.2166ec8b3a32.css → unauthorized-BjhIc97q.css} +1 -1
- codex/static_root/assets/unauthorized-BjhIc97q.css.br +0 -0
- codex/static_root/assets/unauthorized-BjhIc97q.css.gz +0 -0
- codex/static_root/assets/{user-tab-CHbyN1EW.e7206db08680.js → user-tab-BU31Z92y.35613c713faf.js} +1 -1
- codex/static_root/assets/user-tab-BU31Z92y.35613c713faf.js.br +0 -0
- codex/static_root/assets/user-tab-BU31Z92y.35613c713faf.js.gz +0 -0
- codex/static_root/assets/{user-tab-CHbyN1EW.js → user-tab-BU31Z92y.js} +1 -1
- codex/static_root/assets/user-tab-BU31Z92y.js.br +0 -0
- codex/static_root/assets/user-tab-BU31Z92y.js.gz +0 -0
- codex/static_root/js/choices-admin.8aa64c911203.json +1 -0
- codex/static_root/js/choices-admin.8aa64c911203.json.br +0 -0
- codex/static_root/js/choices-admin.8aa64c911203.json.gz +0 -0
- codex/static_root/js/choices-admin.json +1 -1
- codex/static_root/js/choices-admin.json.br +0 -0
- codex/static_root/js/choices-admin.json.gz +0 -0
- codex/static_root/manifest.45cb46e19686.json +635 -0
- codex/static_root/manifest.45cb46e19686.json.br +0 -0
- codex/static_root/manifest.45cb46e19686.json.gz +0 -0
- codex/static_root/manifest.json +248 -248
- codex/static_root/manifest.json.br +0 -0
- codex/static_root/manifest.json.gz +0 -0
- codex/static_root/staticfiles.json +1 -1
- codex/status_controller.py +10 -4
- codex/views/admin/tasks.py +46 -40
- codex/views/auth.py +15 -4
- codex/views/browser/annotations.py +94 -51
- codex/views/browser/base.py +1 -2
- codex/views/browser/browser.py +52 -21
- codex/views/browser/cover.py +8 -4
- codex/views/browser/filters/annotations.py +28 -6
- codex/views/browser/filters/search/__init__.py +1 -0
- codex/views/browser/filters/search/aliases.py +81 -0
- codex/views/browser/filters/search/field/__init__.py +1 -0
- codex/views/browser/filters/search/field/column.py +53 -0
- codex/views/browser/filters/search/field/expression.py +177 -0
- codex/views/browser/filters/search/field/filter.py +70 -0
- codex/views/browser/filters/search/field/optimize.py +71 -0
- codex/views/browser/filters/search/field/parse.py +187 -0
- codex/views/browser/filters/search/fts.py +38 -0
- codex/views/browser/filters/search/parse.py +220 -0
- codex/views/browser/order_by.py +8 -2
- codex/views/browser/page_in_bounds.py +88 -0
- codex/views/browser/paginate.py +28 -113
- codex/views/const.py +0 -1
- codex/views/opds/v1/feed.py +2 -1
- codex/views/opds/v2/feed.py +2 -1
- codex/views/opds/v2/links.py +1 -1
- codex/views/reader/books.py +1 -1
- codex/views/reader/page.py +3 -3
- {codex-1.6.19.dist-info → codex-1.7.0a0.dist-info}/METADATA +18 -10
- {codex-1.6.19.dist-info → codex-1.7.0a0.dist-info}/RECORD +321 -372
- codex/_vendor/__init__.py +0 -0
- codex/_vendor/django_haystack-3.2.1.dist-info/AUTHORS +0 -127
- codex/_vendor/django_haystack-3.2.1.dist-info/INSTALLER +0 -1
- codex/_vendor/django_haystack-3.2.1.dist-info/LICENSE +0 -31
- codex/_vendor/django_haystack-3.2.1.dist-info/METADATA +0 -98
- codex/_vendor/django_haystack-3.2.1.dist-info/RECORD +0 -97
- codex/_vendor/django_haystack-3.2.1.dist-info/REQUESTED +0 -0
- codex/_vendor/django_haystack-3.2.1.dist-info/WHEEL +0 -5
- codex/_vendor/django_haystack-3.2.1.dist-info/top_level.txt +0 -1
- codex/_vendor/haystack/__init__.py +0 -80
- codex/_vendor/haystack/admin.py +0 -163
- codex/_vendor/haystack/apps.py +0 -31
- codex/_vendor/haystack/backends/__init__.py +0 -1109
- codex/_vendor/haystack/backends/elasticsearch2_backend.py +0 -390
- codex/_vendor/haystack/backends/elasticsearch5_backend.py +0 -483
- codex/_vendor/haystack/backends/elasticsearch7_backend.py +0 -586
- codex/_vendor/haystack/backends/elasticsearch_backend.py +0 -1141
- codex/_vendor/haystack/backends/simple_backend.py +0 -132
- codex/_vendor/haystack/backends/solr_backend.py +0 -991
- codex/_vendor/haystack/backends/whoosh_backend.py +0 -1104
- codex/_vendor/haystack/constants.py +0 -57
- codex/_vendor/haystack/exceptions.py +0 -57
- codex/_vendor/haystack/fields.py +0 -562
- codex/_vendor/haystack/forms.py +0 -135
- codex/_vendor/haystack/generic_views.py +0 -141
- codex/_vendor/haystack/indexes.py +0 -548
- codex/_vendor/haystack/inputs.py +0 -169
- codex/_vendor/haystack/management/__init__.py +0 -0
- codex/_vendor/haystack/management/commands/__init__.py +0 -0
- codex/_vendor/haystack/management/commands/build_solr_schema.py +0 -187
- codex/_vendor/haystack/management/commands/clear_index.py +0 -67
- codex/_vendor/haystack/management/commands/haystack_info.py +0 -22
- codex/_vendor/haystack/management/commands/rebuild_index.py +0 -65
- codex/_vendor/haystack/management/commands/update_index.py +0 -438
- codex/_vendor/haystack/manager.py +0 -105
- codex/_vendor/haystack/models.py +0 -257
- codex/_vendor/haystack/panels.py +0 -89
- codex/_vendor/haystack/query.py +0 -771
- codex/_vendor/haystack/routers.py +0 -14
- codex/_vendor/haystack/signals.py +0 -88
- codex/_vendor/haystack/templates/panels/haystack.html +0 -33
- codex/_vendor/haystack/templates/search_configuration/schema.xml +0 -1056
- codex/_vendor/haystack/templates/search_configuration/solrconfig.xml +0 -1446
- codex/_vendor/haystack/templatetags/__init__.py +0 -0
- codex/_vendor/haystack/templatetags/highlight.py +0 -131
- codex/_vendor/haystack/templatetags/more_like_this.py +0 -119
- codex/_vendor/haystack/urls.py +0 -5
- codex/_vendor/haystack/utils/__init__.py +0 -84
- codex/_vendor/haystack/utils/app_loading.py +0 -34
- codex/_vendor/haystack/utils/geo.py +0 -71
- codex/_vendor/haystack/utils/highlighting.py +0 -165
- codex/_vendor/haystack/utils/loading.py +0 -374
- codex/_vendor/haystack/utils/log.py +0 -21
- codex/_vendor/haystack/version.py +0 -16
- codex/_vendor/haystack/views.py +0 -253
- codex/integrity.py +0 -492
- codex/librarian/search/merge.py +0 -86
- codex/librarian/search/version.py +0 -63
- codex/search/__init__.py +0 -1
- codex/search/backend.py +0 -524
- codex/search/backend_search.py +0 -229
- codex/search/engine.py +0 -33
- codex/search/indexes.py +0 -79
- codex/search/query.py +0 -67
- codex/search/writing.py +0 -180
- codex/static_root/assets/VCheckbox-Cko1sQK-.170726b3b8a1.js.br +0 -0
- codex/static_root/assets/VCheckbox-Cko1sQK-.170726b3b8a1.js.gz +0 -0
- codex/static_root/assets/VCheckbox-Cko1sQK-.js.br +0 -0
- codex/static_root/assets/VCheckbox-Cko1sQK-.js.gz +0 -0
- codex/static_root/assets/VCheckboxBtn-CO3-aqQL.c9b8062fd66f.js.br +0 -0
- codex/static_root/assets/VCheckboxBtn-CO3-aqQL.c9b8062fd66f.js.gz +0 -0
- codex/static_root/assets/VCheckboxBtn-CO3-aqQL.js.br +0 -0
- codex/static_root/assets/VCheckboxBtn-CO3-aqQL.js.gz +0 -0
- codex/static_root/assets/VCombobox-Bjp6lffE.caf59fba486e.js.br +0 -0
- codex/static_root/assets/VCombobox-Bjp6lffE.caf59fba486e.js.gz +0 -0
- codex/static_root/assets/VCombobox-Bjp6lffE.js.br +0 -0
- codex/static_root/assets/VCombobox-Bjp6lffE.js.gz +0 -0
- codex/static_root/assets/VDialog-BkpVGB70.6eea3ca762a5.js.br +0 -0
- codex/static_root/assets/VDialog-BkpVGB70.6eea3ca762a5.js.gz +0 -0
- codex/static_root/assets/VDialog-BkpVGB70.js.br +0 -0
- codex/static_root/assets/VDialog-BkpVGB70.js.gz +0 -0
- codex/static_root/assets/VExpansionPanels-Ci2-j8XX.9c55db4ef49e.js.br +0 -0
- codex/static_root/assets/VExpansionPanels-Ci2-j8XX.9c55db4ef49e.js.gz +0 -0
- codex/static_root/assets/VExpansionPanels-Ci2-j8XX.js.br +0 -0
- codex/static_root/assets/VExpansionPanels-Ci2-j8XX.js.gz +0 -0
- codex/static_root/assets/VRadioGroup-D3Py5BfQ.a4f25edb86d3.js.br +0 -0
- codex/static_root/assets/VRadioGroup-D3Py5BfQ.a4f25edb86d3.js.gz +0 -0
- codex/static_root/assets/VRadioGroup-D3Py5BfQ.js.br +0 -0
- codex/static_root/assets/VRadioGroup-D3Py5BfQ.js.gz +0 -0
- codex/static_root/assets/VSelect-DtvZsYZz.e98745d858eb.js.br +0 -0
- codex/static_root/assets/VSelect-DtvZsYZz.e98745d858eb.js.gz +0 -0
- codex/static_root/assets/VSelect-DtvZsYZz.js.br +0 -0
- codex/static_root/assets/VSelect-DtvZsYZz.js.gz +0 -0
- codex/static_root/assets/VSelectionControl-D6kmykQW.1e1fda62ceba.js.br +0 -0
- codex/static_root/assets/VSelectionControl-D6kmykQW.1e1fda62ceba.js.gz +0 -0
- codex/static_root/assets/VSelectionControl-D6kmykQW.js.br +0 -0
- codex/static_root/assets/VSelectionControl-D6kmykQW.js.gz +0 -0
- codex/static_root/assets/VSlideGroup-UGZWxpF5.360bdefcd215.js.br +0 -0
- codex/static_root/assets/VSlideGroup-UGZWxpF5.360bdefcd215.js.gz +0 -0
- codex/static_root/assets/VSlideGroup-UGZWxpF5.js.br +0 -0
- codex/static_root/assets/VSlideGroup-UGZWxpF5.js.gz +0 -0
- codex/static_root/assets/VTable-DXH_yQ0o.1149f56ad566.js.br +0 -0
- codex/static_root/assets/VTable-DXH_yQ0o.1149f56ad566.js.gz +0 -0
- codex/static_root/assets/VTable-DXH_yQ0o.js.br +0 -0
- codex/static_root/assets/VTable-DXH_yQ0o.js.gz +0 -0
- codex/static_root/assets/VTextField-Blqy56_L.26d8761e0155.js.br +0 -0
- codex/static_root/assets/VTextField-Blqy56_L.26d8761e0155.js.gz +0 -0
- codex/static_root/assets/VTextField-Blqy56_L.js.br +0 -0
- codex/static_root/assets/VTextField-Blqy56_L.js.gz +0 -0
- codex/static_root/assets/VWindowItem-C8_ovV7e.afbc08414765.js.br +0 -0
- codex/static_root/assets/VWindowItem-C8_ovV7e.afbc08414765.js.gz +0 -0
- codex/static_root/assets/VWindowItem-C8_ovV7e.js.br +0 -0
- codex/static_root/assets/VWindowItem-C8_ovV7e.js.gz +0 -0
- codex/static_root/assets/admin-DTRn8bBs.d031ea42d8d0.js.br +0 -0
- codex/static_root/assets/admin-DTRn8bBs.d031ea42d8d0.js.gz +0 -0
- codex/static_root/assets/admin-DTRn8bBs.js.br +0 -0
- codex/static_root/assets/admin-DTRn8bBs.js.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-BaXFOcru.b1a44b05f482.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-BaXFOcru.b1a44b05f482.js.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-BaXFOcru.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-BaXFOcru.js.gz +0 -0
- codex/static_root/assets/browser-BBpUyGmz.c1cb9e2d463b.js +0 -1
- codex/static_root/assets/browser-BBpUyGmz.c1cb9e2d463b.js.br +0 -0
- codex/static_root/assets/browser-BBpUyGmz.c1cb9e2d463b.js.gz +0 -0
- codex/static_root/assets/browser-BBpUyGmz.js +0 -1
- codex/static_root/assets/browser-BBpUyGmz.js.br +0 -0
- codex/static_root/assets/browser-BBpUyGmz.js.gz +0 -0
- codex/static_root/assets/browser-Binc3n9H.313722d27ca3.css +0 -1
- codex/static_root/assets/browser-Binc3n9H.313722d27ca3.css.br +0 -0
- codex/static_root/assets/browser-Binc3n9H.313722d27ca3.css.gz +0 -0
- codex/static_root/assets/browser-Binc3n9H.css +0 -1
- codex/static_root/assets/browser-Binc3n9H.css.br +0 -0
- codex/static_root/assets/browser-Binc3n9H.css.gz +0 -0
- codex/static_root/assets/change-password-dialog-Cebek1-c.aff2975e31f8.css +0 -1
- codex/static_root/assets/change-password-dialog-Cebek1-c.aff2975e31f8.css.br +0 -0
- codex/static_root/assets/change-password-dialog-Cebek1-c.aff2975e31f8.css.gz +0 -0
- codex/static_root/assets/change-password-dialog-Cebek1-c.css +0 -1
- codex/static_root/assets/change-password-dialog-Cebek1-c.css.br +0 -0
- codex/static_root/assets/change-password-dialog-Cebek1-c.css.gz +0 -0
- codex/static_root/assets/change-password-dialog-DlHoLhDF.76ab4a29bb9b.js +0 -1
- codex/static_root/assets/change-password-dialog-DlHoLhDF.76ab4a29bb9b.js.br +0 -0
- codex/static_root/assets/change-password-dialog-DlHoLhDF.76ab4a29bb9b.js.gz +0 -0
- codex/static_root/assets/change-password-dialog-DlHoLhDF.js +0 -1
- codex/static_root/assets/change-password-dialog-DlHoLhDF.js.br +0 -0
- codex/static_root/assets/change-password-dialog-DlHoLhDF.js.gz +0 -0
- codex/static_root/assets/confirm-dialog-D3_s2DQy.9dde66036c72.js.br +0 -0
- codex/static_root/assets/confirm-dialog-D3_s2DQy.9dde66036c72.js.gz +0 -0
- codex/static_root/assets/confirm-dialog-D3_s2DQy.js.br +0 -0
- codex/static_root/assets/confirm-dialog-D3_s2DQy.js.gz +0 -0
- codex/static_root/assets/datetime-column-BxC1Li9e.3dc314b3295d.js.br +0 -0
- codex/static_root/assets/datetime-column-BxC1Li9e.3dc314b3295d.js.gz +0 -0
- codex/static_root/assets/datetime-column-BxC1Li9e.js.br +0 -0
- codex/static_root/assets/datetime-column-BxC1Li9e.js.gz +0 -0
- codex/static_root/assets/filter-Bzk7x841.d0c3b602e62d.js.br +0 -0
- codex/static_root/assets/filter-Bzk7x841.d0c3b602e62d.js.gz +0 -0
- codex/static_root/assets/filter-Bzk7x841.js.br +0 -0
- codex/static_root/assets/filter-Bzk7x841.js.gz +0 -0
- codex/static_root/assets/flag-tab-C2bI5zq3.8f8c297600a6.js +0 -1
- codex/static_root/assets/flag-tab-C2bI5zq3.8f8c297600a6.js.br +0 -0
- codex/static_root/assets/flag-tab-C2bI5zq3.8f8c297600a6.js.gz +0 -0
- codex/static_root/assets/flag-tab-C2bI5zq3.js +0 -1
- codex/static_root/assets/flag-tab-C2bI5zq3.js.br +0 -0
- codex/static_root/assets/flag-tab-C2bI5zq3.js.gz +0 -0
- codex/static_root/assets/group-tab-5esH4lwX.a81fdd258117.js.br +0 -0
- codex/static_root/assets/group-tab-5esH4lwX.a81fdd258117.js.gz +0 -0
- codex/static_root/assets/group-tab-5esH4lwX.js.br +0 -0
- codex/static_root/assets/group-tab-5esH4lwX.js.gz +0 -0
- codex/static_root/assets/http-error-BTOBfcGY.52ced6f28420.js.br +0 -0
- codex/static_root/assets/http-error-BTOBfcGY.52ced6f28420.js.gz +0 -0
- codex/static_root/assets/http-error-BTOBfcGY.js.br +0 -0
- codex/static_root/assets/http-error-BTOBfcGY.js.gz +0 -0
- codex/static_root/assets/library-tab-DiPEdOF7.ece2d642a9dc.js.br +0 -0
- codex/static_root/assets/library-tab-DiPEdOF7.ece2d642a9dc.js.gz +0 -0
- codex/static_root/assets/library-tab-DiPEdOF7.js.br +0 -0
- codex/static_root/assets/library-tab-DiPEdOF7.js.gz +0 -0
- codex/static_root/assets/main-BPIPjzsc.66def8991e38.js +0 -6
- codex/static_root/assets/main-BPIPjzsc.66def8991e38.js.br +0 -0
- codex/static_root/assets/main-BPIPjzsc.66def8991e38.js.gz +0 -0
- codex/static_root/assets/main-BPIPjzsc.js +0 -6
- codex/static_root/assets/main-BPIPjzsc.js.br +0 -0
- codex/static_root/assets/main-BPIPjzsc.js.gz +0 -0
- codex/static_root/assets/pager-full-pdf-CiyC6Al9.278b9978f36f.js +0 -1
- codex/static_root/assets/pager-full-pdf-CiyC6Al9.278b9978f36f.js.br +0 -0
- codex/static_root/assets/pager-full-pdf-CiyC6Al9.278b9978f36f.js.gz +0 -0
- codex/static_root/assets/pager-full-pdf-CiyC6Al9.js +0 -1
- codex/static_root/assets/pager-full-pdf-CiyC6Al9.js.br +0 -0
- codex/static_root/assets/pager-full-pdf-CiyC6Al9.js.gz +0 -0
- codex/static_root/assets/pagination-toolbar-C45-onwe.4722195bb60b.css.br +0 -0
- codex/static_root/assets/pagination-toolbar-C45-onwe.4722195bb60b.css.gz +0 -0
- codex/static_root/assets/pagination-toolbar-C45-onwe.css.br +0 -0
- codex/static_root/assets/pagination-toolbar-C45-onwe.css.gz +0 -0
- codex/static_root/assets/pagination-toolbar-CkIzuyx1.00c354a82e6e.js.br +0 -0
- codex/static_root/assets/pagination-toolbar-CkIzuyx1.00c354a82e6e.js.gz +0 -0
- codex/static_root/assets/pagination-toolbar-CkIzuyx1.js.br +0 -0
- codex/static_root/assets/pagination-toolbar-CkIzuyx1.js.gz +0 -0
- codex/static_root/assets/pdf-doc-BjlRPuf4.0f056fa321b6.js.br +0 -0
- codex/static_root/assets/pdf-doc-BjlRPuf4.0f056fa321b6.js.gz +0 -0
- codex/static_root/assets/pdf-doc-BjlRPuf4.js.br +0 -0
- codex/static_root/assets/pdf-doc-BjlRPuf4.js.gz +0 -0
- codex/static_root/assets/reader-_stAWL-4.5089e9144795.css +0 -1
- codex/static_root/assets/reader-_stAWL-4.5089e9144795.css.br +0 -0
- codex/static_root/assets/reader-_stAWL-4.5089e9144795.css.gz +0 -0
- codex/static_root/assets/reader-_stAWL-4.css +0 -1
- codex/static_root/assets/reader-_stAWL-4.css.br +0 -0
- codex/static_root/assets/reader-_stAWL-4.css.gz +0 -0
- codex/static_root/assets/reader-ucxnLi2K.d0fbce0f03c1.js +0 -2
- codex/static_root/assets/reader-ucxnLi2K.d0fbce0f03c1.js.br +0 -0
- codex/static_root/assets/reader-ucxnLi2K.d0fbce0f03c1.js.gz +0 -0
- codex/static_root/assets/reader-ucxnLi2K.js +0 -2
- codex/static_root/assets/reader-ucxnLi2K.js.br +0 -0
- codex/static_root/assets/reader-ucxnLi2K.js.gz +0 -0
- codex/static_root/assets/relation-chips-CUSatZET.a58031ff8141.js.br +0 -0
- codex/static_root/assets/relation-chips-CUSatZET.a58031ff8141.js.gz +0 -0
- codex/static_root/assets/relation-chips-CUSatZET.js.br +0 -0
- codex/static_root/assets/relation-chips-CUSatZET.js.gz +0 -0
- codex/static_root/assets/settings-drawer-BSHTAsJ9.1d07ce084fc9.js.br +0 -0
- codex/static_root/assets/settings-drawer-BSHTAsJ9.1d07ce084fc9.js.gz +0 -0
- codex/static_root/assets/settings-drawer-BSHTAsJ9.js.br +0 -0
- codex/static_root/assets/settings-drawer-BSHTAsJ9.js.gz +0 -0
- codex/static_root/assets/stats-tab-BXGisn5N.9b7a71e7fe28.css.br +0 -0
- codex/static_root/assets/stats-tab-BXGisn5N.9b7a71e7fe28.css.gz +0 -0
- codex/static_root/assets/stats-tab-BXGisn5N.css.br +0 -0
- codex/static_root/assets/stats-tab-BXGisn5N.css.gz +0 -0
- codex/static_root/assets/stats-tab-DKXVB2Cc.0a3de58bf5ee.js +0 -1
- codex/static_root/assets/stats-tab-DKXVB2Cc.0a3de58bf5ee.js.br +0 -0
- codex/static_root/assets/stats-tab-DKXVB2Cc.0a3de58bf5ee.js.gz +0 -0
- codex/static_root/assets/stats-tab-DKXVB2Cc.js +0 -1
- codex/static_root/assets/stats-tab-DKXVB2Cc.js.br +0 -0
- codex/static_root/assets/stats-tab-DKXVB2Cc.js.gz +0 -0
- codex/static_root/assets/task-tab-NORFAa9p.97e07e366ddd.js.br +0 -0
- codex/static_root/assets/task-tab-NORFAa9p.97e07e366ddd.js.gz +0 -0
- codex/static_root/assets/task-tab-NORFAa9p.js.br +0 -0
- codex/static_root/assets/task-tab-NORFAa9p.js.gz +0 -0
- codex/static_root/assets/unauthorized-RHaQZRfr.a234e9403f60.js +0 -1
- codex/static_root/assets/unauthorized-RHaQZRfr.a234e9403f60.js.br +0 -0
- codex/static_root/assets/unauthorized-RHaQZRfr.a234e9403f60.js.gz +0 -0
- codex/static_root/assets/unauthorized-RHaQZRfr.js +0 -1
- codex/static_root/assets/unauthorized-RHaQZRfr.js.br +0 -0
- codex/static_root/assets/unauthorized-RHaQZRfr.js.gz +0 -0
- codex/static_root/assets/unauthorized-muDsv-rC.2166ec8b3a32.css.br +0 -0
- codex/static_root/assets/unauthorized-muDsv-rC.2166ec8b3a32.css.gz +0 -0
- codex/static_root/assets/unauthorized-muDsv-rC.css.br +0 -0
- codex/static_root/assets/unauthorized-muDsv-rC.css.gz +0 -0
- codex/static_root/assets/user-tab-CHbyN1EW.e7206db08680.js.br +0 -0
- codex/static_root/assets/user-tab-CHbyN1EW.e7206db08680.js.gz +0 -0
- codex/static_root/assets/user-tab-CHbyN1EW.js.br +0 -0
- codex/static_root/assets/user-tab-CHbyN1EW.js.gz +0 -0
- codex/static_root/js/choices-admin.1a20a5648f20.json +0 -1
- codex/static_root/js/choices-admin.1a20a5648f20.json.br +0 -0
- codex/static_root/js/choices-admin.1a20a5648f20.json.gz +0 -0
- codex/static_root/manifest.0fe31a6cb99f.json +0 -635
- codex/static_root/manifest.0fe31a6cb99f.json.br +0 -0
- codex/static_root/manifest.0fe31a6cb99f.json.gz +0 -0
- codex/templates/search/indexes/codex/comic_text.txt +0 -47
- codex/views/browser/filters/search.py +0 -196
- {codex-1.6.19.dist-info → codex-1.7.0a0.dist-info}/LICENSE +0 -0
- {codex-1.6.19.dist-info → codex-1.7.0a0.dist-info}/WHEEL +0 -0
- {codex-1.6.19.dist-info → codex-1.7.0a0.dist-info}/entry_points.txt +0 -0
|
@@ -1,1104 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
|
-
import re
|
|
4
|
-
import shutil
|
|
5
|
-
import threading
|
|
6
|
-
import warnings
|
|
7
|
-
|
|
8
|
-
from django.conf import settings
|
|
9
|
-
from django.core.exceptions import ImproperlyConfigured
|
|
10
|
-
from datetime import date, datetime
|
|
11
|
-
from django.utils.encoding import force_str
|
|
12
|
-
|
|
13
|
-
from ...haystack.backends import (
|
|
14
|
-
BaseEngine,
|
|
15
|
-
BaseSearchBackend,
|
|
16
|
-
BaseSearchQuery,
|
|
17
|
-
EmptyResults,
|
|
18
|
-
log_query,
|
|
19
|
-
)
|
|
20
|
-
from ...haystack.constants import (
|
|
21
|
-
DJANGO_CT,
|
|
22
|
-
DJANGO_ID,
|
|
23
|
-
FUZZY_WHOOSH_MAX_EDITS,
|
|
24
|
-
FUZZY_WHOOSH_MIN_PREFIX,
|
|
25
|
-
ID,
|
|
26
|
-
)
|
|
27
|
-
from ...haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument
|
|
28
|
-
from ...haystack.inputs import Clean, Exact, PythonData, Raw
|
|
29
|
-
from ...haystack.models import SearchResult
|
|
30
|
-
from ...haystack.utils import get_identifier, get_model_ct
|
|
31
|
-
from ...haystack.utils import log as logging
|
|
32
|
-
from ...haystack.utils.app_loading import haystack_get_model
|
|
33
|
-
|
|
34
|
-
try:
|
|
35
|
-
import whoosh
|
|
36
|
-
except ImportError:
|
|
37
|
-
raise MissingDependency(
|
|
38
|
-
"The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation."
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
# Handle minimum requirement.
|
|
42
|
-
if not hasattr(whoosh, "__version__") or whoosh.__version__ < (2, 5, 0):
|
|
43
|
-
raise MissingDependency("The 'whoosh' backend requires version 2.5.0 or greater.")
|
|
44
|
-
|
|
45
|
-
# Bubble up the correct error.
|
|
46
|
-
from whoosh import index
|
|
47
|
-
from whoosh.analysis import StemmingAnalyzer
|
|
48
|
-
from whoosh.fields import BOOLEAN, DATETIME
|
|
49
|
-
from whoosh.fields import ID as WHOOSH_ID
|
|
50
|
-
from whoosh.fields import IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, TEXT, Schema
|
|
51
|
-
from whoosh.filedb.filestore import FileStorage, RamStorage
|
|
52
|
-
from whoosh.highlight import ContextFragmenter, HtmlFormatter
|
|
53
|
-
from whoosh.highlight import highlight as whoosh_highlight
|
|
54
|
-
from whoosh.qparser import FuzzyTermPlugin, QueryParser
|
|
55
|
-
from whoosh.searching import ResultsPage
|
|
56
|
-
from whoosh.sorting import Count, DateRangeFacet, FieldFacet
|
|
57
|
-
from whoosh.support.relativedelta import relativedelta as RelativeDelta
|
|
58
|
-
from whoosh.writing import AsyncWriter
|
|
59
|
-
|
|
60
|
-
DATETIME_REGEX = re.compile(
|
|
61
|
-
r"^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})T(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(\.\d{3,6}Z?)?$"
|
|
62
|
-
)
|
|
63
|
-
LOCALS = threading.local()
|
|
64
|
-
LOCALS.RAM_STORE = None
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class WhooshHtmlFormatter(HtmlFormatter):
|
|
68
|
-
"""
|
|
69
|
-
This is a HtmlFormatter simpler than the whoosh.HtmlFormatter.
|
|
70
|
-
We use it to have consistent results across backends. Specifically,
|
|
71
|
-
Solr, Xapian and Elasticsearch are using this formatting.
|
|
72
|
-
"""
|
|
73
|
-
|
|
74
|
-
template = "<%(tag)s>%(t)s</%(tag)s>"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class WhooshSearchBackend(BaseSearchBackend):
|
|
78
|
-
# Word reserved by Whoosh for special use.
|
|
79
|
-
RESERVED_WORDS = ("AND", "NOT", "OR", "TO")
|
|
80
|
-
|
|
81
|
-
# Characters reserved by Whoosh for special use.
|
|
82
|
-
# The '\\' must come first, so as not to overwrite the other slash replacements.
|
|
83
|
-
RESERVED_CHARACTERS = (
|
|
84
|
-
"\\",
|
|
85
|
-
"+",
|
|
86
|
-
"-",
|
|
87
|
-
"&&",
|
|
88
|
-
"||",
|
|
89
|
-
"!",
|
|
90
|
-
"(",
|
|
91
|
-
")",
|
|
92
|
-
"{",
|
|
93
|
-
"}",
|
|
94
|
-
"[",
|
|
95
|
-
"]",
|
|
96
|
-
"^",
|
|
97
|
-
'"',
|
|
98
|
-
"~",
|
|
99
|
-
"*",
|
|
100
|
-
"?",
|
|
101
|
-
":",
|
|
102
|
-
".",
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
def __init__(self, connection_alias, **connection_options):
|
|
106
|
-
super().__init__(connection_alias, **connection_options)
|
|
107
|
-
self.setup_complete = False
|
|
108
|
-
self.use_file_storage = True
|
|
109
|
-
self.post_limit = getattr(connection_options, "POST_LIMIT", 128 * 1024 * 1024)
|
|
110
|
-
self.path = connection_options.get("PATH")
|
|
111
|
-
|
|
112
|
-
if connection_options.get("STORAGE", "file") != "file":
|
|
113
|
-
self.use_file_storage = False
|
|
114
|
-
|
|
115
|
-
if self.use_file_storage and not self.path:
|
|
116
|
-
raise ImproperlyConfigured(
|
|
117
|
-
"You must specify a 'PATH' in your settings for connection '%s'."
|
|
118
|
-
% connection_alias
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
self.log = logging.getLogger("haystack")
|
|
122
|
-
|
|
123
|
-
def setup(self):
|
|
124
|
-
"""
|
|
125
|
-
Defers loading until needed.
|
|
126
|
-
"""
|
|
127
|
-
from ...haystack import connections
|
|
128
|
-
|
|
129
|
-
new_index = False
|
|
130
|
-
|
|
131
|
-
# Make sure the index is there.
|
|
132
|
-
if self.use_file_storage and not os.path.exists(self.path):
|
|
133
|
-
os.makedirs(self.path)
|
|
134
|
-
new_index = True
|
|
135
|
-
|
|
136
|
-
if self.use_file_storage and not os.access(self.path, os.W_OK):
|
|
137
|
-
raise IOError(
|
|
138
|
-
"The path to your Whoosh index '%s' is not writable for the current user/group."
|
|
139
|
-
% self.path
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
if self.use_file_storage:
|
|
143
|
-
self.storage = FileStorage(self.path)
|
|
144
|
-
else:
|
|
145
|
-
global LOCALS
|
|
146
|
-
|
|
147
|
-
if getattr(LOCALS, "RAM_STORE", None) is None:
|
|
148
|
-
LOCALS.RAM_STORE = RamStorage()
|
|
149
|
-
|
|
150
|
-
self.storage = LOCALS.RAM_STORE
|
|
151
|
-
|
|
152
|
-
self.content_field_name, self.schema = self.build_schema(
|
|
153
|
-
connections[self.connection_alias].get_unified_index().all_searchfields()
|
|
154
|
-
)
|
|
155
|
-
self.parser = QueryParser(self.content_field_name, schema=self.schema)
|
|
156
|
-
self.parser.add_plugins([FuzzyTermPlugin])
|
|
157
|
-
|
|
158
|
-
if new_index is True:
|
|
159
|
-
self.index = self.storage.create_index(self.schema)
|
|
160
|
-
else:
|
|
161
|
-
try:
|
|
162
|
-
self.index = self.storage.open_index(schema=self.schema)
|
|
163
|
-
except index.EmptyIndexError:
|
|
164
|
-
self.index = self.storage.create_index(self.schema)
|
|
165
|
-
|
|
166
|
-
self.setup_complete = True
|
|
167
|
-
|
|
168
|
-
def build_schema(self, fields):
|
|
169
|
-
schema_fields = {
|
|
170
|
-
ID: WHOOSH_ID(stored=True, unique=True),
|
|
171
|
-
DJANGO_CT: WHOOSH_ID(stored=True),
|
|
172
|
-
DJANGO_ID: WHOOSH_ID(stored=True),
|
|
173
|
-
}
|
|
174
|
-
# Grab the number of keys that are hard-coded into Haystack.
|
|
175
|
-
# We'll use this to (possibly) fail slightly more gracefully later.
|
|
176
|
-
initial_key_count = len(schema_fields)
|
|
177
|
-
content_field_name = ""
|
|
178
|
-
|
|
179
|
-
for _, field_class in fields.items():
|
|
180
|
-
if field_class.is_multivalued:
|
|
181
|
-
if field_class.indexed is False:
|
|
182
|
-
schema_fields[field_class.index_fieldname] = IDLIST(
|
|
183
|
-
stored=True, field_boost=field_class.boost
|
|
184
|
-
)
|
|
185
|
-
else:
|
|
186
|
-
schema_fields[field_class.index_fieldname] = KEYWORD(
|
|
187
|
-
stored=True,
|
|
188
|
-
commas=True,
|
|
189
|
-
scorable=True,
|
|
190
|
-
field_boost=field_class.boost,
|
|
191
|
-
)
|
|
192
|
-
elif field_class.field_type in ["date", "datetime"]:
|
|
193
|
-
schema_fields[field_class.index_fieldname] = DATETIME(
|
|
194
|
-
stored=field_class.stored, sortable=True
|
|
195
|
-
)
|
|
196
|
-
elif field_class.field_type == "integer":
|
|
197
|
-
schema_fields[field_class.index_fieldname] = NUMERIC(
|
|
198
|
-
stored=field_class.stored,
|
|
199
|
-
numtype=int,
|
|
200
|
-
field_boost=field_class.boost,
|
|
201
|
-
)
|
|
202
|
-
elif field_class.field_type == "float":
|
|
203
|
-
schema_fields[field_class.index_fieldname] = NUMERIC(
|
|
204
|
-
stored=field_class.stored,
|
|
205
|
-
numtype=float,
|
|
206
|
-
field_boost=field_class.boost,
|
|
207
|
-
)
|
|
208
|
-
elif field_class.field_type == "boolean":
|
|
209
|
-
# Field boost isn't supported on BOOLEAN as of 1.8.2.
|
|
210
|
-
schema_fields[field_class.index_fieldname] = BOOLEAN(
|
|
211
|
-
stored=field_class.stored
|
|
212
|
-
)
|
|
213
|
-
elif field_class.field_type == "ngram":
|
|
214
|
-
schema_fields[field_class.index_fieldname] = NGRAM(
|
|
215
|
-
minsize=3,
|
|
216
|
-
maxsize=15,
|
|
217
|
-
stored=field_class.stored,
|
|
218
|
-
field_boost=field_class.boost,
|
|
219
|
-
)
|
|
220
|
-
elif field_class.field_type == "edge_ngram":
|
|
221
|
-
schema_fields[field_class.index_fieldname] = NGRAMWORDS(
|
|
222
|
-
minsize=2,
|
|
223
|
-
maxsize=15,
|
|
224
|
-
at="start",
|
|
225
|
-
stored=field_class.stored,
|
|
226
|
-
field_boost=field_class.boost,
|
|
227
|
-
)
|
|
228
|
-
else:
|
|
229
|
-
schema_fields[field_class.index_fieldname] = TEXT(
|
|
230
|
-
stored=True,
|
|
231
|
-
analyzer=field_class.analyzer or StemmingAnalyzer(),
|
|
232
|
-
field_boost=field_class.boost,
|
|
233
|
-
sortable=True,
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
if field_class.document is True:
|
|
237
|
-
content_field_name = field_class.index_fieldname
|
|
238
|
-
schema_fields[field_class.index_fieldname].spelling = True
|
|
239
|
-
|
|
240
|
-
# Fail more gracefully than relying on the backend to die if no fields
|
|
241
|
-
# are found.
|
|
242
|
-
if len(schema_fields) <= initial_key_count:
|
|
243
|
-
raise SearchBackendError(
|
|
244
|
-
"No fields were found in any search_indexes. Please correct this before attempting to search."
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
return (content_field_name, Schema(**schema_fields))
|
|
248
|
-
|
|
249
|
-
def update(self, index, iterable, commit=True):
|
|
250
|
-
if not self.setup_complete:
|
|
251
|
-
self.setup()
|
|
252
|
-
|
|
253
|
-
self.index = self.index.refresh()
|
|
254
|
-
writer = AsyncWriter(self.index)
|
|
255
|
-
|
|
256
|
-
for obj in iterable:
|
|
257
|
-
try:
|
|
258
|
-
doc = index.full_prepare(obj)
|
|
259
|
-
except SkipDocument:
|
|
260
|
-
self.log.debug("Indexing for object `%s` skipped", obj)
|
|
261
|
-
else:
|
|
262
|
-
# Really make sure it's unicode, because Whoosh won't have it any
|
|
263
|
-
# other way.
|
|
264
|
-
for key in doc:
|
|
265
|
-
doc[key] = self._from_python(doc[key])
|
|
266
|
-
|
|
267
|
-
# Document boosts aren't supported in Whoosh 2.5.0+.
|
|
268
|
-
if "boost" in doc:
|
|
269
|
-
del doc["boost"]
|
|
270
|
-
|
|
271
|
-
try:
|
|
272
|
-
writer.update_document(**doc)
|
|
273
|
-
except Exception as e:
|
|
274
|
-
if not self.silently_fail:
|
|
275
|
-
raise
|
|
276
|
-
|
|
277
|
-
# We'll log the object identifier but won't include the actual object
|
|
278
|
-
# to avoid the possibility of that generating encoding errors while
|
|
279
|
-
# processing the log message:
|
|
280
|
-
self.log.error(
|
|
281
|
-
"%s while preparing object for update" % e.__class__.__name__,
|
|
282
|
-
exc_info=True,
|
|
283
|
-
extra={"data": {"index": index, "object": get_identifier(obj)}},
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
if len(iterable) > 0:
|
|
287
|
-
# For now, commit no matter what, as we run into locking issues otherwise.
|
|
288
|
-
writer.commit()
|
|
289
|
-
if writer.ident is not None:
|
|
290
|
-
writer.join()
|
|
291
|
-
|
|
292
|
-
def remove(self, obj_or_string, commit=True):
|
|
293
|
-
if not self.setup_complete:
|
|
294
|
-
self.setup()
|
|
295
|
-
|
|
296
|
-
self.index = self.index.refresh()
|
|
297
|
-
whoosh_id = get_identifier(obj_or_string)
|
|
298
|
-
|
|
299
|
-
try:
|
|
300
|
-
self.index.delete_by_query(q=self.parser.parse('%s:"%s"' % (ID, whoosh_id)))
|
|
301
|
-
except Exception as e:
|
|
302
|
-
if not self.silently_fail:
|
|
303
|
-
raise
|
|
304
|
-
|
|
305
|
-
self.log.error(
|
|
306
|
-
"Failed to remove document '%s' from Whoosh: %s",
|
|
307
|
-
whoosh_id,
|
|
308
|
-
e,
|
|
309
|
-
exc_info=True,
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
def clear(self, models=None, commit=True):
|
|
313
|
-
if not self.setup_complete:
|
|
314
|
-
self.setup()
|
|
315
|
-
|
|
316
|
-
self.index = self.index.refresh()
|
|
317
|
-
|
|
318
|
-
if models is not None:
|
|
319
|
-
assert isinstance(models, (list, tuple))
|
|
320
|
-
|
|
321
|
-
try:
|
|
322
|
-
if models is None:
|
|
323
|
-
self.delete_index()
|
|
324
|
-
else:
|
|
325
|
-
models_to_delete = []
|
|
326
|
-
|
|
327
|
-
for model in models:
|
|
328
|
-
models_to_delete.append("%s:%s" % (DJANGO_CT, get_model_ct(model)))
|
|
329
|
-
|
|
330
|
-
self.index.delete_by_query(
|
|
331
|
-
q=self.parser.parse(" OR ".join(models_to_delete))
|
|
332
|
-
)
|
|
333
|
-
except Exception as e:
|
|
334
|
-
if not self.silently_fail:
|
|
335
|
-
raise
|
|
336
|
-
|
|
337
|
-
if models is not None:
|
|
338
|
-
self.log.error(
|
|
339
|
-
"Failed to clear Whoosh index of models '%s': %s",
|
|
340
|
-
",".join(models_to_delete),
|
|
341
|
-
e,
|
|
342
|
-
exc_info=True,
|
|
343
|
-
)
|
|
344
|
-
else:
|
|
345
|
-
self.log.error("Failed to clear Whoosh index: %s", e, exc_info=True)
|
|
346
|
-
|
|
347
|
-
def delete_index(self):
|
|
348
|
-
# Per the Whoosh mailing list, if wiping out everything from the index,
|
|
349
|
-
# it's much more efficient to simply delete the index files.
|
|
350
|
-
if self.use_file_storage and os.path.exists(self.path):
|
|
351
|
-
shutil.rmtree(self.path)
|
|
352
|
-
elif not self.use_file_storage:
|
|
353
|
-
self.storage.clean()
|
|
354
|
-
|
|
355
|
-
# Recreate everything.
|
|
356
|
-
self.setup()
|
|
357
|
-
|
|
358
|
-
def optimize(self):
|
|
359
|
-
if not self.setup_complete:
|
|
360
|
-
self.setup()
|
|
361
|
-
|
|
362
|
-
self.index = self.index.refresh()
|
|
363
|
-
self.index.optimize()
|
|
364
|
-
|
|
365
|
-
def calculate_page(self, start_offset=0, end_offset=None):
|
|
366
|
-
# Prevent against Whoosh throwing an error. Requires an end_offset
|
|
367
|
-
# greater than 0.
|
|
368
|
-
if end_offset is not None and end_offset <= 0:
|
|
369
|
-
end_offset = 1
|
|
370
|
-
|
|
371
|
-
# Determine the page.
|
|
372
|
-
page_num = 0
|
|
373
|
-
|
|
374
|
-
if end_offset is None:
|
|
375
|
-
end_offset = 1000000
|
|
376
|
-
|
|
377
|
-
if start_offset is None:
|
|
378
|
-
start_offset = 0
|
|
379
|
-
|
|
380
|
-
page_length = end_offset - start_offset
|
|
381
|
-
|
|
382
|
-
if page_length and page_length > 0:
|
|
383
|
-
page_num = int(start_offset / page_length)
|
|
384
|
-
|
|
385
|
-
# Increment because Whoosh uses 1-based page numbers.
|
|
386
|
-
page_num += 1
|
|
387
|
-
return page_num, page_length
|
|
388
|
-
|
|
389
|
-
@log_query
|
|
390
|
-
def search(
|
|
391
|
-
self,
|
|
392
|
-
query_string,
|
|
393
|
-
sort_by=None,
|
|
394
|
-
start_offset=0,
|
|
395
|
-
end_offset=None,
|
|
396
|
-
fields="",
|
|
397
|
-
highlight=False,
|
|
398
|
-
facets=None,
|
|
399
|
-
date_facets=None,
|
|
400
|
-
query_facets=None,
|
|
401
|
-
narrow_queries=None,
|
|
402
|
-
spelling_query=None,
|
|
403
|
-
within=None,
|
|
404
|
-
dwithin=None,
|
|
405
|
-
distance_point=None,
|
|
406
|
-
models=None,
|
|
407
|
-
limit_to_registered_models=None,
|
|
408
|
-
result_class=None,
|
|
409
|
-
**kwargs
|
|
410
|
-
):
|
|
411
|
-
if not self.setup_complete:
|
|
412
|
-
self.setup()
|
|
413
|
-
|
|
414
|
-
# A zero length query should return no results.
|
|
415
|
-
if len(query_string) == 0:
|
|
416
|
-
return {"results": [], "hits": 0}
|
|
417
|
-
|
|
418
|
-
query_string = force_str(query_string)
|
|
419
|
-
|
|
420
|
-
# A one-character query (non-wildcard) gets nabbed by a stopwords
|
|
421
|
-
# filter and should yield zero results.
|
|
422
|
-
if len(query_string) <= 1 and query_string != "*":
|
|
423
|
-
return {"results": [], "hits": 0}
|
|
424
|
-
|
|
425
|
-
reverse = False
|
|
426
|
-
|
|
427
|
-
if sort_by is not None:
|
|
428
|
-
# Determine if we need to reverse the results and if Whoosh can
|
|
429
|
-
# handle what it's being asked to sort by. Reversing is an
|
|
430
|
-
# all-or-nothing action, unfortunately.
|
|
431
|
-
sort_by_list = []
|
|
432
|
-
reverse_counter = 0
|
|
433
|
-
|
|
434
|
-
for order_by in sort_by:
|
|
435
|
-
if order_by.startswith("-"):
|
|
436
|
-
reverse_counter += 1
|
|
437
|
-
|
|
438
|
-
if reverse_counter and reverse_counter != len(sort_by):
|
|
439
|
-
raise SearchBackendError(
|
|
440
|
-
"Whoosh requires all order_by fields"
|
|
441
|
-
" to use the same sort direction"
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
for order_by in sort_by:
|
|
445
|
-
if order_by.startswith("-"):
|
|
446
|
-
sort_by_list.append(order_by[1:])
|
|
447
|
-
|
|
448
|
-
if len(sort_by_list) == 1:
|
|
449
|
-
reverse = True
|
|
450
|
-
else:
|
|
451
|
-
sort_by_list.append(order_by)
|
|
452
|
-
|
|
453
|
-
if len(sort_by_list) == 1:
|
|
454
|
-
reverse = False
|
|
455
|
-
|
|
456
|
-
sort_by = sort_by_list
|
|
457
|
-
|
|
458
|
-
group_by = []
|
|
459
|
-
facet_types = {}
|
|
460
|
-
if facets is not None:
|
|
461
|
-
group_by += [
|
|
462
|
-
FieldFacet(facet, allow_overlap=True, maptype=Count) for facet in facets
|
|
463
|
-
]
|
|
464
|
-
facet_types.update({facet: "fields" for facet in facets})
|
|
465
|
-
|
|
466
|
-
if date_facets is not None:
|
|
467
|
-
|
|
468
|
-
def _fixup_datetime(dt):
|
|
469
|
-
if isinstance(dt, datetime):
|
|
470
|
-
return dt
|
|
471
|
-
if isinstance(dt, date):
|
|
472
|
-
return datetime(dt.year, dt.month, dt.day)
|
|
473
|
-
raise ValueError
|
|
474
|
-
|
|
475
|
-
for key, value in date_facets.items():
|
|
476
|
-
start = _fixup_datetime(value["start_date"])
|
|
477
|
-
end = _fixup_datetime(value["end_date"])
|
|
478
|
-
gap_by = value["gap_by"]
|
|
479
|
-
gap_amount = value.get("gap_amount", 1)
|
|
480
|
-
gap = RelativeDelta(**{"%ss" % gap_by: gap_amount})
|
|
481
|
-
group_by.append(DateRangeFacet(key, start, end, gap, maptype=Count))
|
|
482
|
-
facet_types[key] = "dates"
|
|
483
|
-
|
|
484
|
-
if query_facets is not None:
|
|
485
|
-
warnings.warn(
|
|
486
|
-
"Whoosh does not handle query faceting.", Warning, stacklevel=2
|
|
487
|
-
)
|
|
488
|
-
|
|
489
|
-
narrowed_results = None
|
|
490
|
-
self.index = self.index.refresh()
|
|
491
|
-
|
|
492
|
-
if limit_to_registered_models is None:
|
|
493
|
-
limit_to_registered_models = getattr(
|
|
494
|
-
settings, "HAYSTACK_LIMIT_TO_REGISTERED_MODELS", True
|
|
495
|
-
)
|
|
496
|
-
|
|
497
|
-
if models and len(models):
|
|
498
|
-
model_choices = sorted(get_model_ct(model) for model in models)
|
|
499
|
-
elif limit_to_registered_models:
|
|
500
|
-
# Using narrow queries, limit the results to only models handled
|
|
501
|
-
# with the current routers.
|
|
502
|
-
model_choices = self.build_models_list()
|
|
503
|
-
else:
|
|
504
|
-
model_choices = []
|
|
505
|
-
|
|
506
|
-
if len(model_choices) > 0:
|
|
507
|
-
if narrow_queries is None:
|
|
508
|
-
narrow_queries = set()
|
|
509
|
-
|
|
510
|
-
narrow_queries.add(
|
|
511
|
-
" OR ".join(["%s:%s" % (DJANGO_CT, rm) for rm in model_choices])
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
narrow_searcher = None
|
|
515
|
-
|
|
516
|
-
if narrow_queries is not None:
|
|
517
|
-
# Potentially expensive? I don't see another way to do it in Whoosh...
|
|
518
|
-
narrow_searcher = self.index.searcher()
|
|
519
|
-
|
|
520
|
-
for nq in narrow_queries:
|
|
521
|
-
recent_narrowed_results = narrow_searcher.search(
|
|
522
|
-
self.parser.parse(force_str(nq)), limit=None
|
|
523
|
-
)
|
|
524
|
-
|
|
525
|
-
if len(recent_narrowed_results) <= 0:
|
|
526
|
-
return {"results": [], "hits": 0}
|
|
527
|
-
|
|
528
|
-
if narrowed_results is not None:
|
|
529
|
-
narrowed_results.filter(recent_narrowed_results)
|
|
530
|
-
else:
|
|
531
|
-
narrowed_results = recent_narrowed_results
|
|
532
|
-
|
|
533
|
-
self.index = self.index.refresh()
|
|
534
|
-
|
|
535
|
-
if self.index.doc_count():
|
|
536
|
-
searcher = self.index.searcher()
|
|
537
|
-
parsed_query = self.parser.parse(query_string)
|
|
538
|
-
|
|
539
|
-
# In the event of an invalid/stopworded query, recover gracefully.
|
|
540
|
-
if parsed_query is None:
|
|
541
|
-
return {"results": [], "hits": 0}
|
|
542
|
-
|
|
543
|
-
page_num, page_length = self.calculate_page(start_offset, end_offset)
|
|
544
|
-
|
|
545
|
-
search_kwargs = {
|
|
546
|
-
"pagelen": page_length,
|
|
547
|
-
"sortedby": sort_by,
|
|
548
|
-
"reverse": reverse,
|
|
549
|
-
"groupedby": group_by,
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
# Handle the case where the results have been narrowed.
|
|
553
|
-
if narrowed_results is not None:
|
|
554
|
-
search_kwargs["filter"] = narrowed_results
|
|
555
|
-
|
|
556
|
-
try:
|
|
557
|
-
raw_page = searcher.search_page(parsed_query, page_num, **search_kwargs)
|
|
558
|
-
except ValueError:
|
|
559
|
-
if not self.silently_fail:
|
|
560
|
-
raise
|
|
561
|
-
|
|
562
|
-
return {"results": [], "hits": 0, "spelling_suggestion": None}
|
|
563
|
-
|
|
564
|
-
# Because as of Whoosh 2.5.1, it will return the wrong page of
|
|
565
|
-
# results if you request something too high. :(
|
|
566
|
-
if raw_page.pagenum < page_num:
|
|
567
|
-
return {"results": [], "hits": 0, "spelling_suggestion": None}
|
|
568
|
-
|
|
569
|
-
results = self._process_results(
|
|
570
|
-
raw_page,
|
|
571
|
-
highlight=highlight,
|
|
572
|
-
query_string=query_string,
|
|
573
|
-
spelling_query=spelling_query,
|
|
574
|
-
result_class=result_class,
|
|
575
|
-
facet_types=facet_types,
|
|
576
|
-
)
|
|
577
|
-
searcher.close()
|
|
578
|
-
|
|
579
|
-
if hasattr(narrow_searcher, "close"):
|
|
580
|
-
narrow_searcher.close()
|
|
581
|
-
|
|
582
|
-
return results
|
|
583
|
-
else:
|
|
584
|
-
if self.include_spelling:
|
|
585
|
-
if spelling_query:
|
|
586
|
-
spelling_suggestion = self.create_spelling_suggestion(
|
|
587
|
-
spelling_query
|
|
588
|
-
)
|
|
589
|
-
else:
|
|
590
|
-
spelling_suggestion = self.create_spelling_suggestion(query_string)
|
|
591
|
-
else:
|
|
592
|
-
spelling_suggestion = None
|
|
593
|
-
|
|
594
|
-
return {
|
|
595
|
-
"results": [],
|
|
596
|
-
"hits": 0,
|
|
597
|
-
"spelling_suggestion": spelling_suggestion,
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
def more_like_this(
|
|
601
|
-
self,
|
|
602
|
-
model_instance,
|
|
603
|
-
additional_query_string=None,
|
|
604
|
-
start_offset=0,
|
|
605
|
-
end_offset=None,
|
|
606
|
-
models=None,
|
|
607
|
-
limit_to_registered_models=None,
|
|
608
|
-
result_class=None,
|
|
609
|
-
**kwargs
|
|
610
|
-
):
|
|
611
|
-
if not self.setup_complete:
|
|
612
|
-
self.setup()
|
|
613
|
-
|
|
614
|
-
field_name = self.content_field_name
|
|
615
|
-
narrow_queries = set()
|
|
616
|
-
narrowed_results = None
|
|
617
|
-
self.index = self.index.refresh()
|
|
618
|
-
|
|
619
|
-
if limit_to_registered_models is None:
|
|
620
|
-
limit_to_registered_models = getattr(
|
|
621
|
-
settings, "HAYSTACK_LIMIT_TO_REGISTERED_MODELS", True
|
|
622
|
-
)
|
|
623
|
-
|
|
624
|
-
if models and len(models):
|
|
625
|
-
model_choices = sorted(get_model_ct(model) for model in models)
|
|
626
|
-
elif limit_to_registered_models:
|
|
627
|
-
# Using narrow queries, limit the results to only models handled
|
|
628
|
-
# with the current routers.
|
|
629
|
-
model_choices = self.build_models_list()
|
|
630
|
-
else:
|
|
631
|
-
model_choices = []
|
|
632
|
-
|
|
633
|
-
if len(model_choices) > 0:
|
|
634
|
-
if narrow_queries is None:
|
|
635
|
-
narrow_queries = set()
|
|
636
|
-
|
|
637
|
-
narrow_queries.add(
|
|
638
|
-
" OR ".join(["%s:%s" % (DJANGO_CT, rm) for rm in model_choices])
|
|
639
|
-
)
|
|
640
|
-
|
|
641
|
-
if additional_query_string and additional_query_string != "*":
|
|
642
|
-
narrow_queries.add(additional_query_string)
|
|
643
|
-
|
|
644
|
-
narrow_searcher = None
|
|
645
|
-
|
|
646
|
-
if narrow_queries is not None:
|
|
647
|
-
# Potentially expensive? I don't see another way to do it in Whoosh...
|
|
648
|
-
narrow_searcher = self.index.searcher()
|
|
649
|
-
|
|
650
|
-
for nq in narrow_queries:
|
|
651
|
-
recent_narrowed_results = narrow_searcher.search(
|
|
652
|
-
self.parser.parse(force_str(nq)), limit=None
|
|
653
|
-
)
|
|
654
|
-
|
|
655
|
-
if len(recent_narrowed_results) <= 0:
|
|
656
|
-
return {"results": [], "hits": 0}
|
|
657
|
-
|
|
658
|
-
if narrowed_results:
|
|
659
|
-
narrowed_results.filter(recent_narrowed_results)
|
|
660
|
-
else:
|
|
661
|
-
narrowed_results = recent_narrowed_results
|
|
662
|
-
|
|
663
|
-
page_num, page_length = self.calculate_page(start_offset, end_offset)
|
|
664
|
-
|
|
665
|
-
self.index = self.index.refresh()
|
|
666
|
-
raw_results = EmptyResults()
|
|
667
|
-
|
|
668
|
-
searcher = None
|
|
669
|
-
if self.index.doc_count():
|
|
670
|
-
query = "%s:%s" % (ID, get_identifier(model_instance))
|
|
671
|
-
searcher = self.index.searcher()
|
|
672
|
-
parsed_query = self.parser.parse(query)
|
|
673
|
-
results = searcher.search(parsed_query)
|
|
674
|
-
|
|
675
|
-
if len(results):
|
|
676
|
-
raw_results = results[0].more_like_this(field_name, top=end_offset)
|
|
677
|
-
|
|
678
|
-
# Handle the case where the results have been narrowed.
|
|
679
|
-
if narrowed_results is not None and hasattr(raw_results, "filter"):
|
|
680
|
-
raw_results.filter(narrowed_results)
|
|
681
|
-
|
|
682
|
-
try:
|
|
683
|
-
raw_page = ResultsPage(raw_results, page_num, page_length)
|
|
684
|
-
except ValueError:
|
|
685
|
-
if not self.silently_fail:
|
|
686
|
-
raise
|
|
687
|
-
|
|
688
|
-
return {"results": [], "hits": 0, "spelling_suggestion": None}
|
|
689
|
-
|
|
690
|
-
# Because as of Whoosh 2.5.1, it will return the wrong page of
|
|
691
|
-
# results if you request something too high. :(
|
|
692
|
-
if raw_page.pagenum < page_num:
|
|
693
|
-
return {"results": [], "hits": 0, "spelling_suggestion": None}
|
|
694
|
-
|
|
695
|
-
results = self._process_results(raw_page, result_class=result_class)
|
|
696
|
-
|
|
697
|
-
if searcher:
|
|
698
|
-
searcher.close()
|
|
699
|
-
|
|
700
|
-
if hasattr(narrow_searcher, "close"):
|
|
701
|
-
narrow_searcher.close()
|
|
702
|
-
|
|
703
|
-
return results
|
|
704
|
-
|
|
705
|
-
def _process_results(
|
|
706
|
-
self,
|
|
707
|
-
raw_page,
|
|
708
|
-
highlight=False,
|
|
709
|
-
query_string="",
|
|
710
|
-
spelling_query=None,
|
|
711
|
-
result_class=None,
|
|
712
|
-
facet_types=None,
|
|
713
|
-
):
|
|
714
|
-
from ...haystack import connections
|
|
715
|
-
|
|
716
|
-
results = []
|
|
717
|
-
|
|
718
|
-
# It's important to grab the hits first before slicing. Otherwise, this
|
|
719
|
-
# can cause pagination failures.
|
|
720
|
-
hits = len(raw_page)
|
|
721
|
-
|
|
722
|
-
if result_class is None:
|
|
723
|
-
result_class = SearchResult
|
|
724
|
-
|
|
725
|
-
spelling_suggestion = None
|
|
726
|
-
unified_index = connections[self.connection_alias].get_unified_index()
|
|
727
|
-
indexed_models = unified_index.get_indexed_models()
|
|
728
|
-
|
|
729
|
-
facets = {}
|
|
730
|
-
|
|
731
|
-
if facet_types:
|
|
732
|
-
facets = {
|
|
733
|
-
"fields": {},
|
|
734
|
-
"dates": {},
|
|
735
|
-
"queries": {},
|
|
736
|
-
}
|
|
737
|
-
for facet_fieldname in raw_page.results.facet_names():
|
|
738
|
-
group = raw_page.results.groups(facet_fieldname)
|
|
739
|
-
facet_type = facet_types[facet_fieldname]
|
|
740
|
-
|
|
741
|
-
# Extract None item for later processing, if present.
|
|
742
|
-
none_item = group.pop(None, None)
|
|
743
|
-
|
|
744
|
-
lst = facets[facet_type][facet_fieldname] = sorted(
|
|
745
|
-
group.items(), key=(lambda itm: (-itm[1], itm[0]))
|
|
746
|
-
)
|
|
747
|
-
|
|
748
|
-
if none_item is not None:
|
|
749
|
-
# Inject None item back into the results.
|
|
750
|
-
none_entry = (None, none_item)
|
|
751
|
-
if not lst or lst[-1][1] >= none_item:
|
|
752
|
-
lst.append(none_entry)
|
|
753
|
-
else:
|
|
754
|
-
for i, value in enumerate(lst):
|
|
755
|
-
if value[1] < none_item:
|
|
756
|
-
lst.insert(i, none_entry)
|
|
757
|
-
break
|
|
758
|
-
|
|
759
|
-
for doc_offset, raw_result in enumerate(raw_page):
|
|
760
|
-
score = raw_page.score(doc_offset) or 0
|
|
761
|
-
app_label, model_name = raw_result[DJANGO_CT].split(".")
|
|
762
|
-
additional_fields = {}
|
|
763
|
-
model = haystack_get_model(app_label, model_name)
|
|
764
|
-
|
|
765
|
-
if model and model in indexed_models:
|
|
766
|
-
for key, value in raw_result.items():
|
|
767
|
-
index = unified_index.get_index(model)
|
|
768
|
-
string_key = str(key)
|
|
769
|
-
|
|
770
|
-
if string_key in index.fields and hasattr(
|
|
771
|
-
index.fields[string_key], "convert"
|
|
772
|
-
):
|
|
773
|
-
# Special-cased due to the nature of KEYWORD fields.
|
|
774
|
-
if index.fields[string_key].is_multivalued:
|
|
775
|
-
if value is None or len(value) == 0:
|
|
776
|
-
additional_fields[string_key] = []
|
|
777
|
-
else:
|
|
778
|
-
additional_fields[string_key] = value.split(",")
|
|
779
|
-
else:
|
|
780
|
-
additional_fields[string_key] = index.fields[
|
|
781
|
-
string_key
|
|
782
|
-
].convert(value)
|
|
783
|
-
else:
|
|
784
|
-
additional_fields[string_key] = self._to_python(value)
|
|
785
|
-
|
|
786
|
-
del additional_fields[DJANGO_CT]
|
|
787
|
-
del additional_fields[DJANGO_ID]
|
|
788
|
-
|
|
789
|
-
if highlight:
|
|
790
|
-
sa = StemmingAnalyzer()
|
|
791
|
-
formatter = WhooshHtmlFormatter("em")
|
|
792
|
-
terms = [token.text for token in sa(query_string)]
|
|
793
|
-
|
|
794
|
-
whoosh_result = whoosh_highlight(
|
|
795
|
-
additional_fields.get(self.content_field_name),
|
|
796
|
-
terms,
|
|
797
|
-
sa,
|
|
798
|
-
ContextFragmenter(),
|
|
799
|
-
formatter,
|
|
800
|
-
)
|
|
801
|
-
additional_fields["highlighted"] = {
|
|
802
|
-
self.content_field_name: [whoosh_result]
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
result = result_class(
|
|
806
|
-
app_label,
|
|
807
|
-
model_name,
|
|
808
|
-
raw_result[DJANGO_ID],
|
|
809
|
-
score,
|
|
810
|
-
**additional_fields
|
|
811
|
-
)
|
|
812
|
-
results.append(result)
|
|
813
|
-
else:
|
|
814
|
-
hits -= 1
|
|
815
|
-
|
|
816
|
-
if self.include_spelling:
|
|
817
|
-
if spelling_query:
|
|
818
|
-
spelling_suggestion = self.create_spelling_suggestion(spelling_query)
|
|
819
|
-
else:
|
|
820
|
-
spelling_suggestion = self.create_spelling_suggestion(query_string)
|
|
821
|
-
|
|
822
|
-
return {
|
|
823
|
-
"results": results,
|
|
824
|
-
"hits": hits,
|
|
825
|
-
"facets": facets,
|
|
826
|
-
"spelling_suggestion": spelling_suggestion,
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
def create_spelling_suggestion(self, query_string):
|
|
830
|
-
spelling_suggestion = None
|
|
831
|
-
reader = self.index.reader()
|
|
832
|
-
corrector = reader.corrector(self.content_field_name)
|
|
833
|
-
cleaned_query = force_str(query_string)
|
|
834
|
-
|
|
835
|
-
if not query_string:
|
|
836
|
-
return spelling_suggestion
|
|
837
|
-
|
|
838
|
-
# Clean the string.
|
|
839
|
-
for rev_word in self.RESERVED_WORDS:
|
|
840
|
-
cleaned_query = cleaned_query.replace(rev_word, "")
|
|
841
|
-
|
|
842
|
-
for rev_char in self.RESERVED_CHARACTERS:
|
|
843
|
-
cleaned_query = cleaned_query.replace(rev_char, "")
|
|
844
|
-
|
|
845
|
-
# Break it down.
|
|
846
|
-
query_words = cleaned_query.split()
|
|
847
|
-
suggested_words = []
|
|
848
|
-
|
|
849
|
-
for word in query_words:
|
|
850
|
-
suggestions = corrector.suggest(word, limit=1)
|
|
851
|
-
|
|
852
|
-
if len(suggestions) > 0:
|
|
853
|
-
suggested_words.append(suggestions[0])
|
|
854
|
-
|
|
855
|
-
spelling_suggestion = " ".join(suggested_words)
|
|
856
|
-
return spelling_suggestion
|
|
857
|
-
|
|
858
|
-
def _from_python(self, value):
|
|
859
|
-
"""
|
|
860
|
-
Converts Python values to a string for Whoosh.
|
|
861
|
-
|
|
862
|
-
Code courtesy of pysolr.
|
|
863
|
-
"""
|
|
864
|
-
if hasattr(value, "strftime"):
|
|
865
|
-
if not hasattr(value, "hour"):
|
|
866
|
-
value = datetime(value.year, value.month, value.day, 0, 0, 0)
|
|
867
|
-
elif isinstance(value, bool):
|
|
868
|
-
if value:
|
|
869
|
-
value = "true"
|
|
870
|
-
else:
|
|
871
|
-
value = "false"
|
|
872
|
-
elif isinstance(value, (list, tuple)):
|
|
873
|
-
value = ",".join([force_str(v) for v in value])
|
|
874
|
-
elif isinstance(value, (int, float)):
|
|
875
|
-
# Leave it alone.
|
|
876
|
-
pass
|
|
877
|
-
else:
|
|
878
|
-
value = force_str(value)
|
|
879
|
-
return value
|
|
880
|
-
|
|
881
|
-
def _to_python(self, value):
|
|
882
|
-
"""
|
|
883
|
-
Converts values from Whoosh to native Python values.
|
|
884
|
-
|
|
885
|
-
A port of the same method in pysolr, as they deal with data the same way.
|
|
886
|
-
"""
|
|
887
|
-
if value == "true":
|
|
888
|
-
return True
|
|
889
|
-
elif value == "false":
|
|
890
|
-
return False
|
|
891
|
-
|
|
892
|
-
if value and isinstance(value, str):
|
|
893
|
-
possible_datetime = DATETIME_REGEX.search(value)
|
|
894
|
-
|
|
895
|
-
if possible_datetime:
|
|
896
|
-
date_values = possible_datetime.groupdict()
|
|
897
|
-
|
|
898
|
-
for dk, dv in date_values.items():
|
|
899
|
-
date_values[dk] = int(dv)
|
|
900
|
-
|
|
901
|
-
return datetime(
|
|
902
|
-
date_values["year"],
|
|
903
|
-
date_values["month"],
|
|
904
|
-
date_values["day"],
|
|
905
|
-
date_values["hour"],
|
|
906
|
-
date_values["minute"],
|
|
907
|
-
date_values["second"],
|
|
908
|
-
)
|
|
909
|
-
|
|
910
|
-
try:
|
|
911
|
-
# Attempt to use json to load the values.
|
|
912
|
-
converted_value = json.loads(value)
|
|
913
|
-
|
|
914
|
-
# Try to handle most built-in types.
|
|
915
|
-
if isinstance(
|
|
916
|
-
converted_value,
|
|
917
|
-
(list, tuple, set, dict, int, float, complex),
|
|
918
|
-
):
|
|
919
|
-
return converted_value
|
|
920
|
-
except Exception:
|
|
921
|
-
# If it fails (SyntaxError or its ilk) or we don't trust it,
|
|
922
|
-
# continue on.
|
|
923
|
-
pass
|
|
924
|
-
|
|
925
|
-
return value
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
class WhooshSearchQuery(BaseSearchQuery):
|
|
929
|
-
def _convert_datetime(self, date):
|
|
930
|
-
if hasattr(date, "hour"):
|
|
931
|
-
return force_str(date.strftime("%Y%m%d%H%M%S"))
|
|
932
|
-
else:
|
|
933
|
-
return force_str(date.strftime("%Y%m%d000000"))
|
|
934
|
-
|
|
935
|
-
def clean(self, query_fragment):
|
|
936
|
-
"""
|
|
937
|
-
Provides a mechanism for sanitizing user input before presenting the
|
|
938
|
-
value to the backend.
|
|
939
|
-
|
|
940
|
-
Whoosh 1.X differs here in that you can no longer use a backslash
|
|
941
|
-
to escape reserved characters. Instead, the whole word should be
|
|
942
|
-
quoted.
|
|
943
|
-
"""
|
|
944
|
-
words = query_fragment.split()
|
|
945
|
-
cleaned_words = []
|
|
946
|
-
|
|
947
|
-
for word in words:
|
|
948
|
-
if word in self.backend.RESERVED_WORDS:
|
|
949
|
-
word = word.replace(word, word.lower())
|
|
950
|
-
|
|
951
|
-
for char in self.backend.RESERVED_CHARACTERS:
|
|
952
|
-
if char in word:
|
|
953
|
-
word = "'%s'" % word
|
|
954
|
-
break
|
|
955
|
-
|
|
956
|
-
cleaned_words.append(word)
|
|
957
|
-
|
|
958
|
-
return " ".join(cleaned_words)
|
|
959
|
-
|
|
960
|
-
def build_query_fragment(self, field, filter_type, value):
|
|
961
|
-
from ...haystack import connections
|
|
962
|
-
|
|
963
|
-
query_frag = ""
|
|
964
|
-
is_datetime = False
|
|
965
|
-
|
|
966
|
-
if not hasattr(value, "input_type_name"):
|
|
967
|
-
# Handle when we've got a ``ValuesListQuerySet``...
|
|
968
|
-
if hasattr(value, "values_list"):
|
|
969
|
-
value = list(value)
|
|
970
|
-
|
|
971
|
-
if hasattr(value, "strftime"):
|
|
972
|
-
is_datetime = True
|
|
973
|
-
|
|
974
|
-
if isinstance(value, str) and value != " ":
|
|
975
|
-
# It's not an ``InputType``. Assume ``Clean``.
|
|
976
|
-
value = Clean(value)
|
|
977
|
-
else:
|
|
978
|
-
value = PythonData(value)
|
|
979
|
-
|
|
980
|
-
# Prepare the query using the InputType.
|
|
981
|
-
prepared_value = value.prepare(self)
|
|
982
|
-
|
|
983
|
-
if not isinstance(prepared_value, (set, list, tuple)):
|
|
984
|
-
# Then convert whatever we get back to what pysolr wants if needed.
|
|
985
|
-
prepared_value = self.backend._from_python(prepared_value)
|
|
986
|
-
|
|
987
|
-
# 'content' is a special reserved word, much like 'pk' in
|
|
988
|
-
# Django's ORM layer. It indicates 'no special field'.
|
|
989
|
-
if field == "content":
|
|
990
|
-
index_fieldname = ""
|
|
991
|
-
else:
|
|
992
|
-
index_fieldname = "%s:" % connections[
|
|
993
|
-
self._using
|
|
994
|
-
].get_unified_index().get_index_fieldname(field)
|
|
995
|
-
|
|
996
|
-
filter_types = {
|
|
997
|
-
"content": "%s",
|
|
998
|
-
"contains": "*%s*",
|
|
999
|
-
"endswith": "*%s",
|
|
1000
|
-
"startswith": "%s*",
|
|
1001
|
-
"exact": "%s",
|
|
1002
|
-
"gt": "{%s to}",
|
|
1003
|
-
"gte": "[%s to]",
|
|
1004
|
-
"lt": "{to %s}",
|
|
1005
|
-
"lte": "[to %s]",
|
|
1006
|
-
"fuzzy": "%s~{}/%d".format(FUZZY_WHOOSH_MAX_EDITS),
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
if value.post_process is False:
|
|
1010
|
-
query_frag = prepared_value
|
|
1011
|
-
else:
|
|
1012
|
-
if filter_type in [
|
|
1013
|
-
"content",
|
|
1014
|
-
"contains",
|
|
1015
|
-
"startswith",
|
|
1016
|
-
"endswith",
|
|
1017
|
-
"fuzzy",
|
|
1018
|
-
]:
|
|
1019
|
-
if value.input_type_name == "exact":
|
|
1020
|
-
query_frag = prepared_value
|
|
1021
|
-
else:
|
|
1022
|
-
# Iterate over terms & incorportate the converted form of each into the query.
|
|
1023
|
-
terms = []
|
|
1024
|
-
|
|
1025
|
-
if isinstance(prepared_value, str):
|
|
1026
|
-
possible_values = prepared_value.split(" ")
|
|
1027
|
-
else:
|
|
1028
|
-
if is_datetime is True:
|
|
1029
|
-
prepared_value = self._convert_datetime(prepared_value)
|
|
1030
|
-
|
|
1031
|
-
possible_values = [prepared_value]
|
|
1032
|
-
|
|
1033
|
-
for possible_value in possible_values:
|
|
1034
|
-
possible_value_str = self.backend._from_python(possible_value)
|
|
1035
|
-
if filter_type == "fuzzy":
|
|
1036
|
-
terms.append(
|
|
1037
|
-
filter_types[filter_type]
|
|
1038
|
-
% (
|
|
1039
|
-
possible_value_str,
|
|
1040
|
-
min(
|
|
1041
|
-
FUZZY_WHOOSH_MIN_PREFIX, len(possible_value_str)
|
|
1042
|
-
),
|
|
1043
|
-
)
|
|
1044
|
-
)
|
|
1045
|
-
else:
|
|
1046
|
-
terms.append(filter_types[filter_type] % possible_value_str)
|
|
1047
|
-
|
|
1048
|
-
if len(terms) == 1:
|
|
1049
|
-
query_frag = terms[0]
|
|
1050
|
-
else:
|
|
1051
|
-
query_frag = "(%s)" % " AND ".join(terms)
|
|
1052
|
-
elif filter_type == "in":
|
|
1053
|
-
in_options = []
|
|
1054
|
-
|
|
1055
|
-
for possible_value in prepared_value:
|
|
1056
|
-
is_datetime = False
|
|
1057
|
-
|
|
1058
|
-
if hasattr(possible_value, "strftime"):
|
|
1059
|
-
is_datetime = True
|
|
1060
|
-
|
|
1061
|
-
pv = self.backend._from_python(possible_value)
|
|
1062
|
-
|
|
1063
|
-
if is_datetime is True:
|
|
1064
|
-
pv = self._convert_datetime(pv)
|
|
1065
|
-
|
|
1066
|
-
if isinstance(pv, str) and not is_datetime:
|
|
1067
|
-
in_options.append('"%s"' % pv)
|
|
1068
|
-
else:
|
|
1069
|
-
in_options.append("%s" % pv)
|
|
1070
|
-
|
|
1071
|
-
query_frag = "(%s)" % " OR ".join(in_options)
|
|
1072
|
-
elif filter_type == "range":
|
|
1073
|
-
start = self.backend._from_python(prepared_value[0])
|
|
1074
|
-
end = self.backend._from_python(prepared_value[1])
|
|
1075
|
-
|
|
1076
|
-
if hasattr(prepared_value[0], "strftime"):
|
|
1077
|
-
start = self._convert_datetime(start)
|
|
1078
|
-
|
|
1079
|
-
if hasattr(prepared_value[1], "strftime"):
|
|
1080
|
-
end = self._convert_datetime(end)
|
|
1081
|
-
|
|
1082
|
-
query_frag = "[%s to %s]" % (start, end)
|
|
1083
|
-
elif filter_type == "exact":
|
|
1084
|
-
if value.input_type_name == "exact":
|
|
1085
|
-
query_frag = prepared_value
|
|
1086
|
-
else:
|
|
1087
|
-
prepared_value = Exact(prepared_value).prepare(self)
|
|
1088
|
-
query_frag = filter_types[filter_type] % prepared_value
|
|
1089
|
-
else:
|
|
1090
|
-
if is_datetime is True:
|
|
1091
|
-
prepared_value = self._convert_datetime(prepared_value)
|
|
1092
|
-
|
|
1093
|
-
query_frag = filter_types[filter_type] % prepared_value
|
|
1094
|
-
|
|
1095
|
-
if len(query_frag) and not isinstance(value, Raw):
|
|
1096
|
-
if not query_frag.startswith("(") and not query_frag.endswith(")"):
|
|
1097
|
-
query_frag = "(%s)" % query_frag
|
|
1098
|
-
|
|
1099
|
-
return "%s%s" % (index_fieldname, query_frag)
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
class WhooshEngine(BaseEngine):
|
|
1103
|
-
backend = WhooshSearchBackend
|
|
1104
|
-
query = WhooshSearchQuery
|