codex 1.4.0a0__py3-none-any.whl → 1.4.1__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 (279) hide show
  1. codex/config_default.yaml +12 -4
  2. codex/db_functions.py +4 -2
  3. codex/integrity.py +17 -6
  4. codex/librarian/covers/coverd.py +3 -11
  5. codex/librarian/covers/create.py +15 -21
  6. codex/librarian/covers/tasks.py +2 -16
  7. codex/librarian/importer/aggregate_metadata.py +75 -41
  8. codex/librarian/importer/clean_metadata.py +30 -7
  9. codex/librarian/importer/create_fks.py +154 -55
  10. codex/librarian/importer/deleted.py +11 -2
  11. codex/librarian/importer/failed_imports.py +44 -5
  12. codex/librarian/importer/importerd.py +37 -12
  13. codex/librarian/importer/link_comics.py +54 -31
  14. codex/librarian/importer/moved.py +55 -11
  15. codex/librarian/importer/query_fks.py +210 -48
  16. codex/librarian/importer/tasks.py +7 -7
  17. codex/librarian/janitor/cleanup.py +17 -5
  18. codex/librarian/librariand.py +10 -0
  19. codex/librarian/watchdog/events.py +11 -14
  20. codex/librarian/watchdog/observers.py +5 -1
  21. codex/logger/loggerd.py +7 -3
  22. codex/logger/logging.py +1 -1
  23. codex/migrations/0024_comic_gtin_comic_story_arc_number.py +24 -0
  24. codex/migrations/0025_add_story_arc_number.py +83 -0
  25. codex/models.py +21 -11
  26. codex/search/backend.py +1 -1
  27. codex/search/indexes.py +1 -1
  28. codex/serializers/browser.py +1 -0
  29. codex/serializers/metadata.py +5 -1
  30. codex/serializers/models.py +16 -1
  31. codex/serializers/opds/v1.py +1 -0
  32. codex/serializers/opds/v2.py +5 -2
  33. codex/serializers/reader.py +55 -16
  34. codex/settings/settings.py +1 -1
  35. codex/static_root/assets/admin-12749881.ef0f50bac290.js +41 -0
  36. codex/static_root/assets/admin-12749881.ef0f50bac290.js.br +0 -0
  37. codex/static_root/assets/admin-12749881.ef0f50bac290.js.gz +0 -0
  38. codex/static_root/assets/admin-12749881.js +41 -0
  39. codex/static_root/assets/admin-12749881.js.br +0 -0
  40. codex/static_root/assets/admin-12749881.js.gz +0 -0
  41. codex/static_root/assets/admin-beda768d.a614eee46307.css +1 -0
  42. codex/static_root/assets/admin-beda768d.a614eee46307.css.br +0 -0
  43. codex/static_root/assets/admin-beda768d.a614eee46307.css.gz +0 -0
  44. codex/static_root/assets/admin-beda768d.css +1 -0
  45. codex/static_root/assets/admin-beda768d.css.br +0 -0
  46. codex/static_root/assets/admin-beda768d.css.gz +0 -0
  47. codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css +1 -0
  48. codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css.br +0 -0
  49. codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css.gz +0 -0
  50. codex/static_root/assets/admin-drawer-panel-41c225cc.css +1 -0
  51. codex/static_root/assets/admin-drawer-panel-41c225cc.css.br +0 -0
  52. codex/static_root/assets/admin-drawer-panel-41c225cc.css.gz +0 -0
  53. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js +1 -0
  54. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.br +0 -0
  55. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.gz +0 -0
  56. codex/static_root/assets/admin-drawer-panel-522f1e6c.js +1 -0
  57. codex/static_root/assets/admin-drawer-panel-522f1e6c.js.br +0 -0
  58. codex/static_root/assets/admin-drawer-panel-522f1e6c.js.gz +0 -0
  59. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css +1 -0
  60. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.br +0 -0
  61. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.gz +0 -0
  62. codex/static_root/assets/browser-7f7d7134.css +1 -0
  63. codex/static_root/assets/browser-7f7d7134.css.br +0 -0
  64. codex/static_root/assets/browser-7f7d7134.css.gz +0 -0
  65. codex/static_root/assets/browser-af622672.d51aca96d64d.js +1 -0
  66. codex/static_root/assets/browser-af622672.d51aca96d64d.js.br +0 -0
  67. codex/static_root/assets/browser-af622672.d51aca96d64d.js.gz +0 -0
  68. codex/static_root/assets/browser-af622672.js +1 -0
  69. codex/static_root/assets/browser-af622672.js.br +0 -0
  70. codex/static_root/assets/browser-af622672.js.gz +0 -0
  71. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js +1 -0
  72. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.br +0 -0
  73. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.gz +0 -0
  74. codex/static_root/assets/http-error-5e17b794.js +1 -0
  75. codex/static_root/assets/http-error-5e17b794.js.br +0 -0
  76. codex/static_root/assets/http-error-5e17b794.js.gz +0 -0
  77. codex/static_root/assets/{main-a6ac9581.2fd9e52cbcc3.css → main-0898f4bb.181e0145c642.css} +1 -1
  78. codex/static_root/assets/main-0898f4bb.181e0145c642.css.br +0 -0
  79. codex/static_root/assets/{main-a6ac9581.2fd9e52cbcc3.css.gz → main-0898f4bb.181e0145c642.css.gz} +0 -0
  80. codex/static_root/assets/{main-a6ac9581.css → main-0898f4bb.css} +1 -1
  81. codex/static_root/assets/main-0898f4bb.css.br +0 -0
  82. codex/static_root/assets/{main-a6ac9581.css.gz → main-0898f4bb.css.gz} +0 -0
  83. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js +1 -0
  84. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.br +0 -0
  85. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.gz +0 -0
  86. codex/static_root/assets/main-9e76a4c3.js +1 -0
  87. codex/static_root/assets/main-9e76a4c3.js.br +0 -0
  88. codex/static_root/assets/main-9e76a4c3.js.gz +0 -0
  89. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js +1 -0
  90. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.br +0 -0
  91. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.gz +0 -0
  92. codex/static_root/assets/metadata-dialog-62c29ce0.js +1 -0
  93. codex/static_root/assets/metadata-dialog-62c29ce0.js.br +0 -0
  94. codex/static_root/assets/metadata-dialog-62c29ce0.js.gz +0 -0
  95. codex/static_root/assets/{metadata-dialog-785c4cfc.694a251cda37.css → metadata-dialog-cb306ffd.cc304996d7bb.css} +1 -1
  96. codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.br +0 -0
  97. codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.gz +0 -0
  98. codex/static_root/assets/{metadata-dialog-785c4cfc.css → metadata-dialog-cb306ffd.css} +1 -1
  99. codex/static_root/assets/metadata-dialog-cb306ffd.css.br +0 -0
  100. codex/static_root/assets/metadata-dialog-cb306ffd.css.gz +0 -0
  101. codex/static_root/assets/{page-pdf-abfd509d.3870dab8eaf4.js → page-pdf-157ba97e.613d7c2beb77.js} +61 -51
  102. codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.br +0 -0
  103. codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.gz +0 -0
  104. codex/static_root/assets/{page-pdf-abfd509d.js → page-pdf-157ba97e.js} +61 -51
  105. codex/static_root/assets/page-pdf-157ba97e.js.br +0 -0
  106. codex/static_root/assets/page-pdf-157ba97e.js.gz +0 -0
  107. codex/static_root/assets/reader-36266549.0b2cf1291f27.js +1 -0
  108. codex/static_root/assets/reader-36266549.0b2cf1291f27.js.br +0 -0
  109. codex/static_root/assets/reader-36266549.0b2cf1291f27.js.gz +0 -0
  110. codex/static_root/assets/reader-36266549.js +1 -0
  111. codex/static_root/assets/reader-36266549.js.br +0 -0
  112. codex/static_root/assets/reader-36266549.js.gz +0 -0
  113. codex/static_root/assets/reader-7f004141.506eecc6954b.css +1 -0
  114. codex/static_root/assets/reader-7f004141.506eecc6954b.css.br +0 -0
  115. codex/static_root/assets/reader-7f004141.506eecc6954b.css.gz +0 -0
  116. codex/static_root/assets/reader-7f004141.css +1 -0
  117. codex/static_root/assets/reader-7f004141.css.br +0 -0
  118. codex/static_root/assets/reader-7f004141.css.gz +0 -0
  119. codex/static_root/js/choices-admin.24cecf0a0568.json +1 -0
  120. codex/static_root/js/choices-admin.24cecf0a0568.json.br +0 -0
  121. codex/static_root/js/choices-admin.24cecf0a0568.json.gz +0 -0
  122. codex/static_root/js/choices-admin.json +1 -1
  123. codex/static_root/js/choices-admin.json.br +0 -0
  124. codex/static_root/js/choices-admin.json.gz +0 -0
  125. codex/static_root/js/choices.8c58714cf5b2.json +1 -0
  126. codex/static_root/js/choices.8c58714cf5b2.json.br +5 -0
  127. codex/static_root/js/choices.8c58714cf5b2.json.gz +0 -0
  128. codex/static_root/js/choices.json +1 -1
  129. codex/static_root/js/choices.json.br +0 -0
  130. codex/static_root/js/choices.json.gz +0 -0
  131. codex/static_root/{manifest.64a989215af8.json → manifest.d2f93a519ada.json} +34 -34
  132. codex/static_root/manifest.d2f93a519ada.json.br +0 -0
  133. codex/static_root/manifest.d2f93a519ada.json.gz +0 -0
  134. codex/static_root/manifest.json +34 -34
  135. codex/static_root/manifest.json.br +0 -0
  136. codex/static_root/manifest.json.gz +0 -0
  137. codex/static_root/staticfiles.json +1 -1
  138. codex/templates/headers-script-globals.html +1 -1
  139. codex/templates/{opds → opds_v1}/index.xml +3 -1
  140. codex/templates/{opds/opensearch.xml → opds_v1/opensearch_v1.xml} +1 -1
  141. codex/templates/search/indexes/codex/comic_text.txt +2 -2
  142. codex/urls/converters.py +1 -1
  143. codex/urls/opds/authentication.py +1 -1
  144. codex/urls/opds/root.py +8 -12
  145. codex/urls/opds/v1.py +12 -5
  146. codex/urls/opds/v2.py +2 -2
  147. codex/views/admin/tasks.py +6 -1
  148. codex/views/bookmark.py +2 -2
  149. codex/views/browser/base.py +23 -7
  150. codex/views/browser/browser.py +66 -56
  151. codex/views/browser/browser_annotations.py +159 -50
  152. codex/views/browser/browser_order_by.py +51 -105
  153. codex/views/browser/choices.py +75 -38
  154. codex/views/browser/filters/bookmark.py +6 -9
  155. codex/views/browser/filters/field.py +9 -6
  156. codex/views/browser/filters/group.py +12 -27
  157. codex/views/browser/filters/search.py +5 -10
  158. codex/views/browser/metadata.py +44 -19
  159. codex/views/download.py +1 -1
  160. codex/views/frontend.py +2 -3
  161. codex/views/mixins.py +15 -2
  162. codex/views/opds/const.py +8 -1
  163. codex/views/opds/util.py +37 -1
  164. codex/views/opds/v1/__init__.py +1 -1
  165. codex/views/opds/v1/data.py +21 -0
  166. codex/views/opds/v1/entry/__init__.py +1 -0
  167. codex/views/opds/v1/entry/data.py +23 -0
  168. codex/views/opds/v1/entry/entry.py +151 -0
  169. codex/views/opds/v1/entry/links.py +135 -0
  170. codex/views/opds/v1/facets.py +190 -0
  171. codex/views/opds/v1/feed.py +199 -0
  172. codex/views/opds/v1/links.py +198 -0
  173. codex/views/opds/{opensearch.py → v1/opensearch_v1.py} +3 -3
  174. codex/views/opds/v2/__init__.py +1 -1
  175. codex/views/opds/v2/const.py +10 -2
  176. codex/views/opds/v2/feed.py +82 -21
  177. codex/views/opds/v2/links.py +1 -1
  178. codex/views/opds/v2/publications.py +1 -1
  179. codex/views/opds/v2/top_links.py +1 -1
  180. codex/views/reader/page.py +6 -7
  181. codex/views/reader/reader.py +191 -61
  182. codex/views/session.py +2 -1
  183. {codex-1.4.0a0.dist-info → codex-1.4.1.dist-info}/METADATA +10 -41
  184. {codex-1.4.0a0.dist-info → codex-1.4.1.dist-info}/RECORD +187 -185
  185. codex/librarian/importer/db_ops.py +0 -248
  186. codex/pdf.py +0 -115
  187. codex/static_root/assets/admin-73d93dc7.2c3eb62e50a0.js +0 -48
  188. codex/static_root/assets/admin-73d93dc7.2c3eb62e50a0.js.br +0 -0
  189. codex/static_root/assets/admin-73d93dc7.2c3eb62e50a0.js.gz +0 -0
  190. codex/static_root/assets/admin-73d93dc7.js +0 -48
  191. codex/static_root/assets/admin-73d93dc7.js.br +0 -0
  192. codex/static_root/assets/admin-73d93dc7.js.gz +0 -0
  193. codex/static_root/assets/admin-79555229.5f2c4cb3a73c.css +0 -1
  194. codex/static_root/assets/admin-79555229.5f2c4cb3a73c.css.br +0 -0
  195. codex/static_root/assets/admin-79555229.5f2c4cb3a73c.css.gz +0 -0
  196. codex/static_root/assets/admin-79555229.css +0 -1
  197. codex/static_root/assets/admin-79555229.css.br +0 -0
  198. codex/static_root/assets/admin-79555229.css.gz +0 -0
  199. codex/static_root/assets/admin-drawer-panel-64bcc083.a85324c9ccd8.js +0 -1
  200. codex/static_root/assets/admin-drawer-panel-64bcc083.a85324c9ccd8.js.br +0 -0
  201. codex/static_root/assets/admin-drawer-panel-64bcc083.a85324c9ccd8.js.gz +0 -0
  202. codex/static_root/assets/admin-drawer-panel-64bcc083.js +0 -1
  203. codex/static_root/assets/admin-drawer-panel-64bcc083.js.br +0 -0
  204. codex/static_root/assets/admin-drawer-panel-64bcc083.js.gz +0 -0
  205. codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css +0 -1
  206. codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css.br +0 -2
  207. codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css.gz +0 -0
  208. codex/static_root/assets/admin-drawer-panel-cce8c0aa.css +0 -1
  209. codex/static_root/assets/admin-drawer-panel-cce8c0aa.css.br +0 -2
  210. codex/static_root/assets/admin-drawer-panel-cce8c0aa.css.gz +0 -0
  211. codex/static_root/assets/browser-7325db61.css +0 -1
  212. codex/static_root/assets/browser-7325db61.css.br +0 -0
  213. codex/static_root/assets/browser-7325db61.css.gz +0 -0
  214. codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css +0 -1
  215. codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css.br +0 -0
  216. codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css.gz +0 -0
  217. codex/static_root/assets/browser-d2caeed7.2262000a6d55.js +0 -1
  218. codex/static_root/assets/browser-d2caeed7.2262000a6d55.js.br +0 -0
  219. codex/static_root/assets/browser-d2caeed7.2262000a6d55.js.gz +0 -0
  220. codex/static_root/assets/browser-d2caeed7.js +0 -1
  221. codex/static_root/assets/browser-d2caeed7.js.br +0 -0
  222. codex/static_root/assets/browser-d2caeed7.js.gz +0 -0
  223. codex/static_root/assets/http-error-0221c37d.480d5066da92.js +0 -1
  224. codex/static_root/assets/http-error-0221c37d.480d5066da92.js.br +0 -0
  225. codex/static_root/assets/http-error-0221c37d.480d5066da92.js.gz +0 -0
  226. codex/static_root/assets/http-error-0221c37d.js +0 -1
  227. codex/static_root/assets/http-error-0221c37d.js.br +0 -0
  228. codex/static_root/assets/http-error-0221c37d.js.gz +0 -0
  229. codex/static_root/assets/main-a6ac9581.2fd9e52cbcc3.css.br +0 -0
  230. codex/static_root/assets/main-a6ac9581.css.br +0 -0
  231. codex/static_root/assets/main-e33dcfb0.a65044fc1a08.js +0 -1
  232. codex/static_root/assets/main-e33dcfb0.a65044fc1a08.js.br +0 -0
  233. codex/static_root/assets/main-e33dcfb0.a65044fc1a08.js.gz +0 -0
  234. codex/static_root/assets/main-e33dcfb0.js +0 -1
  235. codex/static_root/assets/main-e33dcfb0.js.br +0 -0
  236. codex/static_root/assets/main-e33dcfb0.js.gz +0 -0
  237. codex/static_root/assets/metadata-dialog-785c4cfc.694a251cda37.css.br +0 -0
  238. codex/static_root/assets/metadata-dialog-785c4cfc.694a251cda37.css.gz +0 -0
  239. codex/static_root/assets/metadata-dialog-785c4cfc.css.br +0 -0
  240. codex/static_root/assets/metadata-dialog-785c4cfc.css.gz +0 -0
  241. codex/static_root/assets/metadata-dialog-8b0e8aaa.d12b42b1c9da.js +0 -1
  242. codex/static_root/assets/metadata-dialog-8b0e8aaa.d12b42b1c9da.js.br +0 -0
  243. codex/static_root/assets/metadata-dialog-8b0e8aaa.d12b42b1c9da.js.gz +0 -0
  244. codex/static_root/assets/metadata-dialog-8b0e8aaa.js +0 -1
  245. codex/static_root/assets/metadata-dialog-8b0e8aaa.js.br +0 -0
  246. codex/static_root/assets/metadata-dialog-8b0e8aaa.js.gz +0 -0
  247. codex/static_root/assets/page-pdf-abfd509d.3870dab8eaf4.js.br +0 -0
  248. codex/static_root/assets/page-pdf-abfd509d.3870dab8eaf4.js.gz +0 -0
  249. codex/static_root/assets/page-pdf-abfd509d.js.br +0 -0
  250. codex/static_root/assets/page-pdf-abfd509d.js.gz +0 -0
  251. codex/static_root/assets/reader-a8b8f766.875abdd0d22e.css +0 -1
  252. codex/static_root/assets/reader-a8b8f766.875abdd0d22e.css.br +0 -0
  253. codex/static_root/assets/reader-a8b8f766.875abdd0d22e.css.gz +0 -0
  254. codex/static_root/assets/reader-a8b8f766.css +0 -1
  255. codex/static_root/assets/reader-a8b8f766.css.br +0 -0
  256. codex/static_root/assets/reader-a8b8f766.css.gz +0 -0
  257. codex/static_root/assets/reader-fe9345d2.759c31f82998.js +0 -1
  258. codex/static_root/assets/reader-fe9345d2.759c31f82998.js.br +0 -0
  259. codex/static_root/assets/reader-fe9345d2.759c31f82998.js.gz +0 -0
  260. codex/static_root/assets/reader-fe9345d2.js +0 -1
  261. codex/static_root/assets/reader-fe9345d2.js.br +0 -0
  262. codex/static_root/assets/reader-fe9345d2.js.gz +0 -0
  263. codex/static_root/js/choices-admin.3d958ea7f83b.json +0 -1
  264. codex/static_root/js/choices-admin.3d958ea7f83b.json.br +0 -0
  265. codex/static_root/js/choices-admin.3d958ea7f83b.json.gz +0 -0
  266. codex/static_root/js/choices.6bfc2a3d293f.json +0 -1
  267. codex/static_root/js/choices.6bfc2a3d293f.json.br +0 -0
  268. codex/static_root/js/choices.6bfc2a3d293f.json.gz +0 -0
  269. codex/static_root/manifest.64a989215af8.json.br +0 -0
  270. codex/static_root/manifest.64a989215af8.json.gz +0 -0
  271. codex/urls/opds/opensearch.py +0 -18
  272. codex/views/opds/v1/browser.py +0 -346
  273. codex/views/opds/v1/entry.py +0 -278
  274. codex/views/opds/v1/start.py +0 -28
  275. codex/views/opds/v1/util.py +0 -162
  276. codex/views/opds/v2/start.py +0 -28
  277. {codex-1.4.0a0.dist-info → codex-1.4.1.dist-info}/LICENSE +0 -0
  278. {codex-1.4.0a0.dist-info → codex-1.4.1.dist-info}/WHEEL +0 -0
  279. {codex-1.4.0a0.dist-info → codex-1.4.1.dist-info}/entry_points.txt +0 -0
