codex 1.4.0a1__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 (259) 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/create.py +6 -8
  5. codex/librarian/importer/aggregate_metadata.py +75 -41
  6. codex/librarian/importer/clean_metadata.py +30 -7
  7. codex/librarian/importer/create_fks.py +154 -55
  8. codex/librarian/importer/deleted.py +11 -2
  9. codex/librarian/importer/failed_imports.py +41 -5
  10. codex/librarian/importer/importerd.py +34 -11
  11. codex/librarian/importer/link_comics.py +54 -31
  12. codex/librarian/importer/moved.py +55 -11
  13. codex/librarian/importer/query_fks.py +210 -48
  14. codex/librarian/importer/tasks.py +7 -7
  15. codex/librarian/janitor/cleanup.py +17 -5
  16. codex/librarian/librariand.py +10 -0
  17. codex/librarian/watchdog/events.py +11 -14
  18. codex/librarian/watchdog/observers.py +5 -1
  19. codex/logger/loggerd.py +7 -3
  20. codex/logger/logging.py +1 -1
  21. codex/migrations/0024_comic_gtin_comic_story_arc_number.py +24 -0
  22. codex/migrations/0025_add_story_arc_number.py +83 -0
  23. codex/models.py +21 -11
  24. codex/search/backend.py +1 -1
  25. codex/search/indexes.py +1 -1
  26. codex/serializers/browser.py +1 -0
  27. codex/serializers/metadata.py +5 -1
  28. codex/serializers/models.py +16 -1
  29. codex/serializers/opds/v1.py +1 -0
  30. codex/serializers/opds/v2.py +5 -2
  31. codex/serializers/reader.py +55 -16
  32. codex/settings/settings.py +1 -1
  33. codex/static_root/assets/admin-12749881.ef0f50bac290.js +41 -0
  34. codex/static_root/assets/admin-12749881.ef0f50bac290.js.br +0 -0
  35. codex/static_root/assets/admin-12749881.ef0f50bac290.js.gz +0 -0
  36. codex/static_root/assets/admin-12749881.js +41 -0
  37. codex/static_root/assets/admin-12749881.js.br +0 -0
  38. codex/static_root/assets/admin-12749881.js.gz +0 -0
  39. codex/static_root/assets/admin-beda768d.a614eee46307.css +1 -0
  40. codex/static_root/assets/admin-beda768d.a614eee46307.css.br +0 -0
  41. codex/static_root/assets/admin-beda768d.a614eee46307.css.gz +0 -0
  42. codex/static_root/assets/admin-beda768d.css +1 -0
  43. codex/static_root/assets/admin-beda768d.css.br +0 -0
  44. codex/static_root/assets/admin-beda768d.css.gz +0 -0
  45. codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css +1 -0
  46. codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css.br +0 -0
  47. codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css.gz +0 -0
  48. codex/static_root/assets/admin-drawer-panel-41c225cc.css +1 -0
  49. codex/static_root/assets/admin-drawer-panel-41c225cc.css.br +0 -0
  50. codex/static_root/assets/admin-drawer-panel-41c225cc.css.gz +0 -0
  51. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js +1 -0
  52. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.br +0 -0
  53. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.gz +0 -0
  54. codex/static_root/assets/admin-drawer-panel-522f1e6c.js +1 -0
  55. codex/static_root/assets/admin-drawer-panel-522f1e6c.js.br +0 -0
  56. codex/static_root/assets/admin-drawer-panel-522f1e6c.js.gz +0 -0
  57. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css +1 -0
  58. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.br +0 -0
  59. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.gz +0 -0
  60. codex/static_root/assets/browser-7f7d7134.css +1 -0
  61. codex/static_root/assets/browser-7f7d7134.css.br +0 -0
  62. codex/static_root/assets/browser-7f7d7134.css.gz +0 -0
  63. codex/static_root/assets/browser-af622672.d51aca96d64d.js +1 -0
  64. codex/static_root/assets/browser-af622672.d51aca96d64d.js.br +0 -0
  65. codex/static_root/assets/browser-af622672.d51aca96d64d.js.gz +0 -0
  66. codex/static_root/assets/browser-af622672.js +1 -0
  67. codex/static_root/assets/browser-af622672.js.br +0 -0
  68. codex/static_root/assets/browser-af622672.js.gz +0 -0
  69. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js +1 -0
  70. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.br +0 -0
  71. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.gz +0 -0
  72. codex/static_root/assets/http-error-5e17b794.js +1 -0
  73. codex/static_root/assets/http-error-5e17b794.js.br +0 -0
  74. codex/static_root/assets/http-error-5e17b794.js.gz +0 -0
  75. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js +1 -0
  76. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.br +0 -0
  77. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.gz +0 -0
  78. codex/static_root/assets/main-9e76a4c3.js +1 -0
  79. codex/static_root/assets/main-9e76a4c3.js.br +0 -0
  80. codex/static_root/assets/main-9e76a4c3.js.gz +0 -0
  81. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js +1 -0
  82. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.br +0 -0
  83. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.gz +0 -0
  84. codex/static_root/assets/metadata-dialog-62c29ce0.js +1 -0
  85. codex/static_root/assets/metadata-dialog-62c29ce0.js.br +0 -0
  86. codex/static_root/assets/metadata-dialog-62c29ce0.js.gz +0 -0
  87. codex/static_root/assets/{metadata-dialog-785c4cfc.694a251cda37.css → metadata-dialog-cb306ffd.cc304996d7bb.css} +1 -1
  88. codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.br +0 -0
  89. codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.gz +0 -0
  90. codex/static_root/assets/{metadata-dialog-785c4cfc.css → metadata-dialog-cb306ffd.css} +1 -1
  91. codex/static_root/assets/metadata-dialog-cb306ffd.css.br +0 -0
  92. codex/static_root/assets/metadata-dialog-cb306ffd.css.gz +0 -0
  93. codex/static_root/assets/{page-pdf-c603e996.ab2d147c9ae1.js → page-pdf-157ba97e.613d7c2beb77.js} +61 -51
  94. codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.br +0 -0
  95. codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.gz +0 -0
  96. codex/static_root/assets/{page-pdf-c603e996.js → page-pdf-157ba97e.js} +61 -51
  97. codex/static_root/assets/page-pdf-157ba97e.js.br +0 -0
  98. codex/static_root/assets/page-pdf-157ba97e.js.gz +0 -0
  99. codex/static_root/assets/reader-36266549.0b2cf1291f27.js +1 -0
  100. codex/static_root/assets/reader-36266549.0b2cf1291f27.js.br +0 -0
  101. codex/static_root/assets/reader-36266549.0b2cf1291f27.js.gz +0 -0
  102. codex/static_root/assets/reader-36266549.js +1 -0
  103. codex/static_root/assets/reader-36266549.js.br +0 -0
  104. codex/static_root/assets/reader-36266549.js.gz +0 -0
  105. codex/static_root/assets/reader-7f004141.506eecc6954b.css +1 -0
  106. codex/static_root/assets/reader-7f004141.506eecc6954b.css.br +0 -0
  107. codex/static_root/assets/reader-7f004141.506eecc6954b.css.gz +0 -0
  108. codex/static_root/assets/reader-7f004141.css +1 -0
  109. codex/static_root/assets/reader-7f004141.css.br +0 -0
  110. codex/static_root/assets/reader-7f004141.css.gz +0 -0
  111. codex/static_root/js/choices.8c58714cf5b2.json +1 -0
  112. codex/static_root/js/choices.8c58714cf5b2.json.br +5 -0
  113. codex/static_root/js/choices.8c58714cf5b2.json.gz +0 -0
  114. codex/static_root/js/choices.json +1 -1
  115. codex/static_root/js/choices.json.br +0 -0
  116. codex/static_root/js/choices.json.gz +0 -0
  117. codex/static_root/{manifest.c0e270b2e6b6.json → manifest.d2f93a519ada.json} +32 -32
  118. codex/static_root/manifest.d2f93a519ada.json.br +0 -0
  119. codex/static_root/manifest.d2f93a519ada.json.gz +0 -0
  120. codex/static_root/manifest.json +32 -32
  121. codex/static_root/manifest.json.br +0 -0
  122. codex/static_root/manifest.json.gz +0 -0
  123. codex/static_root/staticfiles.json +1 -1
  124. codex/templates/headers-script-globals.html +1 -1
  125. codex/templates/{opds → opds_v1}/index.xml +3 -1
  126. codex/templates/{opds/opensearch.xml → opds_v1/opensearch_v1.xml} +1 -1
  127. codex/templates/search/indexes/codex/comic_text.txt +2 -2
  128. codex/urls/converters.py +1 -1
  129. codex/urls/opds/authentication.py +1 -1
  130. codex/urls/opds/root.py +8 -12
  131. codex/urls/opds/v1.py +12 -5
  132. codex/urls/opds/v2.py +2 -2
  133. codex/views/bookmark.py +2 -2
  134. codex/views/browser/base.py +23 -7
  135. codex/views/browser/browser.py +51 -41
  136. codex/views/browser/browser_annotations.py +159 -50
  137. codex/views/browser/browser_order_by.py +50 -106
  138. codex/views/browser/choices.py +75 -38
  139. codex/views/browser/filters/bookmark.py +6 -9
  140. codex/views/browser/filters/field.py +9 -6
  141. codex/views/browser/filters/group.py +12 -27
  142. codex/views/browser/filters/search.py +5 -10
  143. codex/views/browser/metadata.py +44 -19
  144. codex/views/download.py +1 -1
  145. codex/views/frontend.py +2 -3
  146. codex/views/mixins.py +15 -2
  147. codex/views/opds/const.py +8 -1
  148. codex/views/opds/util.py +37 -1
  149. codex/views/opds/v1/__init__.py +1 -1
  150. codex/views/opds/v1/data.py +21 -0
  151. codex/views/opds/v1/entry/__init__.py +1 -0
  152. codex/views/opds/v1/entry/data.py +23 -0
  153. codex/views/opds/v1/entry/entry.py +151 -0
  154. codex/views/opds/v1/entry/links.py +135 -0
  155. codex/views/opds/v1/facets.py +190 -0
  156. codex/views/opds/v1/feed.py +199 -0
  157. codex/views/opds/v1/links.py +198 -0
  158. codex/views/opds/{opensearch.py → v1/opensearch_v1.py} +3 -3
  159. codex/views/opds/v2/__init__.py +1 -1
  160. codex/views/opds/v2/const.py +10 -2
  161. codex/views/opds/v2/feed.py +82 -21
  162. codex/views/opds/v2/links.py +1 -1
  163. codex/views/opds/v2/publications.py +1 -1
  164. codex/views/opds/v2/top_links.py +1 -1
  165. codex/views/reader/page.py +6 -7
  166. codex/views/reader/reader.py +191 -61
  167. codex/views/session.py +2 -1
  168. {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/METADATA +10 -41
  169. {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/RECORD +172 -170
  170. codex/librarian/importer/db_ops.py +0 -251
  171. codex/pdf.py +0 -115
  172. codex/static_root/assets/admin-75c007ce.199fccf24c8d.js +0 -48
  173. codex/static_root/assets/admin-75c007ce.199fccf24c8d.js.br +0 -0
  174. codex/static_root/assets/admin-75c007ce.199fccf24c8d.js.gz +0 -0
  175. codex/static_root/assets/admin-75c007ce.js +0 -48
  176. codex/static_root/assets/admin-75c007ce.js.br +0 -0
  177. codex/static_root/assets/admin-75c007ce.js.gz +0 -0
  178. codex/static_root/assets/admin-848d48b1.5de8a0c45636.css +0 -1
  179. codex/static_root/assets/admin-848d48b1.5de8a0c45636.css.br +0 -0
  180. codex/static_root/assets/admin-848d48b1.5de8a0c45636.css.gz +0 -0
  181. codex/static_root/assets/admin-848d48b1.css +0 -1
  182. codex/static_root/assets/admin-848d48b1.css.br +0 -0
  183. codex/static_root/assets/admin-848d48b1.css.gz +0 -0
  184. codex/static_root/assets/admin-drawer-panel-a110c068.edf187333272.js +0 -1
  185. codex/static_root/assets/admin-drawer-panel-a110c068.edf187333272.js.br +0 -0
  186. codex/static_root/assets/admin-drawer-panel-a110c068.edf187333272.js.gz +0 -0
  187. codex/static_root/assets/admin-drawer-panel-a110c068.js +0 -1
  188. codex/static_root/assets/admin-drawer-panel-a110c068.js.br +0 -0
  189. codex/static_root/assets/admin-drawer-panel-a110c068.js.gz +0 -0
  190. codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css +0 -1
  191. codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css.br +0 -2
  192. codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css.gz +0 -0
  193. codex/static_root/assets/admin-drawer-panel-cce8c0aa.css +0 -1
  194. codex/static_root/assets/admin-drawer-panel-cce8c0aa.css.br +0 -2
  195. codex/static_root/assets/admin-drawer-panel-cce8c0aa.css.gz +0 -0
  196. codex/static_root/assets/browser-2c2380fd.8b515af7a743.js +0 -1
  197. codex/static_root/assets/browser-2c2380fd.8b515af7a743.js.br +0 -0
  198. codex/static_root/assets/browser-2c2380fd.8b515af7a743.js.gz +0 -0
  199. codex/static_root/assets/browser-2c2380fd.js +0 -1
  200. codex/static_root/assets/browser-2c2380fd.js.br +0 -0
  201. codex/static_root/assets/browser-2c2380fd.js.gz +0 -0
  202. codex/static_root/assets/browser-7325db61.css +0 -1
  203. codex/static_root/assets/browser-7325db61.css.br +0 -0
  204. codex/static_root/assets/browser-7325db61.css.gz +0 -0
  205. codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css +0 -1
  206. codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css.br +0 -0
  207. codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css.gz +0 -0
  208. codex/static_root/assets/http-error-402decbe.9ea8de1df13f.js +0 -1
  209. codex/static_root/assets/http-error-402decbe.9ea8de1df13f.js.br +0 -0
  210. codex/static_root/assets/http-error-402decbe.9ea8de1df13f.js.gz +0 -0
  211. codex/static_root/assets/http-error-402decbe.js +0 -1
  212. codex/static_root/assets/http-error-402decbe.js.br +0 -0
  213. codex/static_root/assets/http-error-402decbe.js.gz +0 -0
  214. codex/static_root/assets/main-a7f327e9.6641fe833335.js +0 -1
  215. codex/static_root/assets/main-a7f327e9.6641fe833335.js.br +0 -0
  216. codex/static_root/assets/main-a7f327e9.6641fe833335.js.gz +0 -0
  217. codex/static_root/assets/main-a7f327e9.js +0 -1
  218. codex/static_root/assets/main-a7f327e9.js.br +0 -0
  219. codex/static_root/assets/main-a7f327e9.js.gz +0 -0
  220. codex/static_root/assets/metadata-dialog-785c4cfc.694a251cda37.css.br +0 -0
  221. codex/static_root/assets/metadata-dialog-785c4cfc.694a251cda37.css.gz +0 -0
  222. codex/static_root/assets/metadata-dialog-785c4cfc.css.br +0 -0
  223. codex/static_root/assets/metadata-dialog-785c4cfc.css.gz +0 -0
  224. codex/static_root/assets/metadata-dialog-8a0bd8e1.c213b08d582f.js +0 -1
  225. codex/static_root/assets/metadata-dialog-8a0bd8e1.c213b08d582f.js.br +0 -0
  226. codex/static_root/assets/metadata-dialog-8a0bd8e1.c213b08d582f.js.gz +0 -0
  227. codex/static_root/assets/metadata-dialog-8a0bd8e1.js +0 -1
  228. codex/static_root/assets/metadata-dialog-8a0bd8e1.js.br +0 -0
  229. codex/static_root/assets/metadata-dialog-8a0bd8e1.js.gz +0 -0
  230. codex/static_root/assets/page-pdf-c603e996.ab2d147c9ae1.js.br +0 -0
  231. codex/static_root/assets/page-pdf-c603e996.ab2d147c9ae1.js.gz +0 -0
  232. codex/static_root/assets/page-pdf-c603e996.js.br +0 -0
  233. codex/static_root/assets/page-pdf-c603e996.js.gz +0 -0
  234. codex/static_root/assets/reader-c2965a5f.b011260169f7.js +0 -1
  235. codex/static_root/assets/reader-c2965a5f.b011260169f7.js.br +0 -0
  236. codex/static_root/assets/reader-c2965a5f.b011260169f7.js.gz +0 -0
  237. codex/static_root/assets/reader-c2965a5f.js +0 -1
  238. codex/static_root/assets/reader-c2965a5f.js.br +0 -0
  239. codex/static_root/assets/reader-c2965a5f.js.gz +0 -0
  240. codex/static_root/assets/reader-d8534888.2821de925986.css +0 -1
  241. codex/static_root/assets/reader-d8534888.2821de925986.css.br +0 -0
  242. codex/static_root/assets/reader-d8534888.2821de925986.css.gz +0 -0
  243. codex/static_root/assets/reader-d8534888.css +0 -1
  244. codex/static_root/assets/reader-d8534888.css.br +0 -0
  245. codex/static_root/assets/reader-d8534888.css.gz +0 -0
  246. codex/static_root/js/choices.6bfc2a3d293f.json +0 -1
  247. codex/static_root/js/choices.6bfc2a3d293f.json.br +0 -0
  248. codex/static_root/js/choices.6bfc2a3d293f.json.gz +0 -0
  249. codex/static_root/manifest.c0e270b2e6b6.json.br +0 -0
  250. codex/static_root/manifest.c0e270b2e6b6.json.gz +0 -0
  251. codex/urls/opds/opensearch.py +0 -18
  252. codex/views/opds/v1/browser.py +0 -346
  253. codex/views/opds/v1/entry.py +0 -278
  254. codex/views/opds/v1/start.py +0 -28
  255. codex/views/opds/v1/util.py +0 -162
  256. codex/views/opds/v2/start.py +0 -28
  257. {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/LICENSE +0 -0
  258. {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/WHEEL +0 -0
  259. {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/entry_points.txt +0 -0
@@ -1 +1 @@
1
- {"paths": {"rest_framework/docs/css/highlight.css": "rest_framework/docs/css/highlight.e0e4d973c6d7.css", "rest_framework/docs/css/base.css": "rest_framework/docs/css/base.e630f8f4990e.css", "rest_framework/docs/css/jquery.json-view.min.css": "rest_framework/docs/css/jquery.json-view.min.a2e6beeb6710.css", "rest_framework/docs/img/favicon.ico": "rest_framework/docs/img/favicon.5195b4d0f3eb.ico", "rest_framework/docs/img/grid.png": "rest_framework/docs/img/grid.a4b938cf382b.png", "rest_framework/docs/js/jquery.json-view.min.js": "rest_framework/docs/js/jquery.json-view.min.b7c2d6981377.js", "rest_framework/docs/js/api.js": "rest_framework/docs/js/api.18a5ba8a1bd8.js", "rest_framework/docs/js/highlight.pack.js": "rest_framework/docs/js/highlight.pack.479b5f21dcba.js", "admin/css/codex-admin.css": "admin/css/codex-admin.7ad6b144bf84.css", "admin/css/codex-admin-dark.css": "admin/css/codex-admin-dark.4d45b3679935.css", "admin/css/queue-job.css": "admin/css/queue-job.bee4dbac67f1.css", "admin/js/queue-job.js": "admin/js/queue-job.628c5e325415.js", "rest_framework/css/default.css": "rest_framework/css/default.789dfb5732d7.css", "rest_framework/css/font-awesome-4.0.3.css": "rest_framework/css/font-awesome-4.0.3.c1e1ea213abf.css", "rest_framework/css/bootstrap.min.css": "rest_framework/css/bootstrap.min.f17d4516b026.css", "rest_framework/css/prettify.css": "rest_framework/css/prettify.a987f72342ee.css", "rest_framework/css/bootstrap-theme.min.css": "rest_framework/css/bootstrap-theme.min.1d4b05b397c3.css", "rest_framework/css/bootstrap-theme.min.css.map": "rest_framework/css/bootstrap-theme.min.css.51806092cc05.map", "rest_framework/css/bootstrap.min.css.map": "rest_framework/css/bootstrap.min.css.cafbda9c0e9e.map", "rest_framework/css/bootstrap-tweaks.css": "rest_framework/css/bootstrap-tweaks.46ed116b0edd.css", "rest_framework/img/glyphicons-halflings.png": "rest_framework/img/glyphicons-halflings.90233c9067e9.png", "rest_framework/img/glyphicons-halflings-white.png": "rest_framework/img/glyphicons-halflings-white.9bbc6e960299.png", "rest_framework/img/grid.png": "rest_framework/img/grid.a4b938cf382b.png", "rest_framework/js/csrf.js": "rest_framework/js/csrf.969930007329.js", "rest_framework/js/jquery-3.5.1.min.js": "rest_framework/js/jquery-3.5.1.min.dc5e7f18c8d3.js", "rest_framework/js/prettify-min.js": "rest_framework/js/prettify-min.709bfcc456c6.js", "rest_framework/js/bootstrap.min.js": "rest_framework/js/bootstrap.min.2f34b630ffe3.js", "rest_framework/js/default.js": "rest_framework/js/default.5b08897dbdc3.js", "rest_framework/js/coreapi-0.1.1.js": "rest_framework/js/coreapi-0.1.1.e580e3854595.js", "rest_framework/js/ajax-form.js": "rest_framework/js/ajax-form.0ea6e6052ab5.js", "rest_framework/fonts/fontawesome-webfont.woff": "rest_framework/fonts/fontawesome-webfont.3293616ec0c6.woff", "rest_framework/fonts/glyphicons-halflings-regular.ttf": "rest_framework/fonts/glyphicons-halflings-regular.e18bbf611f2a.ttf", "rest_framework/fonts/fontawesome-webfont.svg": "rest_framework/fonts/fontawesome-webfont.83e37a11f9d7.svg", "rest_framework/fonts/fontawesome-webfont.eot": "rest_framework/fonts/fontawesome-webfont.8b27bc96115c.eot", "rest_framework/fonts/glyphicons-halflings-regular.eot": "rest_framework/fonts/glyphicons-halflings-regular.f4769f9bdb74.eot", "rest_framework/fonts/fontawesome-webfont.ttf": "rest_framework/fonts/fontawesome-webfont.dcb26c7239d8.ttf", "rest_framework/fonts/glyphicons-halflings-regular.woff": "rest_framework/fonts/glyphicons-halflings-regular.fa2772327f55.woff", "rest_framework/fonts/glyphicons-halflings-regular.svg": "rest_framework/fonts/glyphicons-halflings-regular.08eda92397ae.svg", "rest_framework/fonts/glyphicons-halflings-regular.woff2": "rest_framework/fonts/glyphicons-halflings-regular.448c34a56d69.woff2", "img/missing-cover.webp": "img/missing-cover.8c8c1d4f6782.webp", "img/logo-maskable-180.webp": "img/logo-maskable-180.201cbde62d55.webp", "img/logo-32.webp": "img/logo-32.dbb98f6dfde4.webp", "img/missing-page.svg": "img/missing-page.71bb10f7c72a.svg", "img/logo.svg": "img/logo.3fb129bfbd92.svg", "img/logo-maskable.svg": "img/logo-maskable.d618c2d7943f.svg", "pwa/offline.html": "pwa/offline.37a4206d79f0.html", "js/choices.json": "js/choices.6bfc2a3d293f.json", "js/choices-admin.json": "js/choices-admin.24cecf0a0568.json", "assets/admin-75c007ce.js": "assets/admin-75c007ce.199fccf24c8d.js", "assets/reader-d8534888.css": "assets/reader-d8534888.2821de925986.css", "assets/page-pdf-c603e996.js": "assets/page-pdf-c603e996.ab2d147c9ae1.js", "assets/browser-2c2380fd.js": "assets/browser-2c2380fd.8b515af7a743.js", "assets/materialdesignicons-webfont-80bb28b3.woff": "assets/materialdesignicons-webfont-80bb28b3.20b6ebf31bfa.woff", "assets/admin-drawer-panel-cce8c0aa.css": "assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css", "assets/metadata-dialog-785c4cfc.css": "assets/metadata-dialog-785c4cfc.694a251cda37.css", "assets/http-error-402decbe.js": "assets/http-error-402decbe.9ea8de1df13f.js", "assets/materialdesignicons-webfont-c1c004a9.woff2": "assets/materialdesignicons-webfont-c1c004a9.a295367092b3.woff2", "assets/admin-drawer-panel-a110c068.js": "assets/admin-drawer-panel-a110c068.edf187333272.js", "assets/reader-c2965a5f.js": "assets/reader-c2965a5f.b011260169f7.js", "assets/browser-7325db61.css": "assets/browser-7325db61.ed2cfbf8e8ee.css", "assets/page-pdf-2d7d4f5d.css": "assets/page-pdf-2d7d4f5d.963cecd9ec4e.css", "assets/materialdesignicons-webfont-a58ecb54.ttf": "assets/materialdesignicons-webfont-a58ecb54.d10ac4ee5ebe.ttf", "assets/main-0898f4bb.css": "assets/main-0898f4bb.181e0145c642.css", "assets/metadata-dialog-8a0bd8e1.js": "assets/metadata-dialog-8a0bd8e1.c213b08d582f.js", "assets/admin-848d48b1.css": "assets/admin-848d48b1.5de8a0c45636.css", "assets/http-error-40ce966f.css": "assets/http-error-40ce966f.1c7337ad6e1d.css", "assets/main-a7f327e9.js": "assets/main-a7f327e9.6641fe833335.js", "assets/materialdesignicons-webfont-67d24abe.eot": "assets/materialdesignicons-webfont-67d24abe.5ce4e52cebe3.eot", "robots.txt": "robots.b6216d61c03e.txt", "manifest.json": "manifest.c0e270b2e6b6.json"}, "version": "1.1", "hash": "3abae4f43488"}
1
+ {"paths": {"rest_framework/docs/css/highlight.css": "rest_framework/docs/css/highlight.e0e4d973c6d7.css", "rest_framework/docs/css/base.css": "rest_framework/docs/css/base.e630f8f4990e.css", "rest_framework/docs/css/jquery.json-view.min.css": "rest_framework/docs/css/jquery.json-view.min.a2e6beeb6710.css", "rest_framework/docs/img/favicon.ico": "rest_framework/docs/img/favicon.5195b4d0f3eb.ico", "rest_framework/docs/img/grid.png": "rest_framework/docs/img/grid.a4b938cf382b.png", "rest_framework/docs/js/jquery.json-view.min.js": "rest_framework/docs/js/jquery.json-view.min.b7c2d6981377.js", "rest_framework/docs/js/api.js": "rest_framework/docs/js/api.18a5ba8a1bd8.js", "rest_framework/docs/js/highlight.pack.js": "rest_framework/docs/js/highlight.pack.479b5f21dcba.js", "admin/css/codex-admin.css": "admin/css/codex-admin.7ad6b144bf84.css", "admin/css/codex-admin-dark.css": "admin/css/codex-admin-dark.4d45b3679935.css", "admin/css/queue-job.css": "admin/css/queue-job.bee4dbac67f1.css", "admin/js/queue-job.js": "admin/js/queue-job.628c5e325415.js", "rest_framework/css/default.css": "rest_framework/css/default.789dfb5732d7.css", "rest_framework/css/font-awesome-4.0.3.css": "rest_framework/css/font-awesome-4.0.3.c1e1ea213abf.css", "rest_framework/css/bootstrap.min.css": "rest_framework/css/bootstrap.min.f17d4516b026.css", "rest_framework/css/prettify.css": "rest_framework/css/prettify.a987f72342ee.css", "rest_framework/css/bootstrap-theme.min.css": "rest_framework/css/bootstrap-theme.min.1d4b05b397c3.css", "rest_framework/css/bootstrap-theme.min.css.map": "rest_framework/css/bootstrap-theme.min.css.51806092cc05.map", "rest_framework/css/bootstrap.min.css.map": "rest_framework/css/bootstrap.min.css.cafbda9c0e9e.map", "rest_framework/css/bootstrap-tweaks.css": "rest_framework/css/bootstrap-tweaks.46ed116b0edd.css", "rest_framework/img/glyphicons-halflings.png": "rest_framework/img/glyphicons-halflings.90233c9067e9.png", "rest_framework/img/glyphicons-halflings-white.png": "rest_framework/img/glyphicons-halflings-white.9bbc6e960299.png", "rest_framework/img/grid.png": "rest_framework/img/grid.a4b938cf382b.png", "rest_framework/js/csrf.js": "rest_framework/js/csrf.969930007329.js", "rest_framework/js/jquery-3.5.1.min.js": "rest_framework/js/jquery-3.5.1.min.dc5e7f18c8d3.js", "rest_framework/js/prettify-min.js": "rest_framework/js/prettify-min.709bfcc456c6.js", "rest_framework/js/bootstrap.min.js": "rest_framework/js/bootstrap.min.2f34b630ffe3.js", "rest_framework/js/default.js": "rest_framework/js/default.5b08897dbdc3.js", "rest_framework/js/coreapi-0.1.1.js": "rest_framework/js/coreapi-0.1.1.e580e3854595.js", "rest_framework/js/ajax-form.js": "rest_framework/js/ajax-form.0ea6e6052ab5.js", "rest_framework/fonts/fontawesome-webfont.woff": "rest_framework/fonts/fontawesome-webfont.3293616ec0c6.woff", "rest_framework/fonts/glyphicons-halflings-regular.ttf": "rest_framework/fonts/glyphicons-halflings-regular.e18bbf611f2a.ttf", "rest_framework/fonts/fontawesome-webfont.svg": "rest_framework/fonts/fontawesome-webfont.83e37a11f9d7.svg", "rest_framework/fonts/fontawesome-webfont.eot": "rest_framework/fonts/fontawesome-webfont.8b27bc96115c.eot", "rest_framework/fonts/glyphicons-halflings-regular.eot": "rest_framework/fonts/glyphicons-halflings-regular.f4769f9bdb74.eot", "rest_framework/fonts/fontawesome-webfont.ttf": "rest_framework/fonts/fontawesome-webfont.dcb26c7239d8.ttf", "rest_framework/fonts/glyphicons-halflings-regular.woff": "rest_framework/fonts/glyphicons-halflings-regular.fa2772327f55.woff", "rest_framework/fonts/glyphicons-halflings-regular.svg": "rest_framework/fonts/glyphicons-halflings-regular.08eda92397ae.svg", "rest_framework/fonts/glyphicons-halflings-regular.woff2": "rest_framework/fonts/glyphicons-halflings-regular.448c34a56d69.woff2", "img/missing-cover.webp": "img/missing-cover.8c8c1d4f6782.webp", "img/logo-maskable-180.webp": "img/logo-maskable-180.201cbde62d55.webp", "img/logo-32.webp": "img/logo-32.dbb98f6dfde4.webp", "img/missing-page.svg": "img/missing-page.71bb10f7c72a.svg", "img/logo.svg": "img/logo.3fb129bfbd92.svg", "img/logo-maskable.svg": "img/logo-maskable.d618c2d7943f.svg", "pwa/offline.html": "pwa/offline.37a4206d79f0.html", "js/choices.json": "js/choices.8c58714cf5b2.json", "js/choices-admin.json": "js/choices-admin.24cecf0a0568.json", "assets/http-error-5e17b794.js": "assets/http-error-5e17b794.77ceeb2d4641.js", "assets/reader-36266549.js": "assets/reader-36266549.0b2cf1291f27.js", "assets/metadata-dialog-cb306ffd.css": "assets/metadata-dialog-cb306ffd.cc304996d7bb.css", "assets/admin-12749881.js": "assets/admin-12749881.ef0f50bac290.js", "assets/materialdesignicons-webfont-80bb28b3.woff": "assets/materialdesignicons-webfont-80bb28b3.20b6ebf31bfa.woff", "assets/admin-beda768d.css": "assets/admin-beda768d.a614eee46307.css", "assets/materialdesignicons-webfont-c1c004a9.woff2": "assets/materialdesignicons-webfont-c1c004a9.a295367092b3.woff2", "assets/metadata-dialog-62c29ce0.js": "assets/metadata-dialog-62c29ce0.8418785c0453.js", "assets/admin-drawer-panel-41c225cc.css": "assets/admin-drawer-panel-41c225cc.3f84583b435b.css", "assets/main-9e76a4c3.js": "assets/main-9e76a4c3.6844a407d14c.js", "assets/page-pdf-2d7d4f5d.css": "assets/page-pdf-2d7d4f5d.963cecd9ec4e.css", "assets/materialdesignicons-webfont-a58ecb54.ttf": "assets/materialdesignicons-webfont-a58ecb54.d10ac4ee5ebe.ttf", "assets/page-pdf-157ba97e.js": "assets/page-pdf-157ba97e.613d7c2beb77.js", "assets/main-0898f4bb.css": "assets/main-0898f4bb.181e0145c642.css", "assets/http-error-40ce966f.css": "assets/http-error-40ce966f.1c7337ad6e1d.css", "assets/materialdesignicons-webfont-67d24abe.eot": "assets/materialdesignicons-webfont-67d24abe.5ce4e52cebe3.eot", "assets/reader-7f004141.css": "assets/reader-7f004141.506eecc6954b.css", "assets/browser-af622672.js": "assets/browser-af622672.d51aca96d64d.js", "assets/browser-7f7d7134.css": "assets/browser-7f7d7134.0fe3749b0f2f.css", "assets/admin-drawer-panel-522f1e6c.js": "assets/admin-drawer-panel-522f1e6c.089d70878270.js", "robots.txt": "robots.b6216d61c03e.txt", "manifest.json": "manifest.d2f93a519ada.json"}, "version": "1.1", "hash": "d46bd0e7f8fb"}
@@ -6,7 +6,7 @@
6
6
  LAST_ROUTE: {{ last_route | safe }},
7
7
  MISSING_COVER: "{% static 'img/missing-cover.webp' %}",
8
8
  MISSING_PAGE: "{% static 'img/missing-page.svg' %}",
9
- OPDS_V1_PATH: "{% url 'opds:v1:browser' 'r' 0 1 %}",
9
+ OPDS_V1_PATH: "{% url 'opds:v1:feed' 'r' 0 1 %}",
10
10
  OPDS_V2_PATH: "{% url 'opds:v2:feed' 'r' 0 1 %}",
11
11
  };
12
12
  Object.freeze(CODEX);
@@ -7,7 +7,9 @@
7
7
  xmlns:dc="http://purl.org/dc/terms/"
8
8
  xmlns:thr="http://purl.org/syndication/thread/1.0"
9
9
  xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"
10
- xmlns:pse="http://vaemendis.net/opds-pse/ns"
10
+ {% if is_acquisition %}
11
+ xmlns:pse="http://vaemendis.net/opds-pse/ns"
12
+ {% endif %}
11
13
  >
12
14
  <id>{{ id_tag }}</id>
13
15
  <icon>{% static 'img/logo-32.webp' %}</icon>
@@ -6,5 +6,5 @@
6
6
  <InputEncoding>UTF-8</InputEncoding>
7
7
  <OutputEncoding>UTF-8</OutputEncoding>
8
8
  <Image width="512" height="512" type="image/svg+xml">{% static 'img/logo.svg' %}</Image>
9
- <Url type="application/atom+xml;profile=opds-catalog;kind=acquisition" template="{% url 'opds:v1:browser' 's' 0 1 %}?q={searchTerms}"/>
9
+ <Url type="application/atom+xml;profile=opds-catalog;kind=acquisition" template="{% url 'opds:v1:feed' 's' 0 1 %}?q={searchTerms}"/>
10
10
  </OpenSearchDescription>
@@ -27,8 +27,8 @@
27
27
  {% for series_group in object.series_groups.all %}
28
28
  {{ series_group.name }}
29
29
  {% endfor %}
30
- {% for story_arc in object.story_arcs.all %}
31
- {{ story_arc.name }}
30
+ {% for story_arc_number in object.story_arc_numbers.all %}
31
+ {{ story_arc_number.story_arc.name }}
32
32
  {% endfor %}
33
33
  {% for tag in object.tags.all %}
34
34
  {{ tag.name }}
codex/urls/converters.py CHANGED
@@ -5,4 +5,4 @@ from django.urls.converters import StringConverter
5
5
  class GroupConverter(StringConverter):
6
6
  """Only accept valid browser groups."""
7
7
 
8
- regex = "[rpisvcf]"
8
+ regex = "[rpisvcfa]"
@@ -11,7 +11,7 @@ app_name = "authentication"
11
11
 
12
12
  urlpatterns = [
13
13
  path(
14
- "/v1",
14
+ "v1",
15
15
  cache_page(TIMEOUT)(OPDSAuthentication1View.as_view()),
16
16
  name="v1",
17
17
  ),
codex/urls/opds/root.py CHANGED
@@ -1,27 +1,23 @@
1
1
  """codex:opds URL Configuration."""
2
2
  from django.urls import include, path, re_path
3
3
 
4
- from codex.views.opds.v1.start import opds_1_start_view
5
- from codex.views.opds.v2.start import opds_2_start_view
4
+ from codex.views.opds.util import full_redirect_view
6
5
 
7
6
  app_name = "opds"
8
7
 
8
+ opds_v1_start_view = full_redirect_view("opds:v1:feed")
9
+
9
10
  urlpatterns = [
10
11
  path(
11
- "authentication",
12
+ "authentication/",
12
13
  include("codex.urls.opds.authentication"),
13
14
  name="authentication",
14
15
  ),
15
- path(
16
- "opensearch",
17
- include("codex.urls.opds.opensearch"),
18
- name="opensearch",
19
- ),
20
16
  path("bin/", include("codex.urls.opds.binary")),
21
17
  path("v1.2/", include("codex.urls.opds.v1")),
22
- path("v1/", opds_1_start_view, name="v1_start"),
18
+ path("v1/", opds_v1_start_view, name="v1_start"),
23
19
  path("v2.0/", include("codex.urls.opds.v2")),
24
- path("v2/", opds_2_start_view, name="v2_start"),
25
- path("", opds_1_start_view, name="start"),
26
- re_path(".*", opds_1_start_view, name="not_found"),
20
+ path("v2/", full_redirect_view("opds:v2:feed"), name="v2_start"),
21
+ path("", opds_v1_start_view, name="start"),
22
+ re_path(".*", opds_v1_start_view, name="not_found"),
27
23
  ]
codex/urls/opds/v1.py CHANGED
@@ -1,8 +1,10 @@
1
1
  """codex:opds:v1 URL Configuration."""
2
2
  from django.urls import path
3
+ from django.views.decorators.cache import cache_page
3
4
 
4
- from codex.views.opds.v1.browser import OPDS1BrowserView
5
- from codex.views.opds.v1.start import opds_1_start_view
5
+ from codex.views.opds.util import full_redirect_view
6
+ from codex.views.opds.v1.feed import OPDS1FeedView
7
+ from codex.views.opds.v1.opensearch_v1 import OpenSearch1View
6
8
 
7
9
  TIMEOUT = 60 * 60
8
10
 
@@ -14,10 +16,15 @@ urlpatterns = [
14
16
  # Browser
15
17
  path(
16
18
  "<group:group>/<int:pk>/<int:page>",
17
- OPDS1BrowserView.as_view(),
18
- name="browser",
19
+ OPDS1FeedView.as_view(),
20
+ name="feed",
21
+ ),
22
+ path(
23
+ "opensearch/v1.1",
24
+ cache_page(TIMEOUT)(OpenSearch1View.as_view()),
25
+ name="opensearch_v1",
19
26
  ),
20
27
  #
21
28
  # Catch All
22
- path("", opds_1_start_view, name="start"),
29
+ path("", full_redirect_view("opds:v1:feed"), name="start"),
23
30
  ]
codex/urls/opds/v2.py CHANGED
@@ -1,8 +1,8 @@
1
1
  """codex:opds:v1 URL Configuration."""
2
2
  from django.urls import path
3
3
 
4
+ from codex.views.opds.util import full_redirect_view
4
5
  from codex.views.opds.v2.feed import OPDS2FeedView
5
- from codex.views.opds.v2.start import opds_2_start_view
6
6
 
7
7
  app_name = "v2"
8
8
 
@@ -16,5 +16,5 @@ urlpatterns = [
16
16
  ),
17
17
  #
18
18
  # Catch All
19
- path("", opds_2_start_view, name="start"),
19
+ path("", full_redirect_view("opds:v2:feed"), name="start"),
20
20
  ]
codex/views/bookmark.py CHANGED
@@ -56,7 +56,7 @@ class BookmarkBaseView(GenericAPIView, GroupACLMixin):
56
56
 
57
57
  def _update_bookmarks(self, search_kwargs, updates):
58
58
  """Update existing bookmarks."""
59
- group_acl_filter = self.get_group_acl_filter(False)
59
+ group_acl_filter = self.get_group_acl_filter(Bookmark)
60
60
  existing_bookmarks = (
61
61
  Bookmark.objects.filter(group_acl_filter)
62
62
  .filter(**search_kwargs)
@@ -82,7 +82,7 @@ class BookmarkBaseView(GenericAPIView, GroupACLMixin):
82
82
  ):
83
83
  """Create new bookmarks for comics that don't exist yet."""
84
84
  create_bookmarks = []
85
- group_acl_filter = self.get_group_acl_filter(True)
85
+ group_acl_filter = self.get_group_acl_filter(Comic)
86
86
  create_bookmark_comics = (
87
87
  Comic.objects.filter(group_acl_filter)
88
88
  .filter(**comic_filter)
@@ -8,6 +8,7 @@ from djangorestframework_camel_case.settings import api_settings
8
8
  from djangorestframework_camel_case.util import underscoreize
9
9
 
10
10
  from codex.logger.logging import get_logger
11
+ from codex.models import Comic, Folder, Imprint, Publisher, Series, StoryArc, Volume
11
12
  from codex.serializers.browser import BrowserSettingsSerializer
12
13
  from codex.views.browser.filters.bookmark import BookmarkFilterMixin
13
14
  from codex.views.browser.filters.field import ComicFieldFilter
@@ -24,6 +25,17 @@ class BrowserBaseView(
24
25
 
25
26
  input_serializer_class = BrowserSettingsSerializer
26
27
 
28
+ GROUP_MODEL_MAP = {
29
+ GroupFilterMixin.ROOT_GROUP: None,
30
+ "p": Publisher,
31
+ "i": Imprint,
32
+ "s": Series,
33
+ "v": Volume,
34
+ GroupFilterMixin.COMIC_GROUP: Comic,
35
+ GroupFilterMixin.FOLDER_GROUP: Folder,
36
+ GroupFilterMixin.STORY_ARC_GROUP: StoryArc,
37
+ }
38
+
27
39
  _GET_JSON_KEYS = frozenset(("filters", "show"))
28
40
 
29
41
  def __init__(self, *args, **kwargs):
@@ -39,22 +51,22 @@ class BrowserBaseView(
39
51
  self._is_admin = user and isinstance(user, User) and user.is_staff
40
52
  return self._is_admin
41
53
 
42
- def get_query_filters_without_group(self, is_model_comic):
54
+ def get_query_filters_without_group(self, model):
43
55
  """Return all the filters except the group filter."""
44
- object_filter = self.get_group_acl_filter(is_model_comic)
56
+ object_filter = self.get_group_acl_filter(model)
45
57
 
46
- search_filter, search_scores = self.get_search_filter(is_model_comic)
58
+ search_filter, search_scores = self.get_search_filter()
47
59
  object_filter &= search_filter
48
- object_filter &= self.get_bookmark_filter(is_model_comic, None)
49
- object_filter &= self.get_comic_field_filter(is_model_comic)
60
+ object_filter &= self.get_bookmark_filter(model)
61
+ object_filter &= self.get_comic_field_filter()
50
62
  return object_filter, search_scores
51
63
 
52
- def get_query_filters(self, is_model_comic, choices=False):
64
+ def get_query_filters(self, model, choices=False):
53
65
  """Return the main object filter and the one for aggregates."""
54
66
  (
55
67
  object_filter,
56
68
  search_scores,
57
- ) = self.get_query_filters_without_group(is_model_comic)
69
+ ) = self.get_query_filters_without_group(model)
58
70
 
59
71
  object_filter &= self.get_group_filter(choices)
60
72
 
@@ -91,3 +103,7 @@ class BrowserBaseView(
91
103
 
92
104
  serializer.is_valid(raise_exception=True)
93
105
  self.params.update(serializer.validated_data)
106
+
107
+ def set_rel_prefix(self, model):
108
+ """Set the relation prefix for most fields."""
109
+ self.rel_prefix = self.get_rel_prefix(model)
@@ -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
@@ -148,7 +134,7 @@ class BrowserView(BrowserAnnotationsView):
148
134
  else:
149
135
  qs = self.model.objects.filter(object_filter)
150
136
  qs = self._add_annotations(qs, self.model, search_scores)
151
- qs = self.get_order_by(self.model, qs)
137
+ qs = self.add_order_by(qs, self.model)
152
138
  return qs
153
139
 
154
140
  def _get_book_queryset(self, object_filter, search_scores):
@@ -157,7 +143,7 @@ class BrowserView(BrowserAnnotationsView):
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
@@ -165,7 +151,9 @@ class BrowserView(BrowserAnnotationsView):
165
151
 
166
152
  qs = Comic.objects.filter(comic_object_filter)
167
153
  qs = self._add_annotations(qs, Comic, comic_search_scores)
168
- qs = self.get_order_by(Comic, qs)
154
+ qs = self.add_order_by(qs, Comic)
155
+ if limit := self.params.get("limit"):
156
+ qs = qs[:limit]
169
157
  else:
170
158
  qs = Comic.objects.none()
171
159
  return qs
@@ -185,11 +173,17 @@ class BrowserView(BrowserAnnotationsView):
185
173
 
186
174
  return up_group, up_pk
187
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
+
188
182
  def _set_group_instance(self):
189
183
  """Create group_class instance."""
190
184
  pk = self.kwargs.get("pk")
191
185
  self.group_instance: Optional[
192
- Union[Folder, Publisher, Imprint, Series, Volume]
186
+ Union[Folder, Publisher, Imprint, Series, Volume, StoryArc]
193
187
  ] = None
194
188
  if not pk:
195
189
  return
@@ -321,7 +315,7 @@ class BrowserView(BrowserAnnotationsView):
321
315
  )
322
316
 
323
317
  if book_qs.count():
324
- 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())
325
319
  page_counts = (num_pages, total_count, Comic)
326
320
  group_qs, num_pages, total_count = self._paginate_section(
327
321
  group_qs, book_max_obj_per_page, page_counts
@@ -332,11 +326,11 @@ class BrowserView(BrowserAnnotationsView):
332
326
  def get_object(self):
333
327
  """Validate settings and get the querysets."""
334
328
  self._set_browse_model()
329
+ self.set_rel_prefix(self.model)
335
330
  self._set_group_instance() # Placed up here to invalidate earlier
336
331
  # Create the main query with the filters
337
- is_model_comic = self.model == Comic
338
332
  try:
339
- object_filter, search_scores = self.get_query_filters(is_model_comic, False)
333
+ object_filter, search_scores = self.get_query_filters(self.model, False)
340
334
  except Folder.DoesNotExist:
341
335
  pk = self.kwargs.get("pk")
342
336
  self._raise_redirect(
@@ -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)