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
@@ -1,10 +1,10 @@
1
1
  """Base view for ordering the query."""
2
2
  from os import sep
3
3
 
4
- from django.db.models import Avg, Case, CharField, F, Max, Min, Q, Sum, Value, When
5
- from django.db.models.functions import Lower, Reverse, Right, StrIndex, Substr
4
+ from django.db.models import Avg, F, Max, Min, Sum, Value
5
+ from django.db.models.functions import Reverse, Right, StrIndex
6
6
 
7
- from codex.models import Comic, Folder
7
+ from codex.models import Comic, Folder, StoryArc
8
8
  from codex.views.browser.base import BrowserBaseView
9
9
 
10
10
 
@@ -22,33 +22,18 @@ class BrowserOrderByView(BrowserBaseView):
22
22
  "size": Sum,
23
23
  "updated_at": Min,
24
24
  "search_score": Min,
25
+ "story_arc_number": Min,
25
26
  }
26
27
  _SEP_VALUE = Value(sep)
27
- _ARTICLES = frozenset(
28
- ("a", "an", "the") # en
29
- + ("un", "unos", "unas", "el", "los", "la", "las") # es
30
- + ("un", "une", "le", "les", "la", "les", "l'") # fr
31
- + ("o", "a", "os") # pt
32
- # pt "as" conflicts with English
33
- + ("der", "dem", "des", "das") # de
34
- # de: "den & die conflict with English
35
- + ("il", "lo", "gli", "la", "le", "l'") # it
36
- # it: "i" conflicts with English
37
- + ("de", "het", "een") # nl
38
- + ("en", "ett") # sw
39
- + ("en", "ei", "et") # no
40
- + ("en", "et") # da
41
- + ("el", "la", "els", "les", "un", "una", "uns", "unes", "na") # ct
42
- )
43
- NONE_CHARFIELD = Value(None, CharField())
28
+ _ANNOTATED_ORDER_FIELDS = frozenset(("sort_name", "bookmark_updated_at"))
44
29
 
45
- def get_order_key(self):
30
+ def set_order_key(self):
46
31
  """Get the default order key for the view."""
47
32
  order_key = self.params.get("order_by")
48
33
  if not order_key:
49
34
  group = self.kwargs.get("group")
50
35
  order_key = "path" if group == self.FOLDER_GROUP else "sort_name"
51
- return order_key
36
+ self.order_key = order_key
52
37
 
53
38
  @classmethod
54
39
  def _get_path_query_func(cls, field):
@@ -57,95 +42,56 @@ class BrowserOrderByView(BrowserBaseView):
57
42
  field, StrIndex(Reverse(field), cls._SEP_VALUE) - 1 # type: ignore
58
43
  )
59
44
 
60
- def get_aggregate_func(self, order_key, model):
61
- """Get a complete function for aggregating an attribute."""
62
- field = None if order_key == "sort_name" or not order_key else order_key
45
+ def get_aggregate_func(self, model, field):
46
+ """Order by aggregate."""
47
+ # get agg_func
48
+ agg_func = self._ORDER_AGGREGATE_FUNCS[field]
49
+ if agg_func == Min and self.params.get("order_reverse"):
50
+ agg_func = Max
51
+
52
+ # get full_field
53
+ self.kwargs.get("group")
54
+ if model == StoryArc and field == "story_arc_number":
55
+ full_field = "storyarcnumber__number"
56
+ else:
57
+ if self.order_key == "story_arc_number":
58
+ field = "story_arc_numbers__number"
59
+ full_field = self.rel_prefix + field
60
+ if field == "path":
61
+ full_field = self._get_path_query_func(full_field)
62
+
63
+ return agg_func(full_field)
63
64
 
65
+ def get_order_value(self, model):
66
+ """Get a complete function for aggregating an attribute."""
64
67
  # Determine order func
65
- if not field:
66
- # use default sorting.
67
- func = self.NONE_CHARFIELD
68
- elif field == "path" and model in (Comic, Folder):
68
+ if self.order_key == "path" and model in (Comic, Folder):
69
69
  # special path sorting.
70
- func = self._get_path_query_func(field)
71
- elif model == Comic:
70
+ func = self._get_path_query_func(self.order_key)
71
+ elif model == Comic or self.order_key in self._ANNOTATED_ORDER_FIELDS:
72
72
  # agg_none uses group fields not comic fields.
