codex 1.6.0a5__py3-none-any.whl → 1.6.0a7__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 (564) hide show
  1. codex/exceptions.py +1 -0
  2. codex/librarian/covers/purge.py +1 -2
  3. codex/librarian/importer/aggregate_metadata.py +2 -10
  4. codex/librarian/importer/cache.py +47 -0
  5. codex/librarian/importer/const.py +8 -0
  6. codex/librarian/importer/deleted.py +45 -7
  7. codex/librarian/importer/importerd.py +11 -1
  8. codex/librarian/importer/update_comics.py +0 -2
  9. codex/librarian/janitor/janitor.py +1 -1
  10. codex/librarian/librariand.py +1 -1
  11. codex/migrations/0029_choices_adminflag_and_timestamp.py +44 -0
  12. codex/models/admin.py +0 -1
  13. codex/permissions.py +1 -1
  14. codex/serializers/admin.py +1 -2
  15. codex/serializers/browser/filters.py +8 -101
  16. codex/serializers/browser/mixins.py +20 -3
  17. codex/serializers/browser/page.py +2 -3
  18. codex/serializers/browser/settings.py +2 -0
  19. codex/serializers/fields.py +123 -0
  20. codex/serializers/mtime.py +25 -0
  21. codex/serializers/opds/v2.py +7 -11
  22. codex/serializers/opds/v2_unused.py +5 -3
  23. codex/serializers/reader.py +5 -2
  24. codex/serializers/route.py +7 -3
  25. codex/static_root/assets/VCheckbox-BOUtyxuo.c690f0cdbe48.js +1 -0
  26. codex/static_root/assets/VCheckbox-BOUtyxuo.c690f0cdbe48.js.br +0 -0
  27. codex/static_root/assets/VCheckbox-BOUtyxuo.c690f0cdbe48.js.gz +0 -0
  28. codex/static_root/assets/VCheckbox-BOUtyxuo.js +1 -0
  29. codex/static_root/assets/VCheckbox-BOUtyxuo.js.br +0 -0
  30. codex/static_root/assets/VCheckbox-BOUtyxuo.js.gz +0 -0
  31. codex/static_root/assets/VCheckboxBtn-B1-m5pEh.e2b8cbdb92c9.js +1 -0
  32. codex/static_root/assets/VCheckboxBtn-B1-m5pEh.e2b8cbdb92c9.js.br +0 -0
  33. codex/static_root/assets/VCheckboxBtn-B1-m5pEh.e2b8cbdb92c9.js.gz +0 -0
  34. codex/static_root/assets/VCheckboxBtn-B1-m5pEh.js +1 -0
  35. codex/static_root/assets/VCheckboxBtn-B1-m5pEh.js.br +0 -0
  36. codex/static_root/assets/VCheckboxBtn-B1-m5pEh.js.gz +0 -0
  37. codex/static_root/assets/VCombobox-DjkDXe33.22617ea193b1.js +1 -0
  38. codex/static_root/assets/VCombobox-DjkDXe33.22617ea193b1.js.br +0 -0
  39. codex/static_root/assets/VCombobox-DjkDXe33.22617ea193b1.js.gz +0 -0
  40. codex/static_root/assets/VCombobox-DjkDXe33.js +1 -0
  41. codex/static_root/assets/VCombobox-DjkDXe33.js.br +0 -0
  42. codex/static_root/assets/VCombobox-DjkDXe33.js.gz +0 -0
  43. codex/static_root/assets/VDialog-X0zn9AGX.0d89c749b9a6.js +1 -0
  44. codex/static_root/assets/VDialog-X0zn9AGX.0d89c749b9a6.js.br +0 -0
  45. codex/static_root/assets/VDialog-X0zn9AGX.0d89c749b9a6.js.gz +0 -0
  46. codex/static_root/assets/VDialog-X0zn9AGX.js +1 -0
  47. codex/static_root/assets/VDialog-X0zn9AGX.js.br +0 -0
  48. codex/static_root/assets/VDialog-X0zn9AGX.js.gz +0 -0
  49. codex/static_root/assets/VExpansionPanels-CRevojaF.43d65c777c58.js +1 -0
  50. codex/static_root/assets/VExpansionPanels-CRevojaF.43d65c777c58.js.br +0 -0
  51. codex/static_root/assets/VExpansionPanels-CRevojaF.43d65c777c58.js.gz +0 -0
  52. codex/static_root/assets/VExpansionPanels-CRevojaF.js +1 -0
  53. codex/static_root/assets/VExpansionPanels-CRevojaF.js.br +0 -0
  54. codex/static_root/assets/VExpansionPanels-CRevojaF.js.gz +0 -0
  55. codex/static_root/assets/VRadioGroup-8nrrxjDv.2b4dfb984a8c.js +1 -0
  56. codex/static_root/assets/VRadioGroup-8nrrxjDv.2b4dfb984a8c.js.br +0 -0
  57. codex/static_root/assets/VRadioGroup-8nrrxjDv.2b4dfb984a8c.js.gz +0 -0
  58. codex/static_root/assets/VRadioGroup-8nrrxjDv.js +1 -0
  59. codex/static_root/assets/VRadioGroup-8nrrxjDv.js.br +0 -0
  60. codex/static_root/assets/VRadioGroup-8nrrxjDv.js.gz +0 -0
  61. codex/static_root/assets/VSelect-DgfIKRnC.fa7a73a9c415.js +1 -0
  62. codex/static_root/assets/VSelect-DgfIKRnC.fa7a73a9c415.js.br +0 -0
  63. codex/static_root/assets/VSelect-DgfIKRnC.fa7a73a9c415.js.gz +0 -0
  64. codex/static_root/assets/VSelect-DgfIKRnC.js +1 -0
  65. codex/static_root/assets/VSelect-DgfIKRnC.js.br +0 -0
  66. codex/static_root/assets/VSelect-DgfIKRnC.js.gz +0 -0
  67. codex/static_root/assets/VSelectionControl-CGu0oz0K.3030b7a270d6.js +1 -0
  68. codex/static_root/assets/VSelectionControl-CGu0oz0K.3030b7a270d6.js.br +0 -0
  69. codex/static_root/assets/VSelectionControl-CGu0oz0K.3030b7a270d6.js.gz +0 -0
  70. codex/static_root/assets/VSelectionControl-CGu0oz0K.js +1 -0
  71. codex/static_root/assets/VSelectionControl-CGu0oz0K.js.br +0 -0
  72. codex/static_root/assets/VSelectionControl-CGu0oz0K.js.gz +0 -0
  73. codex/static_root/assets/VSlideGroup-D_oNvCOd.71a7101cdddd.js +1 -0
  74. codex/static_root/assets/VSlideGroup-D_oNvCOd.71a7101cdddd.js.br +0 -0
  75. codex/static_root/assets/VSlideGroup-D_oNvCOd.71a7101cdddd.js.gz +0 -0
  76. codex/static_root/assets/VSlideGroup-D_oNvCOd.js +1 -0
  77. codex/static_root/assets/VSlideGroup-D_oNvCOd.js.br +0 -0
  78. codex/static_root/assets/VSlideGroup-D_oNvCOd.js.gz +0 -0
  79. codex/static_root/assets/{VTable-FKMTcdkF.6c1eed84d25e.js → VTable-C7pKI7gU.087912f706f5.js} +1 -1
  80. codex/static_root/assets/VTable-C7pKI7gU.087912f706f5.js.br +0 -0
  81. codex/static_root/assets/VTable-C7pKI7gU.087912f706f5.js.gz +0 -0
  82. codex/static_root/assets/{VTable-FKMTcdkF.js → VTable-C7pKI7gU.js} +1 -1
  83. codex/static_root/assets/VTable-C7pKI7gU.js.br +0 -0
  84. codex/static_root/assets/VTable-C7pKI7gU.js.gz +0 -0
  85. codex/static_root/assets/{VTextField-Dftcik85.js → VTextField-Bs8oq9mk.7999fef12a03.js} +1 -1
  86. codex/static_root/assets/VTextField-Bs8oq9mk.7999fef12a03.js.br +0 -0
  87. codex/static_root/assets/VTextField-Bs8oq9mk.7999fef12a03.js.gz +0 -0
  88. codex/static_root/assets/{VTextField-Dftcik85.cb2f1dda87e1.js → VTextField-Bs8oq9mk.js} +1 -1
  89. codex/static_root/assets/VTextField-Bs8oq9mk.js.br +0 -0
  90. codex/static_root/assets/VTextField-Bs8oq9mk.js.gz +0 -0
  91. codex/static_root/assets/{VWindowItem-CYCwnt3s.28025dc36c6a.js → VWindowItem-iyZ1XOkP.409f7d292253.js} +1 -1
  92. codex/static_root/assets/VWindowItem-iyZ1XOkP.409f7d292253.js.br +0 -0
  93. codex/static_root/assets/VWindowItem-iyZ1XOkP.409f7d292253.js.gz +0 -0
  94. codex/static_root/assets/{VWindowItem-CYCwnt3s.js → VWindowItem-iyZ1XOkP.js} +1 -1
  95. codex/static_root/assets/VWindowItem-iyZ1XOkP.js.br +0 -0
  96. codex/static_root/assets/VWindowItem-iyZ1XOkP.js.gz +0 -0
  97. codex/static_root/assets/admin-B0pCBjma.2407da79bd20.js +1 -0
  98. codex/static_root/assets/admin-B0pCBjma.2407da79bd20.js.br +0 -0
  99. codex/static_root/assets/admin-B0pCBjma.2407da79bd20.js.gz +0 -0
  100. codex/static_root/assets/admin-B0pCBjma.js +1 -0
  101. codex/static_root/assets/admin-B0pCBjma.js.br +0 -0
  102. codex/static_root/assets/admin-B0pCBjma.js.gz +0 -0
  103. codex/static_root/assets/admin-drawer-panel-BxoNUHQr.5e2e1a4a255d.js +30 -0
  104. codex/static_root/assets/admin-drawer-panel-BxoNUHQr.5e2e1a4a255d.js.br +0 -0
  105. codex/static_root/assets/admin-drawer-panel-BxoNUHQr.5e2e1a4a255d.js.gz +0 -0
  106. codex/static_root/assets/admin-drawer-panel-BxoNUHQr.js +30 -0
  107. codex/static_root/assets/admin-drawer-panel-BxoNUHQr.js.br +0 -0
  108. codex/static_root/assets/admin-drawer-panel-BxoNUHQr.js.gz +0 -0
  109. codex/static_root/assets/{admin-drawer-panel-BHdNPwzF.bd2c1d6148bb.css → admin-drawer-panel-o87BwD38.ce39427c8201.css} +1 -1
  110. codex/static_root/assets/admin-drawer-panel-o87BwD38.ce39427c8201.css.br +0 -0
  111. codex/static_root/assets/{admin-drawer-panel-BHdNPwzF.bd2c1d6148bb.css.gz → admin-drawer-panel-o87BwD38.ce39427c8201.css.gz} +0 -0
  112. codex/static_root/assets/{admin-drawer-panel-BHdNPwzF.css → admin-drawer-panel-o87BwD38.css} +1 -1
  113. codex/static_root/assets/admin-drawer-panel-o87BwD38.css.br +0 -0
  114. codex/static_root/assets/{admin-drawer-panel-BHdNPwzF.css.gz → admin-drawer-panel-o87BwD38.css.gz} +0 -0
  115. codex/static_root/assets/browser-DbyAjCNh.577a228a966b.js +1 -0
  116. codex/static_root/assets/browser-DbyAjCNh.577a228a966b.js.br +0 -0
  117. codex/static_root/assets/browser-DbyAjCNh.577a228a966b.js.gz +0 -0
  118. codex/static_root/assets/browser-DbyAjCNh.js +1 -0
  119. codex/static_root/assets/browser-DbyAjCNh.js.br +0 -0
  120. codex/static_root/assets/browser-DbyAjCNh.js.gz +0 -0
  121. codex/static_root/assets/browser-VnzNj1t3.cff861ad2ca3.css +1 -0
  122. codex/static_root/assets/browser-VnzNj1t3.cff861ad2ca3.css.br +0 -0
  123. codex/static_root/assets/browser-VnzNj1t3.cff861ad2ca3.css.gz +0 -0
  124. codex/static_root/assets/browser-VnzNj1t3.css +1 -0
  125. codex/static_root/assets/browser-VnzNj1t3.css.br +0 -0
  126. codex/static_root/assets/browser-VnzNj1t3.css.gz +0 -0
  127. codex/static_root/assets/{change-password-dialog-BIBG6Bk8.3583021c124a.js → change-password-dialog-DZ9dP8_F.930f4cae3cb0.js} +1 -1
  128. codex/static_root/assets/change-password-dialog-DZ9dP8_F.930f4cae3cb0.js.br +0 -0
  129. codex/static_root/assets/change-password-dialog-DZ9dP8_F.930f4cae3cb0.js.gz +0 -0
  130. codex/static_root/assets/{change-password-dialog-BIBG6Bk8.js → change-password-dialog-DZ9dP8_F.js} +1 -1
  131. codex/static_root/assets/change-password-dialog-DZ9dP8_F.js.br +0 -0
  132. codex/static_root/assets/change-password-dialog-DZ9dP8_F.js.gz +0 -0
  133. codex/static_root/assets/{confirm-dialog-DARLmXRf.53fc6938fb8c.js → confirm-dialog-RP7JNStF.a752a7c1e697.js} +1 -1
  134. codex/static_root/assets/confirm-dialog-RP7JNStF.a752a7c1e697.js.br +0 -0
  135. codex/static_root/assets/confirm-dialog-RP7JNStF.a752a7c1e697.js.gz +0 -0
  136. codex/static_root/assets/{confirm-dialog-DARLmXRf.js → confirm-dialog-RP7JNStF.js} +1 -1
  137. codex/static_root/assets/confirm-dialog-RP7JNStF.js.br +0 -0
  138. codex/static_root/assets/confirm-dialog-RP7JNStF.js.gz +0 -0
  139. codex/static_root/assets/copy-to-clipboard-DoUvDJVx.bec27f144aca.js +1 -0
  140. codex/static_root/assets/copy-to-clipboard-DoUvDJVx.bec27f144aca.js.br +0 -0
  141. codex/static_root/assets/copy-to-clipboard-DoUvDJVx.js +1 -0
  142. codex/static_root/assets/copy-to-clipboard-DoUvDJVx.js.br +0 -0
  143. codex/static_root/assets/{datetime-column-LpLgWpkf.16ce3324970f.js → datetime-column-Cu3WYyPj.54b4c3817b74.js} +1 -1
  144. codex/static_root/assets/datetime-column-Cu3WYyPj.54b4c3817b74.js.br +0 -0
  145. codex/static_root/assets/datetime-column-Cu3WYyPj.54b4c3817b74.js.gz +0 -0
  146. codex/static_root/assets/{datetime-column-LpLgWpkf.js → datetime-column-Cu3WYyPj.js} +1 -1
  147. codex/static_root/assets/datetime-column-Cu3WYyPj.js.br +0 -0
  148. codex/static_root/assets/datetime-column-Cu3WYyPj.js.gz +0 -0
  149. codex/static_root/assets/filter-DAdGUt-1.3261cbcd50b5.js +1 -0
  150. codex/static_root/assets/filter-DAdGUt-1.3261cbcd50b5.js.br +0 -0
  151. codex/static_root/assets/filter-DAdGUt-1.3261cbcd50b5.js.gz +0 -0
  152. codex/static_root/assets/filter-DAdGUt-1.js +1 -0
  153. codex/static_root/assets/filter-DAdGUt-1.js.br +0 -0
  154. codex/static_root/assets/filter-DAdGUt-1.js.gz +0 -0
  155. codex/static_root/assets/flag-tab-BP7Zs7iQ.cb16ef6363d4.js +1 -0
  156. codex/static_root/assets/flag-tab-BP7Zs7iQ.cb16ef6363d4.js.br +0 -0
  157. codex/static_root/assets/flag-tab-BP7Zs7iQ.cb16ef6363d4.js.gz +0 -0
  158. codex/static_root/assets/flag-tab-BP7Zs7iQ.js +1 -0
  159. codex/static_root/assets/flag-tab-BP7Zs7iQ.js.br +0 -0
  160. codex/static_root/assets/flag-tab-BP7Zs7iQ.js.gz +0 -0
  161. codex/static_root/assets/{group-tab-MK1lcjUU.98406d5babdc.js → group-tab-DEwh4jfB.a95097b5e320.js} +1 -1
  162. codex/static_root/assets/group-tab-DEwh4jfB.a95097b5e320.js.br +0 -0
  163. codex/static_root/assets/group-tab-DEwh4jfB.a95097b5e320.js.gz +0 -0
  164. codex/static_root/assets/{group-tab-MK1lcjUU.js → group-tab-DEwh4jfB.js} +1 -1
  165. codex/static_root/assets/group-tab-DEwh4jfB.js.br +0 -0
  166. codex/static_root/assets/group-tab-DEwh4jfB.js.gz +0 -0
  167. codex/static_root/assets/http-error-xs3mIL8U.e143c472a96c.js +1 -0
  168. codex/static_root/assets/http-error-xs3mIL8U.e143c472a96c.js.br +0 -0
  169. codex/static_root/assets/http-error-xs3mIL8U.e143c472a96c.js.gz +0 -0
  170. codex/static_root/assets/http-error-xs3mIL8U.js +1 -0
  171. codex/static_root/assets/http-error-xs3mIL8U.js.br +0 -0
  172. codex/static_root/assets/http-error-xs3mIL8U.js.gz +0 -0
  173. codex/static_root/assets/{library-tab-DYeEBYlT.a2a2ef088d1c.js → library-tab-DrXvD9B2.f765f9d3bae4.js} +1 -1
  174. codex/static_root/assets/library-tab-DrXvD9B2.f765f9d3bae4.js.br +0 -0
  175. codex/static_root/assets/library-tab-DrXvD9B2.f765f9d3bae4.js.gz +0 -0
  176. codex/static_root/assets/{library-tab-DYeEBYlT.js → library-tab-DrXvD9B2.js} +1 -1
  177. codex/static_root/assets/library-tab-DrXvD9B2.js.br +0 -0
  178. codex/static_root/assets/library-tab-DrXvD9B2.js.gz +0 -0
  179. codex/static_root/assets/main-Dy9Uqpmh.2f99b1914b3e.js +32 -0
  180. codex/static_root/assets/main-Dy9Uqpmh.2f99b1914b3e.js.br +0 -0
  181. codex/static_root/assets/main-Dy9Uqpmh.2f99b1914b3e.js.gz +0 -0
  182. codex/static_root/assets/main-Dy9Uqpmh.js +32 -0
  183. codex/static_root/assets/main-Dy9Uqpmh.js.br +0 -0
  184. codex/static_root/assets/main-Dy9Uqpmh.js.gz +0 -0
  185. codex/static_root/assets/{main-sK7HC5h4.css → main-Ztp855PW.css} +1 -1
  186. codex/static_root/assets/main-Ztp855PW.css.br +0 -0
  187. codex/static_root/assets/{main-sK7HC5h4.css.gz → main-Ztp855PW.css.gz} +0 -0
  188. codex/static_root/assets/{main-sK7HC5h4.6f04ddfc5f1a.css → main-Ztp855PW.d46d718fac45.css} +1 -1
  189. codex/static_root/assets/main-Ztp855PW.d46d718fac45.css.br +0 -0
  190. codex/static_root/assets/{main-sK7HC5h4.6f04ddfc5f1a.css.gz → main-Ztp855PW.d46d718fac45.css.gz} +0 -0
  191. codex/static_root/assets/pagination-toolbar-DbS_HOT1.81222ab51ef4.js +1 -0
  192. codex/static_root/assets/pagination-toolbar-DbS_HOT1.81222ab51ef4.js.br +0 -0
  193. codex/static_root/assets/pagination-toolbar-DbS_HOT1.81222ab51ef4.js.gz +0 -0
  194. codex/static_root/assets/pagination-toolbar-DbS_HOT1.js +1 -0
  195. codex/static_root/assets/pagination-toolbar-DbS_HOT1.js.br +0 -0
  196. codex/static_root/assets/pagination-toolbar-DbS_HOT1.js.gz +0 -0
  197. codex/static_root/assets/{pagination-toolbar-ClZui9Zf.697d7ae11c21.css → pagination-toolbar-r6nbtWxg.4088ab042c0c.css} +1 -1
  198. codex/static_root/assets/pagination-toolbar-r6nbtWxg.4088ab042c0c.css.br +0 -0
  199. codex/static_root/assets/pagination-toolbar-r6nbtWxg.4088ab042c0c.css.gz +0 -0
  200. codex/static_root/assets/{pagination-toolbar-ClZui9Zf.css → pagination-toolbar-r6nbtWxg.css} +1 -1
  201. codex/static_root/assets/pagination-toolbar-r6nbtWxg.css.br +0 -0
  202. codex/static_root/assets/pagination-toolbar-r6nbtWxg.css.gz +0 -0
  203. codex/static_root/assets/pdf-doc-obrBwxa6.ec9bd13a5786.js +305 -0
  204. codex/static_root/assets/pdf-doc-obrBwxa6.ec9bd13a5786.js.br +0 -0
  205. codex/static_root/assets/pdf-doc-obrBwxa6.ec9bd13a5786.js.gz +0 -0
  206. codex/static_root/assets/pdf-doc-obrBwxa6.js +305 -0
  207. codex/static_root/assets/pdf-doc-obrBwxa6.js.br +0 -0
  208. codex/static_root/assets/pdf-doc-obrBwxa6.js.gz +0 -0
  209. codex/static_root/assets/reader-BXKUaUeC.953d30b969ef.js +2 -0
  210. codex/static_root/assets/reader-BXKUaUeC.953d30b969ef.js.br +0 -0
  211. codex/static_root/assets/reader-BXKUaUeC.953d30b969ef.js.gz +0 -0
  212. codex/static_root/assets/reader-BXKUaUeC.js +2 -0
  213. codex/static_root/assets/reader-BXKUaUeC.js.br +0 -0
  214. codex/static_root/assets/reader-BXKUaUeC.js.gz +0 -0
  215. codex/static_root/assets/{reader-CF3xt6JG.19fad7f62450.css → reader-OXJ1KVaW.a463b19d0508.css} +1 -1
  216. codex/static_root/assets/reader-OXJ1KVaW.a463b19d0508.css.br +0 -0
  217. codex/static_root/assets/reader-OXJ1KVaW.a463b19d0508.css.gz +0 -0
  218. codex/static_root/assets/{reader-CF3xt6JG.css → reader-OXJ1KVaW.css} +1 -1
  219. codex/static_root/assets/reader-OXJ1KVaW.css.br +0 -0
  220. codex/static_root/assets/reader-OXJ1KVaW.css.gz +0 -0
  221. codex/static_root/assets/relation-chips-CZJcLjc8.1d7426dab654.js +1 -0
  222. codex/static_root/assets/relation-chips-CZJcLjc8.1d7426dab654.js.br +0 -0
  223. codex/static_root/assets/relation-chips-CZJcLjc8.1d7426dab654.js.gz +0 -0
  224. codex/static_root/assets/relation-chips-CZJcLjc8.js +1 -0
  225. codex/static_root/assets/relation-chips-CZJcLjc8.js.br +0 -0
  226. codex/static_root/assets/relation-chips-CZJcLjc8.js.gz +0 -0
  227. codex/static_root/assets/settings-drawer-7iTnAdjf.337eecf2fdbb.js +2 -0
  228. codex/static_root/assets/settings-drawer-7iTnAdjf.337eecf2fdbb.js.br +0 -0
  229. codex/static_root/assets/settings-drawer-7iTnAdjf.337eecf2fdbb.js.gz +0 -0
  230. codex/static_root/assets/settings-drawer-7iTnAdjf.js +2 -0
  231. codex/static_root/assets/settings-drawer-7iTnAdjf.js.br +0 -0
  232. codex/static_root/assets/settings-drawer-7iTnAdjf.js.gz +0 -0
  233. codex/static_root/assets/{settings-drawer-BJOfMtqA.6471a50fc05b.css → settings-drawer-nZ9HvKSZ.01744e131806.css} +1 -1
  234. codex/static_root/assets/settings-drawer-nZ9HvKSZ.01744e131806.css.br +0 -0
  235. codex/static_root/assets/settings-drawer-nZ9HvKSZ.01744e131806.css.gz +0 -0
  236. codex/static_root/assets/{settings-drawer-BJOfMtqA.css → settings-drawer-nZ9HvKSZ.css} +1 -1
  237. codex/static_root/assets/settings-drawer-nZ9HvKSZ.css.br +0 -0
  238. codex/static_root/assets/settings-drawer-nZ9HvKSZ.css.gz +0 -0
  239. codex/static_root/assets/stats-tab-CQsK1n8O.544bf50ffb22.js +1 -0
  240. codex/static_root/assets/stats-tab-CQsK1n8O.544bf50ffb22.js.br +0 -0
  241. codex/static_root/assets/stats-tab-CQsK1n8O.544bf50ffb22.js.gz +0 -0
  242. codex/static_root/assets/stats-tab-CQsK1n8O.js +1 -0
  243. codex/static_root/assets/stats-tab-CQsK1n8O.js.br +0 -0
  244. codex/static_root/assets/stats-tab-CQsK1n8O.js.gz +0 -0
  245. codex/static_root/assets/task-tab-C60TLP6e.63aa929a4ed6.js +1 -0
  246. codex/static_root/assets/task-tab-C60TLP6e.63aa929a4ed6.js.br +0 -0
  247. codex/static_root/assets/task-tab-C60TLP6e.63aa929a4ed6.js.gz +0 -0
  248. codex/static_root/assets/task-tab-C60TLP6e.js +1 -0
  249. codex/static_root/assets/task-tab-C60TLP6e.js.br +0 -0
  250. codex/static_root/assets/task-tab-C60TLP6e.js.gz +0 -0
  251. codex/static_root/assets/to-case-CR9beRR0.21bb805fdab4.js +1 -0
  252. codex/static_root/assets/to-case-CR9beRR0.js +1 -0
  253. codex/static_root/assets/unauthorized-Bs7NSqea.c55650fac055.js +1 -0
  254. codex/static_root/assets/unauthorized-Bs7NSqea.c55650fac055.js.br +0 -0
  255. codex/static_root/assets/unauthorized-Bs7NSqea.c55650fac055.js.gz +0 -0
  256. codex/static_root/assets/unauthorized-Bs7NSqea.js +1 -0
  257. codex/static_root/assets/unauthorized-Bs7NSqea.js.br +0 -0
  258. codex/static_root/assets/unauthorized-Bs7NSqea.js.gz +0 -0
  259. codex/static_root/assets/user-tab-ChuH7Atm.7e80cf1fb78e.js +1 -0
  260. codex/static_root/assets/user-tab-ChuH7Atm.7e80cf1fb78e.js.br +0 -0
  261. codex/static_root/assets/user-tab-ChuH7Atm.7e80cf1fb78e.js.gz +0 -0
  262. codex/static_root/assets/user-tab-ChuH7Atm.js +1 -0
  263. codex/static_root/assets/user-tab-ChuH7Atm.js.br +0 -0
  264. codex/static_root/assets/user-tab-ChuH7Atm.js.gz +0 -0
  265. codex/static_root/js/choices.079a01e0be1a.json +1 -0
  266. codex/static_root/js/choices.079a01e0be1a.json.br +0 -0
  267. codex/static_root/js/choices.079a01e0be1a.json.gz +0 -0
  268. codex/static_root/js/choices.json +1 -1
  269. codex/static_root/js/choices.json.br +0 -0
  270. codex/static_root/js/choices.json.gz +0 -0
  271. codex/static_root/manifest.dad79c8475ae.json +629 -0
  272. codex/static_root/manifest.dad79c8475ae.json.br +0 -0
  273. codex/static_root/manifest.dad79c8475ae.json.gz +0 -0
  274. codex/static_root/manifest.json +265 -278
  275. codex/static_root/manifest.json.br +0 -0
  276. codex/static_root/manifest.json.gz +0 -0
  277. codex/static_root/staticfiles.json +1 -1
  278. codex/urls/api/auth.py +2 -1
  279. codex/urls/api/browser.py +2 -2
  280. codex/urls/api/reader.py +2 -2
  281. codex/urls/api/v3.py +2 -0
  282. codex/util.py +10 -0
  283. codex/views/admin/api_key.py +2 -6
  284. codex/views/admin/auth.py +30 -0
  285. codex/views/admin/flag.py +2 -7
  286. codex/views/admin/group.py +2 -6
  287. codex/views/admin/library.py +7 -20
  288. codex/views/admin/stats.py +7 -66
  289. codex/views/admin/tasks.py +12 -11
  290. codex/views/admin/user.py +3 -9
  291. codex/views/auth.py +31 -52
  292. codex/views/bookmark.py +7 -11
  293. codex/views/browser/{browser_annotations.py → annotations.py} +119 -78
  294. codex/views/browser/base.py +24 -72
  295. codex/views/browser/breadcrumbs.py +200 -0
  296. codex/views/browser/browser.py +64 -344
  297. codex/views/browser/choices.py +25 -33
  298. codex/views/browser/filters/annotations.py +2 -2
  299. codex/views/browser/filters/bookmark.py +9 -13
  300. codex/views/browser/filters/field.py +9 -6
  301. codex/views/browser/filters/group.py +8 -5
  302. codex/views/browser/filters/search.py +11 -8
  303. codex/views/browser/metadata.py +3 -7
  304. codex/views/browser/{browser_order_by.py → order_by.py} +2 -6
  305. codex/views/browser/paginate.py +106 -0
  306. codex/views/browser/{session.py → settings.py} +4 -3
  307. codex/views/browser/title.py +48 -0
  308. codex/views/browser/validate.py +158 -0
  309. codex/views/const.py +97 -0
  310. codex/views/cover.py +4 -6
  311. codex/views/download.py +5 -8
  312. codex/views/frontend.py +4 -2
  313. codex/views/mtime.py +70 -0
  314. codex/views/opds/auth.py +17 -0
  315. codex/views/opds/authentication_v1.py +46 -45
  316. codex/views/opds/binary.py +4 -9
  317. codex/views/opds/const.py +13 -0
  318. codex/views/opds/urls.py +5 -4
  319. codex/views/opds/util.py +2 -8
  320. codex/views/opds/v1/facets.py +6 -7
  321. codex/views/opds/v1/feed.py +10 -17
  322. codex/views/opds/v1/links.py +6 -5
  323. codex/views/opds/v1/opensearch_v1.py +2 -6
  324. codex/views/opds/v2/feed.py +12 -15
  325. codex/views/opds/v2/links.py +3 -2
  326. codex/views/opds/v2/publications.py +21 -20
  327. codex/views/opds/v2/top_links.py +2 -2
  328. codex/views/public.py +38 -0
  329. codex/views/reader/arcs.py +101 -0
  330. codex/views/reader/books.py +199 -0
  331. codex/views/reader/page.py +3 -7
  332. codex/views/reader/reader.py +53 -298
  333. codex/views/reader/{session.py → settings.py} +4 -2
  334. codex/views/session.py +74 -102
  335. codex/views/settings.py +34 -0
  336. codex/views/template.py +3 -3
  337. codex/views/util.py +31 -0
  338. codex/views/utils.py +26 -0
  339. codex/views/version.py +2 -2
  340. {codex-1.6.0a5.dist-info → codex-1.6.0a7.dist-info}/METADATA +12 -8
  341. {codex-1.6.0a5.dist-info → codex-1.6.0a7.dist-info}/RECORD +344 -330
  342. codex/static_root/assets/VCheckbox-BF_99tgB.7fe59e56d9d4.js +0 -1
  343. codex/static_root/assets/VCheckbox-BF_99tgB.7fe59e56d9d4.js.br +0 -0
  344. codex/static_root/assets/VCheckbox-BF_99tgB.7fe59e56d9d4.js.gz +0 -0
  345. codex/static_root/assets/VCheckbox-BF_99tgB.js +0 -1
  346. codex/static_root/assets/VCheckbox-BF_99tgB.js.br +0 -0
  347. codex/static_root/assets/VCheckbox-BF_99tgB.js.gz +0 -0
  348. codex/static_root/assets/VCheckboxBtn-Coc7g8m7.31e1e6aa44c9.js +0 -1
  349. codex/static_root/assets/VCheckboxBtn-Coc7g8m7.31e1e6aa44c9.js.br +0 -0
  350. codex/static_root/assets/VCheckboxBtn-Coc7g8m7.31e1e6aa44c9.js.gz +0 -0
  351. codex/static_root/assets/VCheckboxBtn-Coc7g8m7.js +0 -1
  352. codex/static_root/assets/VCheckboxBtn-Coc7g8m7.js.br +0 -0
  353. codex/static_root/assets/VCheckboxBtn-Coc7g8m7.js.gz +0 -0
  354. codex/static_root/assets/VCombobox-CTRWnOuM.0f00903c2ebc.js +0 -1
  355. codex/static_root/assets/VCombobox-CTRWnOuM.0f00903c2ebc.js.br +0 -0
  356. codex/static_root/assets/VCombobox-CTRWnOuM.0f00903c2ebc.js.gz +0 -0
  357. codex/static_root/assets/VCombobox-CTRWnOuM.js +0 -1
  358. codex/static_root/assets/VCombobox-CTRWnOuM.js.br +0 -0
  359. codex/static_root/assets/VCombobox-CTRWnOuM.js.gz +0 -0
  360. codex/static_root/assets/VDialog-pzwzInEh.51acf566a069.js +0 -1
  361. codex/static_root/assets/VDialog-pzwzInEh.51acf566a069.js.br +0 -0
  362. codex/static_root/assets/VDialog-pzwzInEh.51acf566a069.js.gz +0 -0
  363. codex/static_root/assets/VDialog-pzwzInEh.js +0 -1
  364. codex/static_root/assets/VDialog-pzwzInEh.js.br +0 -0
  365. codex/static_root/assets/VDialog-pzwzInEh.js.gz +0 -0
  366. codex/static_root/assets/VExpansionPanels-CKSq1GpG.0ecf702c5b67.js +0 -1
  367. codex/static_root/assets/VExpansionPanels-CKSq1GpG.0ecf702c5b67.js.br +0 -0
  368. codex/static_root/assets/VExpansionPanels-CKSq1GpG.0ecf702c5b67.js.gz +0 -0
  369. codex/static_root/assets/VExpansionPanels-CKSq1GpG.js +0 -1
  370. codex/static_root/assets/VExpansionPanels-CKSq1GpG.js.br +0 -0
  371. codex/static_root/assets/VExpansionPanels-CKSq1GpG.js.gz +0 -0
  372. codex/static_root/assets/VRadioGroup-CW5GobGU.b72925bca3de.js +0 -1
  373. codex/static_root/assets/VRadioGroup-CW5GobGU.b72925bca3de.js.br +0 -0
  374. codex/static_root/assets/VRadioGroup-CW5GobGU.b72925bca3de.js.gz +0 -0
  375. codex/static_root/assets/VRadioGroup-CW5GobGU.js +0 -1
  376. codex/static_root/assets/VRadioGroup-CW5GobGU.js.br +0 -0
  377. codex/static_root/assets/VRadioGroup-CW5GobGU.js.gz +0 -0
  378. codex/static_root/assets/VSelect-ChOlLRyH.00b11751b25e.js +0 -1
  379. codex/static_root/assets/VSelect-ChOlLRyH.00b11751b25e.js.br +0 -0
  380. codex/static_root/assets/VSelect-ChOlLRyH.00b11751b25e.js.gz +0 -0
  381. codex/static_root/assets/VSelect-ChOlLRyH.js +0 -1
  382. codex/static_root/assets/VSelect-ChOlLRyH.js.br +0 -0
  383. codex/static_root/assets/VSelect-ChOlLRyH.js.gz +0 -0
  384. codex/static_root/assets/VSelectionControl-DKOhc2zB.061dca094303.js +0 -1
  385. codex/static_root/assets/VSelectionControl-DKOhc2zB.061dca094303.js.br +0 -0
  386. codex/static_root/assets/VSelectionControl-DKOhc2zB.061dca094303.js.gz +0 -0
  387. codex/static_root/assets/VSelectionControl-DKOhc2zB.js +0 -1
  388. codex/static_root/assets/VSelectionControl-DKOhc2zB.js.br +0 -0
  389. codex/static_root/assets/VSelectionControl-DKOhc2zB.js.gz +0 -0
  390. codex/static_root/assets/VSlideGroup-zFz5_byd.e0e246b7acbb.js +0 -1
  391. codex/static_root/assets/VSlideGroup-zFz5_byd.e0e246b7acbb.js.br +0 -0
  392. codex/static_root/assets/VSlideGroup-zFz5_byd.e0e246b7acbb.js.gz +0 -0
  393. codex/static_root/assets/VSlideGroup-zFz5_byd.js +0 -1
  394. codex/static_root/assets/VSlideGroup-zFz5_byd.js.br +0 -0
  395. codex/static_root/assets/VSlideGroup-zFz5_byd.js.gz +0 -0
  396. codex/static_root/assets/VTable-FKMTcdkF.6c1eed84d25e.js.br +0 -0
  397. codex/static_root/assets/VTable-FKMTcdkF.6c1eed84d25e.js.gz +0 -0
  398. codex/static_root/assets/VTable-FKMTcdkF.js.br +0 -0
  399. codex/static_root/assets/VTable-FKMTcdkF.js.gz +0 -0
  400. codex/static_root/assets/VTextField-Dftcik85.cb2f1dda87e1.js.br +0 -0
  401. codex/static_root/assets/VTextField-Dftcik85.cb2f1dda87e1.js.gz +0 -0
  402. codex/static_root/assets/VTextField-Dftcik85.js.br +0 -0
  403. codex/static_root/assets/VTextField-Dftcik85.js.gz +0 -0
  404. codex/static_root/assets/VWindowItem-CYCwnt3s.28025dc36c6a.js.br +0 -0
  405. codex/static_root/assets/VWindowItem-CYCwnt3s.28025dc36c6a.js.gz +0 -0
  406. codex/static_root/assets/VWindowItem-CYCwnt3s.js.br +0 -0
  407. codex/static_root/assets/VWindowItem-CYCwnt3s.js.gz +0 -0
  408. codex/static_root/assets/admin-DH_DrB0F.dc45fb726e07.js +0 -1
  409. codex/static_root/assets/admin-DH_DrB0F.dc45fb726e07.js.br +0 -0
  410. codex/static_root/assets/admin-DH_DrB0F.dc45fb726e07.js.gz +0 -0
  411. codex/static_root/assets/admin-DH_DrB0F.js +0 -1
  412. codex/static_root/assets/admin-DH_DrB0F.js.br +0 -0
  413. codex/static_root/assets/admin-DH_DrB0F.js.gz +0 -0
  414. codex/static_root/assets/admin-drawer-panel-B2rmpAVA.a9e72fbfdadc.js +0 -30
  415. codex/static_root/assets/admin-drawer-panel-B2rmpAVA.a9e72fbfdadc.js.br +0 -0
  416. codex/static_root/assets/admin-drawer-panel-B2rmpAVA.a9e72fbfdadc.js.gz +0 -0
  417. codex/static_root/assets/admin-drawer-panel-B2rmpAVA.js +0 -30
  418. codex/static_root/assets/admin-drawer-panel-B2rmpAVA.js.br +0 -0
  419. codex/static_root/assets/admin-drawer-panel-B2rmpAVA.js.gz +0 -0
  420. codex/static_root/assets/admin-drawer-panel-BHdNPwzF.bd2c1d6148bb.css.br +0 -0
  421. codex/static_root/assets/admin-drawer-panel-BHdNPwzF.css.br +0 -0
  422. codex/static_root/assets/browser-CpppBvim.a1b5d4fe97b1.css +0 -1
  423. codex/static_root/assets/browser-CpppBvim.a1b5d4fe97b1.css.br +0 -0
  424. codex/static_root/assets/browser-CpppBvim.a1b5d4fe97b1.css.gz +0 -0
  425. codex/static_root/assets/browser-CpppBvim.css +0 -1
  426. codex/static_root/assets/browser-CpppBvim.css.br +0 -0
  427. codex/static_root/assets/browser-CpppBvim.css.gz +0 -0
  428. codex/static_root/assets/browser-lKU7rqLd.cdb86e625ed9.js +0 -1
  429. codex/static_root/assets/browser-lKU7rqLd.cdb86e625ed9.js.br +0 -0
  430. codex/static_root/assets/browser-lKU7rqLd.cdb86e625ed9.js.gz +0 -0
  431. codex/static_root/assets/browser-lKU7rqLd.js +0 -1
  432. codex/static_root/assets/browser-lKU7rqLd.js.br +0 -0
  433. codex/static_root/assets/browser-lKU7rqLd.js.gz +0 -0
  434. codex/static_root/assets/change-password-dialog-BIBG6Bk8.3583021c124a.js.br +0 -0
  435. codex/static_root/assets/change-password-dialog-BIBG6Bk8.3583021c124a.js.gz +0 -0
  436. codex/static_root/assets/change-password-dialog-BIBG6Bk8.js.br +0 -0
  437. codex/static_root/assets/change-password-dialog-BIBG6Bk8.js.gz +0 -0
  438. codex/static_root/assets/confirm-dialog-DARLmXRf.53fc6938fb8c.js.br +0 -0
  439. codex/static_root/assets/confirm-dialog-DARLmXRf.53fc6938fb8c.js.gz +0 -0
  440. codex/static_root/assets/confirm-dialog-DARLmXRf.js.br +0 -0
  441. codex/static_root/assets/confirm-dialog-DARLmXRf.js.gz +0 -0
  442. codex/static_root/assets/datetime-column-LpLgWpkf.16ce3324970f.js.br +0 -0
  443. codex/static_root/assets/datetime-column-LpLgWpkf.16ce3324970f.js.gz +0 -0
  444. codex/static_root/assets/datetime-column-LpLgWpkf.js.br +0 -0
  445. codex/static_root/assets/datetime-column-LpLgWpkf.js.gz +0 -0
  446. codex/static_root/assets/filter-jRB1Ob6r.6a559a53e805.js +0 -1
  447. codex/static_root/assets/filter-jRB1Ob6r.6a559a53e805.js.br +0 -0
  448. codex/static_root/assets/filter-jRB1Ob6r.6a559a53e805.js.gz +0 -0
  449. codex/static_root/assets/filter-jRB1Ob6r.js +0 -1
  450. codex/static_root/assets/filter-jRB1Ob6r.js.br +0 -0
  451. codex/static_root/assets/filter-jRB1Ob6r.js.gz +0 -0
  452. codex/static_root/assets/flag-tab-mYS2wg5e.17829c46cae9.js +0 -1
  453. codex/static_root/assets/flag-tab-mYS2wg5e.17829c46cae9.js.br +0 -0
  454. codex/static_root/assets/flag-tab-mYS2wg5e.17829c46cae9.js.gz +0 -0
  455. codex/static_root/assets/flag-tab-mYS2wg5e.js +0 -1
  456. codex/static_root/assets/flag-tab-mYS2wg5e.js.br +0 -0
  457. codex/static_root/assets/flag-tab-mYS2wg5e.js.gz +0 -0
  458. codex/static_root/assets/group-tab-MK1lcjUU.98406d5babdc.js.br +0 -0
  459. codex/static_root/assets/group-tab-MK1lcjUU.98406d5babdc.js.gz +0 -0
  460. codex/static_root/assets/group-tab-MK1lcjUU.js.br +0 -0
  461. codex/static_root/assets/group-tab-MK1lcjUU.js.gz +0 -0
  462. codex/static_root/assets/http-error-HFeYnF0n.a3c578a0265e.js +0 -1
  463. codex/static_root/assets/http-error-HFeYnF0n.a3c578a0265e.js.br +0 -0
  464. codex/static_root/assets/http-error-HFeYnF0n.a3c578a0265e.js.gz +0 -0
  465. codex/static_root/assets/http-error-HFeYnF0n.js +0 -1
  466. codex/static_root/assets/http-error-HFeYnF0n.js.br +0 -0
  467. codex/static_root/assets/http-error-HFeYnF0n.js.gz +0 -0
  468. codex/static_root/assets/index-DUjeNg4x.36d50c91b2c1.js +0 -1
  469. codex/static_root/assets/index-DUjeNg4x.36d50c91b2c1.js.br +0 -0
  470. codex/static_root/assets/index-DUjeNg4x.36d50c91b2c1.js.gz +0 -0
  471. codex/static_root/assets/index-DUjeNg4x.js +0 -1
  472. codex/static_root/assets/index-DUjeNg4x.js.br +0 -0
  473. codex/static_root/assets/index-DUjeNg4x.js.gz +0 -0
  474. codex/static_root/assets/library-tab-DYeEBYlT.a2a2ef088d1c.js.br +0 -0
  475. codex/static_root/assets/library-tab-DYeEBYlT.a2a2ef088d1c.js.gz +0 -0
  476. codex/static_root/assets/library-tab-DYeEBYlT.js.br +0 -0
  477. codex/static_root/assets/library-tab-DYeEBYlT.js.gz +0 -0
  478. codex/static_root/assets/main-gSW7TFDY.329223b148d4.js +0 -32
  479. codex/static_root/assets/main-gSW7TFDY.329223b148d4.js.br +0 -0
  480. codex/static_root/assets/main-gSW7TFDY.329223b148d4.js.gz +0 -0
  481. codex/static_root/assets/main-gSW7TFDY.js +0 -32
  482. codex/static_root/assets/main-gSW7TFDY.js.br +0 -0
  483. codex/static_root/assets/main-gSW7TFDY.js.gz +0 -0
  484. codex/static_root/assets/main-sK7HC5h4.6f04ddfc5f1a.css.br +0 -0
  485. codex/static_root/assets/main-sK7HC5h4.css.br +0 -0
  486. codex/static_root/assets/pagination-toolbar-ClZui9Zf.697d7ae11c21.css.br +0 -0
  487. codex/static_root/assets/pagination-toolbar-ClZui9Zf.697d7ae11c21.css.gz +0 -0
  488. codex/static_root/assets/pagination-toolbar-ClZui9Zf.css.br +0 -0
  489. codex/static_root/assets/pagination-toolbar-ClZui9Zf.css.gz +0 -0
  490. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.8f23f8bd9a05.js +0 -1
  491. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.8f23f8bd9a05.js.br +0 -0
  492. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.8f23f8bd9a05.js.gz +0 -0
  493. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.js +0 -1
  494. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.js.br +0 -0
  495. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.js.gz +0 -0
  496. codex/static_root/assets/pdf-doc-BTdS9hUz.7c966524280a.js +0 -35
  497. codex/static_root/assets/pdf-doc-BTdS9hUz.7c966524280a.js.br +0 -0
  498. codex/static_root/assets/pdf-doc-BTdS9hUz.7c966524280a.js.gz +0 -0
  499. codex/static_root/assets/pdf-doc-BTdS9hUz.js +0 -35
  500. codex/static_root/assets/pdf-doc-BTdS9hUz.js.br +0 -0
  501. codex/static_root/assets/pdf-doc-BTdS9hUz.js.gz +0 -0
  502. codex/static_root/assets/reader-BtSTIETq.f940f66c0e58.js +0 -2
  503. codex/static_root/assets/reader-BtSTIETq.f940f66c0e58.js.br +0 -0
  504. codex/static_root/assets/reader-BtSTIETq.f940f66c0e58.js.gz +0 -0
  505. codex/static_root/assets/reader-BtSTIETq.js +0 -2
  506. codex/static_root/assets/reader-BtSTIETq.js.br +0 -0
  507. codex/static_root/assets/reader-BtSTIETq.js.gz +0 -0
  508. codex/static_root/assets/reader-CF3xt6JG.19fad7f62450.css.br +0 -0
  509. codex/static_root/assets/reader-CF3xt6JG.19fad7f62450.css.gz +0 -0
  510. codex/static_root/assets/reader-CF3xt6JG.css.br +0 -0
  511. codex/static_root/assets/reader-CF3xt6JG.css.gz +0 -0
  512. codex/static_root/assets/relation-chips-C4Ob7ySi.95855a010f4a.js +0 -1
  513. codex/static_root/assets/relation-chips-C4Ob7ySi.95855a010f4a.js.br +0 -0
  514. codex/static_root/assets/relation-chips-C4Ob7ySi.95855a010f4a.js.gz +0 -0
  515. codex/static_root/assets/relation-chips-C4Ob7ySi.js +0 -1
  516. codex/static_root/assets/relation-chips-C4Ob7ySi.js.br +0 -0
  517. codex/static_root/assets/relation-chips-C4Ob7ySi.js.gz +0 -0
  518. codex/static_root/assets/settings-drawer-BJOfMtqA.6471a50fc05b.css.br +0 -0
  519. codex/static_root/assets/settings-drawer-BJOfMtqA.6471a50fc05b.css.gz +0 -0
  520. codex/static_root/assets/settings-drawer-BJOfMtqA.css.br +0 -0
  521. codex/static_root/assets/settings-drawer-BJOfMtqA.css.gz +0 -0
  522. codex/static_root/assets/settings-drawer-xSctoxZn.4ce8afcb879a.js +0 -2
  523. codex/static_root/assets/settings-drawer-xSctoxZn.4ce8afcb879a.js.br +0 -0
  524. codex/static_root/assets/settings-drawer-xSctoxZn.4ce8afcb879a.js.gz +0 -0
  525. codex/static_root/assets/settings-drawer-xSctoxZn.js +0 -2
  526. codex/static_root/assets/settings-drawer-xSctoxZn.js.br +0 -0
  527. codex/static_root/assets/settings-drawer-xSctoxZn.js.gz +0 -0
  528. codex/static_root/assets/stats-tab-C_uC_U7o.e6b014b6e8c6.js +0 -1
  529. codex/static_root/assets/stats-tab-C_uC_U7o.e6b014b6e8c6.js.br +0 -0
  530. codex/static_root/assets/stats-tab-C_uC_U7o.e6b014b6e8c6.js.gz +0 -0
  531. codex/static_root/assets/stats-tab-C_uC_U7o.js +0 -1
  532. codex/static_root/assets/stats-tab-C_uC_U7o.js.br +0 -0
  533. codex/static_root/assets/stats-tab-C_uC_U7o.js.gz +0 -0
  534. codex/static_root/assets/task-tab-VIpy1Ula.c07877250c88.js +0 -1
  535. codex/static_root/assets/task-tab-VIpy1Ula.c07877250c88.js.br +0 -0
  536. codex/static_root/assets/task-tab-VIpy1Ula.c07877250c88.js.gz +0 -0
  537. codex/static_root/assets/task-tab-VIpy1Ula.js +0 -1
  538. codex/static_root/assets/task-tab-VIpy1Ula.js.br +0 -0
  539. codex/static_root/assets/task-tab-VIpy1Ula.js.gz +0 -0
  540. codex/static_root/assets/to-case-kUm8NPpW.5b3369f511b8.js +0 -1
  541. codex/static_root/assets/to-case-kUm8NPpW.js +0 -1
  542. codex/static_root/assets/unauthorized-kEuFL5br.475729603aeb.js +0 -1
  543. codex/static_root/assets/unauthorized-kEuFL5br.475729603aeb.js.br +0 -0
  544. codex/static_root/assets/unauthorized-kEuFL5br.475729603aeb.js.gz +0 -0
  545. codex/static_root/assets/unauthorized-kEuFL5br.js +0 -1
  546. codex/static_root/assets/unauthorized-kEuFL5br.js.br +0 -0
  547. codex/static_root/assets/unauthorized-kEuFL5br.js.gz +0 -0
  548. codex/static_root/assets/user-tab-CZNFTTEI.f3940a75daae.js +0 -1
  549. codex/static_root/assets/user-tab-CZNFTTEI.f3940a75daae.js.br +0 -0
  550. codex/static_root/assets/user-tab-CZNFTTEI.f3940a75daae.js.gz +0 -0
  551. codex/static_root/assets/user-tab-CZNFTTEI.js +0 -1
  552. codex/static_root/assets/user-tab-CZNFTTEI.js.br +0 -0
  553. codex/static_root/assets/user-tab-CZNFTTEI.js.gz +0 -0
  554. codex/static_root/js/choices.23fedac2b9ba.json +0 -1
  555. codex/static_root/js/choices.23fedac2b9ba.json.br +0 -0
  556. codex/static_root/js/choices.23fedac2b9ba.json.gz +0 -0
  557. codex/static_root/manifest.6ef112683dae.json +0 -642
  558. codex/static_root/manifest.6ef112683dae.json.br +0 -0
  559. codex/static_root/manifest.6ef112683dae.json.gz +0 -0
  560. codex/views/browser/browser_breadcrumbs.py +0 -175
  561. codex/views/browser/const.py +0 -3
  562. {codex-1.6.0a5.dist-info → codex-1.6.0a7.dist-info}/LICENSE +0 -0
  563. {codex-1.6.0a5.dist-info → codex-1.6.0a7.dist-info}/WHEEL +0 -0
  564. {codex-1.6.0a5.dist-info → codex-1.6.0a7.dist-info}/entry_points.txt +0 -0
