codex 1.6.0a7__py3-none-any.whl → 1.6.0a8__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 (510) hide show
  1. codex/integrity.py +4 -4
  2. codex/librarian/importer/{aggregate_metadata.py → aggregate.py} +54 -76
  3. codex/librarian/importer/cache.py +162 -29
  4. codex/librarian/importer/const.py +15 -6
  5. codex/librarian/importer/create_comics.py +102 -17
  6. codex/librarian/importer/create_covers.py +98 -0
  7. codex/librarian/importer/create_fks.py +73 -170
  8. codex/librarian/importer/deleted.py +59 -46
  9. codex/librarian/importer/{clean_metadata.py → extract.py} +33 -4
  10. codex/librarian/importer/failed_imports.py +41 -62
  11. codex/librarian/importer/importer.py +96 -0
  12. codex/librarian/importer/importerd.py +71 -397
  13. codex/librarian/importer/init.py +265 -0
  14. codex/librarian/importer/link_comics.py +26 -81
  15. codex/librarian/importer/link_covers.py +73 -0
  16. codex/librarian/importer/moved.py +72 -96
  17. codex/librarian/importer/query_covers.py +52 -0
  18. codex/librarian/importer/query_fks.py +128 -172
  19. codex/librarian/importer/status.py +2 -39
  20. codex/librarian/importer/tasks.py +22 -13
  21. codex/librarian/janitor/cleanup.py +2 -2
  22. codex/librarian/janitor/janitor.py +10 -0
  23. codex/librarian/librariand.py +7 -1
  24. codex/migrations/0027_import_order_and_covers.py +345 -0
  25. codex/models/comic.py +1 -2
  26. codex/models/groups.py +3 -2
  27. codex/models/paths.py +0 -1
  28. codex/search/query.py +25 -2
  29. codex/search/writing.py +1 -1
  30. codex/serializers/browser/mixins.py +0 -18
  31. codex/serializers/browser/settings.py +2 -3
  32. codex/serializers/choices.py +10 -1
  33. codex/settings/settings.py +2 -2
  34. codex/static_root/assets/{VCheckbox-BOUtyxuo.c690f0cdbe48.js → VCheckbox-DI-DgMb4.8eeb1bd3e18b.js} +1 -1
  35. codex/static_root/assets/VCheckbox-DI-DgMb4.8eeb1bd3e18b.js.br +0 -0
  36. codex/static_root/assets/VCheckbox-DI-DgMb4.8eeb1bd3e18b.js.gz +0 -0
  37. codex/static_root/assets/{VCheckbox-BOUtyxuo.js → VCheckbox-DI-DgMb4.js} +1 -1
  38. codex/static_root/assets/VCheckbox-DI-DgMb4.js.br +0 -0
  39. codex/static_root/assets/VCheckbox-DI-DgMb4.js.gz +0 -0
  40. codex/static_root/assets/{VCheckboxBtn-B1-m5pEh.e2b8cbdb92c9.js → VCheckboxBtn-B8Z4NA-t.3d8921f98ad9.js} +1 -1
  41. codex/static_root/assets/VCheckboxBtn-B8Z4NA-t.3d8921f98ad9.js.br +0 -0
  42. codex/static_root/assets/VCheckboxBtn-B8Z4NA-t.3d8921f98ad9.js.gz +0 -0
  43. codex/static_root/assets/{VCheckboxBtn-B1-m5pEh.js → VCheckboxBtn-B8Z4NA-t.js} +1 -1
  44. codex/static_root/assets/VCheckboxBtn-B8Z4NA-t.js.br +0 -0
  45. codex/static_root/assets/VCheckboxBtn-B8Z4NA-t.js.gz +0 -0
  46. codex/static_root/assets/{VCombobox-DjkDXe33.22617ea193b1.js → VCombobox-D1_0lCri.6ef85a29c2dc.js} +1 -1
  47. codex/static_root/assets/VCombobox-D1_0lCri.6ef85a29c2dc.js.br +0 -0
  48. codex/static_root/assets/VCombobox-D1_0lCri.6ef85a29c2dc.js.gz +0 -0
  49. codex/static_root/assets/{VCombobox-DjkDXe33.js → VCombobox-D1_0lCri.js} +1 -1
  50. codex/static_root/assets/VCombobox-D1_0lCri.js.br +0 -0
  51. codex/static_root/assets/VCombobox-D1_0lCri.js.gz +0 -0
  52. codex/static_root/assets/{VDialog-X0zn9AGX.0d89c749b9a6.js → VDialog-opmV0YZz.d77216e97599.js} +1 -1
  53. codex/static_root/assets/VDialog-opmV0YZz.d77216e97599.js.br +0 -0
  54. codex/static_root/assets/VDialog-opmV0YZz.d77216e97599.js.gz +0 -0
  55. codex/static_root/assets/{VDialog-X0zn9AGX.js → VDialog-opmV0YZz.js} +1 -1
  56. codex/static_root/assets/VDialog-opmV0YZz.js.br +0 -0
  57. codex/static_root/assets/VDialog-opmV0YZz.js.gz +0 -0
  58. codex/static_root/assets/{VExpansionPanels-CRevojaF.43d65c777c58.js → VExpansionPanels-DX2dhUf5.ecc2ebfa62ec.js} +1 -1
  59. codex/static_root/assets/VExpansionPanels-DX2dhUf5.ecc2ebfa62ec.js.br +0 -0
  60. codex/static_root/assets/VExpansionPanels-DX2dhUf5.ecc2ebfa62ec.js.gz +0 -0
  61. codex/static_root/assets/{VExpansionPanels-CRevojaF.js → VExpansionPanels-DX2dhUf5.js} +1 -1
  62. codex/static_root/assets/VExpansionPanels-DX2dhUf5.js.br +0 -0
  63. codex/static_root/assets/VExpansionPanels-DX2dhUf5.js.gz +0 -0
  64. codex/static_root/assets/{VRadioGroup-8nrrxjDv.2b4dfb984a8c.js → VRadioGroup-BGa6_q5a.d1a3ad7266bd.js} +1 -1
  65. codex/static_root/assets/VRadioGroup-BGa6_q5a.d1a3ad7266bd.js.br +0 -0
  66. codex/static_root/assets/VRadioGroup-BGa6_q5a.d1a3ad7266bd.js.gz +0 -0
  67. codex/static_root/assets/{VRadioGroup-8nrrxjDv.js → VRadioGroup-BGa6_q5a.js} +1 -1
  68. codex/static_root/assets/VRadioGroup-BGa6_q5a.js.br +0 -0
  69. codex/static_root/assets/VRadioGroup-BGa6_q5a.js.gz +0 -0
  70. codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css +1 -0
  71. codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css.br +0 -0
  72. codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css.gz +0 -0
  73. codex/static_root/assets/VSelect-ARDhJiQK.css +1 -0
  74. codex/static_root/assets/VSelect-ARDhJiQK.css.br +0 -0
  75. codex/static_root/assets/VSelect-ARDhJiQK.css.gz +0 -0
  76. codex/static_root/assets/VSelect-sIYW_-ud.7d40da0ec3ff.js +1 -0
  77. codex/static_root/assets/VSelect-sIYW_-ud.7d40da0ec3ff.js.br +0 -0
  78. codex/static_root/assets/VSelect-sIYW_-ud.7d40da0ec3ff.js.gz +0 -0
  79. codex/static_root/assets/VSelect-sIYW_-ud.js +1 -0
  80. codex/static_root/assets/VSelect-sIYW_-ud.js.br +0 -0
  81. codex/static_root/assets/VSelect-sIYW_-ud.js.gz +0 -0
  82. codex/static_root/assets/{VSelectionControl-CGu0oz0K.3030b7a270d6.js → VSelectionControl-C8DP9rgp.ef80e8e3dfc1.js} +1 -1
  83. codex/static_root/assets/VSelectionControl-C8DP9rgp.ef80e8e3dfc1.js.br +0 -0
  84. codex/static_root/assets/VSelectionControl-C8DP9rgp.ef80e8e3dfc1.js.gz +0 -0
  85. codex/static_root/assets/{VSelectionControl-CGu0oz0K.js → VSelectionControl-C8DP9rgp.js} +1 -1
  86. codex/static_root/assets/VSelectionControl-C8DP9rgp.js.br +0 -0
  87. codex/static_root/assets/VSelectionControl-C8DP9rgp.js.gz +0 -0
  88. codex/static_root/assets/{VSlideGroup-D_oNvCOd.js → VSlideGroup-BRYhkLH8.3534ec94b2f1.js} +1 -1
  89. codex/static_root/assets/VSlideGroup-BRYhkLH8.3534ec94b2f1.js.br +0 -0
  90. codex/static_root/assets/VSlideGroup-BRYhkLH8.3534ec94b2f1.js.gz +0 -0
  91. codex/static_root/assets/{VSlideGroup-D_oNvCOd.71a7101cdddd.js → VSlideGroup-BRYhkLH8.js} +1 -1
  92. codex/static_root/assets/VSlideGroup-BRYhkLH8.js.br +0 -0
  93. codex/static_root/assets/VSlideGroup-BRYhkLH8.js.gz +0 -0
  94. codex/static_root/assets/{VTable-C7pKI7gU.087912f706f5.js → VTable-D1esKtlw.52165930e3b9.js} +1 -1
  95. codex/static_root/assets/VTable-D1esKtlw.52165930e3b9.js.br +0 -0
  96. codex/static_root/assets/VTable-D1esKtlw.52165930e3b9.js.gz +0 -0
  97. codex/static_root/assets/{VTable-C7pKI7gU.js → VTable-D1esKtlw.js} +1 -1
  98. codex/static_root/assets/VTable-D1esKtlw.js.br +0 -0
  99. codex/static_root/assets/VTable-D1esKtlw.js.gz +0 -0
  100. codex/static_root/assets/VTextField-BnGscKcY.54032adf3a4b.js +1 -0
  101. codex/static_root/assets/VTextField-BnGscKcY.54032adf3a4b.js.br +0 -0
  102. codex/static_root/assets/VTextField-BnGscKcY.54032adf3a4b.js.gz +0 -0
  103. codex/static_root/assets/VTextField-BnGscKcY.js +1 -0
  104. codex/static_root/assets/VTextField-BnGscKcY.js.br +0 -0
  105. codex/static_root/assets/VTextField-BnGscKcY.js.gz +0 -0
  106. codex/static_root/assets/{VWindowItem-iyZ1XOkP.409f7d292253.js → VWindowItem-B0KokcAk.ea94224aecb9.js} +1 -1
  107. codex/static_root/assets/VWindowItem-B0KokcAk.ea94224aecb9.js.br +0 -0
  108. codex/static_root/assets/VWindowItem-B0KokcAk.ea94224aecb9.js.gz +0 -0
  109. codex/static_root/assets/{VWindowItem-iyZ1XOkP.js → VWindowItem-B0KokcAk.js} +1 -1
  110. codex/static_root/assets/VWindowItem-B0KokcAk.js.br +0 -0
  111. codex/static_root/assets/VWindowItem-B0KokcAk.js.gz +0 -0
  112. codex/static_root/assets/{admin-B0pCBjma.2407da79bd20.js → admin-Y0bL21AB.42d4f0d68163.js} +1 -1
  113. codex/static_root/assets/admin-Y0bL21AB.42d4f0d68163.js.br +0 -0
  114. codex/static_root/assets/admin-Y0bL21AB.42d4f0d68163.js.gz +0 -0
  115. codex/static_root/assets/{admin-B0pCBjma.js → admin-Y0bL21AB.js} +1 -1
  116. codex/static_root/assets/admin-Y0bL21AB.js.br +0 -0
  117. codex/static_root/assets/admin-Y0bL21AB.js.gz +0 -0
  118. codex/static_root/assets/{admin-drawer-panel-BxoNUHQr.5e2e1a4a255d.js → admin-drawer-panel-DNngWUEM.4e992e3fe86e.js} +11 -11
  119. codex/static_root/assets/admin-drawer-panel-DNngWUEM.4e992e3fe86e.js.br +0 -0
  120. codex/static_root/assets/admin-drawer-panel-DNngWUEM.4e992e3fe86e.js.gz +0 -0
  121. codex/static_root/assets/{admin-drawer-panel-BxoNUHQr.js → admin-drawer-panel-DNngWUEM.js} +11 -11
  122. codex/static_root/assets/admin-drawer-panel-DNngWUEM.js.br +0 -0
  123. codex/static_root/assets/admin-drawer-panel-DNngWUEM.js.gz +0 -0
  124. codex/static_root/assets/browser-CMAtBCLx.17a277ce43a9.css +1 -0
  125. codex/static_root/assets/browser-CMAtBCLx.17a277ce43a9.css.br +0 -0
  126. codex/static_root/assets/browser-CMAtBCLx.17a277ce43a9.css.gz +0 -0
  127. codex/static_root/assets/browser-CMAtBCLx.css +1 -0
  128. codex/static_root/assets/browser-CMAtBCLx.css.br +0 -0
  129. codex/static_root/assets/browser-CMAtBCLx.css.gz +0 -0
  130. codex/static_root/assets/browser-DoJk8uvb.2c97a3858932.js +1 -0
  131. codex/static_root/assets/browser-DoJk8uvb.2c97a3858932.js.br +0 -0
  132. codex/static_root/assets/browser-DoJk8uvb.2c97a3858932.js.gz +0 -0
  133. codex/static_root/assets/browser-DoJk8uvb.js +1 -0
  134. codex/static_root/assets/browser-DoJk8uvb.js.br +0 -0
  135. codex/static_root/assets/browser-DoJk8uvb.js.gz +0 -0
  136. codex/static_root/assets/{change-password-dialog-DZ9dP8_F.js → change-password-dialog-CH4xbjJa.b749aa6ea07e.js} +1 -1
  137. codex/static_root/assets/change-password-dialog-CH4xbjJa.b749aa6ea07e.js.br +0 -0
  138. codex/static_root/assets/change-password-dialog-CH4xbjJa.b749aa6ea07e.js.gz +0 -0
  139. codex/static_root/assets/{change-password-dialog-DZ9dP8_F.930f4cae3cb0.js → change-password-dialog-CH4xbjJa.js} +1 -1
  140. codex/static_root/assets/change-password-dialog-CH4xbjJa.js.br +0 -0
  141. codex/static_root/assets/change-password-dialog-CH4xbjJa.js.gz +0 -0
  142. codex/static_root/assets/{confirm-dialog-RP7JNStF.a752a7c1e697.js → confirm-dialog-DkU0twE6.a20006aa5ba6.js} +1 -1
  143. codex/static_root/assets/{confirm-dialog-RP7JNStF.a752a7c1e697.js.br → confirm-dialog-DkU0twE6.a20006aa5ba6.js.br} +0 -0
  144. codex/static_root/assets/confirm-dialog-DkU0twE6.a20006aa5ba6.js.gz +0 -0
  145. codex/static_root/assets/{confirm-dialog-RP7JNStF.js → confirm-dialog-DkU0twE6.js} +1 -1
  146. codex/static_root/assets/{confirm-dialog-RP7JNStF.js.br → confirm-dialog-DkU0twE6.js.br} +0 -0
  147. codex/static_root/assets/confirm-dialog-DkU0twE6.js.gz +0 -0
  148. codex/static_root/assets/datetime-column-BfW_j_3Q.6a614321a435.js +1 -0
  149. codex/static_root/assets/datetime-column-BfW_j_3Q.6a614321a435.js.br +0 -0
  150. codex/static_root/assets/datetime-column-BfW_j_3Q.6a614321a435.js.gz +0 -0
  151. codex/static_root/assets/datetime-column-BfW_j_3Q.js +1 -0
  152. codex/static_root/assets/datetime-column-BfW_j_3Q.js.br +0 -0
  153. codex/static_root/assets/datetime-column-BfW_j_3Q.js.gz +0 -0
  154. codex/static_root/assets/datetime-column-DeCthByU.1f3bf499e063.css +1 -0
  155. codex/static_root/assets/datetime-column-DeCthByU.css +1 -0
  156. codex/static_root/assets/{filter-DAdGUt-1.3261cbcd50b5.js → filter-DjxN78cK.35673f6e49b1.js} +1 -1
  157. codex/static_root/assets/filter-DjxN78cK.35673f6e49b1.js.br +0 -0
  158. codex/static_root/assets/filter-DjxN78cK.35673f6e49b1.js.gz +0 -0
  159. codex/static_root/assets/{filter-DAdGUt-1.js → filter-DjxN78cK.js} +1 -1
  160. codex/static_root/assets/filter-DjxN78cK.js.br +0 -0
  161. codex/static_root/assets/filter-DjxN78cK.js.gz +0 -0
  162. codex/static_root/assets/flag-tab-D4ZNSbMR.e763a9937701.js +1 -0
  163. codex/static_root/assets/flag-tab-D4ZNSbMR.e763a9937701.js.br +0 -0
  164. codex/static_root/assets/flag-tab-D4ZNSbMR.e763a9937701.js.gz +0 -0
  165. codex/static_root/assets/flag-tab-D4ZNSbMR.js +1 -0
  166. codex/static_root/assets/flag-tab-D4ZNSbMR.js.br +0 -0
  167. codex/static_root/assets/flag-tab-D4ZNSbMR.js.gz +0 -0
  168. codex/static_root/assets/{group-tab-DEwh4jfB.a95097b5e320.js → group-tab-DucpNoBs.a6f37ae940c0.js} +1 -1
  169. codex/static_root/assets/group-tab-DucpNoBs.a6f37ae940c0.js.br +0 -0
  170. codex/static_root/assets/group-tab-DucpNoBs.a6f37ae940c0.js.gz +0 -0
  171. codex/static_root/assets/{group-tab-DEwh4jfB.js → group-tab-DucpNoBs.js} +1 -1
  172. codex/static_root/assets/group-tab-DucpNoBs.js.br +0 -0
  173. codex/static_root/assets/group-tab-DucpNoBs.js.gz +0 -0
  174. codex/static_root/assets/http-error-Cj0hTq-c.062a88ddd4dc.js +1 -0
  175. codex/static_root/assets/http-error-Cj0hTq-c.062a88ddd4dc.js.br +0 -0
  176. codex/static_root/assets/http-error-Cj0hTq-c.062a88ddd4dc.js.gz +0 -0
  177. codex/static_root/assets/http-error-Cj0hTq-c.js +1 -0
  178. codex/static_root/assets/http-error-Cj0hTq-c.js.br +0 -0
  179. codex/static_root/assets/http-error-Cj0hTq-c.js.gz +0 -0
  180. codex/static_root/assets/{library-tab-DrXvD9B2.js → library-tab-CzksdS3Q.566acdb45dec.js} +1 -1
  181. codex/static_root/assets/library-tab-CzksdS3Q.566acdb45dec.js.br +0 -0
  182. codex/static_root/assets/library-tab-CzksdS3Q.566acdb45dec.js.gz +0 -0
  183. codex/static_root/assets/{library-tab-DrXvD9B2.f765f9d3bae4.js → library-tab-CzksdS3Q.js} +1 -1
  184. codex/static_root/assets/library-tab-CzksdS3Q.js.br +0 -0
  185. codex/static_root/assets/library-tab-CzksdS3Q.js.gz +0 -0
  186. codex/static_root/assets/main-DX-k2724.f73d08e9ad64.js +32 -0
  187. codex/static_root/assets/main-DX-k2724.f73d08e9ad64.js.br +0 -0
  188. codex/static_root/assets/main-DX-k2724.f73d08e9ad64.js.gz +0 -0
  189. codex/static_root/assets/main-DX-k2724.js +32 -0
  190. codex/static_root/assets/main-DX-k2724.js.br +0 -0
  191. codex/static_root/assets/main-DX-k2724.js.gz +0 -0
  192. codex/static_root/assets/pagination-toolbar-CQcR0a3T.7a2d33ed8c39.js +1 -0
  193. codex/static_root/assets/pagination-toolbar-CQcR0a3T.7a2d33ed8c39.js.br +0 -0
  194. codex/static_root/assets/pagination-toolbar-CQcR0a3T.7a2d33ed8c39.js.gz +0 -0
  195. codex/static_root/assets/pagination-toolbar-CQcR0a3T.js +1 -0
  196. codex/static_root/assets/pagination-toolbar-CQcR0a3T.js.br +0 -0
  197. codex/static_root/assets/pagination-toolbar-CQcR0a3T.js.gz +0 -0
  198. codex/static_root/assets/pagination-toolbar-DWdp_kmz.css +1 -0
  199. codex/static_root/assets/pagination-toolbar-DWdp_kmz.css.br +0 -0
  200. codex/static_root/assets/pagination-toolbar-DWdp_kmz.css.gz +0 -0
  201. codex/static_root/assets/pagination-toolbar-DWdp_kmz.f98f47e87612.css +1 -0
  202. codex/static_root/assets/pagination-toolbar-DWdp_kmz.f98f47e87612.css.br +0 -0
  203. codex/static_root/assets/pagination-toolbar-DWdp_kmz.f98f47e87612.css.gz +0 -0
  204. codex/static_root/assets/{pdf-doc-obrBwxa6.ec9bd13a5786.js → pdf-doc-GkMk4o5L.ef24c859a7a4.js} +1 -1
  205. codex/static_root/assets/pdf-doc-GkMk4o5L.ef24c859a7a4.js.br +0 -0
  206. codex/static_root/assets/pdf-doc-GkMk4o5L.ef24c859a7a4.js.gz +0 -0
  207. codex/static_root/assets/{pdf-doc-obrBwxa6.js → pdf-doc-GkMk4o5L.js} +1 -1
  208. codex/static_root/assets/pdf-doc-GkMk4o5L.js.br +0 -0
  209. codex/static_root/assets/pdf-doc-GkMk4o5L.js.gz +0 -0
  210. codex/static_root/assets/reader-CCCfpM3x.1553e45bd775.css +1 -0
  211. codex/static_root/assets/reader-CCCfpM3x.1553e45bd775.css.br +0 -0
  212. codex/static_root/assets/reader-CCCfpM3x.1553e45bd775.css.gz +0 -0
  213. codex/static_root/assets/reader-CCCfpM3x.css +1 -0
  214. codex/static_root/assets/reader-CCCfpM3x.css.br +0 -0
  215. codex/static_root/assets/reader-CCCfpM3x.css.gz +0 -0
  216. codex/static_root/assets/reader-Cl6mmQB_.7b9e7591954f.js +2 -0
  217. codex/static_root/assets/reader-Cl6mmQB_.7b9e7591954f.js.br +0 -0
  218. codex/static_root/assets/reader-Cl6mmQB_.7b9e7591954f.js.gz +0 -0
  219. codex/static_root/assets/reader-Cl6mmQB_.js +2 -0
  220. codex/static_root/assets/reader-Cl6mmQB_.js.br +0 -0
  221. codex/static_root/assets/reader-Cl6mmQB_.js.gz +0 -0
  222. codex/static_root/assets/{relation-chips-CZJcLjc8.1d7426dab654.js → relation-chips-CZ8VvR4y.d4232f3c2518.js} +1 -1
  223. codex/static_root/assets/relation-chips-CZ8VvR4y.d4232f3c2518.js.br +0 -0
  224. codex/static_root/assets/relation-chips-CZ8VvR4y.d4232f3c2518.js.gz +0 -0
  225. codex/static_root/assets/{relation-chips-CZJcLjc8.js → relation-chips-CZ8VvR4y.js} +1 -1
  226. codex/static_root/assets/relation-chips-CZ8VvR4y.js.br +0 -0
  227. codex/static_root/assets/relation-chips-CZ8VvR4y.js.gz +0 -0
  228. codex/static_root/assets/{settings-drawer-7iTnAdjf.337eecf2fdbb.js → settings-drawer-DyS0A2pN.17a98928589f.js} +2 -2
  229. codex/static_root/assets/settings-drawer-DyS0A2pN.17a98928589f.js.br +0 -0
  230. codex/static_root/assets/settings-drawer-DyS0A2pN.17a98928589f.js.gz +0 -0
  231. codex/static_root/assets/{settings-drawer-7iTnAdjf.js → settings-drawer-DyS0A2pN.js} +2 -2
  232. codex/static_root/assets/settings-drawer-DyS0A2pN.js.br +0 -0
  233. codex/static_root/assets/settings-drawer-DyS0A2pN.js.gz +0 -0
  234. codex/static_root/assets/{stats-tab-CQsK1n8O.544bf50ffb22.js → stats-tab-R3Ekobfm.70fa09a67518.js} +1 -1
  235. codex/static_root/assets/stats-tab-R3Ekobfm.70fa09a67518.js.br +0 -0
  236. codex/static_root/assets/stats-tab-R3Ekobfm.70fa09a67518.js.gz +0 -0
  237. codex/static_root/assets/{stats-tab-CQsK1n8O.js → stats-tab-R3Ekobfm.js} +1 -1
  238. codex/static_root/assets/stats-tab-R3Ekobfm.js.br +0 -0
  239. codex/static_root/assets/stats-tab-R3Ekobfm.js.gz +0 -0
  240. codex/static_root/assets/task-tab-BhLDK_7Q.28ff51b3ed01.js +1 -0
  241. codex/static_root/assets/task-tab-BhLDK_7Q.28ff51b3ed01.js.br +0 -0
  242. codex/static_root/assets/task-tab-BhLDK_7Q.28ff51b3ed01.js.gz +0 -0
  243. codex/static_root/assets/task-tab-BhLDK_7Q.js +1 -0
  244. codex/static_root/assets/task-tab-BhLDK_7Q.js.br +0 -0
  245. codex/static_root/assets/task-tab-BhLDK_7Q.js.gz +0 -0
  246. codex/static_root/assets/to-case-BR2frjFx.87de5d7a0503.js +1 -0
  247. codex/static_root/assets/to-case-BR2frjFx.js +1 -0
  248. codex/static_root/assets/{unauthorized-Bs7NSqea.c55650fac055.js → unauthorized-BbXjAe5l.d48912d34a68.js} +1 -1
  249. codex/static_root/assets/unauthorized-BbXjAe5l.d48912d34a68.js.br +0 -0
  250. codex/static_root/assets/unauthorized-BbXjAe5l.d48912d34a68.js.gz +0 -0
  251. codex/static_root/assets/{unauthorized-Bs7NSqea.js → unauthorized-BbXjAe5l.js} +1 -1
  252. codex/static_root/assets/unauthorized-BbXjAe5l.js.br +0 -0
  253. codex/static_root/assets/unauthorized-BbXjAe5l.js.gz +0 -0
  254. codex/static_root/assets/{user-tab-ChuH7Atm.7e80cf1fb78e.js → user-tab-DxITIl2d.d79d7f062930.js} +1 -1
  255. codex/static_root/assets/user-tab-DxITIl2d.d79d7f062930.js.br +0 -0
  256. codex/static_root/assets/user-tab-DxITIl2d.d79d7f062930.js.gz +0 -0
  257. codex/static_root/assets/{user-tab-ChuH7Atm.js → user-tab-DxITIl2d.js} +1 -1
  258. codex/static_root/assets/user-tab-DxITIl2d.js.br +0 -0
  259. codex/static_root/assets/user-tab-DxITIl2d.js.gz +0 -0
  260. codex/static_root/js/choices-admin.d790df01c20a.json +1 -0
  261. codex/static_root/js/choices-admin.d790df01c20a.json.br +0 -0
  262. codex/static_root/js/choices-admin.d790df01c20a.json.gz +0 -0
  263. codex/static_root/js/choices-admin.json +1 -1
  264. codex/static_root/js/choices-admin.json.br +0 -0
  265. codex/static_root/js/choices-admin.json.gz +0 -0
  266. codex/static_root/js/{choices.079a01e0be1a.json → choices.a0aaaa6c7ef7.json} +1 -1
  267. codex/static_root/js/choices.a0aaaa6c7ef7.json.br +0 -0
  268. codex/static_root/js/choices.a0aaaa6c7ef7.json.gz +0 -0
  269. codex/static_root/js/choices.json +1 -1
  270. codex/static_root/js/choices.json.br +0 -0
  271. codex/static_root/js/choices.json.gz +0 -0
  272. codex/static_root/{manifest.dad79c8475ae.json → manifest.16d52597ef50.json} +239 -239
  273. codex/static_root/manifest.16d52597ef50.json.br +0 -0
  274. codex/static_root/manifest.16d52597ef50.json.gz +0 -0
  275. codex/static_root/manifest.json +239 -239
  276. codex/static_root/manifest.json.br +0 -0
  277. codex/static_root/manifest.json.gz +0 -0
  278. codex/static_root/staticfiles.json +1 -1
  279. codex/urls/api/browser.py +11 -2
  280. codex/urls/api/reader.py +1 -7
  281. codex/urls/opds/binary.py +1 -1
  282. codex/views/admin/auth.py +0 -2
  283. codex/views/admin/tasks.py +13 -1
  284. codex/views/auth.py +2 -4
  285. codex/views/browser/annotations.py +27 -148
  286. codex/views/browser/base.py +1 -18
  287. codex/views/browser/breadcrumbs.py +16 -14
  288. codex/views/browser/browser.py +5 -10
  289. codex/views/browser/choices.py +15 -39
  290. codex/views/browser/cover.py +177 -0
  291. codex/views/browser/filters/annotations.py +23 -22
  292. codex/views/browser/filters/field.py +1 -2
  293. codex/views/browser/filters/group.py +13 -8
  294. codex/views/browser/filters/search.py +19 -10
  295. codex/views/browser/metadata.py +20 -25
  296. codex/views/const.py +12 -0
  297. codex/views/mtime.py +14 -12
  298. codex/views/opds/binary.py +1 -1
  299. codex/views/opds/urls.py +2 -1
  300. codex/views/opds/util.py +2 -4
  301. codex/views/opds/v1/entry/links.py +18 -33
  302. codex/views/opds/v1/facets.py +2 -1
  303. codex/views/opds/v1/feed.py +42 -23
  304. codex/views/opds/v1/links.py +2 -1
  305. codex/views/opds/v2/feed.py +11 -9
  306. codex/views/opds/v2/links.py +2 -1
  307. codex/views/opds/v2/publications.py +13 -34
  308. codex/views/public.py +0 -2
  309. codex/views/reader/books.py +1 -1
  310. codex/views/reader/reader.py +2 -2
  311. codex/views/session.py +5 -3
  312. codex/views/util.py +32 -0
  313. {codex-1.6.0a7.dist-info → codex-1.6.0a8.dist-info}/METADATA +8 -7
  314. {codex-1.6.0a7.dist-info → codex-1.6.0a8.dist-info}/RECORD +317 -318
  315. codex/librarian/importer/update_comics.py +0 -85
  316. codex/migrations/0027_sort_name.py +0 -139
  317. codex/migrations/0028_custom_covers.py +0 -166
  318. codex/migrations/0029_choices_adminflag_and_timestamp.py +0 -44
  319. codex/static_root/assets/VCheckbox-BOUtyxuo.c690f0cdbe48.js.br +0 -0
  320. codex/static_root/assets/VCheckbox-BOUtyxuo.c690f0cdbe48.js.gz +0 -0
  321. codex/static_root/assets/VCheckbox-BOUtyxuo.js.br +0 -0
  322. codex/static_root/assets/VCheckbox-BOUtyxuo.js.gz +0 -0
  323. codex/static_root/assets/VCheckboxBtn-B1-m5pEh.e2b8cbdb92c9.js.br +0 -0
  324. codex/static_root/assets/VCheckboxBtn-B1-m5pEh.e2b8cbdb92c9.js.gz +0 -0
  325. codex/static_root/assets/VCheckboxBtn-B1-m5pEh.js.br +0 -0
  326. codex/static_root/assets/VCheckboxBtn-B1-m5pEh.js.gz +0 -0
  327. codex/static_root/assets/VCombobox-DjkDXe33.22617ea193b1.js.br +0 -0
  328. codex/static_root/assets/VCombobox-DjkDXe33.22617ea193b1.js.gz +0 -0
  329. codex/static_root/assets/VCombobox-DjkDXe33.js.br +0 -0
  330. codex/static_root/assets/VCombobox-DjkDXe33.js.gz +0 -0
  331. codex/static_root/assets/VDialog-X0zn9AGX.0d89c749b9a6.js.br +0 -0
  332. codex/static_root/assets/VDialog-X0zn9AGX.0d89c749b9a6.js.gz +0 -0
  333. codex/static_root/assets/VDialog-X0zn9AGX.js.br +0 -0
  334. codex/static_root/assets/VDialog-X0zn9AGX.js.gz +0 -0
  335. codex/static_root/assets/VExpansionPanels-CRevojaF.43d65c777c58.js.br +0 -0
  336. codex/static_root/assets/VExpansionPanels-CRevojaF.43d65c777c58.js.gz +0 -0
  337. codex/static_root/assets/VExpansionPanels-CRevojaF.js.br +0 -0
  338. codex/static_root/assets/VExpansionPanels-CRevojaF.js.gz +0 -0
  339. codex/static_root/assets/VRadioGroup-8nrrxjDv.2b4dfb984a8c.js.br +0 -0
  340. codex/static_root/assets/VRadioGroup-8nrrxjDv.2b4dfb984a8c.js.gz +0 -0
  341. codex/static_root/assets/VRadioGroup-8nrrxjDv.js.br +0 -0
  342. codex/static_root/assets/VRadioGroup-8nrrxjDv.js.gz +0 -0
  343. codex/static_root/assets/VSelect-DgfIKRnC.fa7a73a9c415.js +0 -1
  344. codex/static_root/assets/VSelect-DgfIKRnC.fa7a73a9c415.js.br +0 -0
  345. codex/static_root/assets/VSelect-DgfIKRnC.fa7a73a9c415.js.gz +0 -0
  346. codex/static_root/assets/VSelect-DgfIKRnC.js +0 -1
  347. codex/static_root/assets/VSelect-DgfIKRnC.js.br +0 -0
  348. codex/static_root/assets/VSelect-DgfIKRnC.js.gz +0 -0
  349. codex/static_root/assets/VSelect-MGVSeLgr.7cabd30bc5e4.css +0 -1
  350. codex/static_root/assets/VSelect-MGVSeLgr.7cabd30bc5e4.css.br +0 -0
  351. codex/static_root/assets/VSelect-MGVSeLgr.7cabd30bc5e4.css.gz +0 -0
  352. codex/static_root/assets/VSelect-MGVSeLgr.css +0 -1
  353. codex/static_root/assets/VSelect-MGVSeLgr.css.br +0 -0
  354. codex/static_root/assets/VSelect-MGVSeLgr.css.gz +0 -0
  355. codex/static_root/assets/VSelectionControl-CGu0oz0K.3030b7a270d6.js.br +0 -0
  356. codex/static_root/assets/VSelectionControl-CGu0oz0K.3030b7a270d6.js.gz +0 -0
  357. codex/static_root/assets/VSelectionControl-CGu0oz0K.js.br +0 -0
  358. codex/static_root/assets/VSelectionControl-CGu0oz0K.js.gz +0 -0
  359. codex/static_root/assets/VSlideGroup-D_oNvCOd.71a7101cdddd.js.br +0 -0
  360. codex/static_root/assets/VSlideGroup-D_oNvCOd.71a7101cdddd.js.gz +0 -0
  361. codex/static_root/assets/VSlideGroup-D_oNvCOd.js.br +0 -0
  362. codex/static_root/assets/VSlideGroup-D_oNvCOd.js.gz +0 -0
  363. codex/static_root/assets/VTable-C7pKI7gU.087912f706f5.js.br +0 -0
  364. codex/static_root/assets/VTable-C7pKI7gU.087912f706f5.js.gz +0 -0
  365. codex/static_root/assets/VTable-C7pKI7gU.js.br +0 -0
  366. codex/static_root/assets/VTable-C7pKI7gU.js.gz +0 -0
  367. codex/static_root/assets/VTextField-Bs8oq9mk.7999fef12a03.js +0 -1
  368. codex/static_root/assets/VTextField-Bs8oq9mk.7999fef12a03.js.br +0 -0
  369. codex/static_root/assets/VTextField-Bs8oq9mk.7999fef12a03.js.gz +0 -0
  370. codex/static_root/assets/VTextField-Bs8oq9mk.js +0 -1
  371. codex/static_root/assets/VTextField-Bs8oq9mk.js.br +0 -0
  372. codex/static_root/assets/VTextField-Bs8oq9mk.js.gz +0 -0
  373. codex/static_root/assets/VWindowItem-iyZ1XOkP.409f7d292253.js.br +0 -0
  374. codex/static_root/assets/VWindowItem-iyZ1XOkP.409f7d292253.js.gz +0 -0
  375. codex/static_root/assets/VWindowItem-iyZ1XOkP.js.br +0 -0
  376. codex/static_root/assets/VWindowItem-iyZ1XOkP.js.gz +0 -0
  377. codex/static_root/assets/admin-B0pCBjma.2407da79bd20.js.br +0 -0
  378. codex/static_root/assets/admin-B0pCBjma.2407da79bd20.js.gz +0 -0
  379. codex/static_root/assets/admin-B0pCBjma.js.br +0 -0
  380. codex/static_root/assets/admin-B0pCBjma.js.gz +0 -0
  381. codex/static_root/assets/admin-drawer-panel-BxoNUHQr.5e2e1a4a255d.js.br +0 -0
  382. codex/static_root/assets/admin-drawer-panel-BxoNUHQr.5e2e1a4a255d.js.gz +0 -0
  383. codex/static_root/assets/admin-drawer-panel-BxoNUHQr.js.br +0 -0
  384. codex/static_root/assets/admin-drawer-panel-BxoNUHQr.js.gz +0 -0
  385. codex/static_root/assets/browser-DbyAjCNh.577a228a966b.js +0 -1
  386. codex/static_root/assets/browser-DbyAjCNh.577a228a966b.js.br +0 -0
  387. codex/static_root/assets/browser-DbyAjCNh.577a228a966b.js.gz +0 -0
  388. codex/static_root/assets/browser-DbyAjCNh.js +0 -1
  389. codex/static_root/assets/browser-DbyAjCNh.js.br +0 -0
  390. codex/static_root/assets/browser-DbyAjCNh.js.gz +0 -0
  391. codex/static_root/assets/browser-VnzNj1t3.cff861ad2ca3.css +0 -1
  392. codex/static_root/assets/browser-VnzNj1t3.cff861ad2ca3.css.br +0 -0
  393. codex/static_root/assets/browser-VnzNj1t3.cff861ad2ca3.css.gz +0 -0
  394. codex/static_root/assets/browser-VnzNj1t3.css +0 -1
  395. codex/static_root/assets/browser-VnzNj1t3.css.br +0 -0
  396. codex/static_root/assets/browser-VnzNj1t3.css.gz +0 -0
  397. codex/static_root/assets/change-password-dialog-DZ9dP8_F.930f4cae3cb0.js.br +0 -0
  398. codex/static_root/assets/change-password-dialog-DZ9dP8_F.930f4cae3cb0.js.gz +0 -0
  399. codex/static_root/assets/change-password-dialog-DZ9dP8_F.js.br +0 -0
  400. codex/static_root/assets/change-password-dialog-DZ9dP8_F.js.gz +0 -0
  401. codex/static_root/assets/confirm-dialog-RP7JNStF.a752a7c1e697.js.gz +0 -0
  402. codex/static_root/assets/confirm-dialog-RP7JNStF.js.gz +0 -0
  403. codex/static_root/assets/datetime-column-Cu3WYyPj.54b4c3817b74.js +0 -1
  404. codex/static_root/assets/datetime-column-Cu3WYyPj.54b4c3817b74.js.br +0 -0
  405. codex/static_root/assets/datetime-column-Cu3WYyPj.54b4c3817b74.js.gz +0 -0
  406. codex/static_root/assets/datetime-column-Cu3WYyPj.js +0 -1
  407. codex/static_root/assets/datetime-column-Cu3WYyPj.js.br +0 -0
  408. codex/static_root/assets/datetime-column-Cu3WYyPj.js.gz +0 -0
  409. codex/static_root/assets/datetime-column-Dg_RxTVM.1fa7c5cbead7.css +0 -1
  410. codex/static_root/assets/datetime-column-Dg_RxTVM.1fa7c5cbead7.css.br +0 -0
  411. codex/static_root/assets/datetime-column-Dg_RxTVM.css +0 -1
  412. codex/static_root/assets/datetime-column-Dg_RxTVM.css.br +0 -0
  413. codex/static_root/assets/filter-DAdGUt-1.3261cbcd50b5.js.br +0 -0
  414. codex/static_root/assets/filter-DAdGUt-1.3261cbcd50b5.js.gz +0 -0
  415. codex/static_root/assets/filter-DAdGUt-1.js.br +0 -0
  416. codex/static_root/assets/filter-DAdGUt-1.js.gz +0 -0
  417. codex/static_root/assets/flag-tab-BP7Zs7iQ.cb16ef6363d4.js +0 -1
  418. codex/static_root/assets/flag-tab-BP7Zs7iQ.cb16ef6363d4.js.br +0 -0
  419. codex/static_root/assets/flag-tab-BP7Zs7iQ.cb16ef6363d4.js.gz +0 -0
  420. codex/static_root/assets/flag-tab-BP7Zs7iQ.js +0 -1
  421. codex/static_root/assets/flag-tab-BP7Zs7iQ.js.br +0 -0
  422. codex/static_root/assets/flag-tab-BP7Zs7iQ.js.gz +0 -0
  423. codex/static_root/assets/group-tab-DEwh4jfB.a95097b5e320.js.br +0 -0
  424. codex/static_root/assets/group-tab-DEwh4jfB.a95097b5e320.js.gz +0 -0
  425. codex/static_root/assets/group-tab-DEwh4jfB.js.br +0 -0
  426. codex/static_root/assets/group-tab-DEwh4jfB.js.gz +0 -0
  427. codex/static_root/assets/http-error-xs3mIL8U.e143c472a96c.js +0 -1
  428. codex/static_root/assets/http-error-xs3mIL8U.e143c472a96c.js.br +0 -0
  429. codex/static_root/assets/http-error-xs3mIL8U.e143c472a96c.js.gz +0 -0
  430. codex/static_root/assets/http-error-xs3mIL8U.js +0 -1
  431. codex/static_root/assets/http-error-xs3mIL8U.js.br +0 -0
  432. codex/static_root/assets/http-error-xs3mIL8U.js.gz +0 -0
  433. codex/static_root/assets/library-tab-DrXvD9B2.f765f9d3bae4.js.br +0 -0
  434. codex/static_root/assets/library-tab-DrXvD9B2.f765f9d3bae4.js.gz +0 -0
  435. codex/static_root/assets/library-tab-DrXvD9B2.js.br +0 -0
  436. codex/static_root/assets/library-tab-DrXvD9B2.js.gz +0 -0
  437. codex/static_root/assets/main-Dy9Uqpmh.2f99b1914b3e.js +0 -32
  438. codex/static_root/assets/main-Dy9Uqpmh.2f99b1914b3e.js.br +0 -0
  439. codex/static_root/assets/main-Dy9Uqpmh.2f99b1914b3e.js.gz +0 -0
  440. codex/static_root/assets/main-Dy9Uqpmh.js +0 -32
  441. codex/static_root/assets/main-Dy9Uqpmh.js.br +0 -0
  442. codex/static_root/assets/main-Dy9Uqpmh.js.gz +0 -0
  443. codex/static_root/assets/pagination-toolbar-DbS_HOT1.81222ab51ef4.js +0 -1
  444. codex/static_root/assets/pagination-toolbar-DbS_HOT1.81222ab51ef4.js.br +0 -0
  445. codex/static_root/assets/pagination-toolbar-DbS_HOT1.81222ab51ef4.js.gz +0 -0
  446. codex/static_root/assets/pagination-toolbar-DbS_HOT1.js +0 -1
  447. codex/static_root/assets/pagination-toolbar-DbS_HOT1.js.br +0 -0
  448. codex/static_root/assets/pagination-toolbar-DbS_HOT1.js.gz +0 -0
  449. codex/static_root/assets/pagination-toolbar-r6nbtWxg.4088ab042c0c.css +0 -1
  450. codex/static_root/assets/pagination-toolbar-r6nbtWxg.4088ab042c0c.css.br +0 -0
  451. codex/static_root/assets/pagination-toolbar-r6nbtWxg.4088ab042c0c.css.gz +0 -0
  452. codex/static_root/assets/pagination-toolbar-r6nbtWxg.css +0 -1
  453. codex/static_root/assets/pagination-toolbar-r6nbtWxg.css.br +0 -0
  454. codex/static_root/assets/pagination-toolbar-r6nbtWxg.css.gz +0 -0
  455. codex/static_root/assets/pdf-doc-obrBwxa6.ec9bd13a5786.js.br +0 -0
  456. codex/static_root/assets/pdf-doc-obrBwxa6.ec9bd13a5786.js.gz +0 -0
  457. codex/static_root/assets/pdf-doc-obrBwxa6.js.br +0 -0
  458. codex/static_root/assets/pdf-doc-obrBwxa6.js.gz +0 -0
  459. codex/static_root/assets/reader-BXKUaUeC.953d30b969ef.js +0 -2
  460. codex/static_root/assets/reader-BXKUaUeC.953d30b969ef.js.br +0 -0
  461. codex/static_root/assets/reader-BXKUaUeC.953d30b969ef.js.gz +0 -0
  462. codex/static_root/assets/reader-BXKUaUeC.js +0 -2
  463. codex/static_root/assets/reader-BXKUaUeC.js.br +0 -0
  464. codex/static_root/assets/reader-BXKUaUeC.js.gz +0 -0
  465. codex/static_root/assets/reader-OXJ1KVaW.a463b19d0508.css +0 -1
  466. codex/static_root/assets/reader-OXJ1KVaW.a463b19d0508.css.br +0 -0
  467. codex/static_root/assets/reader-OXJ1KVaW.a463b19d0508.css.gz +0 -0
  468. codex/static_root/assets/reader-OXJ1KVaW.css +0 -1
  469. codex/static_root/assets/reader-OXJ1KVaW.css.br +0 -0
  470. codex/static_root/assets/reader-OXJ1KVaW.css.gz +0 -0
  471. codex/static_root/assets/relation-chips-CZJcLjc8.1d7426dab654.js.br +0 -0
  472. codex/static_root/assets/relation-chips-CZJcLjc8.1d7426dab654.js.gz +0 -0
  473. codex/static_root/assets/relation-chips-CZJcLjc8.js.br +0 -0
  474. codex/static_root/assets/relation-chips-CZJcLjc8.js.gz +0 -0
  475. codex/static_root/assets/settings-drawer-7iTnAdjf.337eecf2fdbb.js.br +0 -0
  476. codex/static_root/assets/settings-drawer-7iTnAdjf.337eecf2fdbb.js.gz +0 -0
  477. codex/static_root/assets/settings-drawer-7iTnAdjf.js.br +0 -0
  478. codex/static_root/assets/settings-drawer-7iTnAdjf.js.gz +0 -0
  479. codex/static_root/assets/stats-tab-CQsK1n8O.544bf50ffb22.js.br +0 -0
  480. codex/static_root/assets/stats-tab-CQsK1n8O.544bf50ffb22.js.gz +0 -0
  481. codex/static_root/assets/stats-tab-CQsK1n8O.js.br +0 -0
  482. codex/static_root/assets/stats-tab-CQsK1n8O.js.gz +0 -0
  483. codex/static_root/assets/task-tab-C60TLP6e.63aa929a4ed6.js +0 -1
  484. codex/static_root/assets/task-tab-C60TLP6e.63aa929a4ed6.js.br +0 -0
  485. codex/static_root/assets/task-tab-C60TLP6e.63aa929a4ed6.js.gz +0 -0
  486. codex/static_root/assets/task-tab-C60TLP6e.js +0 -1
  487. codex/static_root/assets/task-tab-C60TLP6e.js.br +0 -0
  488. codex/static_root/assets/task-tab-C60TLP6e.js.gz +0 -0
  489. codex/static_root/assets/to-case-CR9beRR0.21bb805fdab4.js +0 -1
  490. codex/static_root/assets/to-case-CR9beRR0.js +0 -1
  491. codex/static_root/assets/unauthorized-Bs7NSqea.c55650fac055.js.br +0 -0
  492. codex/static_root/assets/unauthorized-Bs7NSqea.c55650fac055.js.gz +0 -0
  493. codex/static_root/assets/unauthorized-Bs7NSqea.js.br +0 -0
  494. codex/static_root/assets/unauthorized-Bs7NSqea.js.gz +0 -0
  495. codex/static_root/assets/user-tab-ChuH7Atm.7e80cf1fb78e.js.br +0 -0
  496. codex/static_root/assets/user-tab-ChuH7Atm.7e80cf1fb78e.js.gz +0 -0
  497. codex/static_root/assets/user-tab-ChuH7Atm.js.br +0 -0
  498. codex/static_root/assets/user-tab-ChuH7Atm.js.gz +0 -0
  499. codex/static_root/js/choices-admin.ef1ff3a8b9da.json +0 -1
  500. codex/static_root/js/choices-admin.ef1ff3a8b9da.json.br +0 -0
  501. codex/static_root/js/choices-admin.ef1ff3a8b9da.json.gz +0 -0
  502. codex/static_root/js/choices.079a01e0be1a.json.br +0 -0
  503. codex/static_root/js/choices.079a01e0be1a.json.gz +0 -0
  504. codex/static_root/manifest.dad79c8475ae.json.br +0 -0
  505. codex/static_root/manifest.dad79c8475ae.json.gz +0 -0
  506. codex/views/cover.py +0 -76
  507. codex/views/utils.py +0 -26
  508. {codex-1.6.0a7.dist-info → codex-1.6.0a8.dist-info}/LICENSE +0 -0
  509. {codex-1.6.0a7.dist-info → codex-1.6.0a8.dist-info}/WHEEL +0 -0
  510. {codex-1.6.0a7.dist-info → codex-1.6.0a8.dist-info}/entry_points.txt +0 -0