73
- func = F(field)
73
+ func = F(self.order_key)
74
74
  else:
75
- # order by aggregate.
76
-
77
- # get agg_func
78
- agg_func = self._ORDER_AGGREGATE_FUNCS[field]
79
- if agg_func == Min and self.params.get("order_reverse"):
80
- agg_func = Max
81
-
82
- # get full_field
83
- full_field = "comic__" + field
84
- if field == "path":
85
- full_field = self._get_path_query_func(full_field)
86
-
87
- func = agg_func(full_field)
75
+ func = self.get_aggregate_func(model, self.order_key)
88
76
  return func
89
77
 
90
- @classmethod
91
- def _order_without_articles(cls, queryset, ordering):
92
- """Sort by name ignoring articles."""
93
- first_field = ordering[0]
94
- queryset = queryset.annotate(
95
- first_space_index=StrIndex(first_field, Value(" "))
96
- )
97
-
98
- lowercase_first_word = Lower(
99
- Substr(first_field, 1, length=(F("first_space_index") - 1)) # type: ignore
100
- )
101
- queryset = queryset.annotate(
102
- lowercase_first_word=Case(
103
- When(Q(first_space_index__gt=0), then=lowercase_first_word)
104
- ),
105
- default=Value(""),
106
- )
107
-
108
- queryset = queryset.annotate(
109
- sort_name=Case(
110
- When(
111
- lowercase_first_word__in=cls._ARTICLES,
112
- then=Substr(
113
- first_field, F("first_space_index") + 1 # type: ignore
114
- ),
115
- ),
116
- default=first_field,
117
- )
118
- )
119
- ordering = ("sort_name", *ordering[1:])
120
- return queryset, ordering
121
-
122
- def get_order_by(self, model, queryset, for_cover_pk=False):
123
- """Create the order_by list.
124
-
125
- Order on pk to give duplicates a consistent position.
126
- """
127
- # order_prefix
128
- prefix = "-" if self.params.get("order_reverse") else ""
129
-
130
- # order_fields
131
- order_key = self.get_order_key()
132
- if for_cover_pk:
133
- prefix += "comic__"
134
- ordering = []
135
- if order_key and order_key != "sort_name":
136
- ordering += [order_key]
137
- ordering += [*Comic.ORDERING]
138
- elif order_key == "sort_name" or not order_key:
139
- ordering = model.ORDERING
140
- group = self.kwargs.get("group")
141
- if group != self.FOLDER_GROUP:
142
- # Can't annotate after union
143
- queryset, ordering = self._order_without_articles(queryset, ordering)
78
+ def add_order_by(self, queryset, model):
79
+ """Create the order_by list."""
80
+ prefix = ""
81
+ if self.params.get("order_reverse"):
82
+ prefix += "-"
83
+
84
+ if self.order_key == "sort_name":
85
+ order_fields = ("order_value", *model.ORDERING[1:])
86
+ elif self.order_key == "bookmark_updated_at":
87
+ order_fields = ("order_value", "updated_at", "created_at", "pk")
88
+ elif self.order_key == "story_arc_number" and model == Comic:
89
+ order_fields = ("order_value", "date", *model.ORDERING)
144
90
  else:
145
- # Use annotated order_value
146
- ordering = ("order_value", *model.ordering)
91
+ order_fields = ("order_value", *model.ORDERING)
92
+
93
+ order_by = []
94
+ for field in order_fields:
95
+ order_by.append(prefix + field)
147
96
 
148
- # order_by
149
- # add prefixes to all order_by fields
150
- ordering = (prefix + field for field in ordering)
151
- return queryset.order_by(*ordering)
97
+ return queryset.order_by(*order_by)
@@ -1,11 +1,21 @@
1
1
  """View for marking comics read and unread."""
2
2
  import pycountry
3
3
  from caseconverter import snakecase
4
+ from django.db.models import QuerySet
4
5
  from drf_spectacular.utils import extend_schema
5
6
  from rest_framework.response import Response
6
7
 
7
8
  from codex.logger.logging import get_logger