codex/views/auth.py CHANGED
@@ -1,25 +1,20 @@
1
1
  """Views authorization."""
2
2
 
3
- from types import MappingProxyType
4
-
5
- from django.conf import settings
6
3
  from django.contrib.auth.models import AnonymousUser
7
4
  from django.db.models import Q
8
5
  from drf_spectacular.utils import extend_schema
9
6
  from rest_framework.generics import GenericAPIView
10
- from rest_framework.mixins import RetrieveModelMixin
7
+ from rest_framework.parsers import JSONParser
11
8
  from rest_framework.permissions import IsAuthenticated
12
9
  from rest_framework.response import Response
10
+ from rest_framework.views import APIView
13
11
 
14
12
  from codex.logger.logging import get_logger
15
13
  from codex.models import AdminFlag, Comic, Folder, StoryArc, UserActive
16
- from codex.serializers.auth import AuthAdminFlagsSerializer, TimezoneSerializer
17
- from codex.serializers.choices import CHOICES
14
+ from codex.serializers.auth import TimezoneSerializer
18
15
  from codex.serializers.mixins import OKSerializer
19
- from codex.views.const import GROUP_NAME_MAP
20
16
 
21
17
  LOG = get_logger(__name__)
22
- NULL_USER = {"pk": None, "username": None, "is_staff": False}
23
18
 