@@ -121,9 +121,7 @@ class BrowserView(BrowserTitleView):
121
121
  ################
122
122
  def _get_common_queryset(self, model):
123
123
  """Create queryset common to group & books."""
124
- object_filter = self.get_query_filters(model, False)
125
- qs = model.objects.filter(object_filter)
126
- qs = self.filter_by_annotations(qs, model)
124
+ qs = self.get_filtered_queryset(model)
127
125
  count_group_by = self.get_group_by(model)
128
126
  count = qs.group_by(count_group_by).count()
129
127
  if count:
@@ -184,14 +182,12 @@ class BrowserView(BrowserTitleView):
184
182
  def _get_group_and_books(self):
185
183
  """Create the main queries with filters, annotation and pagination."""
186
184
  group_qs, group_count = self._get_group_queryset()
185
+ mtime = self._get_page_mtime(group_qs)
187
186
  book_qs, book_count = self._get_book_queryset()
188
187
  # Paginate
189
188
  group_qs, book_qs, num_pages, page_group_count, page_book_count = self.paginate(
190
189
  group_qs, book_qs, group_count, book_count
191
190
  )
192
- mtime = self._get_page_mtime(
193
- group_qs
194
- ) # TODO should this be after paginate or not?
195
191
  if page_group_count:
196
192
  group_qs = self.annotate_card_aggregates(group_qs, self.model)
