codex 1.6.0a8__py3-none-any.whl → 1.6.0a10__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 (557) hide show
  1. codex/integrity.py +1 -2
  2. codex/librarian/importer/cache.py +46 -133
  3. codex/librarian/importer/create_comics.py +3 -4
  4. codex/librarian/importer/create_covers.py +1 -3
  5. codex/librarian/importer/create_fks.py +3 -1
  6. codex/librarian/importer/failed_imports.py +1 -2
  7. codex/librarian/importer/importer.py +1 -1
  8. codex/librarian/importer/importerd.py +7 -7
  9. codex/librarian/importer/moved.py +4 -8
  10. codex/librarian/importer/tasks.py +2 -2
  11. codex/librarian/janitor/janitor.py +0 -3
  12. codex/librarian/librariand.py +0 -2
  13. codex/librarian/notifier/notifierd.py +1 -1
  14. codex/librarian/notifier/tasks.py +4 -4
  15. codex/middleware.py +1 -2
  16. codex/migrations/0027_import_order_and_covers.py +1 -66
  17. codex/models/comic.py +0 -1
  18. codex/models/groups.py +0 -3
  19. codex/serializers/admin.py +41 -40
  20. codex/serializers/browser/choices.py +22 -6
  21. codex/serializers/browser/metadata.py +45 -1
  22. codex/serializers/browser/mtime.py +21 -0
  23. codex/serializers/browser/page.py +2 -4
  24. codex/serializers/browser/settings.py +34 -14
  25. codex/serializers/fields.py +27 -10
  26. codex/serializers/reader.py +16 -6
  27. codex/serializers/route.py +9 -4
  28. codex/serializers/settings.py +12 -0
  29. codex/settings/settings.py +10 -1
  30. codex/static_root/assets/VCheckbox-CY3rcqlS.de5154294920.js +1 -0
  31. codex/static_root/assets/VCheckbox-CY3rcqlS.de5154294920.js.br +0 -0
  32. codex/static_root/assets/VCheckbox-CY3rcqlS.de5154294920.js.gz +0 -0
  33. codex/static_root/assets/VCheckbox-CY3rcqlS.js +1 -0
  34. codex/static_root/assets/VCheckbox-CY3rcqlS.js.br +0 -0
  35. codex/static_root/assets/VCheckbox-CY3rcqlS.js.gz +0 -0
  36. codex/static_root/assets/{VCheckboxBtn-B8Z4NA-t.3d8921f98ad9.js → VCheckboxBtn-D2tPTt6P.9a90d6818bf6.js} +1 -1
  37. codex/static_root/assets/VCheckboxBtn-D2tPTt6P.9a90d6818bf6.js.br +0 -0
  38. codex/static_root/assets/VCheckboxBtn-D2tPTt6P.9a90d6818bf6.js.gz +0 -0
  39. codex/static_root/assets/{VCheckboxBtn-B8Z4NA-t.js → VCheckboxBtn-D2tPTt6P.js} +1 -1
  40. codex/static_root/assets/VCheckboxBtn-D2tPTt6P.js.br +0 -0
  41. codex/static_root/assets/VCheckboxBtn-D2tPTt6P.js.gz +0 -0
  42. codex/static_root/assets/VCombobox-aJr--Ou7.f290ccf2ce25.js +1 -0
  43. codex/static_root/assets/VCombobox-aJr--Ou7.f290ccf2ce25.js.br +0 -0
  44. codex/static_root/assets/VCombobox-aJr--Ou7.f290ccf2ce25.js.gz +0 -0
  45. codex/static_root/assets/VCombobox-aJr--Ou7.js +1 -0
  46. codex/static_root/assets/VCombobox-aJr--Ou7.js.br +0 -0
  47. codex/static_root/assets/VCombobox-aJr--Ou7.js.gz +0 -0
  48. codex/static_root/assets/VDialog-61pzfUB2.33f73beb5224.js +1 -0
  49. codex/static_root/assets/VDialog-61pzfUB2.33f73beb5224.js.br +0 -0
  50. codex/static_root/assets/VDialog-61pzfUB2.33f73beb5224.js.gz +0 -0
  51. codex/static_root/assets/VDialog-61pzfUB2.js +1 -0
  52. codex/static_root/assets/VDialog-61pzfUB2.js.br +0 -0
  53. codex/static_root/assets/VDialog-61pzfUB2.js.gz +0 -0
  54. codex/static_root/assets/{VDialog-C5Nxd_zP.css → VDialog-lQ13FnRR.1c801ad756b2.css} +1 -1
  55. codex/static_root/assets/VDialog-lQ13FnRR.1c801ad756b2.css.br +0 -0
  56. codex/static_root/assets/VDialog-lQ13FnRR.1c801ad756b2.css.gz +0 -0
  57. codex/static_root/assets/{VDialog-C5Nxd_zP.d327f505ef0e.css → VDialog-lQ13FnRR.css} +1 -1
  58. codex/static_root/assets/VDialog-lQ13FnRR.css.br +0 -0
  59. codex/static_root/assets/VDialog-lQ13FnRR.css.gz +0 -0
  60. codex/static_root/assets/VExpansionPanels-rewm0P4z.5c7680f815b6.js +1 -0
  61. codex/static_root/assets/VExpansionPanels-rewm0P4z.5c7680f815b6.js.br +0 -0
  62. codex/static_root/assets/VExpansionPanels-rewm0P4z.5c7680f815b6.js.gz +0 -0
  63. codex/static_root/assets/VExpansionPanels-rewm0P4z.js +1 -0
  64. codex/static_root/assets/VExpansionPanels-rewm0P4z.js.br +0 -0
  65. codex/static_root/assets/VExpansionPanels-rewm0P4z.js.gz +0 -0
  66. codex/static_root/assets/{VRadioGroup-BGa6_q5a.d1a3ad7266bd.js → VRadioGroup-QFQbK1qo.750a42b1b814.js} +1 -1
  67. codex/static_root/assets/VRadioGroup-QFQbK1qo.750a42b1b814.js.br +0 -0
  68. codex/static_root/assets/VRadioGroup-QFQbK1qo.750a42b1b814.js.gz +0 -0
  69. codex/static_root/assets/{VRadioGroup-BGa6_q5a.js → VRadioGroup-QFQbK1qo.js} +1 -1
  70. codex/static_root/assets/VRadioGroup-QFQbK1qo.js.br +0 -0
  71. codex/static_root/assets/VRadioGroup-QFQbK1qo.js.gz +0 -0
  72. codex/static_root/assets/VSelect-Dixy6okt.5d53830daa8a.js +1 -0
  73. codex/static_root/assets/VSelect-Dixy6okt.5d53830daa8a.js.br +0 -0
  74. codex/static_root/assets/VSelect-Dixy6okt.5d53830daa8a.js.gz +0 -0
  75. codex/static_root/assets/VSelect-Dixy6okt.js +1 -0
  76. codex/static_root/assets/VSelect-Dixy6okt.js.br +0 -0
  77. codex/static_root/assets/VSelect-Dixy6okt.js.gz +0 -0
  78. codex/static_root/assets/{VSelectionControl-C8DP9rgp.ef80e8e3dfc1.js → VSelectionControl-MNmxKc3J.08055d00917e.js} +1 -1
  79. codex/static_root/assets/VSelectionControl-MNmxKc3J.08055d00917e.js.br +0 -0
  80. codex/static_root/assets/VSelectionControl-MNmxKc3J.08055d00917e.js.gz +0 -0
  81. codex/static_root/assets/{VSelectionControl-C8DP9rgp.js → VSelectionControl-MNmxKc3J.js} +1 -1
  82. codex/static_root/assets/VSelectionControl-MNmxKc3J.js.br +0 -0
  83. codex/static_root/assets/VSelectionControl-MNmxKc3J.js.gz +0 -0
  84. codex/static_root/assets/VSlideGroup-DZEV0FoV.5f996daf0e98.js +1 -0
  85. codex/static_root/assets/VSlideGroup-DZEV0FoV.5f996daf0e98.js.br +0 -0
  86. codex/static_root/assets/VSlideGroup-DZEV0FoV.5f996daf0e98.js.gz +0 -0
  87. codex/static_root/assets/VSlideGroup-DZEV0FoV.js +1 -0
  88. codex/static_root/assets/VSlideGroup-DZEV0FoV.js.br +0 -0
  89. codex/static_root/assets/VSlideGroup-DZEV0FoV.js.gz +0 -0
  90. codex/static_root/assets/{VTable-D1esKtlw.52165930e3b9.js → VTable-lcnYg9Ee.d72927b13c2e.js} +1 -1
  91. codex/static_root/assets/VTable-lcnYg9Ee.d72927b13c2e.js.br +0 -0
  92. codex/static_root/assets/VTable-lcnYg9Ee.d72927b13c2e.js.gz +0 -0
  93. codex/static_root/assets/{VTable-D1esKtlw.js → VTable-lcnYg9Ee.js} +1 -1
  94. codex/static_root/assets/VTable-lcnYg9Ee.js.br +0 -0
  95. codex/static_root/assets/VTable-lcnYg9Ee.js.gz +0 -0
  96. codex/static_root/assets/VTextField-BkFrjhdV.b02653a9b3b7.js +1 -0
  97. codex/static_root/assets/VTextField-BkFrjhdV.b02653a9b3b7.js.br +0 -0
  98. codex/static_root/assets/VTextField-BkFrjhdV.b02653a9b3b7.js.gz +0 -0
  99. codex/static_root/assets/VTextField-BkFrjhdV.js +1 -0
  100. codex/static_root/assets/VTextField-BkFrjhdV.js.br +0 -0
  101. codex/static_root/assets/VTextField-BkFrjhdV.js.gz +0 -0
  102. codex/static_root/assets/VWindowItem-C__tAqdc.9791c128c3b7.js +1 -0
  103. codex/static_root/assets/VWindowItem-C__tAqdc.9791c128c3b7.js.br +0 -0
  104. codex/static_root/assets/VWindowItem-C__tAqdc.9791c128c3b7.js.gz +0 -0
  105. codex/static_root/assets/VWindowItem-C__tAqdc.js +1 -0
  106. codex/static_root/assets/VWindowItem-C__tAqdc.js.br +0 -0
  107. codex/static_root/assets/VWindowItem-C__tAqdc.js.gz +0 -0
  108. codex/static_root/assets/admin-CXES2HXe.9e48a9bf84e0.js +1 -0
  109. codex/static_root/assets/admin-CXES2HXe.9e48a9bf84e0.js.br +0 -0
  110. codex/static_root/assets/admin-CXES2HXe.9e48a9bf84e0.js.gz +0 -0
  111. codex/static_root/assets/admin-CXES2HXe.js +1 -0
  112. codex/static_root/assets/admin-CXES2HXe.js.br +0 -0
  113. codex/static_root/assets/admin-CXES2HXe.js.gz +0 -0
  114. codex/static_root/assets/{admin-drawer-panel-o87BwD38.ce39427c8201.css → admin-drawer-panel-mne3FIj6.561b3f75fe65.css} +1 -1
  115. codex/static_root/assets/admin-drawer-panel-mne3FIj6.561b3f75fe65.css.br +0 -0
  116. codex/static_root/assets/admin-drawer-panel-mne3FIj6.561b3f75fe65.css.gz +0 -0
  117. codex/static_root/assets/{admin-drawer-panel-o87BwD38.css → admin-drawer-panel-mne3FIj6.css} +1 -1
  118. codex/static_root/assets/admin-drawer-panel-mne3FIj6.css.br +0 -0
  119. codex/static_root/assets/admin-drawer-panel-mne3FIj6.css.gz +0 -0
  120. codex/static_root/assets/admin-drawer-panel-rrNKi_GE.e2b034db7934.js +30 -0
  121. codex/static_root/assets/admin-drawer-panel-rrNKi_GE.e2b034db7934.js.br +0 -0
  122. codex/static_root/assets/admin-drawer-panel-rrNKi_GE.e2b034db7934.js.gz +0 -0
  123. codex/static_root/assets/admin-drawer-panel-rrNKi_GE.js +30 -0
  124. codex/static_root/assets/admin-drawer-panel-rrNKi_GE.js.br +0 -0
  125. codex/static_root/assets/admin-drawer-panel-rrNKi_GE.js.gz +0 -0
  126. codex/static_root/assets/browser-BuDeXGEe.css +1 -0
  127. codex/static_root/assets/browser-BuDeXGEe.css.br +0 -0
  128. codex/static_root/assets/browser-BuDeXGEe.css.gz +0 -0
  129. codex/static_root/assets/browser-BuDeXGEe.f60854183b45.css +1 -0
  130. codex/static_root/assets/browser-BuDeXGEe.f60854183b45.css.br +0 -0
  131. codex/static_root/assets/browser-BuDeXGEe.f60854183b45.css.gz +0 -0
  132. codex/static_root/assets/browser-s6mFzrK9.0d2200b451af.js +1 -0
  133. codex/static_root/assets/browser-s6mFzrK9.0d2200b451af.js.br +0 -0
  134. codex/static_root/assets/browser-s6mFzrK9.0d2200b451af.js.gz +0 -0
  135. codex/static_root/assets/browser-s6mFzrK9.js +1 -0
  136. codex/static_root/assets/browser-s6mFzrK9.js.br +0 -0
  137. codex/static_root/assets/browser-s6mFzrK9.js.gz +0 -0
  138. codex/static_root/assets/change-password-dialog-0u6IJA87.5f9a9aa573b7.css +1 -0
  139. codex/static_root/assets/change-password-dialog-0u6IJA87.5f9a9aa573b7.css.br +0 -0
  140. codex/static_root/assets/change-password-dialog-0u6IJA87.5f9a9aa573b7.css.gz +0 -0
  141. codex/static_root/assets/change-password-dialog-0u6IJA87.css +1 -0
  142. codex/static_root/assets/change-password-dialog-0u6IJA87.css.br +0 -0
  143. codex/static_root/assets/change-password-dialog-0u6IJA87.css.gz +0 -0
  144. codex/static_root/assets/change-password-dialog-Bn20jYzD.593c5af1732b.js +1 -0
  145. codex/static_root/assets/change-password-dialog-Bn20jYzD.593c5af1732b.js.br +0 -0
  146. codex/static_root/assets/change-password-dialog-Bn20jYzD.593c5af1732b.js.gz +0 -0
  147. codex/static_root/assets/change-password-dialog-Bn20jYzD.js +1 -0
  148. codex/static_root/assets/change-password-dialog-Bn20jYzD.js.br +0 -0
  149. codex/static_root/assets/change-password-dialog-Bn20jYzD.js.gz +0 -0
  150. codex/static_root/assets/{confirm-dialog-DkU0twE6.a20006aa5ba6.js → confirm-dialog-CWszioQE.859051c35dd9.js} +1 -1
  151. codex/static_root/assets/confirm-dialog-CWszioQE.859051c35dd9.js.br +0 -0
  152. codex/static_root/assets/confirm-dialog-CWszioQE.859051c35dd9.js.gz +0 -0
  153. codex/static_root/assets/{confirm-dialog-DkU0twE6.js → confirm-dialog-CWszioQE.js} +1 -1
  154. codex/static_root/assets/confirm-dialog-CWszioQE.js.br +0 -0
  155. codex/static_root/assets/confirm-dialog-CWszioQE.js.gz +0 -0
  156. codex/static_root/assets/datetime-column-BSdlLt-4.27ec2df67a0b.js +1 -0
  157. codex/static_root/assets/datetime-column-BSdlLt-4.27ec2df67a0b.js.br +0 -0
  158. codex/static_root/assets/datetime-column-BSdlLt-4.27ec2df67a0b.js.gz +0 -0
  159. codex/static_root/assets/datetime-column-BSdlLt-4.js +1 -0
  160. codex/static_root/assets/datetime-column-BSdlLt-4.js.br +0 -0
  161. codex/static_root/assets/datetime-column-BSdlLt-4.js.gz +0 -0
  162. codex/static_root/assets/filter-D3gaGf87.1f14dbfd9929.js +1 -0
  163. codex/static_root/assets/filter-D3gaGf87.1f14dbfd9929.js.br +0 -0
  164. codex/static_root/assets/filter-D3gaGf87.1f14dbfd9929.js.gz +0 -0
  165. codex/static_root/assets/filter-D3gaGf87.js +1 -0
  166. codex/static_root/assets/filter-D3gaGf87.js.br +0 -0
  167. codex/static_root/assets/filter-D3gaGf87.js.gz +0 -0
  168. codex/static_root/assets/{flag-tab-D4ZNSbMR.e763a9937701.js → flag-tab-8vzpdySX.a982c7f0776a.js} +1 -1
  169. codex/static_root/assets/flag-tab-8vzpdySX.a982c7f0776a.js.br +0 -0
  170. codex/static_root/assets/flag-tab-8vzpdySX.a982c7f0776a.js.gz +0 -0
  171. codex/static_root/assets/{flag-tab-D4ZNSbMR.js → flag-tab-8vzpdySX.js} +1 -1
  172. codex/static_root/assets/flag-tab-8vzpdySX.js.br +0 -0
  173. codex/static_root/assets/flag-tab-8vzpdySX.js.gz +0 -0
  174. codex/static_root/assets/{group-tab-DucpNoBs.a6f37ae940c0.js → group-tab-Cz9t_ECi.a9e8aea79254.js} +1 -1
  175. codex/static_root/assets/group-tab-Cz9t_ECi.a9e8aea79254.js.br +0 -0
  176. codex/static_root/assets/group-tab-Cz9t_ECi.a9e8aea79254.js.gz +0 -0
  177. codex/static_root/assets/{group-tab-DucpNoBs.js → group-tab-Cz9t_ECi.js} +1 -1
  178. codex/static_root/assets/group-tab-Cz9t_ECi.js.br +0 -0
  179. codex/static_root/assets/group-tab-Cz9t_ECi.js.gz +0 -0
  180. codex/static_root/assets/{http-error-Cj0hTq-c.062a88ddd4dc.js → http-error-Cqtszzf1.b31df3d9c29f.js} +1 -1
  181. codex/static_root/assets/http-error-Cqtszzf1.b31df3d9c29f.js.br +0 -0
  182. codex/static_root/assets/http-error-Cqtszzf1.b31df3d9c29f.js.gz +0 -0
  183. codex/static_root/assets/{http-error-Cj0hTq-c.js → http-error-Cqtszzf1.js} +1 -1
  184. codex/static_root/assets/http-error-Cqtszzf1.js.br +0 -0
  185. codex/static_root/assets/http-error-Cqtszzf1.js.gz +0 -0
  186. codex/static_root/assets/{library-tab-CzksdS3Q.566acdb45dec.js → library-tab-DMlnswUU.30f454e84184.js} +1 -1
  187. codex/static_root/assets/library-tab-DMlnswUU.30f454e84184.js.br +0 -0
  188. codex/static_root/assets/library-tab-DMlnswUU.30f454e84184.js.gz +0 -0
  189. codex/static_root/assets/{library-tab-CzksdS3Q.js → library-tab-DMlnswUU.js} +1 -1
  190. codex/static_root/assets/library-tab-DMlnswUU.js.br +0 -0
  191. codex/static_root/assets/library-tab-DMlnswUU.js.gz +0 -0
  192. codex/static_root/assets/main-vjXu_QYL.548bd4c38799.js +32 -0
  193. codex/static_root/assets/main-vjXu_QYL.548bd4c38799.js.br +0 -0
  194. codex/static_root/assets/main-vjXu_QYL.548bd4c38799.js.gz +0 -0
  195. codex/static_root/assets/main-vjXu_QYL.js +32 -0
  196. codex/static_root/assets/main-vjXu_QYL.js.br +0 -0
  197. codex/static_root/assets/main-vjXu_QYL.js.gz +0 -0
  198. codex/static_root/assets/pagination-toolbar-DGdMiBRs.5829b01b8a2f.css +1 -0
  199. codex/static_root/assets/pagination-toolbar-DGdMiBRs.5829b01b8a2f.css.br +0 -0
  200. codex/static_root/assets/pagination-toolbar-DGdMiBRs.5829b01b8a2f.css.gz +0 -0
  201. codex/static_root/assets/pagination-toolbar-DGdMiBRs.css +1 -0
  202. codex/static_root/assets/pagination-toolbar-DGdMiBRs.css.br +0 -0
  203. codex/static_root/assets/pagination-toolbar-DGdMiBRs.css.gz +0 -0
  204. codex/static_root/assets/pagination-toolbar-JW78pa0q.91024effdab9.js +1 -0
  205. codex/static_root/assets/pagination-toolbar-JW78pa0q.91024effdab9.js.br +0 -0
  206. codex/static_root/assets/pagination-toolbar-JW78pa0q.91024effdab9.js.gz +0 -0
  207. codex/static_root/assets/pagination-toolbar-JW78pa0q.js +1 -0
  208. codex/static_root/assets/pagination-toolbar-JW78pa0q.js.br +0 -0
  209. codex/static_root/assets/pagination-toolbar-JW78pa0q.js.gz +0 -0
  210. codex/static_root/assets/{pdf-doc-GkMk4o5L.ef24c859a7a4.js → pdf-doc-BiamrjRZ.4c9be1ade43c.js} +1 -1
  211. codex/static_root/assets/pdf-doc-BiamrjRZ.4c9be1ade43c.js.br +0 -0
  212. codex/static_root/assets/pdf-doc-BiamrjRZ.4c9be1ade43c.js.gz +0 -0
  213. codex/static_root/assets/{pdf-doc-GkMk4o5L.js → pdf-doc-BiamrjRZ.js} +1 -1
  214. codex/static_root/assets/pdf-doc-BiamrjRZ.js.br +0 -0
  215. codex/static_root/assets/pdf-doc-BiamrjRZ.js.gz +0 -0
  216. codex/static_root/assets/{reader-CCCfpM3x.1553e45bd775.css → reader-BpsctRsY.5d72e393911a.css} +1 -1
  217. codex/static_root/assets/reader-BpsctRsY.5d72e393911a.css.br +0 -0
  218. codex/static_root/assets/reader-BpsctRsY.5d72e393911a.css.gz +0 -0
  219. codex/static_root/assets/{reader-CCCfpM3x.css → reader-BpsctRsY.css} +1 -1
  220. codex/static_root/assets/reader-BpsctRsY.css.br +0 -0
  221. codex/static_root/assets/reader-BpsctRsY.css.gz +0 -0
  222. codex/static_root/assets/reader-CDk8oZzB.ad62eebda920.js +2 -0
  223. codex/static_root/assets/reader-CDk8oZzB.ad62eebda920.js.br +0 -0
  224. codex/static_root/assets/reader-CDk8oZzB.ad62eebda920.js.gz +0 -0
  225. codex/static_root/assets/reader-CDk8oZzB.js +2 -0
  226. codex/static_root/assets/reader-CDk8oZzB.js.br +0 -0
  227. codex/static_root/assets/reader-CDk8oZzB.js.gz +0 -0
  228. codex/static_root/assets/relation-chips-C5acBjpy.dc67d4528868.js +1 -0
  229. codex/static_root/assets/relation-chips-C5acBjpy.dc67d4528868.js.br +0 -0
  230. codex/static_root/assets/relation-chips-C5acBjpy.dc67d4528868.js.gz +0 -0
  231. codex/static_root/assets/relation-chips-C5acBjpy.js +1 -0
  232. codex/static_root/assets/relation-chips-C5acBjpy.js.br +0 -0
  233. codex/static_root/assets/relation-chips-C5acBjpy.js.gz +0 -0
  234. codex/static_root/assets/settings-drawer-D_JuRi7F.f02f3f981f8f.js +2 -0
  235. codex/static_root/assets/settings-drawer-D_JuRi7F.f02f3f981f8f.js.br +0 -0
  236. codex/static_root/assets/settings-drawer-D_JuRi7F.f02f3f981f8f.js.gz +0 -0
  237. codex/static_root/assets/settings-drawer-D_JuRi7F.js +2 -0
  238. codex/static_root/assets/settings-drawer-D_JuRi7F.js.br +0 -0
  239. codex/static_root/assets/settings-drawer-D_JuRi7F.js.gz +0 -0
  240. codex/static_root/assets/stats-tab-B4yKuY0c.a5728396494b.css +1 -0
  241. codex/static_root/assets/stats-tab-B4yKuY0c.a5728396494b.css.br +0 -0
  242. codex/static_root/assets/stats-tab-B4yKuY0c.a5728396494b.css.gz +0 -0
  243. codex/static_root/assets/stats-tab-B4yKuY0c.css +1 -0
  244. codex/static_root/assets/stats-tab-B4yKuY0c.css.br +0 -0
  245. codex/static_root/assets/stats-tab-B4yKuY0c.css.gz +0 -0
  246. codex/static_root/assets/stats-tab-ClqRTk1z.c759ace65a87.js +1 -0
  247. codex/static_root/assets/stats-tab-ClqRTk1z.c759ace65a87.js.br +0 -0
  248. codex/static_root/assets/stats-tab-ClqRTk1z.c759ace65a87.js.gz +0 -0
  249. codex/static_root/assets/stats-tab-ClqRTk1z.js +1 -0
  250. codex/static_root/assets/stats-tab-ClqRTk1z.js.br +0 -0
  251. codex/static_root/assets/stats-tab-ClqRTk1z.js.gz +0 -0
  252. codex/static_root/assets/task-tab-DF9qa6LO.0b32cafde4ad.js +1 -0
  253. codex/static_root/assets/task-tab-DF9qa6LO.0b32cafde4ad.js.br +0 -0
  254. codex/static_root/assets/task-tab-DF9qa6LO.0b32cafde4ad.js.gz +0 -0
  255. codex/static_root/assets/task-tab-DF9qa6LO.js +1 -0
  256. codex/static_root/assets/task-tab-DF9qa6LO.js.br +0 -0
  257. codex/static_root/assets/task-tab-DF9qa6LO.js.gz +0 -0
  258. codex/static_root/assets/to-case-CGbULRaE.4c80644f460d.js +1 -0
  259. codex/static_root/assets/to-case-CGbULRaE.4c80644f460d.js.br +0 -0
  260. codex/static_root/assets/to-case-CGbULRaE.js +1 -0
  261. codex/static_root/assets/to-case-CGbULRaE.js.br +0 -0
  262. codex/static_root/assets/unauthorized-BjKuLH6k.2ae00ddd79fb.js +1 -0
  263. codex/static_root/assets/unauthorized-BjKuLH6k.2ae00ddd79fb.js.br +0 -0
  264. codex/static_root/assets/unauthorized-BjKuLH6k.2ae00ddd79fb.js.gz +0 -0
  265. codex/static_root/assets/unauthorized-BjKuLH6k.js +1 -0
  266. codex/static_root/assets/unauthorized-BjKuLH6k.js.br +0 -0
  267. codex/static_root/assets/unauthorized-BjKuLH6k.js.gz +0 -0
  268. codex/static_root/assets/{unauthorized-DHV6Q1n-.b7847b2872a1.css → unauthorized-CibJYD8o.4c2de98c2b53.css} +1 -1
  269. codex/static_root/assets/unauthorized-CibJYD8o.4c2de98c2b53.css.br +0 -0
  270. codex/static_root/assets/unauthorized-CibJYD8o.4c2de98c2b53.css.gz +0 -0
  271. codex/static_root/assets/{unauthorized-DHV6Q1n-.css → unauthorized-CibJYD8o.css} +1 -1
  272. codex/static_root/assets/unauthorized-CibJYD8o.css.br +0 -0
  273. codex/static_root/assets/unauthorized-CibJYD8o.css.gz +0 -0
  274. codex/static_root/assets/user-tab-Dpxe3ygA.7d90174631f0.js +1 -0
  275. codex/static_root/assets/user-tab-Dpxe3ygA.7d90174631f0.js.br +0 -0
  276. codex/static_root/assets/user-tab-Dpxe3ygA.7d90174631f0.js.gz +0 -0
  277. codex/static_root/assets/user-tab-Dpxe3ygA.js +1 -0
  278. codex/static_root/assets/user-tab-Dpxe3ygA.js.br +0 -0
  279. codex/static_root/assets/user-tab-Dpxe3ygA.js.gz +0 -0
  280. codex/static_root/js/choices-admin.3521e2a863df.json +1 -0
  281. codex/static_root/js/choices-admin.3521e2a863df.json.br +0 -0
  282. codex/static_root/js/choices-admin.3521e2a863df.json.gz +0 -0
  283. codex/static_root/js/choices-admin.json +1 -1
  284. codex/static_root/js/choices-admin.json.br +0 -0
  285. codex/static_root/js/choices-admin.json.gz +0 -0
  286. codex/static_root/manifest.35e46a3ec8fa.json +622 -0
  287. codex/static_root/manifest.35e46a3ec8fa.json.br +0 -0
  288. codex/static_root/manifest.35e46a3ec8fa.json.gz +0 -0
  289. codex/static_root/manifest.json +245 -252
  290. codex/static_root/manifest.json.br +0 -0
  291. codex/static_root/manifest.json.gz +0 -0
  292. codex/static_root/staticfiles.json +1 -1
  293. codex/urls/api/v3.py +1 -1
  294. codex/views/admin/api_key.py +5 -2
  295. codex/views/admin/stats.py +13 -20
  296. codex/views/admin/tasks.py +3 -7
  297. codex/views/auth.py +1 -1
  298. codex/views/bookmark.py +69 -51
  299. codex/views/browser/annotations.py +32 -45
  300. codex/views/browser/base.py +25 -8
  301. codex/views/browser/breadcrumbs.py +5 -5
  302. codex/views/browser/browser.py +20 -32
  303. codex/views/browser/choices.py +18 -15
  304. codex/views/browser/cover.py +30 -66
  305. codex/views/browser/filters/annotations.py +66 -14
  306. codex/views/browser/filters/bookmark.py +21 -5
  307. codex/views/browser/filters/group.py +29 -17
  308. codex/views/browser/filters/search.py +3 -4
  309. codex/views/browser/metadata.py +86 -47
  310. codex/views/browser/mtime.py +61 -0
  311. codex/views/browser/order_by.py +1 -6
  312. codex/views/browser/settings.py +53 -0
  313. codex/views/browser/validate.py +3 -0
  314. codex/views/const.py +32 -1
  315. codex/views/opds/v1/feed.py +3 -5
  316. codex/views/opds/v2/feed.py +3 -3
  317. codex/views/reader/arcs.py +42 -51
  318. codex/views/reader/books.py +48 -66
  319. codex/views/reader/init.py +121 -0
  320. codex/views/reader/reader.py +8 -66
  321. codex/views/session.py +15 -11
  322. codex/views/settings.py +23 -10
  323. codex/views/util.py +2 -1
  324. codex/websockets/consumers.py +13 -9
  325. {codex-1.6.0a8.dist-info → codex-1.6.0a10.dist-info}/METADATA +1 -1
  326. {codex-1.6.0a8.dist-info → codex-1.6.0a10.dist-info}/RECORD +329 -325
  327. codex/serializers/mtime.py +0 -25
  328. codex/static_root/assets/VCheckbox-DI-DgMb4.8eeb1bd3e18b.js +0 -1
  329. codex/static_root/assets/VCheckbox-DI-DgMb4.8eeb1bd3e18b.js.br +0 -0
  330. codex/static_root/assets/VCheckbox-DI-DgMb4.8eeb1bd3e18b.js.gz +0 -0
  331. codex/static_root/assets/VCheckbox-DI-DgMb4.js +0 -1
  332. codex/static_root/assets/VCheckbox-DI-DgMb4.js.br +0 -0
  333. codex/static_root/assets/VCheckbox-DI-DgMb4.js.gz +0 -0
  334. codex/static_root/assets/VCheckboxBtn-B8Z4NA-t.3d8921f98ad9.js.br +0 -0
  335. codex/static_root/assets/VCheckboxBtn-B8Z4NA-t.3d8921f98ad9.js.gz +0 -0
  336. codex/static_root/assets/VCheckboxBtn-B8Z4NA-t.js.br +0 -0
  337. codex/static_root/assets/VCheckboxBtn-B8Z4NA-t.js.gz +0 -0
  338. codex/static_root/assets/VCombobox-D1_0lCri.6ef85a29c2dc.js +0 -1
  339. codex/static_root/assets/VCombobox-D1_0lCri.6ef85a29c2dc.js.br +0 -0
  340. codex/static_root/assets/VCombobox-D1_0lCri.6ef85a29c2dc.js.gz +0 -0
  341. codex/static_root/assets/VCombobox-D1_0lCri.js +0 -1
  342. codex/static_root/assets/VCombobox-D1_0lCri.js.br +0 -0
  343. codex/static_root/assets/VCombobox-D1_0lCri.js.gz +0 -0
  344. codex/static_root/assets/VDialog-C5Nxd_zP.css.br +0 -0
  345. codex/static_root/assets/VDialog-C5Nxd_zP.css.gz +0 -0
  346. codex/static_root/assets/VDialog-C5Nxd_zP.d327f505ef0e.css.br +0 -0
  347. codex/static_root/assets/VDialog-C5Nxd_zP.d327f505ef0e.css.gz +0 -0
  348. codex/static_root/assets/VDialog-opmV0YZz.d77216e97599.js +0 -1
  349. codex/static_root/assets/VDialog-opmV0YZz.d77216e97599.js.br +0 -0
  350. codex/static_root/assets/VDialog-opmV0YZz.d77216e97599.js.gz +0 -0
  351. codex/static_root/assets/VDialog-opmV0YZz.js +0 -1
  352. codex/static_root/assets/VDialog-opmV0YZz.js.br +0 -0
  353. codex/static_root/assets/VDialog-opmV0YZz.js.gz +0 -0
  354. codex/static_root/assets/VExpansionPanels-DX2dhUf5.ecc2ebfa62ec.js +0 -1
  355. codex/static_root/assets/VExpansionPanels-DX2dhUf5.ecc2ebfa62ec.js.br +0 -0
  356. codex/static_root/assets/VExpansionPanels-DX2dhUf5.ecc2ebfa62ec.js.gz +0 -0
  357. codex/static_root/assets/VExpansionPanels-DX2dhUf5.js +0 -1
  358. codex/static_root/assets/VExpansionPanels-DX2dhUf5.js.br +0 -0
  359. codex/static_root/assets/VExpansionPanels-DX2dhUf5.js.gz +0 -0
  360. codex/static_root/assets/VRadioGroup-BGa6_q5a.d1a3ad7266bd.js.br +0 -0
  361. codex/static_root/assets/VRadioGroup-BGa6_q5a.d1a3ad7266bd.js.gz +0 -0
  362. codex/static_root/assets/VRadioGroup-BGa6_q5a.js.br +0 -0
  363. codex/static_root/assets/VRadioGroup-BGa6_q5a.js.gz +0 -0
  364. codex/static_root/assets/VSelect-sIYW_-ud.7d40da0ec3ff.js +0 -1
  365. codex/static_root/assets/VSelect-sIYW_-ud.7d40da0ec3ff.js.br +0 -0
  366. codex/static_root/assets/VSelect-sIYW_-ud.7d40da0ec3ff.js.gz +0 -0
  367. codex/static_root/assets/VSelect-sIYW_-ud.js +0 -1
  368. codex/static_root/assets/VSelect-sIYW_-ud.js.br +0 -0
  369. codex/static_root/assets/VSelect-sIYW_-ud.js.gz +0 -0
  370. codex/static_root/assets/VSelectionControl-C8DP9rgp.ef80e8e3dfc1.js.br +0 -0
  371. codex/static_root/assets/VSelectionControl-C8DP9rgp.ef80e8e3dfc1.js.gz +0 -0
  372. codex/static_root/assets/VSelectionControl-C8DP9rgp.js.br +0 -0
  373. codex/static_root/assets/VSelectionControl-C8DP9rgp.js.gz +0 -0
  374. codex/static_root/assets/VSlideGroup-BRYhkLH8.3534ec94b2f1.js +0 -1
  375. codex/static_root/assets/VSlideGroup-BRYhkLH8.3534ec94b2f1.js.br +0 -0
  376. codex/static_root/assets/VSlideGroup-BRYhkLH8.3534ec94b2f1.js.gz +0 -0
  377. codex/static_root/assets/VSlideGroup-BRYhkLH8.js +0 -1
  378. codex/static_root/assets/VSlideGroup-BRYhkLH8.js.br +0 -0
  379. codex/static_root/assets/VSlideGroup-BRYhkLH8.js.gz +0 -0
  380. codex/static_root/assets/VTable-D1esKtlw.52165930e3b9.js.br +0 -0
  381. codex/static_root/assets/VTable-D1esKtlw.52165930e3b9.js.gz +0 -0
  382. codex/static_root/assets/VTable-D1esKtlw.js.br +0 -0
  383. codex/static_root/assets/VTable-D1esKtlw.js.gz +0 -0
  384. codex/static_root/assets/VTextField-BnGscKcY.54032adf3a4b.js +0 -1
  385. codex/static_root/assets/VTextField-BnGscKcY.54032adf3a4b.js.br +0 -0
  386. codex/static_root/assets/VTextField-BnGscKcY.54032adf3a4b.js.gz +0 -0
  387. codex/static_root/assets/VTextField-BnGscKcY.js +0 -1
  388. codex/static_root/assets/VTextField-BnGscKcY.js.br +0 -0
  389. codex/static_root/assets/VTextField-BnGscKcY.js.gz +0 -0
  390. codex/static_root/assets/VWindowItem-B0KokcAk.ea94224aecb9.js +0 -1
  391. codex/static_root/assets/VWindowItem-B0KokcAk.ea94224aecb9.js.br +0 -0
  392. codex/static_root/assets/VWindowItem-B0KokcAk.ea94224aecb9.js.gz +0 -0
  393. codex/static_root/assets/VWindowItem-B0KokcAk.js +0 -1
  394. codex/static_root/assets/VWindowItem-B0KokcAk.js.br +0 -0
  395. codex/static_root/assets/VWindowItem-B0KokcAk.js.gz +0 -0
  396. codex/static_root/assets/admin-Y0bL21AB.42d4f0d68163.js +0 -1
  397. codex/static_root/assets/admin-Y0bL21AB.42d4f0d68163.js.br +0 -0
  398. codex/static_root/assets/admin-Y0bL21AB.42d4f0d68163.js.gz +0 -0
  399. codex/static_root/assets/admin-Y0bL21AB.js +0 -1
  400. codex/static_root/assets/admin-Y0bL21AB.js.br +0 -0
  401. codex/static_root/assets/admin-Y0bL21AB.js.gz +0 -0
  402. codex/static_root/assets/admin-drawer-panel-DNngWUEM.4e992e3fe86e.js +0 -30
  403. codex/static_root/assets/admin-drawer-panel-DNngWUEM.4e992e3fe86e.js.br +0 -0
  404. codex/static_root/assets/admin-drawer-panel-DNngWUEM.4e992e3fe86e.js.gz +0 -0
  405. codex/static_root/assets/admin-drawer-panel-DNngWUEM.js +0 -30
  406. codex/static_root/assets/admin-drawer-panel-DNngWUEM.js.br +0 -0
  407. codex/static_root/assets/admin-drawer-panel-DNngWUEM.js.gz +0 -0
  408. codex/static_root/assets/admin-drawer-panel-o87BwD38.ce39427c8201.css.br +0 -0
  409. codex/static_root/assets/admin-drawer-panel-o87BwD38.ce39427c8201.css.gz +0 -0
  410. codex/static_root/assets/admin-drawer-panel-o87BwD38.css.br +0 -0
  411. codex/static_root/assets/admin-drawer-panel-o87BwD38.css.gz +0 -0
  412. codex/static_root/assets/browser-CMAtBCLx.17a277ce43a9.css +0 -1
  413. codex/static_root/assets/browser-CMAtBCLx.17a277ce43a9.css.br +0 -0
  414. codex/static_root/assets/browser-CMAtBCLx.17a277ce43a9.css.gz +0 -0
  415. codex/static_root/assets/browser-CMAtBCLx.css +0 -1
  416. codex/static_root/assets/browser-CMAtBCLx.css.br +0 -0
  417. codex/static_root/assets/browser-CMAtBCLx.css.gz +0 -0
  418. codex/static_root/assets/browser-DoJk8uvb.2c97a3858932.js +0 -1
  419. codex/static_root/assets/browser-DoJk8uvb.2c97a3858932.js.br +0 -0
  420. codex/static_root/assets/browser-DoJk8uvb.2c97a3858932.js.gz +0 -0
  421. codex/static_root/assets/browser-DoJk8uvb.js +0 -1
  422. codex/static_root/assets/browser-DoJk8uvb.js.br +0 -0
  423. codex/static_root/assets/browser-DoJk8uvb.js.gz +0 -0
  424. codex/static_root/assets/change-password-dialog-CH4xbjJa.b749aa6ea07e.js +0 -1
  425. codex/static_root/assets/change-password-dialog-CH4xbjJa.b749aa6ea07e.js.br +0 -0
  426. codex/static_root/assets/change-password-dialog-CH4xbjJa.b749aa6ea07e.js.gz +0 -0
  427. codex/static_root/assets/change-password-dialog-CH4xbjJa.js +0 -1
  428. codex/static_root/assets/change-password-dialog-CH4xbjJa.js.br +0 -0
  429. codex/static_root/assets/change-password-dialog-CH4xbjJa.js.gz +0 -0
  430. codex/static_root/assets/change-password-dialog-HvIvmmIE.73be0c8a8278.css +0 -1
  431. codex/static_root/assets/change-password-dialog-HvIvmmIE.73be0c8a8278.css.br +0 -0
  432. codex/static_root/assets/change-password-dialog-HvIvmmIE.73be0c8a8278.css.gz +0 -0
  433. codex/static_root/assets/change-password-dialog-HvIvmmIE.css +0 -1
  434. codex/static_root/assets/change-password-dialog-HvIvmmIE.css.br +0 -0
  435. codex/static_root/assets/change-password-dialog-HvIvmmIE.css.gz +0 -0
  436. codex/static_root/assets/confirm-dialog-DkU0twE6.a20006aa5ba6.js.br +0 -0
  437. codex/static_root/assets/confirm-dialog-DkU0twE6.a20006aa5ba6.js.gz +0 -0
  438. codex/static_root/assets/confirm-dialog-DkU0twE6.js.br +0 -0
  439. codex/static_root/assets/confirm-dialog-DkU0twE6.js.gz +0 -0
  440. codex/static_root/assets/datetime-column-BfW_j_3Q.6a614321a435.js +0 -1
  441. codex/static_root/assets/datetime-column-BfW_j_3Q.6a614321a435.js.br +0 -0
  442. codex/static_root/assets/datetime-column-BfW_j_3Q.6a614321a435.js.gz +0 -0
  443. codex/static_root/assets/datetime-column-BfW_j_3Q.js +0 -1
  444. codex/static_root/assets/datetime-column-BfW_j_3Q.js.br +0 -0
  445. codex/static_root/assets/datetime-column-BfW_j_3Q.js.gz +0 -0
  446. codex/static_root/assets/filter-DjxN78cK.35673f6e49b1.js +0 -1
  447. codex/static_root/assets/filter-DjxN78cK.35673f6e49b1.js.br +0 -0
  448. codex/static_root/assets/filter-DjxN78cK.35673f6e49b1.js.gz +0 -0
  449. codex/static_root/assets/filter-DjxN78cK.js +0 -1
  450. codex/static_root/assets/filter-DjxN78cK.js.br +0 -0
  451. codex/static_root/assets/filter-DjxN78cK.js.gz +0 -0
  452. codex/static_root/assets/flag-tab-D4ZNSbMR.e763a9937701.js.br +0 -0
  453. codex/static_root/assets/flag-tab-D4ZNSbMR.e763a9937701.js.gz +0 -0
  454. codex/static_root/assets/flag-tab-D4ZNSbMR.js.br +0 -0
  455. codex/static_root/assets/flag-tab-D4ZNSbMR.js.gz +0 -0
  456. codex/static_root/assets/group-tab-DucpNoBs.a6f37ae940c0.js.br +0 -0
  457. codex/static_root/assets/group-tab-DucpNoBs.a6f37ae940c0.js.gz +0 -0
  458. codex/static_root/assets/group-tab-DucpNoBs.js.br +0 -0
  459. codex/static_root/assets/group-tab-DucpNoBs.js.gz +0 -0
  460. codex/static_root/assets/http-error-Cj0hTq-c.062a88ddd4dc.js.br +0 -0
  461. codex/static_root/assets/http-error-Cj0hTq-c.062a88ddd4dc.js.gz +0 -0
  462. codex/static_root/assets/http-error-Cj0hTq-c.js.br +0 -0
  463. codex/static_root/assets/http-error-Cj0hTq-c.js.gz +0 -0
  464. codex/static_root/assets/library-tab-CzksdS3Q.566acdb45dec.js.br +0 -0
  465. codex/static_root/assets/library-tab-CzksdS3Q.566acdb45dec.js.gz +0 -0
  466. codex/static_root/assets/library-tab-CzksdS3Q.js.br +0 -0
  467. codex/static_root/assets/library-tab-CzksdS3Q.js.gz +0 -0
  468. codex/static_root/assets/main-DX-k2724.f73d08e9ad64.js +0 -32
  469. codex/static_root/assets/main-DX-k2724.f73d08e9ad64.js.br +0 -0
  470. codex/static_root/assets/main-DX-k2724.f73d08e9ad64.js.gz +0 -0
  471. codex/static_root/assets/main-DX-k2724.js +0 -32
  472. codex/static_root/assets/main-DX-k2724.js.br +0 -0
  473. codex/static_root/assets/main-DX-k2724.js.gz +0 -0
  474. codex/static_root/assets/pagination-toolbar-CQcR0a3T.7a2d33ed8c39.js +0 -1
  475. codex/static_root/assets/pagination-toolbar-CQcR0a3T.7a2d33ed8c39.js.br +0 -0
  476. codex/static_root/assets/pagination-toolbar-CQcR0a3T.7a2d33ed8c39.js.gz +0 -0
  477. codex/static_root/assets/pagination-toolbar-CQcR0a3T.js +0 -1
  478. codex/static_root/assets/pagination-toolbar-CQcR0a3T.js.br +0 -0
  479. codex/static_root/assets/pagination-toolbar-CQcR0a3T.js.gz +0 -0
  480. codex/static_root/assets/pagination-toolbar-DWdp_kmz.css +0 -1
  481. codex/static_root/assets/pagination-toolbar-DWdp_kmz.css.br +0 -0
  482. codex/static_root/assets/pagination-toolbar-DWdp_kmz.css.gz +0 -0
  483. codex/static_root/assets/pagination-toolbar-DWdp_kmz.f98f47e87612.css +0 -1
  484. codex/static_root/assets/pagination-toolbar-DWdp_kmz.f98f47e87612.css.br +0 -0
  485. codex/static_root/assets/pagination-toolbar-DWdp_kmz.f98f47e87612.css.gz +0 -0
  486. codex/static_root/assets/pdf-doc-GkMk4o5L.ef24c859a7a4.js.br +0 -0
  487. codex/static_root/assets/pdf-doc-GkMk4o5L.ef24c859a7a4.js.gz +0 -0
  488. codex/static_root/assets/pdf-doc-GkMk4o5L.js.br +0 -0
  489. codex/static_root/assets/pdf-doc-GkMk4o5L.js.gz +0 -0
  490. codex/static_root/assets/reader-CCCfpM3x.1553e45bd775.css.br +0 -0
  491. codex/static_root/assets/reader-CCCfpM3x.1553e45bd775.css.gz +0 -0
  492. codex/static_root/assets/reader-CCCfpM3x.css.br +0 -0
  493. codex/static_root/assets/reader-CCCfpM3x.css.gz +0 -0
  494. codex/static_root/assets/reader-Cl6mmQB_.7b9e7591954f.js +0 -2
  495. codex/static_root/assets/reader-Cl6mmQB_.7b9e7591954f.js.br +0 -0
  496. codex/static_root/assets/reader-Cl6mmQB_.7b9e7591954f.js.gz +0 -0
  497. codex/static_root/assets/reader-Cl6mmQB_.js +0 -2
  498. codex/static_root/assets/reader-Cl6mmQB_.js.br +0 -0
  499. codex/static_root/assets/reader-Cl6mmQB_.js.gz +0 -0
  500. codex/static_root/assets/relation-chips-CZ8VvR4y.d4232f3c2518.js +0 -1
  501. codex/static_root/assets/relation-chips-CZ8VvR4y.d4232f3c2518.js.br +0 -0
  502. codex/static_root/assets/relation-chips-CZ8VvR4y.d4232f3c2518.js.gz +0 -0
  503. codex/static_root/assets/relation-chips-CZ8VvR4y.js +0 -1
  504. codex/static_root/assets/relation-chips-CZ8VvR4y.js.br +0 -0
  505. codex/static_root/assets/relation-chips-CZ8VvR4y.js.gz +0 -0
  506. codex/static_root/assets/settings-drawer-DyS0A2pN.17a98928589f.js +0 -2
  507. codex/static_root/assets/settings-drawer-DyS0A2pN.17a98928589f.js.br +0 -0
  508. codex/static_root/assets/settings-drawer-DyS0A2pN.17a98928589f.js.gz +0 -0
  509. codex/static_root/assets/settings-drawer-DyS0A2pN.js +0 -2
  510. codex/static_root/assets/settings-drawer-DyS0A2pN.js.br +0 -0
  511. codex/static_root/assets/settings-drawer-DyS0A2pN.js.gz +0 -0
  512. codex/static_root/assets/stats-tab-R3Ekobfm.70fa09a67518.js +0 -1
  513. codex/static_root/assets/stats-tab-R3Ekobfm.70fa09a67518.js.br +0 -0
  514. codex/static_root/assets/stats-tab-R3Ekobfm.70fa09a67518.js.gz +0 -0
  515. codex/static_root/assets/stats-tab-R3Ekobfm.js +0 -1
  516. codex/static_root/assets/stats-tab-R3Ekobfm.js.br +0 -0
  517. codex/static_root/assets/stats-tab-R3Ekobfm.js.gz +0 -0
  518. codex/static_root/assets/stats-tab-TsyG3wH_.b93c080356fc.css +0 -1
  519. codex/static_root/assets/stats-tab-TsyG3wH_.b93c080356fc.css.br +0 -2
  520. codex/static_root/assets/stats-tab-TsyG3wH_.b93c080356fc.css.gz +0 -0
  521. codex/static_root/assets/stats-tab-TsyG3wH_.css +0 -1
  522. codex/static_root/assets/stats-tab-TsyG3wH_.css.br +0 -2
  523. codex/static_root/assets/stats-tab-TsyG3wH_.css.gz +0 -0
  524. codex/static_root/assets/task-tab-BhLDK_7Q.28ff51b3ed01.js +0 -1
  525. codex/static_root/assets/task-tab-BhLDK_7Q.28ff51b3ed01.js.br +0 -0
  526. codex/static_root/assets/task-tab-BhLDK_7Q.28ff51b3ed01.js.gz +0 -0
  527. codex/static_root/assets/task-tab-BhLDK_7Q.js +0 -1
  528. codex/static_root/assets/task-tab-BhLDK_7Q.js.br +0 -0
  529. codex/static_root/assets/task-tab-BhLDK_7Q.js.gz +0 -0
  530. codex/static_root/assets/to-case-BR2frjFx.87de5d7a0503.js +0 -1
  531. codex/static_root/assets/to-case-BR2frjFx.js +0 -1
  532. codex/static_root/assets/unauthorized-BbXjAe5l.d48912d34a68.js +0 -1
  533. codex/static_root/assets/unauthorized-BbXjAe5l.d48912d34a68.js.br +0 -0
  534. codex/static_root/assets/unauthorized-BbXjAe5l.d48912d34a68.js.gz +0 -0
  535. codex/static_root/assets/unauthorized-BbXjAe5l.js +0 -1
  536. codex/static_root/assets/unauthorized-BbXjAe5l.js.br +0 -0
  537. codex/static_root/assets/unauthorized-BbXjAe5l.js.gz +0 -0
  538. codex/static_root/assets/unauthorized-DHV6Q1n-.b7847b2872a1.css.br +0 -0
  539. codex/static_root/assets/unauthorized-DHV6Q1n-.b7847b2872a1.css.gz +0 -0
  540. codex/static_root/assets/unauthorized-DHV6Q1n-.css.br +0 -0
  541. codex/static_root/assets/unauthorized-DHV6Q1n-.css.gz +0 -0
  542. codex/static_root/assets/user-tab-DxITIl2d.d79d7f062930.js +0 -1
  543. codex/static_root/assets/user-tab-DxITIl2d.d79d7f062930.js.br +0 -0
  544. codex/static_root/assets/user-tab-DxITIl2d.d79d7f062930.js.gz +0 -0
  545. codex/static_root/assets/user-tab-DxITIl2d.js +0 -1
  546. codex/static_root/assets/user-tab-DxITIl2d.js.br +0 -0
  547. codex/static_root/assets/user-tab-DxITIl2d.js.gz +0 -0
  548. codex/static_root/js/choices-admin.d790df01c20a.json +0 -1
  549. codex/static_root/js/choices-admin.d790df01c20a.json.br +0 -0
  550. codex/static_root/js/choices-admin.d790df01c20a.json.gz +0 -0
  551. codex/static_root/manifest.16d52597ef50.json +0 -629
  552. codex/static_root/manifest.16d52597ef50.json.br +0 -0
  553. codex/static_root/manifest.16d52597ef50.json.gz +0 -0
  554. codex/views/mtime.py +0 -72
  555. {codex-1.6.0a8.dist-info → codex-1.6.0a10.dist-info}/LICENSE +0 -0
  556. {codex-1.6.0a8.dist-info → codex-1.6.0a10.dist-info}/WHEEL +0 -0
  557. {codex-1.6.0a8.dist-info → codex-1.6.0a10.dist-info}/entry_points.txt +0 -0