24
19
 
25
20
  class IsAuthenticatedOrEnabledNonUsers(IsAuthenticated):
@@ -37,20 +32,35 @@ class IsAuthenticatedOrEnabledNonUsers(IsAuthenticated):
37
32
  return super().has_permission(request, view)
38
33
 
39
34
 
40
- class TimezoneView(GenericAPIView):
35
+ class AuthMixin:
36
+ """General Auth Policy."""
37
+
38
+ parser_classes = (JSONParser,)
39
+ permission_classes = (IsAuthenticatedOrEnabledNonUsers,)
40
+
41
+
42
+ class AuthAPIView(AuthMixin, APIView):
43
+ """Auth Policy APIView."""
44
+
45
+
46
+ class AuthGenericAPIView(AuthMixin, GenericAPIView):
47
+ """Auth Policy GenericAPIView."""
48
+
49
+
50
+ class TimezoneView(AuthGenericAPIView):
41
51
  """User info."""
42
52
 
53
+ parser_classes = (JSONParser,)
43
54
  input_serializer_class = TimezoneSerializer
44
55
  serializer_class = OKSerializer
45
56
 
46
- _AUTH_USER_MODEL_TYPE = type(settings.AUTH_USER_MODEL)
47
-
48
57
  @extend_schema(request=input_serializer_class)