8
- from codex.models import Comic, CreatorPerson
9
+ from codex.models import (
10
+ Comic,
11
+ CreatorPerson,
12
+ Folder,
13
+ Imprint,
14
+ Publisher,
15
+ Series,
16
+ StoryArc,
17
+ Volume,
18
+ )
9
19
  from codex.serializers.browser import (
10
20
  BrowserChoicesSerializer,
11
21
  BrowserFilterChoicesSerializer,
@@ -22,23 +32,37 @@ class BrowserChoicesViewBase(BrowserBaseView):
22
32
 
23
33
  permission_classes = [IsAuthenticatedOrEnabledNonUsers]
24
34
 
25
- CREATORS_PERSON_REL = "creators__person"
26
- NULL_NAMED_ROW = {"pk": -1, "name": "_none_"}
35
+ _CREATORS_PERSON_REL = "creators__person"
36
+ _STORY_ARC_REL = "story_arc_numbers__story_arc"
37
+ _NULL_NAMED_ROW = {"pk": -1, "name": "_none_"}
38
+ _BACK_REL_MAP = {CreatorPerson: "creator__", StoryArc: "storyarcnumber__"}
39
+ _REL_MAP = {
40
+ Publisher: "publisher",
41
+ Imprint: "imprint",
42
+ Series: "series",
43
+ Volume: "volume",
44
+ Comic: "pk",
45
+ Folder: "parent_folder",
46
+ StoryArc: "story_arc_numbers__story_arc",
47
+ }
27
48
 
28
49
  @staticmethod
29
- def get_field_choices_query(field_name, comic_qs):
50
+ def get_field_choices_query(comic_qs, field_name):
30
51
  """Get distinct values for the field."""
31
- return comic_qs.values_list(field_name, flat=True).distinct()
52
+ return (
53
+ comic_qs.exclude(**{field_name: None})
54
+ .values_list(field_name, flat=True)
55
+ .distinct()
56
+ )
32
57
 
33
- @classmethod
34
- def get_m2m_field_query(cls, rel, comic_qs, model):
58
+ def get_m2m_field_query(self, model, comic_qs: QuerySet):
35
59
  """Get distinct m2m value objects for the relation."""
36
- comic_rel = "creator__comic" if rel == cls.CREATORS_PERSON_REL else "comic"
60
+ back_rel = self._BACK_REL_MAP.get(model, "")
61
+ back_rel += "comic__"
62
+ back_rel += self._REL_MAP[self.model]
63
+ back_rel += "__in"
37
64
  return (
38
- model.objects.filter(**{f"{comic_rel}__in": comic_qs})
39
- .prefetch_related(comic_rel)
40
- .values("pk", "name")
41
- .distinct()
65
+ model.objects.filter(**{back_rel: comic_qs}).values("pk", "name").distinct()
42
66
  )
43
67
 
44
68
  @staticmethod
@@ -46,12 +70,14 @@ class BrowserChoicesViewBase(BrowserBaseView):
46
70
  """Get if null values exists for an m2m field."""
47
71
  return comic_qs.filter(**{f"{rel}__isnull": True}).exists()
48
72
 
49
- @classmethod
50
- def _get_rel_and_model(cls, field_name):
73
+ def _get_rel_and_model(self, field_name):
51
74
  """Return the relation and model for the field name."""
52
- if field_name == cls.CREATOR_PERSON_UI_FIELD:
53
- rel = cls.CREATORS_PERSON_REL
75
+ if field_name == self.CREATOR_PERSON_UI_FIELD:
76
+ rel = self._CREATORS_PERSON_REL
54
77
  model = CreatorPerson
78
+ elif field_name == self.STORY_ARC_UI_FIELD:
79
+ rel = self._STORY_ARC_REL
80
+ model = StoryArc
55
81
  else:
56
82
  remote_field = getattr(
57
83
  Comic._meta.get_field(field_name), "remote_field", None
@@ -59,12 +85,21 @@ class BrowserChoicesViewBase(BrowserBaseView):
59
85
  rel = field_name
60
86
  model = remote_field.model if remote_field else None
61
87
 
88
+ rel = self.rel_prefix + rel
89
+
62
90
  return rel, model
63
91
 
64
92
  def get_object(self):
65
93
  """Get the comic subquery use for the choices."""
66
- object_filter, _ = self.get_query_filters(True, True)
67
- return Comic.objects.filter(object_filter)
94
+ object_filter, _ = self.get_query_filters(self.model, True)
95
+ return self.model.objects.filter(object_filter)
96
+
97
+ def _set_model(self):
98
+ """Set the model to query."""
99
+ group = self.kwargs["group"]
100
+ if group == self.ROOT_GROUP:
101
+ group = self.params.get("top_group", "p")
102
+ self.model = self.GROUP_MODEL_MAP[group]
68
103
 
69
104
 
70
105
  class BrowserChoicesAvailableView(BrowserChoicesViewBase):
@@ -72,23 +107,20 @@ class BrowserChoicesAvailableView(BrowserChoicesViewBase):
72
107
 
73
108
  serializer_class = BrowserFilterChoicesSerializer
74
109
 
75
- CREATORS_PERSON_REL = "creators__person"
76
-
77
110
  @classmethod
78
- def _get_field_choices_count(cls, field_name, comic_qs):
111
+ def _get_field_choices_count(cls, comic_qs, field_name):
79
112
  """Create a pk:name object for fields without tables."""
80
- return cls.get_field_choices_query(field_name, comic_qs).count()
113
+ return cls.get_field_choices_query(comic_qs, field_name).count()
81
114
 
82
- @classmethod
83
- def _get_m2m_field_choices_count(cls, rel, comic_qs, model):
115
+ def _get_m2m_field_choices_count(self, model, comic_qs, rel):
84
116
  """Get choices with nulls where there are nulls."""
85
- count = cls.get_m2m_field_query(rel, comic_qs, model).count()
117
+ count = self.get_m2m_field_query(model, comic_qs).count()
86
118
 
87
119
  # Detect if there are null choices.
88
120
  # Regretabbly with another query, but doing a forward query
89
121
  # on the comic above restricts all results to only the filtered
90
122
  # rows. :(
91
- if cls.does_m2m_null_exist(comic_qs, rel):
123
+ if self.does_m2m_null_exist(comic_qs, rel):
92
124
  count += 1
93
125
 
94
126
  return count
@@ -97,16 +129,21 @@ class BrowserChoicesAvailableView(BrowserChoicesViewBase):
97
129
  def get(self, *args, **kwargs):
98
130
  """Return all choices with more than one choice."""
99
131
  self.parse_params()
132
+ self._set_model()
133
+ self.set_rel_prefix(self.model)
100
134
  comic_qs = self.get_object()
101
135
 
102
136
  data = {}
103
137
  for field_name in self.serializer_class().get_fields(): # type: ignore
138
+ if field_name == "story_arcs" and self.model == StoryArc:
139
+ # don't allow filtering on story arc in story arc view.
140
+ continue
104
141
  rel, m2m_model = self._get_rel_and_model(field_name)
105
142
 
106
143
  if m2m_model:
107
- count = self._get_m2m_field_choices_count(rel, comic_qs, m2m_model)
144
+ count = self._get_m2m_field_choices_count(m2m_model, comic_qs, rel)
108
145
  else:
109
- count = self._get_field_choices_count(rel, comic_qs)
146
+ count = self._get_field_choices_count(comic_qs, rel)
110
147
 
111
148
  filters = self.params.get("filters", {})
112
149
  data[field_name] = count > 1 or field_name in filters
@@ -120,10 +157,9 @@ class BrowserChoicesView(BrowserChoicesViewBase):
120
157
 
121
158
  serializer_class = BrowserChoicesSerializer
122
159
 
123
- @classmethod
124
- def _get_field_choices(cls, field_name, comic_qs):
160
+ def _get_field_choices(self, comic_qs, field_name):
125
161
  """Create a pk:name object for fields without tables."""
126
- qs = cls.get_field_choices_query(field_name, comic_qs)
162
+ qs = self.get_field_choices_query(comic_qs, field_name)
127
163
 
128
164
  if field_name == "country":
129
165
  lookup = pycountry.countries
@@ -139,18 +175,17 @@ class BrowserChoicesView(BrowserChoicesViewBase):
139
175
 
140
176
  return choices
141
177
 
142
- @classmethod
143
- def _get_m2m_field_choices(cls, rel, comic_qs, model):
178
+ def _get_m2m_field_choices(self, model, comic_qs, rel):
144
179
  """Get choices with nulls where there are nulls."""
145
- qs = cls.get_m2m_field_query(rel, comic_qs, model)
180
+ qs = self.get_m2m_field_query(model, comic_qs)
146
181
 
147
182
  # Detect if there are null choices.
148
183
  # Regretabbly with another query, but doing a forward query
149
184
  # on the comic above restrcts all results to only the filtered
150
185
  # rows. :(
151
- if cls.does_m2m_null_exist(comic_qs, rel):
186
+ if self.does_m2m_null_exist(comic_qs, rel):
152
187
  choices = list(qs)
153
- choices.append(cls.NULL_NAMED_ROW)
188
+ choices.append(self._NULL_NAMED_ROW)
154
189
  else:
155
190
  choices = qs
156
191
  return choices
@@ -159,6 +194,8 @@ class BrowserChoicesView(BrowserChoicesViewBase):
159
194
  def get(self, *args, **kwargs):
160
195
  """Return all choices with more than one choice."""
161
196
  self.parse_params()
197
+ self._set_model()
198
+ self.set_rel_prefix(self.model)
162
199
 
163
200
  field_name = snakecase(self.kwargs["field_name"])
164
201
 
@@ -166,9 +203,9 @@ class BrowserChoicesView(BrowserChoicesViewBase):
166
203
 
167
204
  comic_qs = self.get_object()
168
205
  if m2m_model:
169
- choices = self._get_m2m_field_choices(rel, comic_qs, m2m_model)
206
+ choices = self._get_m2m_field_choices(m2m_model, comic_qs, rel)
170
207
  else:
171
- choices = self._get_field_choices(rel, comic_qs)
208
+ choices = self._get_field_choices(comic_qs, rel)
172
209
 
173
210
  serializer = self.get_serializer(choices, many=True)
174
211
  return Response(serializer.data)
@@ -9,12 +9,10 @@ class BookmarkFilterMixin:
9
9
 
10
10
  _BOOKMARK_FILTERS = frozenset(set(CHOICES["bookmarkFilter"].keys()) - {"ALL"})
11
11
 
12
- def get_bm_rel(self, is_model_comic):
12
+ def get_bm_rel(self, model):
13
13
  """Create bookmark relation."""
14
- bm_rel = "bookmark"
15
- if not is_model_comic:
16
- bm_rel = "comic__" + bm_rel
17
- return bm_rel
14
+ rel_prefix = self.get_rel_prefix(model) # type: ignore
15
+ return rel_prefix + "bookmark"
18
16
 
19
17
  def _get_my_bookmark_filter(self, bm_rel):
20
18
  """Get a filter for my session or user defined bookmarks."""
@@ -27,12 +25,11 @@ class BookmarkFilterMixin:
27
25
  }
28
26
  return Q(**my_bookmarks_kwargs)
29
27
 
30
- def get_bookmark_filter(self, is_model_comic, choice):
28
+ def get_bookmark_filter(self, model):
31
29
  """Build bookmark query."""
32
- if choice is None:
33
- choice = self.params["filters"].get("bookmark", "ALL") # type: ignore
30
+ choice = self.params["filters"].get("bookmark", "ALL") # type: ignore
34
31
  if choice in self._BOOKMARK_FILTERS:
35
- bm_rel = self.get_bm_rel(is_model_comic)
32
+ bm_rel = self.get_bm_rel(model)
36
33
  my_bookmark_filter = self._get_my_bookmark_filter(bm_rel)
37
34
  if choice in ("UNREAD", "IN_PROGRESS"):
38
35
  my_not_finished_filter = my_bookmark_filter & Q(
@@ -7,18 +7,21 @@ from codex.views.session import BrowserSessionViewBase
7
7
  class ComicFieldFilter(BrowserSessionViewBase):
8
8
  """Comic field filters."""
9
9
 
10
- def _filter_by_comic_field(self, field, is_model_comic):
10
+ def _filter_by_comic_field(self, field):
11
11
  """Filter by a comic any2many attribute."""
12
12
  filter_list = self.params["filters"].get(field) # type: ignore
13
13
  filter_query = Q()
14
14
  if not filter_list:
15
15
  return filter_query
16
- query_prefix = "" if is_model_comic else "comic__"
17
16
 
18
17
  if field == self.CREATOR_PERSON_UI_FIELD:
19
- rel = f"{query_prefix}creators__person"
18
+ rel = "creators__person"
19
+ elif field == self.STORY_ARC_UI_FIELD:
20
+ rel = "storyarcnumber__story_arc"
20
21
  else:
21
- rel = f"{query_prefix}{field}"
22
+ rel = field
23
+
24
+ rel = self.rel_prefix + rel # type: ignore
22
25
 
23
26
  for index, val in enumerate(filter_list):
24
27
  # None values in a list don't work right so test for them separately
@@ -29,9 +32,9 @@ class ComicFieldFilter(BrowserSessionViewBase):
29
32
  filter_query |= Q(**{f"{rel}__in": filter_list})
30
33
  return filter_query
31
34
 
32
- def get_comic_field_filter(self, is_model_comic):
35
+ def get_comic_field_filter(self):
33
36
  """Filter the comics based on the form filters."""
34
37
  comic_field_filter = Q()
35
38
  for attribute in self.FILTER_ATTRIBUTES:
36
- comic_field_filter &= self._filter_by_comic_field(attribute, is_model_comic)
39
+ comic_field_filter &= self._filter_by_comic_field(attribute)
37
40
  return comic_field_filter
@@ -7,35 +7,20 @@ from codex.views.mixins import GroupACLMixin
7
7
  class GroupFilterMixin(GroupACLMixin):
8
8
  """Group Filters."""
9
9
 
10
- def _get_folders_filter(self):
11
- """Get a filter for ALL parent folders not just immediate one."""
12
- pk = self.kwargs.get("pk") # type: ignore
13
- return Q(folders__in=[pk]) if pk else Q()
14
-
15
- def _get_browser_group_filter(self):
16
- """Get the objects we'll be displaying."""
17
- # Get the instances that are children of the group_instance
18
- # And the filtered comics that are children of the group_instance
19
- group_filter = Q()
20
- pk = self.kwargs.get("pk") # type: ignore
21
- group = self.kwargs.get("group") # type: ignore
22
- if pk or group == self.FOLDER_GROUP:
23
- if not pk:
24
- pk = None
25
- group_relation = self.GROUP_RELATION[group]
26
- group_filter |= Q(**{group_relation: pk})
27
-
28
- return group_filter
29
-
30
10
  def get_group_filter(self, choices):
31
11
  """Get filter for the displayed group."""
32
- is_folder_view = self.kwargs.get("group") == self.FOLDER_GROUP # type: ignore
33
- if is_folder_view and choices:
34
- # Choice view needs to get all descendant comic attributes
35
- # So filter by all the folders
36
- group_filter = self._get_folders_filter()
12
+ pk = self.kwargs.get("pk") # type: ignore
13
+ group = self.kwargs.get("group") # type: ignore
14
+ if pk:
15
+ group_relation = "comic__" if choices else ""
16
+ if choices and group == self.FOLDER_GROUP:
17
+ group_relation += "folders"
18
+ else:
19
+ group_relation += self.GROUP_RELATION[group]
20
+ group_filter = Q(**{group_relation: pk})
21
+ elif group == self.FOLDER_GROUP:
22
+ group_filter = Q(parent_folder=None)
37
23
  else:
38
- # The browser filter is the same for all views
39
- group_filter = self._get_browser_group_filter()
24
+ group_filter = Q()
40
25
 
41
26
  return group_filter
@@ -23,19 +23,14 @@ class SearchFilterMixin:
23
23
  LOG.warning("While searching:")
24
24
  LOG.exception(exc)
25
25
 
26
- def _get_search_query_filter(self, text, is_model_comic, search_scores):
26
+ def _get_search_query_filter(self, text, search_scores):
27
27
  """Get the search filter and scores."""
28
- # Get search scores
28
+ rel = self.rel_prefix + "pk__in" # type: ignore
29
29
  self._get_search_scores(text, search_scores)
30
-
31
- # Create query
32
- prefix = ""
33
- if not is_model_comic:
34
- prefix = "comic__"
35
- query_dict = {f"{prefix}pk__in": search_scores.keys()}
30
+ query_dict = {rel: search_scores.keys()}
36
31
  return Q(**query_dict)
37
32
 
38
- def get_search_filter(self, is_model_comic):
33
+ def get_search_filter(self):
39
34
  """Preparse search, search and return the filter and scores."""
40
35
  search_filter = Q()
41
36
  search_scores = {}
@@ -48,7 +43,7 @@ class SearchFilterMixin:
48
43
  if query_string:
49
44
  # Query haystack
50
45
  search_filter = self._get_search_query_filter(
51
- query_string, is_model_comic, search_scores
46
+ query_string, search_scores
52
47
  )
53
48
  except Exception as exc:
54
49
  LOG.warning(exc)