@@ -20,6 +20,7 @@ from codex.models import (
20
20
  Library,
21
21
  Publisher,
22
22
  Series,
23
+ StoryArc,
23
24
  Timestamp,
24
25
  Volume,
25
26
  )
@@ -49,9 +50,11 @@ class BrowserView(BrowserAnnotationsView):
49
50
  Imprint: ("publisher",),
50
51
  Publisher: (None,),
51
52
  Folder: ("parent_folder",),
53
+ StoryArc: (None,),
52
54
  }
55
+ DEFAULT_ROUTE_NAME = "browser"
53
56
  _DEFAULT_ROUTE = {
54
- "name": "browser",
57
+ "name": DEFAULT_ROUTE_NAME,
55
58
  "params": deepcopy(DEFAULTS["route"]),
56
59
  }
57
60
  _OPDS_M2M_RELS = (
@@ -59,7 +62,8 @@ class BrowserView(BrowserAnnotationsView):
59
62
  "genres",
60
63
  "locations",
61
64
  "series_groups",
62
- "story_arcs",
65
+ "story_arc_numbers",
66
+ "story_arc_numbers__story_arc",
63
67
  "tags",
64
68
  "teams",
65
69
  "creators",
@@ -88,37 +92,16 @@ class BrowserView(BrowserAnnotationsView):
88
92
  return queryset.annotate(**group_names)
89
93
 
90
94
  def _add_annotations(self, queryset, model, search_scores):
91
- """Annotations for display and sorting.
92
-
93
- model is neccissary because this gets called twice by folder
94
- view. once for folders, once for the comics.
95
- """
96
- is_model_comic = model == Comic
97
- ##############################
98
- # Annotate Common Aggregates #
99
- ##############################
95
+ """Annotations for display and sorting."""
100
96
  queryset = self.annotate_common_aggregates(queryset, model, search_scores)
101
- if not is_model_comic:
102
- # EXTRA FILTER for empty group
103
- queryset = queryset.filter(child_count__gt=0)
104
97
 
105
- ##################
106
- # Annotate Group #
107
- ##################
98
+ # Annotate Group
108
99
  self.model_group = self._MODEL_GROUP_MAP[model]
109
100
  queryset = queryset.annotate(
110
101
  group=Value(self.model_group, CharField(max_length=1))
111
102
  )
112
-
113
- #######################
114
- # Sortable aggregates #
115
- #######################
116
- order_key = self.get_order_key()
117
- order_func = self.get_aggregate_func(order_key, model)
118
- queryset = queryset.annotate(order_value=order_func)
119
-
120
- queryset = self._annotate_group_names(queryset, model)
121
- return queryset
103
+ # Hoist Group Names
104
+ return self._annotate_group_names(queryset, model)
122
105
 
123
106
  def _get_model_group(self):
124
107
  """Get the group of the models to browse."""
@@ -127,7 +110,10 @@ class BrowserView(BrowserAnnotationsView):
127
110
  # the child of the current nav group or 'c'
128
111
  group = self.kwargs["group"]
129
112
  if group == self.FOLDER_GROUP:
130
- return self.FOLDER_GROUP
113
+ return group
114
+ if group == self.STORY_ARC_GROUP:
115
+ pk = self.kwargs["pk"]
116
+ return self.COMIC_GROUP if pk else group
131
117
  if group == self.valid_nav_groups[-1]:
132
118
  # special case for lowest valid group
133
119
  return self.COMIC_GROUP
@@ -141,35 +127,36 @@ class BrowserView(BrowserAnnotationsView):
141
127
  model_group = self._get_model_group()
142
128
  self.model = self.GROUP_MODEL_MAP[model_group]
143
129
 
144
- def _get_queryset(self, object_filter, search_scores):
145
- """Create queryset."""
146
- # GROUP QS
147
- if self.model != Comic:
148
- group_filter = object_filter
149
- group_qs = self.model.objects.filter(group_filter)
150
- group_qs = self._add_annotations(group_qs, self.model, search_scores)
151
- group_qs = self.get_order_by(self.model, group_qs)
130
+ def _get_group_queryset(self, object_filter, search_scores):
131
+ """Create group queryset."""
132
+ if self.model == Comic:
133
+ qs = self.model.objects.none()
152
134
  else:
153
- group_qs = self.model.objects.none()
135
+ qs = self.model.objects.filter(object_filter)
136
+ qs = self._add_annotations(qs, self.model, search_scores)
137
+ qs = self.add_order_by(qs, self.model)
138
+ return qs
154
139
 
155
- # BOOK QS
140
+ def _get_book_queryset(self, object_filter, search_scores):
141
+ """Create book queryset."""
156
142
  group = self.kwargs.get("group")
157
143
  if self.model == Comic or group == self.FOLDER_GROUP:
158
144
  if group == self.FOLDER_GROUP:
159
145
  comic_object_filter, comic_search_scores = self.get_query_filters(
160
- True, False
146
+ self.model, False
161
147
  )
162
148
  else:
163
149
  comic_object_filter = object_filter
164
150
  comic_search_scores = search_scores
165
151
 
166
- book_qs = Comic.objects.filter(comic_object_filter)
167
- book_qs = self._add_annotations(book_qs, Comic, comic_search_scores)
168
- book_qs = self.get_order_by(Comic, book_qs)
152
+ qs = Comic.objects.filter(comic_object_filter)
153
+ qs = self._add_annotations(qs, Comic, comic_search_scores)
154
+ qs = self.add_order_by(qs, Comic)
155
+ if limit := self.params.get("limit"):
156
+ qs = qs[:limit]
169
157
  else:
170
- book_qs = Comic.objects.none()
171
-
172
- return group_qs, book_qs
158
+ qs = Comic.objects.none()
159
+ return qs
173
160
 
174
161
  def _get_folder_up_route(self):
175
162
  """Get out parent's pk."""
@@ -186,11 +173,17 @@ class BrowserView(BrowserAnnotationsView):
186
173
 
187
174
  return up_group, up_pk
188
175
 
176
+ def _get_story_arc_up_route(self):
177
+ """Get one level hierarchy."""
178
+ up_group = self.STORY_ARC_GROUP
179
+ up_group = 0 if self.group_instance else None
180
+ return self.STORY_ARC_GROUP, up_group
181
+
189
182
  def _set_group_instance(self):
190
183
  """Create group_class instance."""
191
184
  pk = self.kwargs.get("pk")
192
185
  self.group_instance: Optional[
193
- Union[Folder, Publisher, Imprint, Series, Volume]
186
+ Union[Folder, Publisher, Imprint, Series, Volume, StoryArc]
194
187
  ] = None
195
188
  if not pk:
196
189
  return
@@ -322,7 +315,7 @@ class BrowserView(BrowserAnnotationsView):
322
315
  )