49
58
  def post(self, request, *args, **kwargs):
50
59
  """Get the user info for the current user."""
51
- serializer = self.input_serializer_class(data=self.request.data)
60
+ data = self.request.data # type: ignore
61
+ serializer = self.input_serializer_class(data=data)
52
62
  serializer.is_valid(raise_exception=True)
53
- request.session["django_timezone"] = serializer.validated_data["timezone"]
63
+ request.session["django_timezone"] = serializer.validated_data["timezone"] # type: ignore
54
64
  request.session.save()
55
65
  user = self.request.user
56
66
  if user.is_authenticated:
@@ -59,48 +69,9 @@ class TimezoneView(GenericAPIView):
59
69
  return Response(serializer.data)
60
70
 
61
71
 
62
- class AdminFlagsView(GenericAPIView, RetrieveModelMixin):
63
- """Get admin flags relevant to auth."""
64
-
65
- _ADMIN_FLAG_KEYS = frozenset(
66
- (
67
- AdminFlag.FlagChoices.NON_USERS.value,
68
- AdminFlag.FlagChoices.REGISTRATION.value,
69
- )
70
- )
71
-
72
- serializer_class = AuthAdminFlagsSerializer
73
- queryset = AdminFlag.objects.filter(key__in=_ADMIN_FLAG_KEYS).only("key", "on")
74
-
75
- def get_object(self):
76
- """Get admin flags."""
77
- flags = {}
78
- for obj in self.get_queryset(): # type: ignore
79
- name = CHOICES["admin"]["adminFlags"][obj.key].lower().replace(" ", "_")
80
- flags[name] = obj.on
81
- return flags
82
-
83
- def get(self, request, *args, **kwargs):
84
- """Get admin flags relevant to auth."""
85
- return self.retrieve(request, *args, **kwargs)
86
-
87
-
88
72
  class GroupACLMixin:
