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,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
@@ -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,97 +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, model):
92
- """Sort groups by name ignoring articles."""
93
- # first_space_index
94
- first_field = model.ORDERING[0]
95
- queryset = queryset.annotate(
96
- first_space_index=StrIndex(first_field, Value(" "))
97
- )
98
-
99
- # lowercase_first_word
100
- lowercase_first_word = Lower(
101
- Substr(first_field, 1, length=(F("first_space_index") - 1)) # type: ignore
102
- )
103
- queryset = queryset.annotate(
104
- lowercase_first_word=Case(
105
- When(Q(first_space_index__gt=0), then=lowercase_first_word)
106
- ),
107
- default=Value(""),
108
- )
109
-
110
- # sort_name
111
- queryset = queryset.annotate(
112
- sort_name=Case(
113
- When(
114
- lowercase_first_word__in=cls._ARTICLES,
115
- then=Substr(
116
- first_field, F("first_space_index") + 1 # type: ignore
117
- ),
118
- ),
119
- default=first_field,
120
- )
121
- )
122
-
123
- # final ordering
124
- ordering = ("sort_name", *model.ORDERING[1:])
125
- return queryset, ordering
126
-
127
- def get_order_by(self, model, queryset, for_cover_pk=False):
78
+ def add_order_by(self, queryset, model):
128
79
  """Create the order_by list."""
129
- # order_fields
130
- comic_rel = ""
131
- order_key = self.get_order_key()
132
- if for_cover_pk:
133
- comic_rel = "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
- group = self.kwargs.get("group")
140
- if group != self.FOLDER_GROUP:
141
- # special annotations for ordering in browser mode
142
- queryset, ordering = self._order_without_articles(queryset, model)
143
- else:
144
- ordering = model.ORDERING
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)
145
90
  else:
146
- # Use annotated order_value
147
- 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)
148
96
 
149
- # add prefixes to all order_by fields
150
- neg_prefix = "-" if self.params.get("order_reverse") else ""
151
- prefix = neg_prefix + comic_rel
152
- ordering = (prefix + field for field in ordering)
153
- return queryset.order_by(*ordering)
97
+ return queryset.order_by(*order_by)