323
316
 
324
317
  if book_qs.count():
325
- book_max_obj_per_page = self.MAX_OBJ_PER_PAGE - group_qs.count()
318
+ book_max_obj_per_page = max(0, self.MAX_OBJ_PER_PAGE - group_qs.count())
326
319
  page_counts = (num_pages, total_count, Comic)
327
320
  group_qs, num_pages, total_count = self._paginate_section(
328
321
  group_qs, book_max_obj_per_page, page_counts
@@ -333,11 +326,11 @@ class BrowserView(BrowserAnnotationsView):
333
326
  def get_object(self):
334
327
  """Validate settings and get the querysets."""
335
328
  self._set_browse_model()
329
+ self.set_rel_prefix(self.model)
336
330
  self._set_group_instance() # Placed up here to invalidate earlier
337
331
  # Create the main query with the filters
338
- is_model_comic = self.model == Comic
339
332
  try:
340
- object_filter, search_scores = self.get_query_filters(is_model_comic, False)
333
+ object_filter, search_scores = self.get_query_filters(self.model, False)
341
334
  except Folder.DoesNotExist:
342
335
  pk = self.kwargs.get("pk")
343
336
  self._raise_redirect(
@@ -348,7 +341,8 @@ class BrowserView(BrowserAnnotationsView):
348
341
  search_scores = {}
349
342
  group = self.kwargs.get("group")
350
343
 
351
- group_qs, book_qs = self._get_queryset(object_filter, search_scores)
344
+ group_qs = self._get_group_queryset(object_filter, search_scores)
345
+ book_qs = self._get_book_queryset(object_filter, search_scores)
352
346
 
353
347
  # Paginate
354
348
  group_qs, book_qs, num_pages, total_count = self._paginate(group_qs, book_qs)
@@ -356,6 +350,8 @@ class BrowserView(BrowserAnnotationsView):
356
350
  # get additional context
357
351
  if group == self.FOLDER_GROUP:
358
352
  up_group, up_pk = self._get_folder_up_route()
353
+ elif group == self.STORY_ARC_GROUP:
354
+ up_group, up_pk = self._get_story_arc_up_route()
359
355
  else:
360
356
  up_group, up_pk = self._get_browse_up_route()
361
357
  browser_page_title = self._get_browser_page_title()
@@ -459,9 +455,7 @@ class BrowserView(BrowserAnnotationsView):
459
455
 
460
456
  top_group = self.params["top_group"]
461
457
  if top_group != self.FOLDER_GROUP:
462
- reason = f"top_group {top_group} doesn't match route {self.FOLDER_GROUP}"
463
- settings_mask = {"top_group": self.FOLDER_GROUP}
464
- self._raise_redirect({"group": self.FOLDER_GROUP}, reason, settings_mask)
458
+ self.params["top_group"] = self.FOLDER_GROUP
465
459
 
466
460
  # set valid folder nav groups
467
461
  self.valid_nav_groups = (self.FOLDER_GROUP,)
@@ -500,12 +494,25 @@ class BrowserView(BrowserAnnotationsView):
500
494
  reason = f"Redirect r with {pk=} to pk 0"
501
495
  self._raise_redirect({"pk": 0}, reason)
502
496
 
497
+ def _validate_story_arc_settings(self):
498
+ """Validate story arc settings."""
499
+ top_group = self.params["top_group"]
500
+ if top_group != self.STORY_ARC_GROUP:
501
+ self.params["top_group"] = self.STORY_ARC_GROUP
502
+
503
+ def _set_route_param(self):
504
+ """Set the route param."""
505
+ group = self.kwargs.get("group", "r")
506
+ pk = self.kwargs.get("pk", 0)
507
+ page = self.kwargs.get("page", 1)
508
+ self.params["route"] = {"group": group, "pk": pk, "page": page}
509
+
503
510
  def validate_settings(self):
504
511
  """Validate group and top group settings."""
505
512
  group = self.kwargs.get("group")
506
- order_key = self.get_order_key()
513
+ self.set_order_key()
507
514
  enable_folder_view = False
508
- if group == self.FOLDER_GROUP or order_key == "path":
515
+ if group == self.FOLDER_GROUP or self.order_key == "path":
509
516
  key = AdminFlag.FlagChoices.FOLDER_VIEW.value
510
517
  try:
511
518
  enable_folder_view = AdminFlag.objects.only("on").get(key=key).on
@@ -515,11 +522,13 @@ class BrowserView(BrowserAnnotationsView):
515
522
 
516
523
  if group == self.FOLDER_GROUP:
517
524
  self._validate_folder_settings(enable_folder_view)
525
+ elif group == self.STORY_ARC_GROUP:
526
+ self._validate_story_arc_settings()
518
527
  else:
519
528
  self._validate_browser_group_settings()
520
529
 
521
530
  # Validate path sort
522
- if order_key == "path" and not enable_folder_view:
531
+ if self.order_key == "path" and not enable_folder_view:
523
532
  pk = self.kwargs("pk")
524
533
  page = self.kwargs("page")
525
534
  route_changes = {"group": group, "pk": pk, "page": page}
@@ -532,6 +541,7 @@ class BrowserView(BrowserAnnotationsView):
532
541
  """Get browser settings."""
533
542
  self.parse_params()
534
543
  self.validate_settings()
544
+ self._set_route_param()
535
545
  data = self.get_object()
536
546
  serializer = self.get_serializer(data)
537
547
  self.save_params_to_session(self.params)
@@ -1,10 +1,15 @@
1
1
  """Base view for metadata annotations."""
2
+ from os.path import sep
3
+
2
4
  from django.db.models import (
3
5
  BooleanField,
4
6
  Case,
7
+ CharField,
5
8
  Count,
6
9
  DateTimeField,
7
10
  F,
11
+ FilteredRelation,
12
+ Min,
8
13
  OuterRef,
9
14
  Q,
10
15
  Subquery,
@@ -13,10 +18,9 @@ from django.db.models import (
13
18
  When,
14
19
  )
15
20
  from django.db.models.fields import PositiveSmallIntegerField
16
- from django.db.models.functions import Least
21
+ from django.db.models.functions import Least, Lower, Reverse, Right, StrIndex, Substr
17
22
 
18
- from codex.models import Comic, Folder, Imprint, Publisher, Series, Volume
19
- from codex.views.browser.base import BrowserBaseView
23
+ from codex.models import Comic
20
24
  from codex.views.browser.browser_order_by import BrowserOrderByView
21
25
 
22
26
 
@@ -24,31 +28,36 @@ class BrowserAnnotationsView(BrowserOrderByView):
24
28
  """Base class for views that need special metadata annotations."""
25
29
 
26
30
  _ONE_INTEGERFIELD = Value(1, PositiveSmallIntegerField())
27
- # used by browser & metadata
28
- GROUP_MODEL_MAP = {
29
- BrowserBaseView.ROOT_GROUP: None,
30
- "p": Publisher,
31
- "i": Imprint,
32
- "s": Series,
33
- "v": Volume,
34
- BrowserBaseView.COMIC_GROUP: Comic,
35
- BrowserBaseView.FOLDER_GROUP: Folder,
36
- }
31
+ _NONE_INTEGERFIELD = Value(None, PositiveSmallIntegerField())
37
32
  _NONE_DATETIMEFIELD = Value(None, DateTimeField())
33
+ _ARTICLES = frozenset(
34
+ ("a", "an", "the") # en # noqa RUF005
35
+ + ("un", "unos", "unas", "el", "los", "la", "las") # es
36
+ + ("un", "une", "le", "les", "la", "les", "l'") # fr
37
+ + ("o", "a", "os") # pt
38
+ # pt "as" conflicts with English
39
+ + ("der", "dem", "des", "das") # de
40
+ # de: "den & die conflict with English
41
+ + ("il", "lo", "gli", "la", "le", "l'") # it
42
+ # it: "i" conflicts with English
43
+ + ("de", "het", "een") # nl
44
+ + ("en", "ett") # sw
45
+ + ("en", "ei", "et") # no
46
+ + ("en", "et") # da
47
+ + ("el", "la", "els", "les", "un", "una", "uns", "unes", "na") # ct
48
+ )
38
49
 
39
50
  is_opds_1_acquisition = False
40
51
 
41
- def _annotate_search_score(self, queryset, is_model_comic, search_scores):
52
+ def _annotate_search_score(self, queryset, search_scores):
42
53
  """Annotate the search score for ordering by search score."""
43
- order_key = self.params.get("order_by")
44
- if order_key != "search_score":
54
+ if self.order_key != "search_score":
45
55
  return queryset
46
- prefix = "comic__" if not is_model_comic else ""
47
56
  whens = []
48
57
  for pk, score in search_scores.items():
49
- when = {prefix + "pk": pk, "then": score}
58
+ when = {self.rel_prefix + "pk": pk, "then": score}
50
59
  whens.append(When(**when))
51
- annotate = {prefix + "search_score": Case(*whens, default=0.0)}
60
+ annotate = {self.rel_prefix + "search_score": Case(*whens, default=0.0)}
52
61
  return queryset.annotate(**annotate)
53
62
 
54
63
  def _annotate_cover_pk(self, queryset, model):
@@ -60,28 +69,134 @@ class BrowserAnnotationsView(BrowserOrderByView):
60
69
  else:
61
70
  # This creates two subqueries. It would be better condensed into one.
62
71
  # but there's no way to annotate an object or multiple values.
63
- qs = queryset.filter(pk=OuterRef("pk"))
64
- cover_comics = self.get_order_by(Comic, qs, for_cover_pk=True)
65
- cover_pk = Subquery(cover_comics.values("comic__pk")[:1])
72
+ cover_qs = queryset.filter(pk=OuterRef("pk"))
73
+ cover_qs = self.add_order_by(cover_qs, model)
74
+ cover_pk = Subquery(cover_qs.values(self.rel_prefix + "pk")[:1])
66
75
  return queryset.annotate(cover_pk=cover_pk)
67
76
 
68
- def _annotate_page_count(self, qs):
77
+ def _annotate_page_count(self, qs, model):
69
78
  """Hoist up total page_count of children."""
70
79
  # Used for sorting and progress
71
- page_count_sum = Sum("comic__page_count", distinct=True)
80
+ if model == Comic:
81
+ return qs
82
+
83
+ page_count_sum = Sum(self.rel_prefix + "page_count", distinct=True)
72
84
  return qs.annotate(page_count=page_count_sum)
73
85
 
74
- def _annotate_bookmarks(self, qs, is_model_comic):
75
- """Hoist up bookmark annoations."""
76
- bm_rel = self.get_bm_rel(is_model_comic)
77
- bm_filter = self._get_my_bookmark_filter(bm_rel)
86
+ def _annotate_child_count(self, qs, model):
87
+ """Annotate Child Count."""
88
+ self.kwargs.get("group")
89
+ if model == Comic:
90
+ child_count_sum = self._ONE_INTEGERFIELD
91
+ else:
92
+ child_count_sum = Count(self.rel_prefix + "pk", distinct=True)
93
+ qs = qs.annotate(child_count=child_count_sum)
94
+ if model != Comic:
95
+ # XXX Extra filter for empty groups
96
+ qs = qs.filter(child_count__gt=0)
97
+ return qs
98
+
99
+ def _annotate_bookmark_updated_at(self, qs, bm_rel, bm_filter):
100
+ """Annotate bookmark_updated_at."""
101
+ if not self.is_opds_1_acquisition and self.order_key != "bookmark_updated_at":
102
+ return qs
103
+
104
+ updated_at_rel = f"{bm_rel}__updated_at"
105
+ bookmark_updated_at_aggregate = Min(
106
+ updated_at_rel,
107
+ default=self._NONE_DATETIMEFIELD,
108
+ filter=bm_filter,
109
+ )
110
+ return qs.annotate(bookmark_updated_at=bookmark_updated_at_aggregate)
111
+
112
+ def _annotate_sort_name(self, queryset, model):
113
+ """Sort groups by name ignoring articles."""
114
+ if self.order_key != "sort_name":
115
+ return queryset
116
+
117
+ if self.kwargs.get("group") == self.FOLDER_GROUP and model == Comic:
118
+ # File View Filename
119
+ queryset = queryset.annotate(
120
+ sort_name=Right(
121
+ "path",
122
+ StrIndex(Reverse(F("path")), Value(sep)) - 1, # type: ignore
123
+ output_field=CharField(),
124
+ )
125
+ )
126
+ return queryset
127
+
128
+ ##################################################
129
+ # Otherwise Remove articles from the browse name #
130
+ ##################################################
131
+
132
+ # first_space_index
133
+ first_field = model.ORDERING[0]
134
+ queryset = queryset.annotate(
135
+ first_space_index=StrIndex(first_field, Value(" "))
136
+ )
137
+
138
+ # lowercase_first_word
139
+ lowercase_first_word = Lower(
140
+ Substr(first_field, 1, length=(F("first_space_index") - 1)) # type: ignore
141
+ )
142
+ queryset = queryset.annotate(
143
+ lowercase_first_word=Case(
144
+ When(Q(first_space_index__gt=0), then=lowercase_first_word)
145
+ ),
146
+ default=Value(""),
147
+ )
78
148
 
149
+ # sort_name
150
+ return queryset.annotate(
151
+ sort_name=Case(
152
+ When(
153
+ lowercase_first_word__in=self._ARTICLES,
154
+ then=Substr(
155
+ first_field, F("first_space_index") + 1 # type: ignore
156
+ ),
157
+ ),
158
+ default=first_field,
159
+ )
160
+ )
161
+
162
+ def _annotate_story_arc_number(self, qs):
163
+ if self.order_key != "story_arc_number":
164
+ return qs
165
+
166
+ # Get story_arc__pk
167
+ group = self.kwargs["group"]
168
+ pk = self.kwargs["pk"]
169
+ if group == self.STORY_ARC_GROUP and pk:
170
+ story_arc_pk = pk
171
+ elif story_arc_pks := self.params.get("filters", {}).get("story_arcs", []):
172
+ story_arc_pk = story_arc_pks[0]
173
+ else:
174
+ story_arc_pk = None
175
+
176
+ # If we have one annotate it.
177
+ if story_arc_pk:
178
+ san_rel = self.rel_prefix + "story_arc_numbers"
179
+ rel = f"{san_rel}"
180
+ condition = Q(**{f"{san_rel}__story_arc": story_arc_pk})
181
+ qs = qs.annotate(
182
+ selected_story_arc_number=FilteredRelation(rel, condition=condition),
183
+ story_arc_number=F("selected_story_arc_number__number"),
184
+ )
185
+ else:
186
+ qs = qs.annotate(story_arc_number=self._NONE_INTEGERFIELD)
187
+ return qs
188
+
189
+ def _annotate_order_value(self, qs, model):
190
+ """Annotate a main key for sorting."""
191
+ order_func = self.get_order_value(model)
192
+ return qs.annotate(order_value=order_func)
193
+
194
+ def _annotate_bookmarks(self, qs, model, bm_rel, bm_filter):
195
+ """Hoist up bookmark annoations."""
79
196
  page_rel = f"{bm_rel}__page"
80
197
  finished_rel = f"{bm_rel}__finished"
81
- updated_at_rel = f"{bm_rel}__updated_at"
82
- bookmark_updated_at = None
83
198
 
84
- if is_model_comic:
199
+ if model == Comic:
85
200
  # Hoist up the bookmark and finished states
86
201
  bookmark_page = Sum(
87
202
  page_rel,
@@ -95,14 +210,12 @@ class BrowserAnnotationsView(BrowserOrderByView):
95
210
  filter=bm_filter,
96
211
  output_field=BooleanField(),
97
212
  )
98
- if self.is_opds_1_acquisition:
99
- bookmark_updated_at = F(updated_at_rel)
100
213
  else:
101
214
  # Aggregate bookmark and finished states
102
215
  bookmark_page = Sum(
103
216
  Case(
104
217
  When(**{bm_rel: None}, then=0),
105
- When(**{finished_rel: True}, then="comic__page_count"),
218
+ When(**{finished_rel: True}, then=f"{self.rel_prefix}page_count"),
106
219
  default=page_rel,
107
220
  output_field=PositiveSmallIntegerField(),
108
221
  ),
@@ -130,16 +243,10 @@ class BrowserAnnotationsView(BrowserOrderByView):
130
243
  output_field=BooleanField(),
131
244
  )
132
245
 
133
- qs = qs.annotate(
246
+ return qs.annotate(
134
247
  page=bookmark_page,
135
248
  finished=finished_aggregate,
136
249
  )
137
- if bookmark_updated_at:
138
- qs = qs.annotate(
139
- bookmark_updated_at=bookmark_updated_at,
140
- )
141
-
142
- return qs
143
250
 
144
251
  @staticmethod
145
252
  def _annotate_progress(queryset):
@@ -154,15 +261,17 @@ class BrowserAnnotationsView(BrowserOrderByView):
154
261
 
155
262
  def annotate_common_aggregates(self, qs, model, search_scores):
156
263
  """Annotate common aggregates between browser and metadata."""
157
- is_model_comic = model == Comic
158
- qs = self._annotate_search_score(qs, is_model_comic, search_scores)
264
+ qs = self._annotate_search_score(qs, search_scores)
265
+ qs = self._annotate_child_count(qs, model)
266
+ qs = self._annotate_page_count(qs, model)
267
+ bm_rel = self.get_bm_rel(model)
268
+ bm_filter = self._get_my_bookmark_filter(bm_rel)
269
+ qs = self._annotate_bookmark_updated_at(qs, bm_rel, bm_filter)
270
+ qs = self._annotate_sort_name(qs, model)
271
+ qs = self._annotate_story_arc_number(qs)
272
+ qs = self._annotate_order_value(qs, model)
273
+ # cover depends on the above annotations for order-by
159
274
  qs = self._annotate_cover_pk(qs, model)
160
- if is_model_comic:
161
- child_count_sum = self._ONE_INTEGERFIELD
162
- else:
163
- qs = self._annotate_page_count(qs)
164
- child_count_sum = Count("comic__pk", distinct=True)
165
- qs = qs.annotate(child_count=child_count_sum)
166
- qs = self._annotate_bookmarks(qs, is_model_comic)
275
+ qs = self._annotate_bookmarks(qs, model, bm_rel, bm_filter)
167
276
  qs = self._annotate_progress(qs)
168
277
  return qs