89
73
  """Filter group ACLS for views."""
90
74
 
91
- ROOT_GROUP = "r"
92
- FOLDER_GROUP = "f"
93
- STORY_ARC_GROUP = "a"
94
- COMIC_GROUP = "c"
95
- GROUP_RELATION = MappingProxyType(
96
- {
97
- **GROUP_NAME_MAP,
98
- COMIC_GROUP: "pk",
99
- FOLDER_GROUP: "parent_folder",
100
- STORY_ARC_GROUP: "story_arc_numbers__story_arc",
101
- }
102
- )
103
-
104
75
  def get_rel_prefix(self, model):
105
76
  """Return the relation prfiex for most fields."""
106
77
  prefix = ""
@@ -133,3 +104,11 @@ class GroupACLMixin:
133
104
  query |= auth_query
134
105
 
135
106
  return query
107
+
108
+
109
+ class AuthFilterGenericAPIView(AuthGenericAPIView, GroupACLMixin):
110
+ """Auth Enabled GenericAPIView."""
111
+
112
+
113
+ class AuthFilterAPIView(AuthAPIView, GroupACLMixin):
114
+ """Auth Enabled APIView."""
codex/views/bookmark.py CHANGED
@@ -1,9 +1,6 @@
1
1
  """Bookmark views."""