@@ -18,10 +18,10 @@ class BrowserBreadcrumbsView(BrowserPaginateView):
18
18
  """Load breadcrumbs and determine if they should be searched for a graft."""
19
19
  breadcrumbs: list[Route] = list(self.params.get("breadcrumbs", []))
20
20
  old_breadcrumbs = [Route(**crumb) for crumb in breadcrumbs]
21
- changed = bool(old_breadcrumbs) and old_breadcrumbs[-1].group in valid_groups
22
- if changed:
21
+ invalid = not old_breadcrumbs or old_breadcrumbs[-1].group not in valid_groups
22
+ if invalid:
23
23
  old_breadcrumbs = []
24
- return old_breadcrumbs, changed
24
+ return old_breadcrumbs, invalid
25
25
 
26
26
  def _breadcrumbs_save(self, breadcrumbs):
27
27
  """Save the breadcrumbs to params."""
@@ -184,7 +184,7 @@ class BrowserBreadcrumbsView(BrowserPaginateView):
184
184
 
185
185
  return tuple(new_breadcrumbs), changed
186
186
 
187
- def get_parent_breadcrumbs(
187
+ def get_breadcrumbs(
188
188
  self,
189
189
  ) -> tuple[Route, ...]:
190
190
  """Graft or create breadcrumbs by browser mode."""
@@ -199,4 +199,4 @@ class BrowserBreadcrumbsView(BrowserPaginateView):
199
199
  if changed:
200
200
  self._breadcrumbs_save(breadcrumbs)
201
201
 
202
- return breadcrumbs[:-1] if breadcrumbs else ()
202
+ return breadcrumbs
@@ -16,13 +16,9 @@ from codex.models import (
16
16
  Folder,
17
17
  Imprint,
18
18
  Library,
19
- Publisher,
20
- Series,
21
- StoryArc,
22
19
  Volume,
23
20
  )
24
21
  from codex.serializers.browser.page import BrowserPageSerializer
25
- from codex.util import max_none
26
22
  from codex.views.browser.title import BrowserTitleView
27
23
  from codex.views.const import (
28
24
  COMIC_GROUP,
@@ -35,6 +31,15 @@ if TYPE_CHECKING:
35
31
 
36
32
 
37
33
  LOG = get_logger(__name__)
34
+ _GROUP_INSTANCE_SELECT_RELATED: MappingProxyType[
35
+ type[BrowserGroupModel], tuple[str | None, ...]
36
+ ] = MappingProxyType(
37
+ {
38
+ Comic: ("series", "volume"),
39
+ Volume: ("series",),
40
+ Imprint: ("publisher",),
41
+ }
42
+ )
38
43
 
39
44
 
40
45
  class BrowserView(BrowserTitleView):
@@ -49,19 +54,6 @@ class BrowserView(BrowserTitleView):
49
54
  }
50
55
  )