197
193
  if page_book_count:
@@ -203,13 +199,12 @@ class BrowserView(BrowserTitleView):
203
199
  # print(group_qs.explain())
204
200
  # print(group_qs.query)
205
201
 
206
- recovered_group_list = self.re_cover_multi_groups(group_qs)
207
202
  total_count = page_group_count + page_book_count
208
- return recovered_group_list, book_qs, num_pages, total_count, zero_pad, mtime
203
+ return group_qs, book_qs, num_pages, total_count, zero_pad, mtime
209
204
 
210
205
  def get_object(self):
211
206
  """Validate settings and get the querysets."""
212
- group_list, book_qs, num_pages, total_count, zero_pad, mtime = (
207
+ group_qs, book_qs, num_pages, total_count, zero_pad, mtime = (
213
208
  self._get_group_and_books()
214
209
  )
215
210
 
@@ -226,7 +221,7 @@ class BrowserView(BrowserTitleView):
226
221
  "breadcrumbs": parent_breadcrumbs,
227
222
  "title": title,
228
223
  "model_group": self.model_group,
229
- "groups": group_list,
224
+ "groups": group_qs,
230
225
  "books": book_qs,
231
226
  "zero_pad": zero_pad,
232
227
  "num_pages": num_pages,
@@ -12,12 +12,7 @@ from codex.logger.logging import get_logger
12
12
  from codex.models import (
13
13
  Comic,
14
14
  ContributorPerson,
15
- Folder,
16
- Imprint,
17
- Publisher,
18
- Series,
19
15
  StoryArc,
20
- Volume,
21
16
  )
22
17
  from codex.models.named import IdentifierType
23
18
  from codex.serializers.browser.choices import CHOICES_SERIALIZER_CLASS_MAP
@@ -48,7 +43,6 @@ _FIELD_TO_REL_MODEL_MAP = MappingProxyType(
48
43
  ),
49
44
  }