2
2
 
3
- from typing import ClassVar
4
-
5
3
  from drf_spectacular.utils import extend_schema
6
- from rest_framework.generics import GenericAPIView
7
4
  from rest_framework.response import Response
8
5
 
9
6
  from codex.logger.logging import get_logger
@@ -12,13 +9,16 @@ from codex.serializers.models.bookmark import (
12
9
  BookmarkFinishedSerializer,
13
10
  BookmarkSerializer,
14
11
  )
15
- from codex.views.auth import GroupACLMixin, IsAuthenticatedOrEnabledNonUsers
12
+ from codex.views.auth import (
13
+ AuthFilterGenericAPIView,
14
+ )
15
+ from codex.views.const import GROUP_RELATION
16
16
 
17
17
  LOG = get_logger(__name__)
18
18
  VERTICAL_READING_DIRECTIONS = frozenset({"ttb", "btt"})
19
19
 
20
20
 
21
- class BookmarkBaseView(GenericAPIView, GroupACLMixin):
21
+ class BookmarkBaseView(AuthFilterGenericAPIView):
22
22
  """Bookmark Updater."""
23
23
 
24
24
  _BOOKMARK_UPDATE_FIELDS = (
@@ -160,12 +160,11 @@ class BookmarkBaseView(GenericAPIView, GroupACLMixin):
160
160
  class BookmarkView(BookmarkBaseView):
161
161
  """User Bookmark View."""
162
162
 
163
- permission_classes: ClassVar[list] = [IsAuthenticatedOrEnabledNonUsers] # type:ignore
164
163
  serializer_class = BookmarkSerializer
165
164
 
166
165
  def _validate(self, serializer_class):
167
166
  """Validate and translate the submitted data."""
168
- data = self.request.data
167
+ data = self.request.data # type: ignore
169
168
  if serializer_class:
170
169
  serializer = serializer_class(data=data, partial=True)
171
170
  else:
@@ -184,10 +183,7 @@ class BookmarkView(BookmarkBaseView):
184
183
 
185
184
  pks = self.kwargs.get("pks")
186
185
 
187
- if group == "f":
188
- relation = "folders__in"
189
- else:
190
- relation = self.GROUP_RELATION[group] + "__in"
186
+ relation = "folders__in" if group == "f" else GROUP_RELATION[group] + "__in"
191
187
  comic_filter = {relation: pks}
192
188
 
193
189
  self.update_bookmarks(updates, comic_filter=comic_filter)
@@ -31,9 +31,10 @@ from codex.models import (
31
31
  Volume,
32
32
  )
33
33
  from codex.models.functions import JsonGroupArray
34
- from codex.views.browser.browser_order_by import (
34
+ from codex.views.browser.order_by import (
35
35
  BrowserOrderByView,
36
36
  )
37
+ from codex.views.const import STORY_ARC_GROUP
37
38
  from codex.views.mixins import SharedAnnotationsMixin
38
39
 
39
40
  _REL_MAP = MappingProxyType(
@@ -46,40 +47,39 @@ _REL_MAP = MappingProxyType(
46
47
  StoryArc: "story_arc_numbers__story_arc",
47
48
  }
48
49
  )
50
+ _NONE_INTEGERFIELD = Value(None, PositiveSmallIntegerField())
51
+ _NONE_DATETIMEFIELD = Value(None, DateTimeField())
52
+ _ORDER_AGGREGATE_FUNCS = MappingProxyType(
53
+ # These are annotated to order_value because they're simple relations
54
+ {
55
+ "age_rating": Avg,
56
+ "community_rating": Avg,
57
+ "created_at": Min,
58
+ "critical_rating": Avg,
59
+ "date": Min,
60
+ "page_count": Sum,
61
+ "size": Sum,
62
+ "updated_at": Min,
63
+ }
64
+ )
65
+ _ANNOTATED_ORDER_FIELDS = frozenset(
66
+ # These are annotated with their own functions
67
+ {
68
+ "filename",
69
+ "sort_name",
70
+ "search_score",
71
+ "bookmark_updated_at",
72
+ "story_arc_number",
73
+ }
74
+ )
75
+ _ALTERNATE_GROUP_BY: MappingProxyType[type[BrowserGroupModel], str] = MappingProxyType(
76
+ {Comic: "id", Volume: "name"}
77
+ )
49
78
 
50
79
 
51
80
  class BrowserAnnotationsView(BrowserOrderByView, SharedAnnotationsMixin):
52
81
  """Base class for views that need special metadata annotations."""
53
82
 
54
- _NONE_INTEGERFIELD = Value(None, PositiveSmallIntegerField())
55
- _NONE_DATETIMEFIELD = Value(None, DateTimeField())
56
- _ORDER_AGGREGATE_FUNCS = MappingProxyType(
57
- # These are annotated to order_value because they're simple relations
58
- {
59
- "age_rating": Avg,
60
- "community_rating": Avg,
61
- "created_at": Min,
62
- "critical_rating": Avg,
63
- "date": Min,
64
- "page_count": Sum,
65
- "size": Sum,
66
- "updated_at": Min,
67
- }
68
- )
69
- _ANNOTATED_ORDER_FIELDS = frozenset(
70
- # These are annotated with their own functions
71
- {
72
- "filename",
73
- "sort_name",
74
- "search_score",
75
- "bookmark_updated_at",
76
- "story_arc_number",
77
- }
78
- )
79
- _ALTERNATE_GROUP_BY: MappingProxyType[type[BrowserGroupModel], str] = (
80
- MappingProxyType({Comic: "id", Volume: "name"})
81
- )
82
-
83
83
  def __init__(self, *args, **kwargs):
84
84
  """Set params for the type checker."""
85
85
  super().__init__(*args, **kwargs)
@@ -92,7 +92,7 @@ class BrowserAnnotationsView(BrowserOrderByView, SharedAnnotationsMixin):
92
92
  """Get the group by for the model."""
93
93
  if model is None:
94
94
  model = self.model
95
- return self._ALTERNATE_GROUP_BY.get(model, "sort_name") # type: ignore
95
+ return _ALTERNATE_GROUP_BY.get(model, "sort_name") # type: ignore
96
96
 
97
97
  def _alias_sort_names(self, qs, model):
98
98
  """Annotate sort_name."""
@@ -133,7 +133,7 @@ class BrowserAnnotationsView(BrowserOrderByView, SharedAnnotationsMixin):
133
133
  # Get story_arc__pk
134
134
  group = self.kwargs["group"]
135
135
  pks = self.kwargs["pks"]
136
- if group == self.STORY_ARC_GROUP and pks:
136
+ if group == STORY_ARC_GROUP and pks:
137
137
  story_arc_pks = pks
138
138
  else:
139
139
  story_arc_pks = self.params.get("filters", {}).get( # type: ignore
@@ -150,7 +150,7 @@ class BrowserAnnotationsView(BrowserOrderByView, SharedAnnotationsMixin):
150
150
  )
151
151
  story_arc_number = self.order_agg_func("selected_story_arc_number__number")
152
152
  else:
153
- story_arc_number = self._NONE_INTEGERFIELD
153
+ story_arc_number = _NONE_INTEGERFIELD
154
154
 
155
155
  return qs.alias(story_arc_number=story_arc_number)
156
156
 
@@ -168,19 +168,30 @@ class BrowserAnnotationsView(BrowserOrderByView, SharedAnnotationsMixin):
168
168
  qs = qs.annotate(page_count=page_count_sum)
169
169
  return qs
170
170
 
171
- def _annotate_bookmark_updated_at(self, qs, model):
172
- if self.is_opds_1_acquisition or self.order_key == "bookmark_updated_at":
173
- bm_rel = self.get_bm_rel(model)
174
- bm_filter = self._get_my_bookmark_filter(bm_rel)
175
- self.bm_annotation_data[model] = bm_rel, bm_filter # type: ignore
171
+ def get_bookmark_updated_at_aggregate(self, model, always_max=False):
172
+ """Get The aggregate function for relevant bookmark.updated_at."""
173
+ bm_rel = self.get_bm_rel(model)
174
+ bm_filter = self.get_my_bookmark_filter(bm_rel)
175
+ self.bm_annotation_data[model] = bm_rel, bm_filter # type: ignore
176
+
177
+ updated_at_rel = f"{bm_rel}__updated_at"
178
+ agg_func = Max if always_max else self.order_agg_func
179
+ return agg_func(
180
+ updated_at_rel,
181
+ default=_NONE_DATETIMEFIELD,
182
+ filter=bm_filter,
183
+ )
176
184
 
177
- updated_at_rel = f"{bm_rel}__updated_at"
178
- bookmark_updated_at_aggregate = self.order_agg_func(
179
- updated_at_rel,
180
- default=self._NONE_DATETIMEFIELD,
181
- filter=bm_filter,
185
+ def _annotate_bookmark_updated_at(self, qs, model):
186
+ if (
187
+ self.is_opds_1_acquisition
188
+ or self.order_key == "bookmark_updated_at"
189
+ or self.is_bookmark_filtered
190
+ ):
191
+ bookmark_updated_at_aggregate = self.get_bookmark_updated_at_aggregate(
192
+ model
182
193
  )
183
- if self.TARGET == "opds1":
194
+ if self.TARGET == "opds1" or self.is_bookmark_filtered:
184
195
  qs = qs.annotate(bookmark_updated_at=bookmark_updated_at_aggregate)
185
196
  else:
186
197
  qs = qs.alias(bookmark_updated_at=bookmark_updated_at_aggregate)
@@ -191,11 +202,11 @@ class BrowserAnnotationsView(BrowserOrderByView, SharedAnnotationsMixin):
191
202
  # Determine order func
192
203
  if model == Folder and self.order_key == "filename":
193
204
  order_value = F("name")
194
- elif model == Comic or self.order_key in self._ANNOTATED_ORDER_FIELDS:
205
+ elif model == Comic or self.order_key in _ANNOTATED_ORDER_FIELDS:
195
206
  # These are annotated in browser_annotaions
196
207
  order_value = F(self.order_key)
197
208
  else:
198
- agg_func = self._ORDER_AGGREGATE_FUNCS[self.order_key]
209
+ agg_func = _ORDER_AGGREGATE_FUNCS[self.order_key]
199
210
  agg_func = self.order_agg_func if agg_func == Min else agg_func
200
211
  field = self.rel_prefix + self.order_key
201
212
  order_value = agg_func(field)
@@ -224,33 +235,49 @@ class BrowserAnnotationsView(BrowserOrderByView, SharedAnnotationsMixin):
224
235
  """Annotate the query set for the coverpath for the sort."""
225
236
  # Select comics for the children by an outer ref for annotation
226
237
  # Order the descendant comics by the sort argumentst
238
+ cover_style = self.params.get("cover_style")
227
239
  if model == Comic:
228
- qs = qs.annotate(cover_pk=F("pk"), cover_mtime=F("updated_at"))
229
- else:
230
- cover_style = self.params.get("cover_style")
231
- if cover_style == "i":
232
- default_cover_pk = None
233
- default_cover_mtime = None
240
+ default_cover_pk = F("pk")
241
+ default_cover_mtime = F("updated_at")
242
+ elif cover_style == "i":
243
+ default_cover_pk = None
244
+ default_cover_mtime = None
245
+ elif cover_style == "f":
246
+ # At this point it's only been filtered.
247
+ default_cover_pk = F(self.rel_prefix + "pk")
248
+ default_cover_mtime = F(self.rel_prefix + "updated_at")
249
+ else: # cover_style == "d"
250
+ default_cover_pk = F(self.rel_prefix + "pk")
251
+ if self.is_bookmark_filtered:
252
+ default_cover_mtime = Case(
253
+ When(bookmark_updated_at__gt=F(self.rel_prefix + "updated_at")),
254
+ then=F("bookmark_updated_at"),
255
+ default=F(self.rel_prefix + "updated_at"),
256
+ )
234
257
  else:
235
- # At this point it's only been filtered.
236
- default_cover_pk = F(self.rel_prefix + "pk")
237
258
  default_cover_mtime = F(self.rel_prefix + "updated_at")
238
259
 
239
- qs = qs.annotate(
240
- cover_custom=F("custom_cover"),
241
- cover_pk=Case(
242
- When(custom_cover__isnull=False, then=F("custom_cover__pk")),
243
- default=default_cover_pk,
244
- ),
245
- cover_mtime=Case(
246
- When(
247
- custom_cover__isnull=False, then=F("custom_cover__updated_at")
248
- ),
249
- default=default_cover_mtime,
250
- ),
251
- cover_pks=JsonGroupArray("cover_pk", distinct=True),
260
+ if model in (Volume, Comic):
261
+ cover_custom = Value(False)
262
+ cover_pk = default_cover_pk
263
+ cover_mtime = default_cover_mtime
264
+ else:
265
+ cover_custom = F("custom_cover")
266
+ cover_pk = Case(
267
+ When(custom_cover__isnull=False, then=F("custom_cover__pk")),
268
+ default=default_cover_pk,
252
269
  )
253
- return qs
270
+ cover_mtime = Case(
271
+ When(custom_cover__isnull=False, then=F("custom_cover__updated_at")),
272
+ default=default_cover_mtime,
273
+ )
274
+
275
+ return qs.annotate(
276
+ cover_custom=cover_custom,
277
+ cover_pk=cover_pk,
278
+ cover_mtime=cover_mtime,
279
+ cover_pks=JsonGroupArray("cover_pk", distinct=True),
280
+ )
254
281
 
255
282
  def _annotate_group(self, qs, model):
256
283
  """Annotate Group."""
@@ -263,7 +290,7 @@ class BrowserAnnotationsView(BrowserOrderByView, SharedAnnotationsMixin):
263
290
  bm_rel, bm_filter = self.bm_annotation_data[model] # type: ignore
264
291
  else:
265
292
  bm_rel = self.get_bm_rel(model)
266
- bm_filter = self._get_my_bookmark_filter(bm_rel)
293
+ bm_filter = self.get_my_bookmark_filter(bm_rel)
267
294
 
268
295
  page_rel = f"{bm_rel}__page"
269
296
  finished_rel = f"{bm_rel}__finished"
@@ -340,6 +367,16 @@ class BrowserAnnotationsView(BrowserOrderByView, SharedAnnotationsMixin):
340
367
  progress = Case(When(page_count__gt=0, then=then), default=0.0)
341
368
  return qs.annotate(progress=progress)
342
369
 
370
+ def _annotate_mtime(self, qs):
371
+ """Annotations mtime."""
372
+ when_filter = {self.rel_prefix + "bookmark__updated_at__gt": F("updated_at")}
373
+ mtime = Case(
374
+ When(**when_filter),
375
+ then=F(self.rel_prefix + "bookmark__updated_at"),
376
+ default=F("updated_at"),
377
+ )
378
+ return qs.annotate(mtime=mtime)
379
+
343
380
  def annotate_card_aggregates(self, qs, model):
344
381
  """Annotate aggregates that appear the browser card."""
345
382
  qs = self._annotate_cover_pk(qs, model)
@@ -349,11 +386,8 @@ class BrowserAnnotationsView(BrowserOrderByView, SharedAnnotationsMixin):
349
386
  qs = self._annotate_group(qs, model)
350
387
  qs = self.annotate_group_names(qs, model)
351
388
  qs = self._annotate_bookmarks(qs, model)
352
- return self._annotate_progress(qs)
353
-
354
- def annotate_for_metadata(self, qs, _model):
355
- """Annotations only for metadata."""
356
- return qs.annotate(mtime=Max("updated_at"))
389
+ qs = self._annotate_progress(qs)
390
+ return self._annotate_mtime(qs)
357
391
 
358
392
  def _get_cover_pk_query(self, cover_pks, group_ids):
359
393
  """Get Cover Pk queryset for comic queryset."""
@@ -400,7 +434,8 @@ class BrowserAnnotationsView(BrowserOrderByView, SharedAnnotationsMixin):
400
434
  """Python hack to re-cover groups collapsed with group_by."""
401
435
  # This would be better in the main query but OuterRef can't access annotations.
402
436
  # So cover_pks aggregates all covers from multi groups like ids does.
403
- if self.is_model_comic or self.params.get("cover_style") == "i":
437
+ cover_style = self.params.get("cover_style")
438
+ if self.is_model_comic or cover_style == "i":
404
439
  return group_qs
405
440
 
406
441
  recovered_group_list = []
@@ -408,15 +443,21 @@ class BrowserAnnotationsView(BrowserOrderByView, SharedAnnotationsMixin):
408
443
  cover_pks = group.cover_pks
409
444
  if len(cover_pks) == 1:
410
445
  cover_pk = cover_pks[0]
411
- cover_updated_at = group.updated_at
412
446
  else:
413
447
  comic_qs = self._get_cover_pk_query(cover_pks, group.ids)
414
448
  # print(comic_qs.explain())
415
449
  # print(comic_qs.query)
416
450
  cover = comic_qs[0]
417
451
  cover_pk = cover.pk
418
- cover_updated_at = cover.updated_at
452
+
453
+ if len(group.ids) == 1:
454
+ cover_mtime = group.updated_at
455
+ else:
456
+ cover_mtime = group.__class__.objects.filter(
457
+ pk__in=group.ids
458
+ ).aggregate(updated_at=Max("updated_at"))["updated_at"]
459
+
460
+ group.cover_mtime = cover_mtime
419
461
  group.cover_pk = cover_pk
420
- group.cover_mtime = cover_updated_at
421
462
  recovered_group_list.append(group)
422
463
  return recovered_group_list
@@ -1,16 +1,11 @@
1
1
  """Views for browsing comic library."""
2
2
 
3
- import json
4
- from copy import deepcopy
5
3
  from types import MappingProxyType
6
- from typing import Any
7
- from urllib.parse import unquote_plus
4
+ from typing import TYPE_CHECKING, Any
8
5
 
9
6
  from django.contrib.auth.models import User
10
7
  from django.db.models import Q
11
8
  from django.db.models.aggregates import Max, Min
12
- from djangorestframework_camel_case.settings import api_settings
13
- from djangorestframework_camel_case.util import underscoreize
14
9
  from rest_framework.exceptions import NotFound
15
10
 
16
11
  from codex.logger.logging import get_logger
@@ -18,43 +13,25 @@ from codex.models import (
18
13
  AdminFlag,
19
14
  Comic,
20
15
  Folder,
21
- Imprint,
22
- Publisher,
23
- Series,
24
- StoryArc,
25
- Volume,
26
16
  )
27
- from codex.models.groups import BrowserGroupModel
28
17
  from codex.serializers.browser.settings import BrowserSettingsSerializer
29
- from codex.views.browser.filters.bookmark import BookmarkFilterMixin
30
- from codex.views.browser.filters.field import ComicFieldFilter
31
- from codex.views.browser.filters.group import GroupFilterMixin
32
- from codex.views.browser.filters.search import SearchFilterMixin
18
+ from codex.views.browser.filters.search import SearchFilterView
19
+ from codex.views.const import GROUP_MODEL_MAP, ROOT_GROUP
20
+ from codex.views.utils import reparse_json_query_params
33
21
 
34
22
  LOG = get_logger(__name__)
35
23
 
24
+ if TYPE_CHECKING:
25
+ from codex.models.groups import BrowserGroupModel
36
26
 
37
- class BrowserBaseView(
38
- ComicFieldFilter, BookmarkFilterMixin, GroupFilterMixin, SearchFilterMixin
39
- ):
27
+ _REPARSE_JSON_FIELDS = frozenset({"filters", "show"})
28
+
29
+
30
+ class BrowserBaseView(SearchFilterView):
40
31
  """Browse comics with a variety of filters and sorts."""
41
32
 
42
33
  input_serializer_class = BrowserSettingsSerializer
43
34
 
44
- GROUP_MODEL_MAP: MappingProxyType[str, type[BrowserGroupModel] | None] = (
45
- MappingProxyType(
46
- {
47
- GroupFilterMixin.ROOT_GROUP: None,
48
- "p": Publisher,
49
- "i": Imprint,
50
- "s": Series,
51
- "v": Volume,
52
- GroupFilterMixin.COMIC_GROUP: Comic,
53
- GroupFilterMixin.FOLDER_GROUP: Folder,
54
- GroupFilterMixin.STORY_ARC_GROUP: StoryArc,
55
- }
56
- )
57
- )
58
35
  ADMIN_FLAG_VALUE_KEY_MAP = MappingProxyType({})
59
36
 
60
37
  _GET_JSON_KEYS = frozenset({"filters", "show"})
@@ -73,6 +50,7 @@ class BrowserBaseView(
73
50
  self.is_model_comic: bool = False
74
51
  self.admin_flags: MappingProxyType[str, bool] = MappingProxyType({})
75
52
  self.order_agg_func: type[Min | Max] = Min
53
+ self.order_key: str = ""
76
54
 
77
55
  def is_admin(self):
78
56
  """Is the current user an admin."""
@@ -95,7 +73,7 @@ class BrowserBaseView(
95
73
  def get_model_group(self):
96
74
  """Determine model group for set_browse_model()."""
97
75
  group = self.kwargs["group"]
98
- if group == self.ROOT_GROUP:
76
+ if group == ROOT_GROUP:
99
77
  group = self.params["top_group"]
100
78
  return group
101
79
 
@@ -104,7 +82,7 @@ class BrowserBaseView(
104
82
  add_acl = not (cover and self.model == Folder) # type: ignore
105
83
  object_filter = self.get_group_acl_filter(model) if add_acl else Q()
106
84
  object_filter &= self.get_bookmark_filter(model)
107
- object_filter &= self.get_comic_field_filter(self.rel_prefix)
85
+ object_filter &= self.get_comic_field_filter(model)
108
86
  return object_filter
109
87
 
110
88
  def get_query_filters(self, model, choices=False):
@@ -113,47 +91,21 @@ class BrowserBaseView(
113
91
  object_filter &= self.get_group_filter(choices)
114
92
  return object_filter
115
93
 
116
- def _parse_query_params(self, query_params):
94
+ def _parse_query_params(self):
117
95
  """Parse GET query parameters: filter object & snake case."""
118
- result = {}
119
- for key, val in query_params.items():
120
- if key in self._GET_JSON_KEYS:
121
- unquoted_val = unquote_plus(val) # for pocketbooks reader
122
- parsed_val = json.loads(unquoted_val)
123
- if not parsed_val:
124
- continue
125
- parsed_key = key
126
- elif key in ("q", "query"):
127
- # parse and strip query param
128
- # "query" is used by opds v2
129
- if "q" in result:
130
- continue
131
- parsed_val = val.strip()
132
- if not parsed_val:
133
- continue
134
- parsed_key = "q"
135
- else:
136
- parsed_key = key
137
- parsed_val = val
138
-
139
- result[parsed_key] = parsed_val
140
- return underscoreize(result, **api_settings.JSON_UNDERSCOREIZE)
96
+ query_params = reparse_json_query_params(self.request.GET, _REPARSE_JSON_FIELDS)
97
+ if "q" not in query_params and (query := query_params.get("query")):
98
+ # parse query param for opds v2
99
+ query_params["q"] = query
100
+ return query_params
141
101
 
142
102
  def parse_params(self):
143
103
  """Validate sbmitted settings and apply them over the session settings."""
144
- if self.request.method == "GET":
145
- data = self._parse_query_params(self.request.GET)
146
- elif hasattr(self.request, "data"):
147
- data = self._parse_query_params(self.request.data)
148
- else:
149
- data = {}
150
-
104
+ data = self._parse_query_params()
151
105
  serializer = self.input_serializer_class(data=data)
152
106
  serializer.is_valid(raise_exception=True)
153
-
154
- params = deepcopy(self.request.session[self.SESSION_KEY])
155
- params.update(serializer.validated_data)
156
- self.params = MappingProxyType(params)
107
+ self.load_params_from_session(serializer.validated_data)
108
+ self.is_bookmark_filtered = bool(self.params.get("filters", {}).get("bookmark"))
157
109
 
158
110
  def set_order_key(self):
159
111
  """Unused until browser."""
@@ -164,10 +116,10 @@ class BrowserBaseView(
164
116
  def set_model(self):
165
117
  """Set the model for the browse list."""
166
118
  group = self.kwargs["group"]
167
- self.group_class = self.GROUP_MODEL_MAP[group]
119
+ self.group_class = GROUP_MODEL_MAP[group]
168
120
 
169
121
  self.model_group = self.get_model_group()
170
- self.model = self.GROUP_MODEL_MAP.get(self.model_group)
122
+ self.model = GROUP_MODEL_MAP.get(self.model_group)
171
123
  if self.model is None:
172
124
  detail = f"Cannot browse {group=}"
173
125
  LOG.debug(detail)