51
56
  TARGET = "browser"
52
- _GROUP_INSTANCE_SELECT_RELATED: MappingProxyType[
53
- type[BrowserGroupModel], tuple[str | None, ...]
54
- ] = MappingProxyType(
55
- {
56
- Comic: ("series", "volume"),
57
- Volume: ("series",),
58
- Series: (None,),
59
- Imprint: ("publisher",),
60
- Publisher: (None,),
61
- Folder: ("parent_folder",),
62
- StoryArc: (None,),
63
- }
64
- )
65
57
 
66
58
  def __init__(self, *args, **kwargs):
67
59
  """Set params for the type checker."""
@@ -76,7 +68,7 @@ class BrowserView(BrowserTitleView):
76
68
  if pks and self.group_class:
77
69
  try:
78
70
  select_related: tuple[str | None, ...] = (
79
- self._GROUP_INSTANCE_SELECT_RELATED[self.group_class]
71
+ _GROUP_INSTANCE_SELECT_RELATED.get(self.group_class, (None,))
80
72
  )
81
73
  self.group_query = self.group_class.objects.select_related(
82
74
  *select_related
@@ -157,16 +149,13 @@ class BrowserView(BrowserTitleView):
157
149
  count = 0
158
150
  return qs, count
159
151
 
160
- def _get_page_mtime(self, group_qs):
161
- if self.group_instance:
162
- page_updated_at_max = self.group_instance.updated_at
163
- else:
164
- page_updated_at_max = group_qs.aggregate(max=Max("updated_at"))["max"]
165
-
166
- agg_func = self.get_bookmark_updated_at_aggregate(self.model, True)
167
- page_bookmark_updated_at = group_qs.aggregate(max=agg_func)["max"]
152
+ def _get_page_mtime(self):
153
+ if not self.is_bookmark_filtered and self.group_instance:
154
+ # Nice optimization if we can get get it.
155
+ return self.group_instance.updated_at
168
156
 
169
- return max_none(page_updated_at_max, page_bookmark_updated_at)
157
+ group_model = self.group_class if self.group_class else self.model
158
+ return self.get_group_mtime(group_model, page_mtime=True)
170
159
 
171
160
  @staticmethod
172
161
  def _get_zero_pad(book_qs):
@@ -182,7 +171,6 @@ class BrowserView(BrowserTitleView):
182
171
  def _get_group_and_books(self):
183
172
  """Create the main queries with filters, annotation and pagination."""
184
173
  group_qs, group_count = self._get_group_queryset()
185
- mtime = self._get_page_mtime(group_qs)
186
174
  book_qs, book_count = self._get_book_queryset()
187
175
  # Paginate
188
176
  group_qs, book_qs, num_pages, page_group_count, page_book_count = self.paginate(
@@ -200,6 +188,7 @@ class BrowserView(BrowserTitleView):
200
188
  # print(group_qs.query)
201
189
 
202
190
  total_count = page_group_count + page_book_count
191
+ mtime = self._get_page_mtime()
203
192
  return group_qs, book_qs, num_pages, total_count, zero_pad, mtime
204
193
 
205
194
  def get_object(self):
@@ -209,7 +198,7 @@ class BrowserView(BrowserTitleView):
209
198
  )
210
199
 
211
200
  # get additional context
212
- parent_breadcrumbs = self.get_parent_breadcrumbs()
201
+ breadcrumbs = self.get_breadcrumbs()
213
202
  title = self.get_browser_page_title()
214
203
  # needs to happen after pagination
215
204
  # runs obj list query twice :/
@@ -218,7 +207,7 @@ class BrowserView(BrowserTitleView):
218
207
  # construct final data structure
219
208
  return MappingProxyType(
220
209
  {
221
- "breadcrumbs": parent_breadcrumbs,
210
+ "breadcrumbs": breadcrumbs,
222
211
  "title": title,
223
212
  "model_group": self.model_group,
224
213
  "groups": group_qs,
@@ -232,11 +221,10 @@ class BrowserView(BrowserTitleView):
232
221
  }
233
222
  )
234
223
 
235
- @extend_schema(request=BrowserTitleView.input_serializer_class)
224
+ @extend_schema(parameters=[BrowserTitleView.input_serializer_class])
236
225
  def get(self, *_args, **_kwargs):
237
226
  """Get browser settings."""
238
227
  self.init_request()
239
228
  data = self.get_object()
240
229
  serializer = self.get_serializer(data)
241
- self.save_params_to_session(self.params)
242
230
  return Response(serializer.data)
@@ -15,11 +15,13 @@ from codex.models import (
15
15
  StoryArc,
16
16
  )
17
17
  from codex.models.named import IdentifierType
18
- from codex.serializers.browser.choices import CHOICES_SERIALIZER_CLASS_MAP
18
+ from codex.serializers.browser.choices import (
19
+ BrowserChoicesFilterSerializer,
20
+ )
19
21
  from codex.serializers.browser.filters import (
20
22
  BrowserFilterChoicesSerializer,
21
- CharListField,
22
23
  )
24
+ from codex.serializers.browser.settings import BrowserFilterChoicesInputSerilalizer
23
25
  from codex.serializers.models.pycountry import PyCountrySerializer
24
26
  from codex.views.browser.filters.annotations import (
25
27
  BrowserAnnotationsFilterView,
@@ -56,9 +58,15 @@ _NULL_NAMED_ROW = MappingProxyType({"pk": -1, "name": "_none_"})
56
58
  class BrowserChoicesViewBase(BrowserAnnotationsFilterView):
57
59
  """Get choices for filter dialog."""
58
60
 
59
- SERIALIZER_MANY = False
61
+ input_serializer_class = BrowserFilterChoicesInputSerilalizer
60
62
  TARGET = "choices"
61
63
 
64
+ def init_request(self):
65
+ """Initialieze request."""
66
+ self.parse_params()
67
+ self.set_model()
68
+ self.set_rel_prefix()
69
+
62
70
  @staticmethod
63
71
  def get_field_choices_query(comic_qs, field_name):
64
72
  """Get distinct values for the field."""
@@ -97,12 +105,12 @@ class BrowserChoicesViewBase(BrowserAnnotationsFilterView):
97
105
  """Get the comic subquery use for the choices."""
98
106
  return self.get_filtered_queryset(Comic)
99
107
 
100
- @extend_schema(request=BrowserAnnotationsFilterView.input_serializer_class)
108
+ @extend_schema(parameters=[input_serializer_class])
101
109
  def get(self, *_args, **_kwargs):
102
110
  """Return choices."""
103
111
  self.init_request()
104
112
  obj = self.get_object()
105
- serializer = self.get_serializer(obj, many=self.SERIALIZER_MANY)
113
+ serializer = self.get_serializer(obj)
106
114
  return Response(serializer.data)
107
115
 
108
116
 
@@ -160,10 +168,7 @@ class BrowserChoicesAvailableView(BrowserChoicesViewBase):
160
168
  class BrowserChoicesView(BrowserChoicesViewBase):
161
169
  """Get choices for filter dialog."""
162
170
 
163
- # serializer_class = Dynamic class determined in get()
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()
171
+ serializer_class = BrowserChoicesFilterSerializer
167
172
 
168
173
  def _get_field_choices(self, comic_qs, field_name):
169
174
  """Create a pk:name object for fields without tables."""
@@ -202,11 +207,6 @@ class BrowserChoicesView(BrowserChoicesViewBase):
202
207
  field_name = self.kwargs.get("field_name", "")
203
208
  return snakecase(field_name)
204
209
 
205
- def get_serializer_class(self): # type: ignore
206
- """Dynamic serializer class."""
207
- field_name = self._get_field_name()
208
- return CHOICES_SERIALIZER_CLASS_MAP.get(field_name, CharListField)
209
-
210
210
  def get_object(self):
211
211
  """Return choices with more than one choice."""
212
212
  qs = super().get_object()
@@ -217,4 +217,7 @@ class BrowserChoicesView(BrowserChoicesViewBase):
217
217
  choices = self._get_m2m_field_choices(m2m_model, qs, rel)
218
218
  else:
219
219
  choices = self._get_field_choices(qs, rel)
220
- return choices
220
+ return {
221
+ "field_name": field_name,
222
+ "choices": choices,
223
+ }
@@ -9,12 +9,12 @@ from codex.librarian.covers.create import CoverCreateMixin
9
9
  from codex.librarian.covers.path import CoverPathMixin
10
10
  from codex.librarian.mp_queue import LIBRARIAN_QUEUE
11
11
  from codex.logger.logging import get_logger
12
- from codex.models import Comic, Folder, Volume
12
+ from codex.models import Comic, Volume
13
13
  from codex.models.paths import CustomCover
14
- from codex.serializers.choices import DEFAULTS
14
+ from codex.serializers.browser.settings import BrowserCoverInputSerializer
15
15
  from codex.views.browser.annotations import BrowserAnnotationsView
16
16
  from codex.views.const import (
17
- GROUP_RELATION,
17
+ CUSTOM_COVER_GROUP_RELATION,
18
18
  MISSING_COVER_FN,
19
19
  MISSING_COVER_NAME_MAP,
20
20
  STATIC_IMG_PATH,
@@ -39,6 +39,7 @@ class WEBPRenderer(BaseRenderer):
39
39
  class CoverView(BrowserAnnotationsView):
40
40
  """ComicCover View."""
41
41
 
42
+ input_serializer_class = BrowserCoverInputSerializer
42
43
  renderer_classes = (WEBPRenderer,)
43
44
  content_type = "image/webp"
44
45
  TARGET = "cover"
@@ -56,39 +57,21 @@ class CoverView(BrowserAnnotationsView):
56
57
  pks = self.kwargs["pks"]
57
58
  return pks[0], False
58
59
 
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
60
+ def _get_custom_cover(self):
61
+ """Get Custom Cover."""
62
+ if self.model == Volume or not self.params.get("custom_covers"):
63
+ return None
64
+ group = self.kwargs["group"]
65
+ group_rel = CUSTOM_COVER_GROUP_RELATION[group]
66
+ pks = self.kwargs["pks"]
67
+ comic_filter = {f"{group_rel}__in": pks}
68
+ qs = CustomCover.objects.filter(**comic_filter)
69
+ qs = qs.only("pk")
70
+ return qs.first()
78
71
 
79
72
  def _get_dynamic_cover(self):
80
73
  """Get dynamic cover."""
81
74
  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
75
  comic_qs = self.get_filtered_queryset(Comic)
93
76
  comic_qs = self.annotate_order_aggregates(comic_qs, Comic)
94
77
  comic_qs = self.add_order_by(comic_qs, Comic)
@@ -99,40 +82,15 @@ class CoverView(BrowserAnnotationsView):
99
82
  cover_pk = comic.pk if comic else 0
100
83
  return cover_pk, False
101
84
 
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
85
  def _get_cover_pk(self) -> tuple[int, bool]:
129
86
  """Get Cover Pk queryset for comic queryset."""
130
87
  if self.model == Comic:
131
88
  cover_pk, custom = self._get_comic_cover()
132
- elif self._use_dynamic_cover():
133
- cover_pk, custom = self._get_dynamic_cover()
89
+ elif custom_cover := self._get_custom_cover():
90
+ cover_pk = custom_cover.pk
91
+ custom = True
134
92
  else:
135
- cover_pk, custom = self._get_first_cover()
93
+ cover_pk, custom = self._get_dynamic_cover()
136
94
  return cover_pk, custom
137
95
 
138
96
  def _get_missing_cover_path(self):
@@ -168,10 +126,16 @@ class CoverView(BrowserAnnotationsView):
168
126
  thumb_image_data = f.read()
169
127
  return thumb_image_data, content_type
170
128
 
171
- @extend_schema(responses={(200, content_type): OpenApiTypes.BINARY})
129
+ @extend_schema(
130
+ parameters=[BrowserAnnotationsView.input_serializer_class],
131
+ responses={(200, content_type): OpenApiTypes.BINARY},
132
+ )
172
133
  def get(self, *args, **kwargs): # type: ignore
173
134
  """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)
135
+ try:
136
+ self.init_request()
137
+ pk, custom = self._get_cover_pk()
138
+ thumb_image_data, content_type = self._get_cover_data(pk, custom)
139
+ return HttpResponse(thumb_image_data, content_type=content_type)
140
+ except Exception:
141
+ LOG.exception("get")
@@ -1,35 +1,87 @@
1
1
  """Annotations used by a filter."""
2
2
 
3
- from django.db.models.aggregates import Count
3
+ from django.db.models.aggregates import Count, Max
4
+ from django.db.models.functions import Coalesce, Greatest
4
5
 
5
6
  from codex.models.comic import Comic
6
7
  from codex.views.browser.filters.bookmark import BookmarkFilterMixin
7
8
  from codex.views.browser.validate import BrowserValidateView
9
+ from codex.views.const import (
10
+ EPOCH_START_DATETIMEFILED,
11
+ ONE_INTEGERFIELD,
12
+ )
8
13
 
9
14
 
10
15
  class BrowserAnnotationsFilterView(BrowserValidateView, BookmarkFilterMixin):
11
16
  """Annotations that also filter."""
12
17
 
13
- def get_query_filters(self, model):
18
+ def __init__(self, *args, **kwargs) -> None:
19
+ """Initialize."""
20
+ super().__init__(*args, **kwargs)
21
+ self.init_bookmark_data()
22
+
23
+ def _get_query_filters(self, model, group, pks, bookmark_filter, page_mtime=False): # noqa: PLR0913
14
24
  """Return all the filters except the group filter."""
15
25
  object_filter = self.get_group_acl_filter(model)
16
- object_filter &= self.get_bookmark_filter(model)
26
+ object_filter &= self.get_group_filter(group, pks, page_mtime=page_mtime)
17
27
  object_filter &= self.get_comic_field_filter(model)
18
- object_filter &= self.get_group_filter(model)
28
+ if bookmark_filter:
29
+ # not needed because OuterRef is applied next
30
+ object_filter &= self.get_bookmark_filter(model)
19
31
  return object_filter
20
32
 
21
33
  def _filter_by_child_count(self, qs, model):
22
- if model == Comic:
23
- return qs
24
-
34
+ """Filter group by child count."""
25
35
  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)
36
+ ann_name = "child_count"
37
+ count_func = ONE_INTEGERFIELD if model == Comic else Count(rel, distinct=True)
38
+ ann = {ann_name: count_func}
39
+ if self.TARGET == "opds2":
40
+ if model != Comic:
41
+ qs = qs.alias(**ann)
42
+ else:
43
+ qs = qs.annotate(**ann)
44
+ if model != Comic:
45
+ qs = qs.filter(**{f"{ann_name}__gt": 0})
46
+ return qs
28
47
 
29
- def get_filtered_queryset(self, model):
48
+ def get_filtered_queryset( # noqa: PLR0913
49
+ self, model, group=None, pks=None, bookmark_filter=True, page_mtime=False
50
+ ):
30
51
  """Get a filtered queryset for the model."""
31
- object_filter = self.get_query_filters(model)
32
-
52
+ object_filter = self._get_query_filters(
53
+ model, group, pks, bookmark_filter, page_mtime=page_mtime
54
+ )
33
55
  qs = model.objects.filter(object_filter)
34
- qs = self._filter_by_child_count(qs, model)
35
- return self.apply_search_filter(qs, model)
56
+ qs = self.apply_search_filter(qs, model)
57
+ return self._filter_by_child_count(qs, model)
58
+
59
+ def get_group_mtime(self, model, group=None, pks=None, page_mtime=False):
60
+ """Get a filtered mtime for browser pages and mtime checker."""
61
+ qs = self.get_filtered_queryset(
62
+ model,
63
+ group=group,
64
+ pks=pks,
65
+ bookmark_filter=self.is_bookmark_filtered,
66
+ page_mtime=page_mtime,
67
+ )
68
+ qs = qs.annotate(max_updated_at=Max("updated_at"))
69
+
70
+ if self.is_bookmark_filtered:
71
+ bm_rel, bm_filter = self.get_bookmark_rel_and_filter(model)
72
+ qs = qs.filter(bm_filter)
73
+ ua_rel = bm_rel + "__updated_at"
74
+ mbua = Max(ua_rel)
75
+ else:
76
+ mbua = EPOCH_START_DATETIMEFILED
77
+ qs = qs.annotate(max_bookmark_updated_at=mbua)
78
+
79
+ mtime = qs.aggregate(
80
+ max=Greatest(
81
+ Coalesce("max_bookmark_updated_at", EPOCH_START_DATETIMEFILED),
82
+ "max_updated_at",
83
+ )
84
+ )["max"]
85
+ if mtime == NotImplemented:
86
+ mtime = None
87
+ return mtime
@@ -1,17 +1,26 @@
1
1
  """Bookmark filter view methods."""
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
3
5
  from django.db.models import Q
4
6
 
7
+ if TYPE_CHECKING:
8
+ from codex.models import BrowserGroupModel
9
+
5
10
 
6
11
  class BookmarkFilterMixin:
7
12
  """BookmarkFilter view methods."""
8
13
 
9
- def get_bm_rel(self, model):
14
+ def init_bookmark_data(self):
15
+ """Initialize the bm_annotation_data."""
16
+ self._bm_annotation_data: dict[BrowserGroupModel, tuple[str, Q]] = {}
17
+
18
+ def _get_bm_rel(self, model):
10
19
  """Create bookmark relation."""
11
20
  rel_prefix = self.get_rel_prefix(model) # type: ignore
12
21
  return rel_prefix + "bookmark"
13
22
 
14
- def get_my_bookmark_filter(self, bm_rel):
23
+ def _get_my_bookmark_filter(self, bm_rel):
15
24
  """Get a filter for my session or user defined bookmarks."""
16
25
  if self.request.user.is_authenticated: # type: ignore
17
26
  my_bookmarks_kwargs = {f"{bm_rel}__user": self.request.user} # type: ignore
@@ -22,12 +31,19 @@ class BookmarkFilterMixin:
22
31
  }
23
32
  return Q(**my_bookmarks_kwargs)
24
33
 
34
+ def get_bookmark_rel_and_filter(self, model):
35
+ """Get the bookmark rel and filter once."""
36
+ if model not in self._bm_annotation_data:
37
+ bm_rel = self._get_bm_rel(model)
38
+ bm_filter = self._get_my_bookmark_filter(bm_rel)
39
+ self._bm_annotation_data[model] = (bm_rel, bm_filter)
40
+ return self._bm_annotation_data[model]
41
+
25
42
  def get_bookmark_filter(self, model):
26
43
  """Build bookmark query."""
27
- choice = self.params["filters"].get("bookmark", "") # type: ignore
44
+ choice: str = self.params.get("filters", {}).get("bookmark", "") # type: ignore
28
45
  if choice:
29
- bm_rel = self.get_bm_rel(model)
30
- my_bookmark_filter = self.get_my_bookmark_filter(bm_rel)
46
+ bm_rel, my_bookmark_filter = self.get_bookmark_rel_and_filter(model)
31
47
  if choice == "READ":
32
48
  bookmark_filter = my_bookmark_filter & Q(
33
49
  **{f"{bm_rel}__finished": True}
@@ -2,8 +2,11 @@
2
2
 
3
3
  from django.db.models import Q
4
4
 
5
- from codex.models import Comic
6
- from codex.views.const import FOLDER_GROUP, GROUP_MODEL_MAP, GROUP_RELATION
5
+ from codex.views.const import (
6
+ FILTER_ONLY_GROUP_RELATION,
7
+ FOLDER_GROUP,
8
+ GROUP_RELATION,
9
+ )
7
10
  from codex.views.session import SessionView
8
11
 
9
12
 
@@ -12,22 +15,31 @@ class GroupFilterView(SessionView):
12
15
 
13
16
  SESSION_KEY = SessionView.BROWSER_SESSION_KEY
14
17
 
15
- def get_group_filter(self, model):
18
+ def _get_rel_for_pks(self, group, page_mtime=False):
19
+ """Get the relation from the model to the pks."""
20
+ # XXX these TARGET refs might be better as subclass get rel methods.
21
+ target: str = self.TARGET # type: ignore
22
+ if target in ("metadata", "mtime") or page_mtime:
23
+ # metadata, mtime, browser.page_mtime
24
+ rel = "pk"
25
+ elif target in ("choices", "cover"):
26
+ rel = FILTER_ONLY_GROUP_RELATION[group]
27
+ else:
28
+ rel = GROUP_RELATION[group]
29
+
30
+ rel += "__in"
31
+ return rel
32
+
33
+ def get_group_filter(self, group=None, pks=None, page_mtime=False):
16
34
  """Get filter for the displayed group."""
17
- group = self.kwargs["group"] # type: ignore
18
- pks = self.kwargs["pks"] # type: ignore
19
- if pks: # type: ignore
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"
26
- else:
27
- rel = GROUP_RELATION[group]
28
-
29
- rel += "__in"
30
- group_filter_dict = {rel: pks} # type: ignore
35
+ if group is None:
36
+ group = self.kwargs["group"]
37
+ if pks is None:
38
+ pks = self.kwargs["pks"]
39
+
40
+ if pks and 0 not in pks:
41
+ rel = self._get_rel_for_pks(group, page_mtime)
42
+ group_filter_dict = {rel: pks}
31
43
  elif group == FOLDER_GROUP:
32
44
  group_filter_dict = {"parent_folder": None}
33
45
  else:
@@ -135,11 +135,10 @@ class SearchFilterView(ComicFieldFilterView):
135
135
  if comic_ids:
136
136
  # Prefilter comic ids. If nothing is allowed, don't search.
137
137
  sqs = sqs.filter_comic_ids(comic_ids)
138
- binary = (
138
+ if (
139
139
  self.TARGET in frozenset({"cover", "choices"})
140
- or self.params.get("order_by", "") != "search_score"
141
- )
142
- if binary:
140
+ or self.params.get("order_by", "") != "search_score" # type: ignore
141
+ ):
143
142
  scored_pks = self._get_binary_search_scores(sqs)
144
143
  else:
145
144
  score_pairs, prev_pks, next_pks, scored_pks = (