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.

Files changed (576) hide show
  1. codex/db.py +119 -0
  2. codex/librarian/covers/purge.py +1 -1
  3. codex/librarian/importer/const.py +7 -1
  4. codex/librarian/importer/create_fks.py +5 -2
  5. codex/librarian/importer/importerd.py +17 -12
  6. codex/librarian/importer/moved.py +44 -13
  7. codex/librarian/importer/tasks.py +2 -0
  8. codex/librarian/janitor/cleanup.py +21 -8
  9. codex/librarian/janitor/integrity.py +369 -0
  10. codex/librarian/janitor/janitor.py +50 -15
  11. codex/librarian/janitor/latest_version.py +2 -1
  12. codex/librarian/janitor/status.py +5 -0
  13. codex/librarian/janitor/tasks.py +37 -0
  14. codex/librarian/janitor/update.py +1 -1
  15. codex/librarian/librariand.py +32 -24
  16. codex/librarian/search/optimize.py +43 -0
  17. codex/librarian/search/remove.py +18 -46
  18. codex/librarian/search/searchd.py +14 -17
  19. codex/librarian/search/status.py +2 -1
  20. codex/librarian/search/tasks.py +4 -9
  21. codex/librarian/search/update.py +223 -301
  22. codex/librarian/watchdog/observers.py +2 -1
  23. codex/migrations/0029_comicfts.py +58 -0
  24. codex/migrations/0030_nocase_collation_day_month_indexes_status_types.py +198 -0
  25. codex/models/admin.py +15 -6
  26. codex/models/base.py +9 -0
  27. codex/models/bookmark.py +3 -3
  28. codex/models/comic.py +68 -16
  29. codex/models/functions.py +59 -2
  30. codex/models/groups.py +6 -1
  31. codex/models/library.py +1 -3
  32. codex/models/paths.py +9 -3
  33. codex/serializers/browser/page.py +2 -0
  34. codex/settings/settings.py +16 -17
  35. codex/signals/django_signals.py +4 -10
  36. codex/startup.py +1 -35
  37. codex/static_root/assets/{VCheckbox-Cko1sQK-.170726b3b8a1.js → VCheckbox-CWiqcZ7a.3feebd47daab.js} +1 -1
  38. codex/static_root/assets/VCheckbox-CWiqcZ7a.3feebd47daab.js.br +0 -0
  39. codex/static_root/assets/VCheckbox-CWiqcZ7a.3feebd47daab.js.gz +0 -0
  40. codex/static_root/assets/{VCheckbox-Cko1sQK-.js → VCheckbox-CWiqcZ7a.js} +1 -1
  41. codex/static_root/assets/VCheckbox-CWiqcZ7a.js.br +0 -0
  42. codex/static_root/assets/VCheckbox-CWiqcZ7a.js.gz +0 -0
  43. codex/static_root/assets/{VCheckboxBtn-CO3-aqQL.c9b8062fd66f.js → VCheckboxBtn-Dk3iB62X.bc66d5351baa.js} +1 -1
  44. codex/static_root/assets/VCheckboxBtn-Dk3iB62X.bc66d5351baa.js.br +0 -0
  45. codex/static_root/assets/VCheckboxBtn-Dk3iB62X.bc66d5351baa.js.gz +0 -0
  46. codex/static_root/assets/{VCheckboxBtn-CO3-aqQL.js → VCheckboxBtn-Dk3iB62X.js} +1 -1
  47. codex/static_root/assets/VCheckboxBtn-Dk3iB62X.js.br +0 -0
  48. codex/static_root/assets/VCheckboxBtn-Dk3iB62X.js.gz +0 -0
  49. codex/static_root/assets/{VCombobox-Bjp6lffE.js → VCombobox-D1_KU27A.b1d30587a973.js} +1 -1
  50. codex/static_root/assets/VCombobox-D1_KU27A.b1d30587a973.js.br +0 -0
  51. codex/static_root/assets/VCombobox-D1_KU27A.b1d30587a973.js.gz +0 -0
  52. codex/static_root/assets/{VCombobox-Bjp6lffE.caf59fba486e.js → VCombobox-D1_KU27A.js} +1 -1
  53. codex/static_root/assets/VCombobox-D1_KU27A.js.br +0 -0
  54. codex/static_root/assets/VCombobox-D1_KU27A.js.gz +0 -0
  55. codex/static_root/assets/{VDialog-BkpVGB70.6eea3ca762a5.js → VDialog-GbxvFMMN.594fb3a39ced.js} +1 -1
  56. codex/static_root/assets/VDialog-GbxvFMMN.594fb3a39ced.js.br +0 -0
  57. codex/static_root/assets/VDialog-GbxvFMMN.594fb3a39ced.js.gz +0 -0
  58. codex/static_root/assets/{VDialog-BkpVGB70.js → VDialog-GbxvFMMN.js} +1 -1
  59. codex/static_root/assets/VDialog-GbxvFMMN.js.br +0 -0
  60. codex/static_root/assets/VDialog-GbxvFMMN.js.gz +0 -0
  61. codex/static_root/assets/{VExpansionPanels-Ci2-j8XX.9c55db4ef49e.js → VExpansionPanels-Cywv_bEj.a762e63858e5.js} +1 -1
  62. codex/static_root/assets/VExpansionPanels-Cywv_bEj.a762e63858e5.js.br +0 -0
  63. codex/static_root/assets/VExpansionPanels-Cywv_bEj.a762e63858e5.js.gz +0 -0
  64. codex/static_root/assets/{VExpansionPanels-Ci2-j8XX.js → VExpansionPanels-Cywv_bEj.js} +1 -1
  65. codex/static_root/assets/VExpansionPanels-Cywv_bEj.js.br +0 -0
  66. codex/static_root/assets/VExpansionPanels-Cywv_bEj.js.gz +0 -0
  67. codex/static_root/assets/{VRadioGroup-D3Py5BfQ.a4f25edb86d3.js → VRadioGroup-C0etmxpZ.0eb3009a6300.js} +1 -1
  68. codex/static_root/assets/VRadioGroup-C0etmxpZ.0eb3009a6300.js.br +0 -0
  69. codex/static_root/assets/VRadioGroup-C0etmxpZ.0eb3009a6300.js.gz +0 -0
  70. codex/static_root/assets/{VRadioGroup-D3Py5BfQ.js → VRadioGroup-C0etmxpZ.js} +1 -1
  71. codex/static_root/assets/VRadioGroup-C0etmxpZ.js.br +0 -0
  72. codex/static_root/assets/VRadioGroup-C0etmxpZ.js.gz +0 -0
  73. codex/static_root/assets/{VSelect-DtvZsYZz.e98745d858eb.js → VSelect-txrnRiNJ.a5625760018c.js} +1 -1
  74. codex/static_root/assets/VSelect-txrnRiNJ.a5625760018c.js.br +0 -0
  75. codex/static_root/assets/VSelect-txrnRiNJ.a5625760018c.js.gz +0 -0
  76. codex/static_root/assets/{VSelect-DtvZsYZz.js → VSelect-txrnRiNJ.js} +1 -1
  77. codex/static_root/assets/VSelect-txrnRiNJ.js.br +0 -0
  78. codex/static_root/assets/VSelect-txrnRiNJ.js.gz +0 -0
  79. codex/static_root/assets/{VSelectionControl-D6kmykQW.1e1fda62ceba.js → VSelectionControl-CY6RmcvW.7e8824224f6c.js} +1 -1
  80. codex/static_root/assets/VSelectionControl-CY6RmcvW.7e8824224f6c.js.br +0 -0
  81. codex/static_root/assets/VSelectionControl-CY6RmcvW.7e8824224f6c.js.gz +0 -0
  82. codex/static_root/assets/{VSelectionControl-D6kmykQW.js → VSelectionControl-CY6RmcvW.js} +1 -1
  83. codex/static_root/assets/VSelectionControl-CY6RmcvW.js.br +0 -0
  84. codex/static_root/assets/VSelectionControl-CY6RmcvW.js.gz +0 -0
  85. codex/static_root/assets/{VSlideGroup-UGZWxpF5.360bdefcd215.js → VSlideGroup-DtEQMwY1.6c72325635ed.js} +1 -1
  86. codex/static_root/assets/VSlideGroup-DtEQMwY1.6c72325635ed.js.br +0 -0
  87. codex/static_root/assets/VSlideGroup-DtEQMwY1.6c72325635ed.js.gz +0 -0
  88. codex/static_root/assets/{VSlideGroup-UGZWxpF5.js → VSlideGroup-DtEQMwY1.js} +1 -1
  89. codex/static_root/assets/VSlideGroup-DtEQMwY1.js.br +0 -0
  90. codex/static_root/assets/VSlideGroup-DtEQMwY1.js.gz +0 -0
  91. codex/static_root/assets/{VTable-DXH_yQ0o.1149f56ad566.js → VTable-2af995xo.6c5bf36631b2.js} +1 -1
  92. codex/static_root/assets/VTable-2af995xo.6c5bf36631b2.js.br +0 -0
  93. codex/static_root/assets/VTable-2af995xo.6c5bf36631b2.js.gz +0 -0
  94. codex/static_root/assets/{VTable-DXH_yQ0o.js → VTable-2af995xo.js} +1 -1
  95. codex/static_root/assets/VTable-2af995xo.js.br +0 -0
  96. codex/static_root/assets/VTable-2af995xo.js.gz +0 -0
  97. codex/static_root/assets/{VTextField-Blqy56_L.26d8761e0155.js → VTextField-G6CMj7yO.657a130dd302.js} +1 -1
  98. codex/static_root/assets/VTextField-G6CMj7yO.657a130dd302.js.br +0 -0
  99. codex/static_root/assets/VTextField-G6CMj7yO.657a130dd302.js.gz +0 -0
  100. codex/static_root/assets/{VTextField-Blqy56_L.js → VTextField-G6CMj7yO.js} +1 -1
  101. codex/static_root/assets/VTextField-G6CMj7yO.js.br +0 -0
  102. codex/static_root/assets/VTextField-G6CMj7yO.js.gz +0 -0
  103. codex/static_root/assets/{VWindowItem-C8_ovV7e.afbc08414765.js → VWindowItem-CzoGd6Ul.721e5da11ad4.js} +1 -1
  104. codex/static_root/assets/VWindowItem-CzoGd6Ul.721e5da11ad4.js.br +0 -0
  105. codex/static_root/assets/VWindowItem-CzoGd6Ul.721e5da11ad4.js.gz +0 -0
  106. codex/static_root/assets/{VWindowItem-C8_ovV7e.js → VWindowItem-CzoGd6Ul.js} +1 -1
  107. codex/static_root/assets/VWindowItem-CzoGd6Ul.js.br +0 -0
  108. codex/static_root/assets/VWindowItem-CzoGd6Ul.js.gz +0 -0
  109. codex/static_root/assets/{admin-DTRn8bBs.d031ea42d8d0.js → admin-CsPAIQeG.5175467148a3.js} +1 -1
  110. codex/static_root/assets/admin-CsPAIQeG.5175467148a3.js.br +0 -0
  111. codex/static_root/assets/admin-CsPAIQeG.5175467148a3.js.gz +0 -0
  112. codex/static_root/assets/{admin-DTRn8bBs.js → admin-CsPAIQeG.js} +1 -1
  113. codex/static_root/assets/admin-CsPAIQeG.js.br +0 -0
  114. codex/static_root/assets/admin-CsPAIQeG.js.gz +0 -0
  115. codex/static_root/assets/{admin-drawer-panel-BaXFOcru.b1a44b05f482.js → admin-drawer-panel-BAT7GA64.73a064628c19.js} +11 -11
  116. codex/static_root/assets/admin-drawer-panel-BAT7GA64.73a064628c19.js.br +0 -0
  117. codex/static_root/assets/admin-drawer-panel-BAT7GA64.73a064628c19.js.gz +0 -0
  118. codex/static_root/assets/{admin-drawer-panel-BaXFOcru.js → admin-drawer-panel-BAT7GA64.js} +11 -11
  119. codex/static_root/assets/admin-drawer-panel-BAT7GA64.js.br +0 -0
  120. codex/static_root/assets/admin-drawer-panel-BAT7GA64.js.gz +0 -0
  121. codex/static_root/assets/browser-C18l93yc.923860009ef4.js +1 -0
  122. codex/static_root/assets/browser-C18l93yc.923860009ef4.js.br +0 -0
  123. codex/static_root/assets/browser-C18l93yc.923860009ef4.js.gz +0 -0
  124. codex/static_root/assets/browser-C18l93yc.js +1 -0
  125. codex/static_root/assets/browser-C18l93yc.js.br +0 -0
  126. codex/static_root/assets/browser-C18l93yc.js.gz +0 -0
  127. codex/static_root/assets/browser-DaAqlBYO.708ecf0904bb.css +1 -0
  128. codex/static_root/assets/browser-DaAqlBYO.708ecf0904bb.css.br +0 -0
  129. codex/static_root/assets/browser-DaAqlBYO.708ecf0904bb.css.gz +0 -0
  130. codex/static_root/assets/browser-DaAqlBYO.css +1 -0
  131. codex/static_root/assets/browser-DaAqlBYO.css.br +0 -0
  132. codex/static_root/assets/browser-DaAqlBYO.css.gz +0 -0
  133. codex/static_root/assets/change-password-dialog-BazJdNs9.0a4e02656a35.js +1 -0
  134. codex/static_root/assets/change-password-dialog-BazJdNs9.0a4e02656a35.js.br +0 -0
  135. codex/static_root/assets/change-password-dialog-BazJdNs9.0a4e02656a35.js.gz +0 -0
  136. codex/static_root/assets/change-password-dialog-BazJdNs9.js +1 -0
  137. codex/static_root/assets/change-password-dialog-BazJdNs9.js.br +0 -0
  138. codex/static_root/assets/change-password-dialog-BazJdNs9.js.gz +0 -0
  139. codex/static_root/assets/change-password-dialog-Mlmvz8_4.111421f51e79.css +1 -0
  140. codex/static_root/assets/change-password-dialog-Mlmvz8_4.111421f51e79.css.br +0 -0
  141. codex/static_root/assets/change-password-dialog-Mlmvz8_4.111421f51e79.css.gz +0 -0
  142. codex/static_root/assets/change-password-dialog-Mlmvz8_4.css +1 -0
  143. codex/static_root/assets/change-password-dialog-Mlmvz8_4.css.br +0 -0
  144. codex/static_root/assets/change-password-dialog-Mlmvz8_4.css.gz +0 -0
  145. codex/static_root/assets/{confirm-dialog-D3_s2DQy.9dde66036c72.js → confirm-dialog-CbW3hlOp.9dd39b443c31.js} +1 -1
  146. codex/static_root/assets/confirm-dialog-CbW3hlOp.9dd39b443c31.js.br +0 -0
  147. codex/static_root/assets/confirm-dialog-CbW3hlOp.9dd39b443c31.js.gz +0 -0
  148. codex/static_root/assets/{confirm-dialog-D3_s2DQy.js → confirm-dialog-CbW3hlOp.js} +1 -1
  149. codex/static_root/assets/confirm-dialog-CbW3hlOp.js.br +0 -0
  150. codex/static_root/assets/confirm-dialog-CbW3hlOp.js.gz +0 -0
  151. codex/static_root/assets/{datetime-column-BxC1Li9e.3dc314b3295d.js → datetime-column-uSqKZU37.b157b0743db5.js} +1 -1
  152. codex/static_root/assets/datetime-column-uSqKZU37.b157b0743db5.js.br +0 -0
  153. codex/static_root/assets/datetime-column-uSqKZU37.b157b0743db5.js.gz +0 -0
  154. codex/static_root/assets/{datetime-column-BxC1Li9e.js → datetime-column-uSqKZU37.js} +1 -1
  155. codex/static_root/assets/datetime-column-uSqKZU37.js.br +0 -0
  156. codex/static_root/assets/datetime-column-uSqKZU37.js.gz +0 -0
  157. codex/static_root/assets/{filter-Bzk7x841.d0c3b602e62d.js → filter-CBl5KEsB.845a81573c2e.js} +1 -1
  158. codex/static_root/assets/filter-CBl5KEsB.845a81573c2e.js.br +0 -0
  159. codex/static_root/assets/filter-CBl5KEsB.845a81573c2e.js.gz +0 -0
  160. codex/static_root/assets/{filter-Bzk7x841.js → filter-CBl5KEsB.js} +1 -1
  161. codex/static_root/assets/filter-CBl5KEsB.js.br +0 -0
  162. codex/static_root/assets/filter-CBl5KEsB.js.gz +0 -0
  163. codex/static_root/assets/flag-tab-CT8pqM4r.ecb6eee2e7a0.js +1 -0
  164. codex/static_root/assets/flag-tab-CT8pqM4r.ecb6eee2e7a0.js.br +0 -0
  165. codex/static_root/assets/flag-tab-CT8pqM4r.ecb6eee2e7a0.js.gz +0 -0
  166. codex/static_root/assets/flag-tab-CT8pqM4r.js +1 -0
  167. codex/static_root/assets/flag-tab-CT8pqM4r.js.br +0 -0
  168. codex/static_root/assets/flag-tab-CT8pqM4r.js.gz +0 -0
  169. codex/static_root/assets/{group-tab-5esH4lwX.a81fdd258117.js → group-tab-BOW9kigw.7e5007596367.js} +1 -1
  170. codex/static_root/assets/group-tab-BOW9kigw.7e5007596367.js.br +0 -0
  171. codex/static_root/assets/group-tab-BOW9kigw.7e5007596367.js.gz +0 -0
  172. codex/static_root/assets/{group-tab-5esH4lwX.js → group-tab-BOW9kigw.js} +1 -1
  173. codex/static_root/assets/group-tab-BOW9kigw.js.br +0 -0
  174. codex/static_root/assets/group-tab-BOW9kigw.js.gz +0 -0
  175. codex/static_root/assets/{http-error-BTOBfcGY.52ced6f28420.js → http-error-C6yuLApo.45cf99eb7a70.js} +1 -1
  176. codex/static_root/assets/http-error-C6yuLApo.45cf99eb7a70.js.br +4 -0
  177. codex/static_root/assets/http-error-C6yuLApo.45cf99eb7a70.js.gz +0 -0
  178. codex/static_root/assets/{http-error-BTOBfcGY.js → http-error-C6yuLApo.js} +1 -1
  179. codex/static_root/assets/http-error-C6yuLApo.js.br +4 -0
  180. codex/static_root/assets/http-error-C6yuLApo.js.gz +0 -0
  181. codex/static_root/assets/{library-tab-DiPEdOF7.ece2d642a9dc.js → library-tab-D3WOtpE0.9af34e6e7235.js} +1 -1
  182. codex/static_root/assets/library-tab-D3WOtpE0.9af34e6e7235.js.br +0 -0
  183. codex/static_root/assets/library-tab-D3WOtpE0.9af34e6e7235.js.gz +0 -0
  184. codex/static_root/assets/{library-tab-DiPEdOF7.js → library-tab-D3WOtpE0.js} +1 -1
  185. codex/static_root/assets/library-tab-D3WOtpE0.js.br +0 -0
  186. codex/static_root/assets/library-tab-D3WOtpE0.js.gz +0 -0
  187. codex/static_root/assets/main-CmjU2oTp.6a471f6900e5.js +6 -0
  188. codex/static_root/assets/main-CmjU2oTp.6a471f6900e5.js.br +0 -0
  189. codex/static_root/assets/main-CmjU2oTp.6a471f6900e5.js.gz +0 -0
  190. codex/static_root/assets/main-CmjU2oTp.js +6 -0
  191. codex/static_root/assets/main-CmjU2oTp.js.br +0 -0
  192. codex/static_root/assets/main-CmjU2oTp.js.gz +0 -0
  193. codex/static_root/assets/pager-full-pdf-BH54_5f6.6e92d0555cd7.js +1 -0
  194. codex/static_root/assets/pager-full-pdf-BH54_5f6.6e92d0555cd7.js.br +0 -0
  195. codex/static_root/assets/pager-full-pdf-BH54_5f6.6e92d0555cd7.js.gz +0 -0
  196. codex/static_root/assets/pager-full-pdf-BH54_5f6.js +1 -0
  197. codex/static_root/assets/pager-full-pdf-BH54_5f6.js.br +0 -0
  198. codex/static_root/assets/pager-full-pdf-BH54_5f6.js.gz +0 -0
  199. codex/static_root/assets/{pagination-toolbar-C45-onwe.4722195bb60b.css → pagination-toolbar-DGrxp2OD.css} +1 -1
  200. codex/static_root/assets/pagination-toolbar-DGrxp2OD.css.br +0 -0
  201. codex/static_root/assets/pagination-toolbar-DGrxp2OD.css.gz +0 -0
  202. codex/static_root/assets/{pagination-toolbar-C45-onwe.css → pagination-toolbar-DGrxp2OD.f960a624f04f.css} +1 -1
  203. codex/static_root/assets/pagination-toolbar-DGrxp2OD.f960a624f04f.css.br +0 -0
  204. codex/static_root/assets/pagination-toolbar-DGrxp2OD.f960a624f04f.css.gz +0 -0
  205. codex/static_root/assets/{pagination-toolbar-CkIzuyx1.00c354a82e6e.js → pagination-toolbar-JH3Kbffr.451171f7ac33.js} +1 -1
  206. codex/static_root/assets/pagination-toolbar-JH3Kbffr.451171f7ac33.js.br +0 -0
  207. codex/static_root/assets/pagination-toolbar-JH3Kbffr.451171f7ac33.js.gz +0 -0
  208. codex/static_root/assets/{pagination-toolbar-CkIzuyx1.js → pagination-toolbar-JH3Kbffr.js} +1 -1
  209. codex/static_root/assets/pagination-toolbar-JH3Kbffr.js.br +0 -0
  210. codex/static_root/assets/pagination-toolbar-JH3Kbffr.js.gz +0 -0
  211. codex/static_root/assets/{pdf-doc-BjlRPuf4.0f056fa321b6.js → pdf-doc-BcRNpoaW.882f6e967869.js} +1 -1
  212. codex/static_root/assets/pdf-doc-BcRNpoaW.882f6e967869.js.br +0 -0
  213. codex/static_root/assets/pdf-doc-BcRNpoaW.882f6e967869.js.gz +0 -0
  214. codex/static_root/assets/{pdf-doc-BjlRPuf4.js → pdf-doc-BcRNpoaW.js} +1 -1
  215. codex/static_root/assets/pdf-doc-BcRNpoaW.js.br +0 -0
  216. codex/static_root/assets/pdf-doc-BcRNpoaW.js.gz +0 -0
  217. codex/static_root/assets/reader-DIPRg2VB.900370e097aa.js +2 -0
  218. codex/static_root/assets/reader-DIPRg2VB.900370e097aa.js.br +0 -0
  219. codex/static_root/assets/reader-DIPRg2VB.900370e097aa.js.gz +0 -0
  220. codex/static_root/assets/reader-DIPRg2VB.js +2 -0
  221. codex/static_root/assets/reader-DIPRg2VB.js.br +0 -0
  222. codex/static_root/assets/reader-DIPRg2VB.js.gz +0 -0
  223. codex/static_root/assets/reader-DveZZr9x.7b21b9f8f628.css +1 -0
  224. codex/static_root/assets/reader-DveZZr9x.7b21b9f8f628.css.br +0 -0
  225. codex/static_root/assets/reader-DveZZr9x.7b21b9f8f628.css.gz +0 -0
  226. codex/static_root/assets/reader-DveZZr9x.css +1 -0
  227. codex/static_root/assets/reader-DveZZr9x.css.br +0 -0
  228. codex/static_root/assets/reader-DveZZr9x.css.gz +0 -0
  229. codex/static_root/assets/{relation-chips-CUSatZET.js → relation-chips-3tTlR5QH.71f18bc92adc.js} +1 -1
  230. codex/static_root/assets/relation-chips-3tTlR5QH.71f18bc92adc.js.br +0 -0
  231. codex/static_root/assets/relation-chips-3tTlR5QH.71f18bc92adc.js.gz +0 -0
  232. codex/static_root/assets/{relation-chips-CUSatZET.a58031ff8141.js → relation-chips-3tTlR5QH.js} +1 -1
  233. codex/static_root/assets/relation-chips-3tTlR5QH.js.br +0 -0
  234. codex/static_root/assets/relation-chips-3tTlR5QH.js.gz +0 -0
  235. codex/static_root/assets/{settings-drawer-BSHTAsJ9.js → settings-drawer-BdyFhA_r.151f4c3061ed.js} +2 -2
  236. codex/static_root/assets/settings-drawer-BdyFhA_r.151f4c3061ed.js.br +0 -0
  237. codex/static_root/assets/settings-drawer-BdyFhA_r.151f4c3061ed.js.gz +0 -0
  238. codex/static_root/assets/{settings-drawer-BSHTAsJ9.1d07ce084fc9.js → settings-drawer-BdyFhA_r.js} +2 -2
  239. codex/static_root/assets/settings-drawer-BdyFhA_r.js.br +0 -0
  240. codex/static_root/assets/settings-drawer-BdyFhA_r.js.gz +0 -0
  241. codex/static_root/assets/stats-tab-DeKjwvmh.7557c71b4cfa.js +1 -0
  242. codex/static_root/assets/stats-tab-DeKjwvmh.7557c71b4cfa.js.br +0 -0
  243. codex/static_root/assets/stats-tab-DeKjwvmh.7557c71b4cfa.js.gz +0 -0
  244. codex/static_root/assets/stats-tab-DeKjwvmh.js +1 -0
  245. codex/static_root/assets/stats-tab-DeKjwvmh.js.br +0 -0
  246. codex/static_root/assets/stats-tab-DeKjwvmh.js.gz +0 -0
  247. codex/static_root/assets/{stats-tab-BXGisn5N.9b7a71e7fe28.css → stats-tab-DpfGsVX3.1cb66f2649a9.css} +1 -1
  248. codex/static_root/assets/stats-tab-DpfGsVX3.1cb66f2649a9.css.br +0 -0
  249. codex/static_root/assets/stats-tab-DpfGsVX3.1cb66f2649a9.css.gz +0 -0
  250. codex/static_root/assets/{stats-tab-BXGisn5N.css → stats-tab-DpfGsVX3.css} +1 -1
  251. codex/static_root/assets/stats-tab-DpfGsVX3.css.br +0 -0
  252. codex/static_root/assets/stats-tab-DpfGsVX3.css.gz +0 -0
  253. codex/static_root/assets/{task-tab-NORFAa9p.97e07e366ddd.js → task-tab-C8D0qRP_.d4ab174c69a3.js} +1 -1
  254. codex/static_root/assets/task-tab-C8D0qRP_.d4ab174c69a3.js.br +0 -0
  255. codex/static_root/assets/task-tab-C8D0qRP_.d4ab174c69a3.js.gz +0 -0
  256. codex/static_root/assets/{task-tab-NORFAa9p.js → task-tab-C8D0qRP_.js} +1 -1
  257. codex/static_root/assets/task-tab-C8D0qRP_.js.br +0 -0
  258. codex/static_root/assets/task-tab-C8D0qRP_.js.gz +0 -0
  259. codex/static_root/assets/unauthorized-BHgBRF4z.8e5c1b9e52b0.js +1 -0
  260. codex/static_root/assets/unauthorized-BHgBRF4z.8e5c1b9e52b0.js.br +0 -0
  261. codex/static_root/assets/unauthorized-BHgBRF4z.8e5c1b9e52b0.js.gz +0 -0
  262. codex/static_root/assets/unauthorized-BHgBRF4z.js +1 -0
  263. codex/static_root/assets/unauthorized-BHgBRF4z.js.br +0 -0
  264. codex/static_root/assets/unauthorized-BHgBRF4z.js.gz +0 -0
  265. codex/static_root/assets/{unauthorized-muDsv-rC.css → unauthorized-BjhIc97q.728511c92cd0.css} +1 -1
  266. codex/static_root/assets/unauthorized-BjhIc97q.728511c92cd0.css.br +0 -0
  267. codex/static_root/assets/unauthorized-BjhIc97q.728511c92cd0.css.gz +0 -0
  268. codex/static_root/assets/{unauthorized-muDsv-rC.2166ec8b3a32.css → unauthorized-BjhIc97q.css} +1 -1
  269. codex/static_root/assets/unauthorized-BjhIc97q.css.br +0 -0
  270. codex/static_root/assets/unauthorized-BjhIc97q.css.gz +0 -0
  271. codex/static_root/assets/{user-tab-CHbyN1EW.e7206db08680.js → user-tab-BU31Z92y.35613c713faf.js} +1 -1
  272. codex/static_root/assets/user-tab-BU31Z92y.35613c713faf.js.br +0 -0
  273. codex/static_root/assets/user-tab-BU31Z92y.35613c713faf.js.gz +0 -0
  274. codex/static_root/assets/{user-tab-CHbyN1EW.js → user-tab-BU31Z92y.js} +1 -1
  275. codex/static_root/assets/user-tab-BU31Z92y.js.br +0 -0
  276. codex/static_root/assets/user-tab-BU31Z92y.js.gz +0 -0
  277. codex/static_root/js/choices-admin.8aa64c911203.json +1 -0
  278. codex/static_root/js/choices-admin.8aa64c911203.json.br +0 -0
  279. codex/static_root/js/choices-admin.8aa64c911203.json.gz +0 -0
  280. codex/static_root/js/choices-admin.json +1 -1
  281. codex/static_root/js/choices-admin.json.br +0 -0
  282. codex/static_root/js/choices-admin.json.gz +0 -0
  283. codex/static_root/manifest.45cb46e19686.json +635 -0
  284. codex/static_root/manifest.45cb46e19686.json.br +0 -0
  285. codex/static_root/manifest.45cb46e19686.json.gz +0 -0
  286. codex/static_root/manifest.json +248 -248
  287. codex/static_root/manifest.json.br +0 -0
  288. codex/static_root/manifest.json.gz +0 -0
  289. codex/static_root/staticfiles.json +1 -1
  290. codex/status_controller.py +10 -4
  291. codex/views/admin/tasks.py +46 -40
  292. codex/views/auth.py +15 -4
  293. codex/views/browser/annotations.py +94 -51
  294. codex/views/browser/base.py +1 -2
  295. codex/views/browser/browser.py +52 -21
  296. codex/views/browser/cover.py +8 -4
  297. codex/views/browser/filters/annotations.py +28 -6
  298. codex/views/browser/filters/search/__init__.py +1 -0
  299. codex/views/browser/filters/search/aliases.py +81 -0
  300. codex/views/browser/filters/search/field/__init__.py +1 -0
  301. codex/views/browser/filters/search/field/column.py +53 -0
  302. codex/views/browser/filters/search/field/expression.py +177 -0
  303. codex/views/browser/filters/search/field/filter.py +70 -0
  304. codex/views/browser/filters/search/field/optimize.py +71 -0
  305. codex/views/browser/filters/search/field/parse.py +187 -0
  306. codex/views/browser/filters/search/fts.py +38 -0
  307. codex/views/browser/filters/search/parse.py +220 -0
  308. codex/views/browser/order_by.py +8 -2
  309. codex/views/browser/page_in_bounds.py +88 -0
  310. codex/views/browser/paginate.py +28 -113
  311. codex/views/const.py +0 -1
  312. codex/views/opds/v1/feed.py +2 -1
  313. codex/views/opds/v2/feed.py +2 -1
  314. codex/views/opds/v2/links.py +1 -1
  315. codex/views/reader/books.py +1 -1
  316. codex/views/reader/page.py +3 -3
  317. {codex-1.6.19.dist-info → codex-1.7.0a0.dist-info}/METADATA +18 -10
  318. {codex-1.6.19.dist-info → codex-1.7.0a0.dist-info}/RECORD +321 -372
  319. codex/_vendor/__init__.py +0 -0
  320. codex/_vendor/django_haystack-3.2.1.dist-info/AUTHORS +0 -127
  321. codex/_vendor/django_haystack-3.2.1.dist-info/INSTALLER +0 -1
  322. codex/_vendor/django_haystack-3.2.1.dist-info/LICENSE +0 -31
  323. codex/_vendor/django_haystack-3.2.1.dist-info/METADATA +0 -98
  324. codex/_vendor/django_haystack-3.2.1.dist-info/RECORD +0 -97
  325. codex/_vendor/django_haystack-3.2.1.dist-info/REQUESTED +0 -0
  326. codex/_vendor/django_haystack-3.2.1.dist-info/WHEEL +0 -5
  327. codex/_vendor/django_haystack-3.2.1.dist-info/top_level.txt +0 -1
  328. codex/_vendor/haystack/__init__.py +0 -80
  329. codex/_vendor/haystack/admin.py +0 -163
  330. codex/_vendor/haystack/apps.py +0 -31
  331. codex/_vendor/haystack/backends/__init__.py +0 -1109
  332. codex/_vendor/haystack/backends/elasticsearch2_backend.py +0 -390
  333. codex/_vendor/haystack/backends/elasticsearch5_backend.py +0 -483
  334. codex/_vendor/haystack/backends/elasticsearch7_backend.py +0 -586
  335. codex/_vendor/haystack/backends/elasticsearch_backend.py +0 -1141
  336. codex/_vendor/haystack/backends/simple_backend.py +0 -132
  337. codex/_vendor/haystack/backends/solr_backend.py +0 -991
  338. codex/_vendor/haystack/backends/whoosh_backend.py +0 -1104
  339. codex/_vendor/haystack/constants.py +0 -57
  340. codex/_vendor/haystack/exceptions.py +0 -57
  341. codex/_vendor/haystack/fields.py +0 -562
  342. codex/_vendor/haystack/forms.py +0 -135
  343. codex/_vendor/haystack/generic_views.py +0 -141
  344. codex/_vendor/haystack/indexes.py +0 -548
  345. codex/_vendor/haystack/inputs.py +0 -169
  346. codex/_vendor/haystack/management/__init__.py +0 -0
  347. codex/_vendor/haystack/management/commands/__init__.py +0 -0
  348. codex/_vendor/haystack/management/commands/build_solr_schema.py +0 -187
  349. codex/_vendor/haystack/management/commands/clear_index.py +0 -67
  350. codex/_vendor/haystack/management/commands/haystack_info.py +0 -22
  351. codex/_vendor/haystack/management/commands/rebuild_index.py +0 -65
  352. codex/_vendor/haystack/management/commands/update_index.py +0 -438
  353. codex/_vendor/haystack/manager.py +0 -105
  354. codex/_vendor/haystack/models.py +0 -257
  355. codex/_vendor/haystack/panels.py +0 -89
  356. codex/_vendor/haystack/query.py +0 -771
  357. codex/_vendor/haystack/routers.py +0 -14
  358. codex/_vendor/haystack/signals.py +0 -88
  359. codex/_vendor/haystack/templates/panels/haystack.html +0 -33
  360. codex/_vendor/haystack/templates/search_configuration/schema.xml +0 -1056
  361. codex/_vendor/haystack/templates/search_configuration/solrconfig.xml +0 -1446
  362. codex/_vendor/haystack/templatetags/__init__.py +0 -0
  363. codex/_vendor/haystack/templatetags/highlight.py +0 -131
  364. codex/_vendor/haystack/templatetags/more_like_this.py +0 -119
  365. codex/_vendor/haystack/urls.py +0 -5
  366. codex/_vendor/haystack/utils/__init__.py +0 -84
  367. codex/_vendor/haystack/utils/app_loading.py +0 -34
  368. codex/_vendor/haystack/utils/geo.py +0 -71
  369. codex/_vendor/haystack/utils/highlighting.py +0 -165
  370. codex/_vendor/haystack/utils/loading.py +0 -374
  371. codex/_vendor/haystack/utils/log.py +0 -21
  372. codex/_vendor/haystack/version.py +0 -16
  373. codex/_vendor/haystack/views.py +0 -253
  374. codex/integrity.py +0 -492
  375. codex/librarian/search/merge.py +0 -86
  376. codex/librarian/search/version.py +0 -63
  377. codex/search/__init__.py +0 -1
  378. codex/search/backend.py +0 -524
  379. codex/search/backend_search.py +0 -229
  380. codex/search/engine.py +0 -33
  381. codex/search/indexes.py +0 -79
  382. codex/search/query.py +0 -67
  383. codex/search/writing.py +0 -180
  384. codex/static_root/assets/VCheckbox-Cko1sQK-.170726b3b8a1.js.br +0 -0
  385. codex/static_root/assets/VCheckbox-Cko1sQK-.170726b3b8a1.js.gz +0 -0
  386. codex/static_root/assets/VCheckbox-Cko1sQK-.js.br +0 -0
  387. codex/static_root/assets/VCheckbox-Cko1sQK-.js.gz +0 -0
  388. codex/static_root/assets/VCheckboxBtn-CO3-aqQL.c9b8062fd66f.js.br +0 -0
  389. codex/static_root/assets/VCheckboxBtn-CO3-aqQL.c9b8062fd66f.js.gz +0 -0
  390. codex/static_root/assets/VCheckboxBtn-CO3-aqQL.js.br +0 -0
  391. codex/static_root/assets/VCheckboxBtn-CO3-aqQL.js.gz +0 -0
  392. codex/static_root/assets/VCombobox-Bjp6lffE.caf59fba486e.js.br +0 -0
  393. codex/static_root/assets/VCombobox-Bjp6lffE.caf59fba486e.js.gz +0 -0
  394. codex/static_root/assets/VCombobox-Bjp6lffE.js.br +0 -0
  395. codex/static_root/assets/VCombobox-Bjp6lffE.js.gz +0 -0
  396. codex/static_root/assets/VDialog-BkpVGB70.6eea3ca762a5.js.br +0 -0
  397. codex/static_root/assets/VDialog-BkpVGB70.6eea3ca762a5.js.gz +0 -0
  398. codex/static_root/assets/VDialog-BkpVGB70.js.br +0 -0
  399. codex/static_root/assets/VDialog-BkpVGB70.js.gz +0 -0
  400. codex/static_root/assets/VExpansionPanels-Ci2-j8XX.9c55db4ef49e.js.br +0 -0
  401. codex/static_root/assets/VExpansionPanels-Ci2-j8XX.9c55db4ef49e.js.gz +0 -0
  402. codex/static_root/assets/VExpansionPanels-Ci2-j8XX.js.br +0 -0
  403. codex/static_root/assets/VExpansionPanels-Ci2-j8XX.js.gz +0 -0
  404. codex/static_root/assets/VRadioGroup-D3Py5BfQ.a4f25edb86d3.js.br +0 -0
  405. codex/static_root/assets/VRadioGroup-D3Py5BfQ.a4f25edb86d3.js.gz +0 -0
  406. codex/static_root/assets/VRadioGroup-D3Py5BfQ.js.br +0 -0
  407. codex/static_root/assets/VRadioGroup-D3Py5BfQ.js.gz +0 -0
  408. codex/static_root/assets/VSelect-DtvZsYZz.e98745d858eb.js.br +0 -0
  409. codex/static_root/assets/VSelect-DtvZsYZz.e98745d858eb.js.gz +0 -0
  410. codex/static_root/assets/VSelect-DtvZsYZz.js.br +0 -0
  411. codex/static_root/assets/VSelect-DtvZsYZz.js.gz +0 -0
  412. codex/static_root/assets/VSelectionControl-D6kmykQW.1e1fda62ceba.js.br +0 -0
  413. codex/static_root/assets/VSelectionControl-D6kmykQW.1e1fda62ceba.js.gz +0 -0
  414. codex/static_root/assets/VSelectionControl-D6kmykQW.js.br +0 -0
  415. codex/static_root/assets/VSelectionControl-D6kmykQW.js.gz +0 -0
  416. codex/static_root/assets/VSlideGroup-UGZWxpF5.360bdefcd215.js.br +0 -0
  417. codex/static_root/assets/VSlideGroup-UGZWxpF5.360bdefcd215.js.gz +0 -0
  418. codex/static_root/assets/VSlideGroup-UGZWxpF5.js.br +0 -0
  419. codex/static_root/assets/VSlideGroup-UGZWxpF5.js.gz +0 -0
  420. codex/static_root/assets/VTable-DXH_yQ0o.1149f56ad566.js.br +0 -0
  421. codex/static_root/assets/VTable-DXH_yQ0o.1149f56ad566.js.gz +0 -0
  422. codex/static_root/assets/VTable-DXH_yQ0o.js.br +0 -0
  423. codex/static_root/assets/VTable-DXH_yQ0o.js.gz +0 -0
  424. codex/static_root/assets/VTextField-Blqy56_L.26d8761e0155.js.br +0 -0
  425. codex/static_root/assets/VTextField-Blqy56_L.26d8761e0155.js.gz +0 -0
  426. codex/static_root/assets/VTextField-Blqy56_L.js.br +0 -0
  427. codex/static_root/assets/VTextField-Blqy56_L.js.gz +0 -0
  428. codex/static_root/assets/VWindowItem-C8_ovV7e.afbc08414765.js.br +0 -0
  429. codex/static_root/assets/VWindowItem-C8_ovV7e.afbc08414765.js.gz +0 -0
  430. codex/static_root/assets/VWindowItem-C8_ovV7e.js.br +0 -0
  431. codex/static_root/assets/VWindowItem-C8_ovV7e.js.gz +0 -0
  432. codex/static_root/assets/admin-DTRn8bBs.d031ea42d8d0.js.br +0 -0
  433. codex/static_root/assets/admin-DTRn8bBs.d031ea42d8d0.js.gz +0 -0
  434. codex/static_root/assets/admin-DTRn8bBs.js.br +0 -0
  435. codex/static_root/assets/admin-DTRn8bBs.js.gz +0 -0
  436. codex/static_root/assets/admin-drawer-panel-BaXFOcru.b1a44b05f482.js.br +0 -0
  437. codex/static_root/assets/admin-drawer-panel-BaXFOcru.b1a44b05f482.js.gz +0 -0
  438. codex/static_root/assets/admin-drawer-panel-BaXFOcru.js.br +0 -0
  439. codex/static_root/assets/admin-drawer-panel-BaXFOcru.js.gz +0 -0
  440. codex/static_root/assets/browser-BBpUyGmz.c1cb9e2d463b.js +0 -1
  441. codex/static_root/assets/browser-BBpUyGmz.c1cb9e2d463b.js.br +0 -0
  442. codex/static_root/assets/browser-BBpUyGmz.c1cb9e2d463b.js.gz +0 -0
  443. codex/static_root/assets/browser-BBpUyGmz.js +0 -1
  444. codex/static_root/assets/browser-BBpUyGmz.js.br +0 -0
  445. codex/static_root/assets/browser-BBpUyGmz.js.gz +0 -0
  446. codex/static_root/assets/browser-Binc3n9H.313722d27ca3.css +0 -1
  447. codex/static_root/assets/browser-Binc3n9H.313722d27ca3.css.br +0 -0
  448. codex/static_root/assets/browser-Binc3n9H.313722d27ca3.css.gz +0 -0
  449. codex/static_root/assets/browser-Binc3n9H.css +0 -1
  450. codex/static_root/assets/browser-Binc3n9H.css.br +0 -0
  451. codex/static_root/assets/browser-Binc3n9H.css.gz +0 -0
  452. codex/static_root/assets/change-password-dialog-Cebek1-c.aff2975e31f8.css +0 -1
  453. codex/static_root/assets/change-password-dialog-Cebek1-c.aff2975e31f8.css.br +0 -0
  454. codex/static_root/assets/change-password-dialog-Cebek1-c.aff2975e31f8.css.gz +0 -0
  455. codex/static_root/assets/change-password-dialog-Cebek1-c.css +0 -1
  456. codex/static_root/assets/change-password-dialog-Cebek1-c.css.br +0 -0
  457. codex/static_root/assets/change-password-dialog-Cebek1-c.css.gz +0 -0
  458. codex/static_root/assets/change-password-dialog-DlHoLhDF.76ab4a29bb9b.js +0 -1
  459. codex/static_root/assets/change-password-dialog-DlHoLhDF.76ab4a29bb9b.js.br +0 -0
  460. codex/static_root/assets/change-password-dialog-DlHoLhDF.76ab4a29bb9b.js.gz +0 -0
  461. codex/static_root/assets/change-password-dialog-DlHoLhDF.js +0 -1
  462. codex/static_root/assets/change-password-dialog-DlHoLhDF.js.br +0 -0
  463. codex/static_root/assets/change-password-dialog-DlHoLhDF.js.gz +0 -0
  464. codex/static_root/assets/confirm-dialog-D3_s2DQy.9dde66036c72.js.br +0 -0
  465. codex/static_root/assets/confirm-dialog-D3_s2DQy.9dde66036c72.js.gz +0 -0
  466. codex/static_root/assets/confirm-dialog-D3_s2DQy.js.br +0 -0
  467. codex/static_root/assets/confirm-dialog-D3_s2DQy.js.gz +0 -0
  468. codex/static_root/assets/datetime-column-BxC1Li9e.3dc314b3295d.js.br +0 -0
  469. codex/static_root/assets/datetime-column-BxC1Li9e.3dc314b3295d.js.gz +0 -0
  470. codex/static_root/assets/datetime-column-BxC1Li9e.js.br +0 -0
  471. codex/static_root/assets/datetime-column-BxC1Li9e.js.gz +0 -0
  472. codex/static_root/assets/filter-Bzk7x841.d0c3b602e62d.js.br +0 -0
  473. codex/static_root/assets/filter-Bzk7x841.d0c3b602e62d.js.gz +0 -0
  474. codex/static_root/assets/filter-Bzk7x841.js.br +0 -0
  475. codex/static_root/assets/filter-Bzk7x841.js.gz +0 -0
  476. codex/static_root/assets/flag-tab-C2bI5zq3.8f8c297600a6.js +0 -1
  477. codex/static_root/assets/flag-tab-C2bI5zq3.8f8c297600a6.js.br +0 -0
  478. codex/static_root/assets/flag-tab-C2bI5zq3.8f8c297600a6.js.gz +0 -0
  479. codex/static_root/assets/flag-tab-C2bI5zq3.js +0 -1
  480. codex/static_root/assets/flag-tab-C2bI5zq3.js.br +0 -0
  481. codex/static_root/assets/flag-tab-C2bI5zq3.js.gz +0 -0
  482. codex/static_root/assets/group-tab-5esH4lwX.a81fdd258117.js.br +0 -0
  483. codex/static_root/assets/group-tab-5esH4lwX.a81fdd258117.js.gz +0 -0
  484. codex/static_root/assets/group-tab-5esH4lwX.js.br +0 -0
  485. codex/static_root/assets/group-tab-5esH4lwX.js.gz +0 -0
  486. codex/static_root/assets/http-error-BTOBfcGY.52ced6f28420.js.br +0 -0
  487. codex/static_root/assets/http-error-BTOBfcGY.52ced6f28420.js.gz +0 -0
  488. codex/static_root/assets/http-error-BTOBfcGY.js.br +0 -0
  489. codex/static_root/assets/http-error-BTOBfcGY.js.gz +0 -0
  490. codex/static_root/assets/library-tab-DiPEdOF7.ece2d642a9dc.js.br +0 -0
  491. codex/static_root/assets/library-tab-DiPEdOF7.ece2d642a9dc.js.gz +0 -0
  492. codex/static_root/assets/library-tab-DiPEdOF7.js.br +0 -0
  493. codex/static_root/assets/library-tab-DiPEdOF7.js.gz +0 -0
  494. codex/static_root/assets/main-BPIPjzsc.66def8991e38.js +0 -6
  495. codex/static_root/assets/main-BPIPjzsc.66def8991e38.js.br +0 -0
  496. codex/static_root/assets/main-BPIPjzsc.66def8991e38.js.gz +0 -0
  497. codex/static_root/assets/main-BPIPjzsc.js +0 -6
  498. codex/static_root/assets/main-BPIPjzsc.js.br +0 -0
  499. codex/static_root/assets/main-BPIPjzsc.js.gz +0 -0
  500. codex/static_root/assets/pager-full-pdf-CiyC6Al9.278b9978f36f.js +0 -1
  501. codex/static_root/assets/pager-full-pdf-CiyC6Al9.278b9978f36f.js.br +0 -0
  502. codex/static_root/assets/pager-full-pdf-CiyC6Al9.278b9978f36f.js.gz +0 -0
  503. codex/static_root/assets/pager-full-pdf-CiyC6Al9.js +0 -1
  504. codex/static_root/assets/pager-full-pdf-CiyC6Al9.js.br +0 -0
  505. codex/static_root/assets/pager-full-pdf-CiyC6Al9.js.gz +0 -0
  506. codex/static_root/assets/pagination-toolbar-C45-onwe.4722195bb60b.css.br +0 -0
  507. codex/static_root/assets/pagination-toolbar-C45-onwe.4722195bb60b.css.gz +0 -0
  508. codex/static_root/assets/pagination-toolbar-C45-onwe.css.br +0 -0
  509. codex/static_root/assets/pagination-toolbar-C45-onwe.css.gz +0 -0
  510. codex/static_root/assets/pagination-toolbar-CkIzuyx1.00c354a82e6e.js.br +0 -0
  511. codex/static_root/assets/pagination-toolbar-CkIzuyx1.00c354a82e6e.js.gz +0 -0
  512. codex/static_root/assets/pagination-toolbar-CkIzuyx1.js.br +0 -0
  513. codex/static_root/assets/pagination-toolbar-CkIzuyx1.js.gz +0 -0
  514. codex/static_root/assets/pdf-doc-BjlRPuf4.0f056fa321b6.js.br +0 -0
  515. codex/static_root/assets/pdf-doc-BjlRPuf4.0f056fa321b6.js.gz +0 -0
  516. codex/static_root/assets/pdf-doc-BjlRPuf4.js.br +0 -0
  517. codex/static_root/assets/pdf-doc-BjlRPuf4.js.gz +0 -0
  518. codex/static_root/assets/reader-_stAWL-4.5089e9144795.css +0 -1
  519. codex/static_root/assets/reader-_stAWL-4.5089e9144795.css.br +0 -0
  520. codex/static_root/assets/reader-_stAWL-4.5089e9144795.css.gz +0 -0
  521. codex/static_root/assets/reader-_stAWL-4.css +0 -1
  522. codex/static_root/assets/reader-_stAWL-4.css.br +0 -0
  523. codex/static_root/assets/reader-_stAWL-4.css.gz +0 -0
  524. codex/static_root/assets/reader-ucxnLi2K.d0fbce0f03c1.js +0 -2
  525. codex/static_root/assets/reader-ucxnLi2K.d0fbce0f03c1.js.br +0 -0
  526. codex/static_root/assets/reader-ucxnLi2K.d0fbce0f03c1.js.gz +0 -0
  527. codex/static_root/assets/reader-ucxnLi2K.js +0 -2
  528. codex/static_root/assets/reader-ucxnLi2K.js.br +0 -0
  529. codex/static_root/assets/reader-ucxnLi2K.js.gz +0 -0
  530. codex/static_root/assets/relation-chips-CUSatZET.a58031ff8141.js.br +0 -0
  531. codex/static_root/assets/relation-chips-CUSatZET.a58031ff8141.js.gz +0 -0
  532. codex/static_root/assets/relation-chips-CUSatZET.js.br +0 -0
  533. codex/static_root/assets/relation-chips-CUSatZET.js.gz +0 -0
  534. codex/static_root/assets/settings-drawer-BSHTAsJ9.1d07ce084fc9.js.br +0 -0
  535. codex/static_root/assets/settings-drawer-BSHTAsJ9.1d07ce084fc9.js.gz +0 -0
  536. codex/static_root/assets/settings-drawer-BSHTAsJ9.js.br +0 -0
  537. codex/static_root/assets/settings-drawer-BSHTAsJ9.js.gz +0 -0
  538. codex/static_root/assets/stats-tab-BXGisn5N.9b7a71e7fe28.css.br +0 -0
  539. codex/static_root/assets/stats-tab-BXGisn5N.9b7a71e7fe28.css.gz +0 -0
  540. codex/static_root/assets/stats-tab-BXGisn5N.css.br +0 -0
  541. codex/static_root/assets/stats-tab-BXGisn5N.css.gz +0 -0
  542. codex/static_root/assets/stats-tab-DKXVB2Cc.0a3de58bf5ee.js +0 -1
  543. codex/static_root/assets/stats-tab-DKXVB2Cc.0a3de58bf5ee.js.br +0 -0
  544. codex/static_root/assets/stats-tab-DKXVB2Cc.0a3de58bf5ee.js.gz +0 -0
  545. codex/static_root/assets/stats-tab-DKXVB2Cc.js +0 -1
  546. codex/static_root/assets/stats-tab-DKXVB2Cc.js.br +0 -0
  547. codex/static_root/assets/stats-tab-DKXVB2Cc.js.gz +0 -0
  548. codex/static_root/assets/task-tab-NORFAa9p.97e07e366ddd.js.br +0 -0
  549. codex/static_root/assets/task-tab-NORFAa9p.97e07e366ddd.js.gz +0 -0
  550. codex/static_root/assets/task-tab-NORFAa9p.js.br +0 -0
  551. codex/static_root/assets/task-tab-NORFAa9p.js.gz +0 -0
  552. codex/static_root/assets/unauthorized-RHaQZRfr.a234e9403f60.js +0 -1
  553. codex/static_root/assets/unauthorized-RHaQZRfr.a234e9403f60.js.br +0 -0
  554. codex/static_root/assets/unauthorized-RHaQZRfr.a234e9403f60.js.gz +0 -0
  555. codex/static_root/assets/unauthorized-RHaQZRfr.js +0 -1
  556. codex/static_root/assets/unauthorized-RHaQZRfr.js.br +0 -0
  557. codex/static_root/assets/unauthorized-RHaQZRfr.js.gz +0 -0
  558. codex/static_root/assets/unauthorized-muDsv-rC.2166ec8b3a32.css.br +0 -0
  559. codex/static_root/assets/unauthorized-muDsv-rC.2166ec8b3a32.css.gz +0 -0
  560. codex/static_root/assets/unauthorized-muDsv-rC.css.br +0 -0
  561. codex/static_root/assets/unauthorized-muDsv-rC.css.gz +0 -0
  562. codex/static_root/assets/user-tab-CHbyN1EW.e7206db08680.js.br +0 -0
  563. codex/static_root/assets/user-tab-CHbyN1EW.e7206db08680.js.gz +0 -0
  564. codex/static_root/assets/user-tab-CHbyN1EW.js.br +0 -0
  565. codex/static_root/assets/user-tab-CHbyN1EW.js.gz +0 -0
  566. codex/static_root/js/choices-admin.1a20a5648f20.json +0 -1
  567. codex/static_root/js/choices-admin.1a20a5648f20.json.br +0 -0
  568. codex/static_root/js/choices-admin.1a20a5648f20.json.gz +0 -0
  569. codex/static_root/manifest.0fe31a6cb99f.json +0 -635
  570. codex/static_root/manifest.0fe31a6cb99f.json.br +0 -0
  571. codex/static_root/manifest.0fe31a6cb99f.json.gz +0 -0
  572. codex/templates/search/indexes/codex/comic_text.txt +0 -47
  573. codex/views/browser/filters/search.py +0 -196
  574. {codex-1.6.19.dist-info → codex-1.7.0a0.dist-info}/LICENSE +0 -0
  575. {codex-1.6.19.dist-info → codex-1.7.0a0.dist-info}/WHEEL +0 -0
  576. {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