50
45
  )
51
- _NULL_NAMED_ROW = MappingProxyType({"pk": -1, "name": "_none_"})
52
46
  _BACK_REL_MAP = MappingProxyType(
53
47
  {
54
48
  ContributorPerson: "contributor__",
@@ -56,24 +50,15 @@ _BACK_REL_MAP = MappingProxyType(
56
50
  IdentifierType: "identifier__",
57
51
  }
58
52
  )
59
- _CLASS_REL_MAP = MappingProxyType(
60
- {
61
- Publisher: "publisher",
62
- Imprint: "imprint",
63
- Series: "series",
64
- Volume: "volume",
65
- Comic: "pk",
66
- Folder: "parent_folder",
67
- StoryArc: "story_arc_numbers__story_arc",
68
- IdentifierType: "identifiers__identifier_type",
69
- }
70
- )
71
- _SERIALIZER_MANY = False
53
+ _NULL_NAMED_ROW = MappingProxyType({"pk": -1, "name": "_none_"})
72
54
 
73
55
 
74
56
  class BrowserChoicesViewBase(BrowserAnnotationsFilterView):
75
57
  """Get choices for filter dialog."""
76
58
 
59
+ SERIALIZER_MANY = False
60
+ TARGET = "choices"
61
+
77
62
  @staticmethod
78
63
  def get_field_choices_query(comic_qs, field_name):
79
64
  """Get distinct values for the field."""
@@ -85,21 +70,13 @@ class BrowserChoicesViewBase(BrowserAnnotationsFilterView):
85
70
 
86
71
  def get_m2m_field_query(self, model, comic_qs: QuerySet):
87
72
  """Get distinct m2m value objects for the relation."""
88
- if self.model is None:
89
- LOG.error("No model to make filter choices!")
90
- m2m_filter = {}
91
- else:
92
- model_rel: str = _CLASS_REL_MAP[self.model] # type: ignore
93
- back_rel = _BACK_REL_MAP.get(model, "")
94
- back_rel += "comic__"
95
- back_rel += model_rel
96
- back_rel += "__in"
97
- m2m_filter = {back_rel: comic_qs}
73
+ back_rel = _BACK_REL_MAP.get(model, "")
74
+ m2m_filter = {f"{back_rel}comic__in": comic_qs}
98
75
  return model.objects.filter(**m2m_filter).values("pk", "name").distinct()
99
76
 
100
77
  @staticmethod
101
78
  def does_m2m_null_exist(comic_qs, rel):
102
- """Get if null values exists for an m2m field."""
79
+ """Get if comics exist in the filter without values exists for an m2m field."""
103
80
  return comic_qs.filter(**{f"{rel}__isnull": True}).exists()
104
81
 
105
82
  def get_rel_and_model(self, field_name):
@@ -114,22 +91,18 @@ class BrowserChoicesViewBase(BrowserAnnotationsFilterView):
114
91
  rel = field_name
115
92
  model = remote_field.model if remote_field else None
116
93
 
117
- rel = self.rel_prefix + rel
118
-
119
94
  return rel, model
120
95
 
121
96
  def get_object(self):
122
97
  """Get the comic subquery use for the choices."""
123
- object_filter = self.get_query_filters(self.model, choices=True)
124
- qs = self.model.objects.filter(object_filter) # type: ignore
125
- return self.filter_by_annotations(qs, self.model, binary=True)
98
+ return self.get_filtered_queryset(Comic)
126
99
 
127
100
  @extend_schema(request=BrowserAnnotationsFilterView.input_serializer_class)
128
101
  def get(self, *_args, **_kwargs):
129
102
  """Return choices."""
130
103
  self.init_request()
131
104
  obj = self.get_object()
132
- serializer = self.get_serializer(obj, many=_SERIALIZER_MANY)
105
+ serializer = self.get_serializer(obj, many=self.SERIALIZER_MANY)
133
106
  return Response(serializer.data)
134
107
 
135
108
 
@@ -162,6 +135,7 @@ class BrowserChoicesAvailableView(BrowserChoicesViewBase):
162
135
  def get_object(self):
163
136
  """Get choice counts."""
164
137
  qs = super().get_object()
138
+ filters = self.params.get("filters", {})
165
139
  data = {}
166
140
  for field_name in self.serializer_class().get_fields(): # type: ignore
167
141
  if field_name == "story_arcs" and self.model == StoryArc:
@@ -174,9 +148,9 @@ class BrowserChoicesAvailableView(BrowserChoicesViewBase):
174
148
  else:
175
149
  count = self._get_field_choices_count(qs, rel)
176
150
 
177
- filters = self.params.get("filters", {})
178
151
  try:
179
- flag = count > 1 or field_name in filters # type: ignore
152
+ is_filter_set = bool(filters.get(field_name))
153
+ flag = is_filter_set or count > 1 # type: ignore
180
154
  except TypeError:
181
155
  flag = False
182
156
  data[field_name] = flag
@@ -187,7 +161,9 @@ class BrowserChoicesView(BrowserChoicesViewBase):
187
161
  """Get choices for filter dialog."""
188
162
 
189
163
  # serializer_class = Dynamic class determined in get()
190
- _SERIALIZER_MANY = True
164
+ SERIALIZER_MANY = True
165
+ # HACK get_serializer(data, many=True) requires this to be set for the debug API view
166
+ queryset = Comic.objects.none()
191
167
 
192
168
  def _get_field_choices(self, comic_qs, field_name):
193
169
  """Create a pk:name object for fields without tables."""
@@ -0,0 +1,177 @@
1
+ """Comic cover thumbnail view."""
2
+
3
+ from django.http import HttpResponse
4
+ from drf_spectacular.types import OpenApiTypes
5
+ from drf_spectacular.utils import extend_schema
6
+ from rest_framework.renderers import BaseRenderer
7
+
8
+ from codex.librarian.covers.create import CoverCreateMixin
9
+ from codex.librarian.covers.path import CoverPathMixin
10
+ from codex.librarian.mp_queue import LIBRARIAN_QUEUE
11
+ from codex.logger.logging import get_logger
12
+ from codex.models import Comic, Folder, Volume
13
+ from codex.models.paths import CustomCover
14
+ from codex.serializers.choices import DEFAULTS
15
+ from codex.views.browser.annotations import BrowserAnnotationsView
16
+ from codex.views.const import (
17
+ GROUP_RELATION,
18
+ MISSING_COVER_FN,
19
+ MISSING_COVER_NAME_MAP,
20
+ STATIC_IMG_PATH,
21
+ )
22
+
23
+ LOG = get_logger(__name__)
24
+
25
+
26
+ class WEBPRenderer(BaseRenderer):
27
+ """Render WEBP images."""
28
+
29
+ media_type = "image/webp"
30
+ format = "webp"
31
+ charset = None
32
+ render_style = "binary"
33
+
34
+ def render(self, data, *_args, **_kwargs):
35
+ """Return raw data."""
36
+ return data
37
+
38
+
39
+ class CoverView(BrowserAnnotationsView):
40
+ """ComicCover View."""
41
+
42
+ renderer_classes = (WEBPRenderer,)
43
+ content_type = "image/webp"
44
+ TARGET = "cover"
45
+
46
+ def get_model_group(self):
47
+ """Return the url group."""
48
+ return self.kwargs["group"]
49
+
50
+ def init_request(self):
51
+ """Initialize request."""
52
+ self.parse_params()
53
+ self.set_model()
54
+
55
+ def _get_comic_cover(self):
56
+ pks = self.kwargs["pks"]
57
+ return pks[0], False
58
+
59
+ def _use_dynamic_cover(self):
60
+ if not self.params.get("dynamic_covers"):
61
+ return False
62
+ filters = self.params.get("filters", {})
63
+ for key, value in filters.items():
64
+ if key == "bookmark" and value != DEFAULTS["bookmarkFilter"]:
65
+ return True
66
+ if value:
67
+ return True
68
+
69
+ order_by = self.params.get("order_by")
70
+ default_order_by = "filename" if self.model == Folder else "sort_name"
71
+ if order_by != default_order_by:
72
+ return True
73
+
74
+ if self.params.get("order_reverse"):
75
+ return True
76
+
77
+ return False
78
+
79
+ def _get_dynamic_cover(self):
80
+ """Get dynamic cover."""
81
+ self.set_order_key()
82
+ pks = self.kwargs["pks"]
83
+
84
+ if self.model != Volume and self.params.get("custom_covers"):
85
+ group = self.kwargs["group"]
86
+ group_rel = "folder" if self.model == Folder else GROUP_RELATION[group] # type: ignore
87
+ comic_filter = {f"{group_rel}__in": pks}
88
+ custom_cover = CustomCover.objects.filter(**comic_filter).only("pk").first()
89
+ if custom_cover:
90
+ return custom_cover.pk, True
91
+
92
+ comic_qs = self.get_filtered_queryset(Comic)
93
+ comic_qs = self.annotate_order_aggregates(comic_qs, Comic)
94
+ comic_qs = self.add_order_by(comic_qs, Comic)
95
+ comic_qs = comic_qs.only("pk")
96
+ group_by = self.get_group_by(Comic)
97
+ comic_qs.group_by(group_by)
98
+ comic = comic_qs.first()
99
+ cover_pk = comic.pk if comic else 0
100
+ return cover_pk, False
101
+
102
+ def _get_first_cover(self):
103
+ """Get first cover."""
104
+ pks = self.kwargs["pks"]
105
+ order_by = (
106
+ "path"
107
+ if self.model == Folder
108
+ else "name"
109
+ if self.model == Volume
110
+ else "sort_name"
111
+ )
112
+ group_qs = self.model.objects.filter(pk__in=pks) # type: ignore
113
+ group_qs = group_qs.order_by(order_by)
114
+ select_related = ["first_comic"]
115
+ custom_covers = self.params.get("custom_covers")
116
+ if custom_covers and self.model != Volume:
117
+ select_related.append("custom_cover")
118
+ group_qs = group_qs.select_related(*select_related)
119
+ group_qs = group_qs.group_by("id") # type: ignore
120
+ group_obj = group_qs.first()
121
+ if custom_covers and self.model != Volume and group_obj.custom_cover:
122
+ return group_obj.custom_cover.pk, True
123
+ cover_pk = (
124
+ group_obj.first_comic.pk if group_obj and group_obj.first_comic else 0
125
+ )
126
+ return cover_pk, False
127
+
128
+ def _get_cover_pk(self) -> tuple[int, bool]:
129
+ """Get Cover Pk queryset for comic queryset."""
130
+ if self.model == Comic:
131
+ cover_pk, custom = self._get_comic_cover()
132
+ elif self._use_dynamic_cover():
133
+ cover_pk, custom = self._get_dynamic_cover()
134
+ else:
135
+ cover_pk, custom = self._get_first_cover()
136
+ return cover_pk, custom
137
+
138
+ def _get_missing_cover_path(self):
139
+ """Get the missing cover, which is a default svg if fetched for a group."""
140
+ group: str = self.kwargs["group"]
141
+ cover_name = MISSING_COVER_NAME_MAP.get(group)
142
+ if cover_name:
143
+ cover_fn = cover_name + ".svg"
144
+ content_type = "image/svg+xml"
145
+ else:
146
+ cover_fn = MISSING_COVER_FN
147
+ content_type = "image/webp"
148
+ cover_path = STATIC_IMG_PATH / cover_fn
149
+ return cover_path, content_type
150
+
151
+ def _get_cover_data(self, pk, custom):
152
+ thumb_image_data = None
153
+ content_type = "image/webp"
154
+
155
+ cover_path = CoverPathMixin.get_cover_path(pk, custom)
156
+ if not cover_path.exists():
157
+ thumb_image_data = CoverCreateMixin.create_cover_from_path(
158
+ pk, cover_path, LOG, LIBRARIAN_QUEUE, custom
159
+ )
160
+ if not thumb_image_data:
161
+ cover_path, content_type = self._get_missing_cover_path()
162
+ elif cover_path.stat().st_size == 0:
163
+ cover_path, content_type = self._get_missing_cover_path()
164
+
165
+ if not thumb_image_data:
166
+ # if not thumb_image_data:
167
+ with cover_path.open("rb") as f:
168
+ thumb_image_data = f.read()
169
+ return thumb_image_data, content_type
170
+
171
+ @extend_schema(responses={(200, content_type): OpenApiTypes.BINARY})
172
+ def get(self, *args, **kwargs): # type: ignore
173
+ """Get comic cover."""
174
+ self.init_request()
175
+ pk, custom = self._get_cover_pk()
176
+ thumb_image_data, content_type = self._get_cover_data(pk, custom)
177
+ return HttpResponse(thumb_image_data, content_type=content_type)
@@ -1,34 +1,35 @@
1
1
  """Annotations used by a filter."""
2
2
 
3
3
  from django.db.models.aggregates import Count
4
- from django.db.models.expressions import Value
5
- from django.db.models.fields import PositiveSmallIntegerField
6
4
 
7
5
  from codex.models.comic import Comic
6
+ from codex.views.browser.filters.bookmark import BookmarkFilterMixin
8
7
  from codex.views.browser.validate import BrowserValidateView
9
8
 
10
9
 
11
- class BrowserAnnotationsFilterView(BrowserValidateView):
10
+ class BrowserAnnotationsFilterView(BrowserValidateView, BookmarkFilterMixin):
12
11
  """Annotations that also filter."""
13
12
 
14
- ONE_INTEGERFIELD = Value(1, PositiveSmallIntegerField())
13
+ def get_query_filters(self, model):
14
+ """Return all the filters except the group filter."""
15
+ object_filter = self.get_group_acl_filter(model)
16
+ object_filter &= self.get_bookmark_filter(model)
17
+ object_filter &= self.get_comic_field_filter(model)
18
+ object_filter &= self.get_group_filter(model)
19
+ return object_filter
15
20
 
16
- def _annotate_child_count(self, qs, model):
17
- """Annotate Child Count."""
21
+ def _filter_by_child_count(self, qs, model):
18
22
  if model == Comic:
19
- child_count_sum = self.ONE_INTEGERFIELD
20
- else:
21
- child_count_sum = Count(self.rel_prefix + "pk", distinct=True)
22
- if self.TARGET == "opds2":
23
- if model != Comic:
24
- qs = qs.alias(child_count=child_count_sum)
25
- else:
26
- qs = qs.annotate(child_count=child_count_sum)
27
- if model != Comic:
28
- qs = qs.filter(child_count__gt=0)
29
- return qs
30
-
31
- def filter_by_annotations(self, qs, model, binary=False):
32
- """Filter queryset by annotations."""
33
- qs = self._annotate_child_count(qs, model)
34
- return self.apply_search_filter(qs, model, binary=binary)
23
+ return qs
24
+
25
+ rel = self.rel_prefix + "pk"
26
+ qs = qs.alias(pre_search_child_count=Count(rel, distinct=True))
27
+ return qs.filter(pre_search_child_count__gt=0)
28
+
29
+ def get_filtered_queryset(self, model):
30
+ """Get a filtered queryset for the model."""
31
+ object_filter = self.get_query_filters(model)
32
+
33
+ qs = model.objects.filter(object_filter)
34
+ qs = self._filter_by_child_count(qs, model)
35
+ return self.apply_search_filter(qs, model)
@@ -5,7 +5,6 @@ from types import MappingProxyType
5
5
  from django.db.models import Q
6
6
 
7
7
  from codex.models.comic import Comic
8
- from codex.views.browser.filters.bookmark import BookmarkFilterMixin
9
8
  from codex.views.browser.filters.group import GroupFilterView
10
9
 
11
10
  _FILTER_REL_MAP = MappingProxyType(
@@ -17,7 +16,7 @@ _FILTER_REL_MAP = MappingProxyType(
17
16
  )
18
17
 
19
18
 
20
- class ComicFieldFilterView(GroupFilterView, BookmarkFilterMixin):
19
+ class ComicFieldFilterView(GroupFilterView):
21
20
  """Comic field filters."""
22
21
 
23
22
  def _filter_by_comic_field(self, field, rel_prefix):
@@ -2,7 +2,8 @@
2
2
 
3
3
  from django.db.models import Q
4
4
 
5
- from codex.views.const import FOLDER_GROUP, GROUP_RELATION
5
+ from codex.models import Comic
6
+ from codex.views.const import FOLDER_GROUP, GROUP_MODEL_MAP, GROUP_RELATION
6
7
  from codex.views.session import SessionView
7
8
 
8
9
 
@@ -11,18 +12,22 @@ class GroupFilterView(SessionView):
11
12
 
12
13
  SESSION_KEY = SessionView.BROWSER_SESSION_KEY
13
14
 
14
- def get_group_filter(self, choices):
15
+ def get_group_filter(self, model):
15
16
  """Get filter for the displayed group."""
16
17
  group = self.kwargs["group"] # type: ignore
17
18
  pks = self.kwargs["pks"] # type: ignore
18
19
  if pks: # type: ignore
19
- group_relation = "comic__" if choices else ""
20
- if choices and group == FOLDER_GROUP:
21
- group_relation += "folders"
20
+ if model == GROUP_MODEL_MAP.get(group):
21
+ # metadata only
22
+ rel = "pk"
23
+ elif model == Comic and group == FOLDER_GROUP:
24
+ # choices & covers
25
+ rel = "folders"
22
26
  else:
23
- group_relation += GROUP_RELATION[group]
24
- group_relation += "__in"
25
- group_filter_dict = {group_relation: pks} # type: ignore
27
+ rel = GROUP_RELATION[group]
28
+
29
+ rel += "__in"
30
+ group_filter_dict = {rel: pks} # type: ignore
26
31
  elif group == FOLDER_GROUP:
27
32
  group_filter_dict = {"parent_folder": None}
28
33
  else:
@@ -30,6 +30,8 @@ class SearchScorePks:
30
30
  class SearchFilterView(ComicFieldFilterView):
31
31
  """Search Filters Methods."""
32
32
 
33
+ TARGET = ""
34
+
33
35
  def _is_search_results_limited(self) -> bool:
34
36
  """Get search result limit from params."""
35
37
  # user = self.request.user # type: ignore
@@ -115,7 +117,7 @@ class SearchFilterView(ComicFieldFilterView):
115
117
 
116
118
  return tuple(scores_pairs), tuple(prev_pks), tuple(next_pks), tuple(scored_pks)
117
119
 
118
- def _get_search_scores(self, binary=False):
120
+ def _get_search_scores(self, model, qs):
119
121
  """Perform the search and return the scores as a dict."""
120
122
  text = self.params.get("q", "") # type: ignore
121
123
  if not text:
@@ -128,13 +130,21 @@ class SearchFilterView(ComicFieldFilterView):
128
130
  try:
129
131
  sqs = CodexSearchQuerySet()
130
132
  sqs = sqs.auto_query(text) # .filter(score__gt=0.0)
131
- binary = binary or self.params.get("order_by") != "search_score" # type: ignore
132
- if binary:
133
- scored_pks = self._get_binary_search_scores(sqs)
134
- else:
135
- score_pairs, prev_pks, next_pks, scored_pks = (
136
- self._get_browser_search_scores(sqs)
133
+ prefix = "" if model == Comic else self.rel_prefix # type: ignore
134
+ comic_ids = qs.values_list(prefix + "pk", flat=True)
135
+ if comic_ids:
136
+ # Prefilter comic ids. If nothing is allowed, don't search.
137
+ sqs = sqs.filter_comic_ids(comic_ids)
138
+ binary = (
139
+ self.TARGET in frozenset({"cover", "choices"})
140
+ or self.params.get("order_by", "") != "search_score"
137
141
  )
142
+ if binary:
143
+ scored_pks = self._get_binary_search_scores(sqs)
144
+ else:
145
+ score_pairs, prev_pks, next_pks, scored_pks = (
146
+ self._get_browser_search_scores(sqs)
147
+ )
138
148
  except MemoryError:
139
149
  LOG.warning("Search engine needs more memory, results truncated.")
140
150
  except Exception:
@@ -171,17 +181,16 @@ class SearchFilterView(ComicFieldFilterView):
171
181
  rel = prefix + "pk__in"
172
182
  return {rel: scored_pks}
173
183
 
174
- def apply_search_filter(self, qs, model, binary=False):
184
+ def apply_search_filter(self, qs, model):
175
185
  """Preparse search, search and return the filter and scores."""
176
186
  try:
177
187
  score_pairs, prev_pks, next_pks, scored_pks = self._get_search_scores(
178
- binary
188
+ model, qs
179
189
  )
180
190
  qs = self.annotate_search_score(qs, model, score_pairs, prev_pks, next_pks)
181
191
  if score_pairs or scored_pks:
182
192
  search_filter = self._get_search_query_filter(model, scored_pks)
183
193
  qs = qs.filter(**search_filter)
184
- self.cover_search_score_pairs = score_pairs
185
194
  except Exception:
186
195
  LOG.exception("Creating the search filter")
187
196
 
@@ -8,7 +8,9 @@ from drf_spectacular.utils import extend_schema
8
8
  from rest_framework.exceptions import NotFound
9
9
  from rest_framework.response import Response
10
10
 
11
- from codex.librarian.importer.const import COMIC_M2M_FIELD_NAMES
11
+ from codex.librarian.importer.const import (
12
+ COMIC_M2M_FIELD_NAMES,
13
+ )
12
14
  from codex.logger.logging import get_logger
13
15
  from codex.models import AdminFlag, Comic
14
16
  from codex.serializers.browser.metadata import MetadataSerializer
@@ -52,6 +54,11 @@ class MetadataView(BrowserAnnotationsView):
52
54
  }
53
55
  )
54
56
 
57
+ def set_valid_browse_nav_groups(self, valid_top_groups): # noqa: ARG002
58
+ """Limited allowed nav groups for metadata."""
59
+ group = self.kwargs["group"]
60
+ self.valid_nav_groups = (group,)
61
+
55
62
  def _get_comic_value_fields(self):
56
63
  """Include the path field for staff."""
57
64
  fields = set(_COMIC_VALUE_FIELD_NAMES)
@@ -153,6 +160,8 @@ class MetadataView(BrowserAnnotationsView):
153
160
  if field_name in _GROUP_RELS:
154
161
  group_by = "name" if field_name == "volume" else "sort_name"
155
162
  intersection_qs = intersection_qs.group_by(group_by)
163
+ if field_name == "identifiers":
164
+ intersection_qs = intersection_qs.select_related("identifier_type")
156
165
  intersection_qs = intersection_qs.alias(count=Count("comic")).filter(
157
166
  count=comic_pks_count
158
167
  )
@@ -185,18 +194,18 @@ class MetadataView(BrowserAnnotationsView):
185
194
  def _get_optimized_m2m_query(key, qs):
186
195
  # XXX The prefetch gets removed by field.set() :(
187
196
  if key == "contributors":
188
- optimized_qs = qs.prefetch_related(*_CONTRIBUTOR_RELATIONS).only(
197
+ qs = qs.prefetch_related(*_CONTRIBUTOR_RELATIONS).only(
189
198
  *_CONTRIBUTOR_RELATIONS
190
199
  )
191
200
  elif key == "story_arc_numbers":
192
- optimized_qs = qs.prefetch_related("story_arc").only("story_arc", "number")
201
+ qs = qs.prefetch_related("story_arc")
202
+ qs = qs.only("story_arc", "number")
193
203
  elif key == "identifiers":
194
- optimized_qs = qs.prefetch_related("identifier_type").only(
195
- "identifier_type", "nss", "url"
196
- )
204
+ qs = qs.prefetch_related("identifier_type")
205
+ qs = qs.only("identifier_type", "nss", "url")
197
206
  else:
198
- optimized_qs = qs.only("name")
199
- return optimized_qs
207
+ qs = qs.only("name")
208
+ return qs
200
209
 
201
210
  @classmethod
202
211
  def _copy_m2m_intersections(cls, obj, m2m_intersections):
@@ -244,26 +253,17 @@ class MetadataView(BrowserAnnotationsView):
244
253
  # values dicts don't copy relations to the serializer. The values
245
254
  # dict is necessary because of the folders view union in browser.py.
246
255
 
247
- if self.model is None:
248
- # TODO this looks redundant after set_browse_model
249
- group = self.kwargs["group"]
250
- raise NotFound(detail=f"Cannot get metadata for {group=}")
256
+ qs = self.get_filtered_queryset(self.model)
251
257
 
252
- object_filter = self.get_query_filters_without_group(self.model) # type: ignore
253
- pks = self.kwargs["pks"]
254
- qs = self.model.objects.filter(object_filter, pk__in=pks)
255
- qs = self.filter_by_annotations(qs, self.model, binary=True)
256
258
  filtered_qs = qs
257
259
  qs = self.annotate_order_aggregates(qs, self.model)
258
260
  qs = self.annotate_card_aggregates(qs, self.model)
261
+ qs = self._annotate_values_and_fks(qs, filtered_qs)
259
262
  group_by = self.get_group_by()
260
263
  qs = qs.group_by(group_by)
261
- qs = self._annotate_values_and_fks(qs, filtered_qs)
262
-
263
- qs_list = self.re_cover_multi_groups(qs)
264
264
 
265
265
  try:
266
- obj = qs_list[0]
266
+ obj = qs[0]
267
267
  if not obj:
268
268
  reason = "Empty obj"
269
269
  raise ValueError(reason) # noqa TRY301
@@ -273,11 +273,6 @@ class MetadataView(BrowserAnnotationsView):
273
273
  m2m_intersections = self._query_m2m_intersections(filtered_qs)
274
274
  return self._copy_annotations_into_comic_fields(obj, m2m_intersections) # type: ignore
275
275
 
276
- def set_valid_browse_nav_groups(self, valid_top_groups): # noqa: ARG002
277
- """Limited allowed nav groups for metadata."""
278
- group = self.kwargs["group"]
279
- self.valid_nav_groups = (group,)
280
-
281
276
  @extend_schema(request=BrowserAnnotationsView.input_serializer_class)
282
277
  def get(self, *_args, **_kwargs):
283
278
  """Get metadata for a filtered browse group."""
codex/views/const.py CHANGED
@@ -64,6 +64,18 @@ GROUP_RELATION = MappingProxyType(
64
64
  STORY_ARC_GROUP: "story_arc_numbers__story_arc",
65
65
  }
66
66
  )
67
+ GROUP_ORDER = "rpisv"
68
+ MODEL_REL_MAP = MappingProxyType(
69
+ {
70
+ Publisher: "publisher",
71
+ Imprint: "imprint",
72
+ Series: "series",
73
+ Volume: "volume",
74
+ Folder: "parent_folder",
75
+ StoryArc: "story_arc_numbers__story_arc",
76
+ Comic: "pk",
77
+ }
78
+ )
67
79
  GROUP_MODEL_MAP: MappingProxyType[str, type[BrowserGroupModel] | None] = (
68
80
  MappingProxyType(
69
81
  {