codex 1.4.1__py3-none-any.whl → 1.4.3__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 (237) hide show
  1. codex/applications/websocket.py +1 -1
  2. codex/librarian/importer/aggregate_metadata.py +38 -25
  3. codex/librarian/importer/clean_metadata.py +26 -13
  4. codex/librarian/importer/create_fks.py +1 -1
  5. codex/librarian/librariand.py +7 -4
  6. codex/librarian/watchdog/db_snapshot.py +1 -1
  7. codex/librarian/watchdog/event_batcherd.py +14 -11
  8. codex/librarian/watchdog/events.py +1 -1
  9. codex/logger/loggerd.py +14 -11
  10. codex/migrations/0001_init.py +3 -2
  11. codex/migrations/0002_auto_20200826_0622.py +4 -2
  12. codex/migrations/0003_auto_20200831_2033.py +4 -2
  13. codex/migrations/0004_failedimport.py +4 -2
  14. codex/migrations/0005_auto_20200918_0146.py +4 -2
  15. codex/migrations/0006_update_default_names_and_remove_duplicate_comics.py +12 -7
  16. codex/migrations/0007_auto_20211210_1710.py +3 -2
  17. codex/migrations/0008_alter_comic_created_at_alter_comic_format_and_more.py +4 -2
  18. codex/migrations/0009_alter_comic_parent_folder.py +4 -2
  19. codex/migrations/0010_haystack.py +4 -2
  20. codex/migrations/0011_library_groups_and_metadata_changes.py +3 -2
  21. codex/migrations/0012_rename_description_comic_comments.py +4 -2
  22. codex/migrations/0013_int_issue_count_longer_charfields.py +3 -3
  23. codex/migrations/0014_pdf_issue_suffix_remove_cover_image_sort_name.py +3 -2
  24. codex/migrations/0015_link_comics_to_top_level_folders.py +5 -2
  25. codex/migrations/0016_remove_comic_cover_path_librarianstatus.py +3 -3
  26. codex/migrations/0017_alter_timestamp_options_alter_adminflag_name_and_more.py +3 -3
  27. codex/migrations/0018_rename_userbookmark_bookmark.py +3 -2
  28. codex/migrations/0019_delete_queuejob.py +3 -2
  29. codex/migrations/0020_remove_search_tables.py +3 -2
  30. codex/migrations/0021_bookmark_fit_to_choices_read_in_reverse.py +3 -2
  31. codex/migrations/0022_bookmark_vertical_useractive_null_statuses.py +3 -2
  32. codex/migrations/0023_rename_credit_creator_and_more.py +3 -2
  33. codex/migrations/0024_comic_gtin_comic_story_arc_number.py +4 -2
  34. codex/migrations/0025_add_story_arc_number.py +4 -2
  35. codex/models.py +3 -4
  36. codex/search/backend.py +34 -31
  37. codex/serializers/auth.py +2 -1
  38. codex/serializers/choices.py +1 -0
  39. codex/static_root/assets/admin-b2b56cd6.f68d07d2bf93.js +41 -0
  40. codex/static_root/assets/admin-b2b56cd6.f68d07d2bf93.js.br +0 -0
  41. codex/static_root/assets/admin-b2b56cd6.f68d07d2bf93.js.gz +0 -0
  42. codex/static_root/assets/admin-b2b56cd6.js +41 -0
  43. codex/static_root/assets/admin-b2b56cd6.js.br +0 -0
  44. codex/static_root/assets/admin-b2b56cd6.js.gz +0 -0
  45. codex/static_root/assets/{admin-drawer-panel-522f1e6c.089d70878270.js → admin-drawer-panel-efc525ec.ddab36a24e08.js} +1 -1
  46. codex/static_root/assets/admin-drawer-panel-efc525ec.ddab36a24e08.js.br +0 -0
  47. codex/static_root/assets/admin-drawer-panel-efc525ec.ddab36a24e08.js.gz +0 -0
  48. codex/static_root/assets/{admin-drawer-panel-522f1e6c.js → admin-drawer-panel-efc525ec.js} +1 -1
  49. codex/static_root/assets/admin-drawer-panel-efc525ec.js.br +0 -0
  50. codex/static_root/assets/admin-drawer-panel-efc525ec.js.gz +0 -0
  51. codex/static_root/assets/admin-f2bb1dc8.css +1 -0
  52. codex/static_root/assets/admin-f2bb1dc8.css.br +0 -0
  53. codex/static_root/assets/admin-f2bb1dc8.css.gz +0 -0
  54. codex/static_root/assets/admin-f2bb1dc8.ecec18791c01.css +1 -0
  55. codex/static_root/assets/admin-f2bb1dc8.ecec18791c01.css.br +0 -0
  56. codex/static_root/assets/admin-f2bb1dc8.ecec18791c01.css.gz +0 -0
  57. codex/static_root/assets/{browser-7f7d7134.0fe3749b0f2f.css → browser-198df919.css} +1 -1
  58. codex/static_root/assets/browser-198df919.css.br +0 -0
  59. codex/static_root/assets/browser-198df919.css.gz +0 -0
  60. codex/static_root/assets/{browser-7f7d7134.css → browser-198df919.f06301531790.css} +1 -1
  61. codex/static_root/assets/browser-198df919.f06301531790.css.br +0 -0
  62. codex/static_root/assets/browser-198df919.f06301531790.css.gz +0 -0
  63. codex/static_root/assets/browser-ca158ba5.980d652eb174.js +1 -0
  64. codex/static_root/assets/browser-ca158ba5.980d652eb174.js.br +0 -0
  65. codex/static_root/assets/browser-ca158ba5.980d652eb174.js.gz +0 -0
  66. codex/static_root/assets/browser-ca158ba5.js +1 -0
  67. codex/static_root/assets/browser-ca158ba5.js.br +0 -0
  68. codex/static_root/assets/browser-ca158ba5.js.gz +0 -0
  69. codex/static_root/assets/{http-error-5e17b794.77ceeb2d4641.js → http-error-d31fd3bd.6ab9acf65973.js} +1 -1
  70. codex/static_root/assets/http-error-d31fd3bd.6ab9acf65973.js.br +0 -0
  71. codex/static_root/assets/http-error-d31fd3bd.6ab9acf65973.js.gz +0 -0
  72. codex/static_root/assets/{http-error-5e17b794.js → http-error-d31fd3bd.js} +1 -1
  73. codex/static_root/assets/http-error-d31fd3bd.js.br +0 -0
  74. codex/static_root/assets/http-error-d31fd3bd.js.gz +0 -0
  75. codex/static_root/assets/{main-0898f4bb.181e0145c642.css → main-c11eb0f1.776522baac3b.css} +1 -1
  76. codex/static_root/assets/main-c11eb0f1.776522baac3b.css.br +0 -0
  77. codex/static_root/assets/{main-0898f4bb.181e0145c642.css.gz → main-c11eb0f1.776522baac3b.css.gz} +0 -0
  78. codex/static_root/assets/{main-0898f4bb.css → main-c11eb0f1.css} +1 -1
  79. codex/static_root/assets/main-c11eb0f1.css.br +0 -0
  80. codex/static_root/assets/{main-0898f4bb.css.gz → main-c11eb0f1.css.gz} +0 -0
  81. codex/static_root/assets/main-c5736dea.a4790dbdb569.js +1 -0
  82. codex/static_root/assets/main-c5736dea.a4790dbdb569.js.br +0 -0
  83. codex/static_root/assets/main-c5736dea.a4790dbdb569.js.gz +0 -0
  84. codex/static_root/assets/main-c5736dea.js +1 -0
  85. codex/static_root/assets/main-c5736dea.js.br +0 -0
  86. codex/static_root/assets/main-c5736dea.js.gz +0 -0
  87. codex/static_root/assets/metadata-dialog-83c74d48.b5cccc13c737.css +1 -0
  88. codex/static_root/assets/metadata-dialog-83c74d48.b5cccc13c737.css.br +0 -0
  89. codex/static_root/assets/metadata-dialog-83c74d48.b5cccc13c737.css.gz +0 -0
  90. codex/static_root/assets/metadata-dialog-83c74d48.css +1 -0
  91. codex/static_root/assets/metadata-dialog-83c74d48.css.br +0 -0
  92. codex/static_root/assets/metadata-dialog-83c74d48.css.gz +0 -0
  93. codex/static_root/assets/metadata-dialog-8c0a11ff.b281b7635db5.js +1 -0
  94. codex/static_root/assets/metadata-dialog-8c0a11ff.b281b7635db5.js.br +0 -0
  95. codex/static_root/assets/metadata-dialog-8c0a11ff.b281b7635db5.js.gz +0 -0
  96. codex/static_root/assets/metadata-dialog-8c0a11ff.js +1 -0
  97. codex/static_root/assets/metadata-dialog-8c0a11ff.js.br +0 -0
  98. codex/static_root/assets/metadata-dialog-8c0a11ff.js.gz +0 -0
  99. codex/static_root/assets/{page-pdf-157ba97e.613d7c2beb77.js → page-pdf-ed976750.730244f14d16.js} +1 -1
  100. codex/static_root/assets/page-pdf-ed976750.730244f14d16.js.br +0 -0
  101. codex/static_root/assets/page-pdf-ed976750.730244f14d16.js.gz +0 -0
  102. codex/static_root/assets/{page-pdf-157ba97e.js → page-pdf-ed976750.js} +1 -1
  103. codex/static_root/assets/page-pdf-ed976750.js.br +0 -0
  104. codex/static_root/assets/page-pdf-ed976750.js.gz +0 -0
  105. codex/static_root/assets/reader-5540ffcb.8ea3c63a3154.css +1 -0
  106. codex/static_root/assets/reader-5540ffcb.8ea3c63a3154.css.br +0 -0
  107. codex/static_root/assets/reader-5540ffcb.8ea3c63a3154.css.gz +0 -0
  108. codex/static_root/assets/reader-5540ffcb.css +1 -0
  109. codex/static_root/assets/reader-5540ffcb.css.br +0 -0
  110. codex/static_root/assets/reader-5540ffcb.css.gz +0 -0
  111. codex/static_root/assets/reader-c562377d.7f78718f4c63.js +1 -0
  112. codex/static_root/assets/reader-c562377d.7f78718f4c63.js.br +0 -0
  113. codex/static_root/assets/reader-c562377d.7f78718f4c63.js.gz +0 -0
  114. codex/static_root/assets/reader-c562377d.js +1 -0
  115. codex/static_root/assets/reader-c562377d.js.br +0 -0
  116. codex/static_root/assets/reader-c562377d.js.gz +0 -0
  117. codex/static_root/{manifest.d2f93a519ada.json → manifest.55457ccaa01c.json} +32 -32
  118. codex/static_root/manifest.55457ccaa01c.json.br +0 -0
  119. codex/static_root/manifest.55457ccaa01c.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/pwa/{offline.37a4206d79f0.html → offline.7bfaf9f94bf9.html} +1 -1
  124. codex/static_root/pwa/offline.7bfaf9f94bf9.html.br +0 -0
  125. codex/static_root/pwa/offline.7bfaf9f94bf9.html.gz +0 -0
  126. codex/static_root/pwa/offline.html +1 -1
  127. codex/static_root/pwa/offline.html.br +0 -0
  128. codex/static_root/pwa/offline.html.gz +0 -0
  129. codex/static_root/staticfiles.json +1 -1
  130. codex/threads.py +1 -1
  131. codex/views/admin/api_key.py +3 -1
  132. codex/views/admin/flag.py +3 -1
  133. codex/views/admin/group.py +3 -1
  134. codex/views/admin/library.py +5 -4
  135. codex/views/admin/stats.py +10 -6
  136. codex/views/admin/tasks.py +35 -30
  137. codex/views/admin/user.py +4 -2
  138. codex/views/bookmark.py +6 -4
  139. codex/views/browser/base.py +30 -28
  140. codex/views/browser/browser.py +78 -80
  141. codex/views/browser/browser_annotations.py +15 -10
  142. codex/views/browser/browser_order_by.py +21 -16
  143. codex/views/browser/choices.py +37 -22
  144. codex/views/browser/filters/search.py +19 -16
  145. codex/views/browser/metadata.py +50 -41
  146. codex/views/cover.py +3 -1
  147. codex/views/download.py +4 -2
  148. codex/views/frontend.py +3 -2
  149. codex/views/mixins.py +13 -9
  150. codex/views/opds/authentication_v1.py +45 -41
  151. codex/views/opds/const.py +20 -13
  152. codex/views/opds/v1/entry/data.py +2 -1
  153. codex/views/opds/v1/facets.py +2 -1
  154. codex/views/opds/v1/feed.py +11 -4
  155. codex/views/opds/v1/links.py +8 -6
  156. codex/views/opds/v1/opensearch_v1.py +1 -1
  157. codex/views/opds/v2/feed.py +2 -1
  158. codex/views/opds/v2/publications.py +15 -12
  159. codex/views/reader/page.py +1 -1
  160. codex/views/session.py +50 -43
  161. codex/views/template.py +2 -2
  162. codex/websockets/listener.py +10 -7
  163. {codex-1.4.1.dist-info → codex-1.4.3.dist-info}/METADATA +24 -28
  164. {codex-1.4.1.dist-info → codex-1.4.3.dist-info}/RECORD +167 -167
  165. {codex-1.4.1.dist-info → codex-1.4.3.dist-info}/WHEEL +1 -1
  166. codex/static_root/assets/admin-12749881.ef0f50bac290.js +0 -41
  167. codex/static_root/assets/admin-12749881.ef0f50bac290.js.br +0 -0
  168. codex/static_root/assets/admin-12749881.ef0f50bac290.js.gz +0 -0
  169. codex/static_root/assets/admin-12749881.js +0 -41
  170. codex/static_root/assets/admin-12749881.js.br +0 -0
  171. codex/static_root/assets/admin-12749881.js.gz +0 -0
  172. codex/static_root/assets/admin-beda768d.a614eee46307.css +0 -1
  173. codex/static_root/assets/admin-beda768d.a614eee46307.css.br +0 -0
  174. codex/static_root/assets/admin-beda768d.a614eee46307.css.gz +0 -0
  175. codex/static_root/assets/admin-beda768d.css +0 -1
  176. codex/static_root/assets/admin-beda768d.css.br +0 -0
  177. codex/static_root/assets/admin-beda768d.css.gz +0 -0
  178. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.br +0 -0
  179. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.gz +0 -0
  180. codex/static_root/assets/admin-drawer-panel-522f1e6c.js.br +0 -0
  181. codex/static_root/assets/admin-drawer-panel-522f1e6c.js.gz +0 -0
  182. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.br +0 -0
  183. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.gz +0 -0
  184. codex/static_root/assets/browser-7f7d7134.css.br +0 -0
  185. codex/static_root/assets/browser-7f7d7134.css.gz +0 -0
  186. codex/static_root/assets/browser-af622672.d51aca96d64d.js +0 -1
  187. codex/static_root/assets/browser-af622672.d51aca96d64d.js.br +0 -0
  188. codex/static_root/assets/browser-af622672.d51aca96d64d.js.gz +0 -0
  189. codex/static_root/assets/browser-af622672.js +0 -1
  190. codex/static_root/assets/browser-af622672.js.br +0 -0
  191. codex/static_root/assets/browser-af622672.js.gz +0 -0
  192. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.br +0 -0
  193. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.gz +0 -0
  194. codex/static_root/assets/http-error-5e17b794.js.br +0 -0
  195. codex/static_root/assets/http-error-5e17b794.js.gz +0 -0
  196. codex/static_root/assets/main-0898f4bb.181e0145c642.css.br +0 -0
  197. codex/static_root/assets/main-0898f4bb.css.br +0 -0
  198. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js +0 -1
  199. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.br +0 -0
  200. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.gz +0 -0
  201. codex/static_root/assets/main-9e76a4c3.js +0 -1
  202. codex/static_root/assets/main-9e76a4c3.js.br +0 -0
  203. codex/static_root/assets/main-9e76a4c3.js.gz +0 -0
  204. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js +0 -1
  205. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.br +0 -0
  206. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.gz +0 -0
  207. codex/static_root/assets/metadata-dialog-62c29ce0.js +0 -1
  208. codex/static_root/assets/metadata-dialog-62c29ce0.js.br +0 -0
  209. codex/static_root/assets/metadata-dialog-62c29ce0.js.gz +0 -0
  210. codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css +0 -1
  211. codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.br +0 -0
  212. codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.gz +0 -0
  213. codex/static_root/assets/metadata-dialog-cb306ffd.css +0 -1
  214. codex/static_root/assets/metadata-dialog-cb306ffd.css.br +0 -0
  215. codex/static_root/assets/metadata-dialog-cb306ffd.css.gz +0 -0
  216. codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.br +0 -0
  217. codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.gz +0 -0
  218. codex/static_root/assets/page-pdf-157ba97e.js.br +0 -0
  219. codex/static_root/assets/page-pdf-157ba97e.js.gz +0 -0
  220. codex/static_root/assets/reader-36266549.0b2cf1291f27.js +0 -1
  221. codex/static_root/assets/reader-36266549.0b2cf1291f27.js.br +0 -0
  222. codex/static_root/assets/reader-36266549.0b2cf1291f27.js.gz +0 -0
  223. codex/static_root/assets/reader-36266549.js +0 -1
  224. codex/static_root/assets/reader-36266549.js.br +0 -0
  225. codex/static_root/assets/reader-36266549.js.gz +0 -0
  226. codex/static_root/assets/reader-7f004141.506eecc6954b.css +0 -1
  227. codex/static_root/assets/reader-7f004141.506eecc6954b.css.br +0 -0
  228. codex/static_root/assets/reader-7f004141.506eecc6954b.css.gz +0 -0
  229. codex/static_root/assets/reader-7f004141.css +0 -1
  230. codex/static_root/assets/reader-7f004141.css.br +0 -0
  231. codex/static_root/assets/reader-7f004141.css.gz +0 -0
  232. codex/static_root/manifest.d2f93a519ada.json.br +0 -0
  233. codex/static_root/manifest.d2f93a519ada.json.gz +0 -0
  234. codex/static_root/pwa/offline.37a4206d79f0.html.br +0 -0
  235. codex/static_root/pwa/offline.37a4206d79f0.html.gz +0 -0
  236. {codex-1.4.1.dist-info → codex-1.4.3.dist-info}/LICENSE +0 -0
  237. {codex-1.4.1.dist-info → codex-1.4.3.dist-info}/entry_points.txt +0 -0
codex/views/bookmark.py CHANGED
@@ -1,4 +1,6 @@
1
1
  """Bookmark views."""
2
+ from typing import ClassVar
3
+
2
4
  from drf_spectacular.utils import extend_schema
3
5
  from rest_framework.generics import GenericAPIView
4
6
  from rest_framework.response import Response
@@ -15,16 +17,16 @@ LOG = get_logger(__name__)
15
17
  class BookmarkBaseView(GenericAPIView, GroupACLMixin):
16
18
  """Bookmark Updater."""
17
19
 
18
- permission_classes = [IsAuthenticatedOrEnabledNonUsers]
19
- _BOOKMARK_UPDATE_FIELDS = [
20
+ permission_classes: ClassVar[list] = [IsAuthenticatedOrEnabledNonUsers]
21
+ _BOOKMARK_UPDATE_FIELDS = (
20
22
  "page",
21
23
  "finished",
22
24
  "fit_to",
23
25
  "two_pages",
24
26
  "vertical",
25
27
  "read_in_reverse",
26
- ]
27
- _BOOKMARK_ONLY_FIELDS = [*_BOOKMARK_UPDATE_FIELDS, "pk", "comic"]
28
+ )
29
+ _BOOKMARK_ONLY_FIELDS = (*_BOOKMARK_UPDATE_FIELDS, "pk", "comic")
28
30
  _COMIC_ONLY_FIELDS = ("pk", "max_page")
29
31
 
30
32
  def get_bookmark_filter(self):
@@ -1,6 +1,7 @@
1
1
  """Views for browsing comic library."""
2
2
  import json
3
3
  from copy import deepcopy
4
+ from types import MappingProxyType
4
5
  from urllib.parse import unquote_plus
5
6
 
6
7
  from django.contrib.auth.models import User
@@ -8,7 +9,15 @@ from djangorestframework_camel_case.settings import api_settings
8
9
  from djangorestframework_camel_case.util import underscoreize
9
10
 
10
11
  from codex.logger.logging import get_logger
11
- from codex.models import Comic, Folder, Imprint, Publisher, Series, StoryArc, Volume
12
+ from codex.models import (
13
+ Comic,
14
+ Folder,
15
+ Imprint,
16
+ Publisher,
17
+ Series,
18
+ StoryArc,
19
+ Volume,
20
+ )
12
21
  from codex.serializers.browser import BrowserSettingsSerializer
13
22
  from codex.views.browser.filters.bookmark import BookmarkFilterMixin
14
23
  from codex.views.browser.filters.field import ComicFieldFilter
@@ -25,16 +34,18 @@ class BrowserBaseView(
25
34
 
26
35
  input_serializer_class = BrowserSettingsSerializer
27
36
 
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
- }
37
+ GROUP_MODEL_MAP = MappingProxyType(
38
+ {
39
+ GroupFilterMixin.ROOT_GROUP: None,
40
+ "p": Publisher,
41
+ "i": Imprint,
42
+ "s": Series,
43
+ "v": Volume,
44
+ GroupFilterMixin.COMIC_GROUP: Comic,
45
+ GroupFilterMixin.FOLDER_GROUP: Folder,
46
+ GroupFilterMixin.STORY_ARC_GROUP: StoryArc,
47
+ }
48
+ )
38
49
 
39
50
  _GET_JSON_KEYS = frozenset(("filters", "show"))
40
51
 
@@ -51,26 +62,19 @@ class BrowserBaseView(
51
62
  self._is_admin = user and isinstance(user, User) and user.is_staff
52
63
  return self._is_admin
53
64
 
54
- def get_query_filters_without_group(self, model):
65
+ def get_query_filters_without_group(self, model, search_scores: dict):
55
66
  """Return all the filters except the group filter."""
56
67
  object_filter = self.get_group_acl_filter(model)
57
-
58
- search_filter, search_scores = self.get_search_filter()
59
- object_filter &= search_filter
68
+ object_filter &= self.get_search_filter(model, search_scores)
60
69
  object_filter &= self.get_bookmark_filter(model)
61
70
  object_filter &= self.get_comic_field_filter()
62
- return object_filter, search_scores
71
+ return object_filter
63
72
 
64
- def get_query_filters(self, model, choices=False):
73
+ def get_query_filters(self, model, search_scores: dict, choices=False):
65
74
  """Return the main object filter and the one for aggregates."""
66
- (
67
- object_filter,
68
- search_scores,
69
- ) = self.get_query_filters_without_group(model)
70
-
75
+ object_filter = self.get_query_filters_without_group(model, search_scores)
71
76
  object_filter &= self.get_group_filter(choices)
72
-
73
- return object_filter, search_scores
77
+ return object_filter
74
78
 
75
79
  def _parse_query_params(self, query_params):
76
80
  """Parse GET query parameters: filter object & snake case."""
@@ -85,13 +89,11 @@ class BrowserBaseView(
85
89
  parsed_val = val
86
90
 
87
91
  result[key] = parsed_val
88
- result = underscoreize(result, **api_settings.JSON_UNDERSCOREIZE)
89
-
90
- return result
92
+ return underscoreize(result, **api_settings.JSON_UNDERSCOREIZE)
91
93
 
92
94
  def parse_params(self):
93
95
  """Validate sbmitted settings and apply them over the session settings."""
94
- self.params = deepcopy(self.SESSION_DEFAULTS)
96
+ self.params = deepcopy(dict(self.SESSION_DEFAULTS))
95
97
  if self.request.method == "GET":
96
98
  data = self._parse_query_params(self.request.GET)
97
99
  elif hasattr(self.request, "data"):
@@ -1,9 +1,10 @@
1
1
  """Views for browsing comic library."""
2
2
  from copy import deepcopy
3
- from typing import Optional, Union
3
+ from types import MappingProxyType
4
+ from typing import ClassVar, Optional
4
5
 
5
6
  from django.core.paginator import EmptyPage, Paginator
6
- from django.db.models import F, Max, Q, Value
7
+ from django.db.models import F, Max, Value
7
8
  from django.db.models.fields import (
8
9
  CharField,
9
10
  )
@@ -14,6 +15,7 @@ from codex.exceptions import SeeOtherRedirectError
14
15
  from codex.logger.logging import get_logger
15
16
  from codex.models import (
16
17
  AdminFlag,
18
+ BrowserGroupModel,
17
19
  Comic,
18
20
  Folder,
19
21
  Imprint,
@@ -35,28 +37,34 @@ LOG = get_logger(__name__)
35
37
  class BrowserView(BrowserAnnotationsView):
36
38
  """Browse comics with a variety of filters and sorts."""
37
39
 
38
- permission_classes = [IsAuthenticatedOrEnabledNonUsers]
40
+ permission_classes: ClassVar[list] = [IsAuthenticatedOrEnabledNonUsers]
39
41
  serializer_class = BrowserPageSerializer
40
42
 
41
43
  MAX_OBJ_PER_PAGE = 100
42
- _MODEL_GROUP_MAP = {v: k for k, v in BrowserAnnotationsView.GROUP_MODEL_MAP.items()}
44
+ _MODEL_GROUP_MAP = MappingProxyType(
45
+ {v: k for k, v in BrowserAnnotationsView.GROUP_MODEL_MAP.items()}
46
+ )
43
47
  _NAV_GROUPS = "rpisv"
44
48
  _ORPHANS = int(MAX_OBJ_PER_PAGE / 20)
45
49
 
46
- _GROUP_INSTANCE_SELECT_RELATED = {
47
- Comic: ("series", "volume"),
48
- Volume: ("series",),
49
- Series: (None,),
50
- Imprint: ("publisher",),
51
- Publisher: (None,),
52
- Folder: ("parent_folder",),
53
- StoryArc: (None,),
54
- }
50
+ _GROUP_INSTANCE_SELECT_RELATED = MappingProxyType(
51
+ {
52
+ Comic: ("series", "volume"),
53
+ Volume: ("series",),
54
+ Series: (None,),
55
+ Imprint: ("publisher",),
56
+ Publisher: (None,),
57
+ Folder: ("parent_folder",),
58
+ StoryArc: (None,),
59
+ }
60
+ )
55
61
  DEFAULT_ROUTE_NAME = "browser"
56
- _DEFAULT_ROUTE = {
57
- "name": DEFAULT_ROUTE_NAME,
58
- "params": deepcopy(DEFAULTS["route"]),
59
- }
62
+ _DEFAULT_ROUTE = MappingProxyType(
63
+ {
64
+ "name": DEFAULT_ROUTE_NAME,
65
+ "params": deepcopy(DEFAULTS["route"]),
66
+ }
67
+ )
60
68
  _OPDS_M2M_RELS = (
61
69
  "characters",
62
70
  "genres",
@@ -91,7 +99,7 @@ class BrowserView(BrowserAnnotationsView):
91
99
  group_names["publisher_name"] = F("publisher__name")
92
100
  return queryset.annotate(**group_names)
93
101
 
94
- def _add_annotations(self, queryset, model, search_scores):
102
+ def _add_annotations(self, queryset, model, search_scores: dict):
95
103
  """Annotations for display and sorting."""
96
104
  queryset = self.annotate_common_aggregates(queryset, model, search_scores)
97
105
 
@@ -127,30 +135,26 @@ class BrowserView(BrowserAnnotationsView):
127
135
  model_group = self._get_model_group()
128
136
  self.model = self.GROUP_MODEL_MAP[model_group]
129
137
 
130
- def _get_group_queryset(self, object_filter, search_scores):
138
+ def _get_group_queryset(self, search_scores: dict):
131
139
  """Create group queryset."""
132
- if self.model == Comic:
133
- qs = self.model.objects.none()
134
- else:
140
+ if not self.model:
141
+ reason = "Model not set in browser."
142
+ raise ValueError(reason)
143
+ if self.model != Comic:
144
+ object_filter = self.get_query_filters(self.model, search_scores, False)
135
145
  qs = self.model.objects.filter(object_filter)
136
146
  qs = self._add_annotations(qs, self.model, search_scores)
137
147
  qs = self.add_order_by(qs, self.model)
148
+ else:
149
+ qs = self.model.objects.none()
138
150
  return qs
139
151
 
140
- def _get_book_queryset(self, object_filter, search_scores):
152
+ def _get_book_queryset(self, search_scores: dict):
141
153
  """Create book queryset."""
142
- group = self.kwargs.get("group")
143
- if self.model == Comic or group == self.FOLDER_GROUP:
144
- if group == self.FOLDER_GROUP:
145
- comic_object_filter, comic_search_scores = self.get_query_filters(
146
- self.model, False
147
- )
148
- else:
149
- comic_object_filter = object_filter
150
- comic_search_scores = search_scores
151
-
152
- qs = Comic.objects.filter(comic_object_filter)
153
- qs = self._add_annotations(qs, Comic, comic_search_scores)
154
+ if self.model in (Comic, Folder):
155
+ object_filter = self.get_query_filters(Comic, search_scores, False)
156
+ qs = Comic.objects.filter(object_filter)
157
+ qs = self._add_annotations(qs, Comic, search_scores)
154
158
  qs = self.add_order_by(qs, Comic)
155
159
  if limit := self.params.get("limit"):
156
160
  qs = qs[:limit]
@@ -182,20 +186,20 @@ class BrowserView(BrowserAnnotationsView):
182
186
  def _set_group_instance(self):
183
187
  """Create group_class instance."""
184
188
  pk = self.kwargs.get("pk")
185
- self.group_instance: Optional[
186
- Union[Folder, Publisher, Imprint, Series, Volume, StoryArc]
187
- ] = None
188
- if not pk:
189
- return
190
- try:
191
- select_related = self._GROUP_INSTANCE_SELECT_RELATED[self.group_class]
192
- self.group_instance = self.group_class.objects.select_related(
193
- *select_related
194
- ).get(pk=pk)
195
- except self.group_class.DoesNotExist:
196
- group = self.kwargs.get("group")
197
- reason = f"{group}={pk} does not exist!"
198
- self._raise_redirect({"group": group, "pk": 0, "page": 1}, reason)
189
+ if pk and self.group_class:
190
+ try:
191
+ select_related: tuple[str, ...] = self._GROUP_INSTANCE_SELECT_RELATED[
192
+ self.group_class
193
+ ] # type: ignore
194
+ self.group_instance: Optional[
195
+ BrowserGroupModel
196
+ ] = self.group_class.objects.select_related(*select_related).get(pk=pk)
197
+ except self.group_class.DoesNotExist:
198
+ group = self.kwargs.get("group")
199
+ reason = f"{group}={pk} does not exist!"
200
+ self._raise_redirect({"group": group, "pk": 0, "page": 1}, reason)
201
+ else:
202
+ self.group_instance: Optional[BrowserGroupModel] = None
199
203
 
200
204
  def _get_browse_up_route(self):
201
205
  """Get the up route from the first valid ancestor."""
@@ -247,8 +251,7 @@ class BrowserView(BrowserAnnotationsView):
247
251
  # remove library path for not admins
248
252
  parent_name = parent_name.removeprefix(prefix)
249
253
  suffix = "/" + group_instance.name
250
- parent_name = parent_name.removesuffix(suffix)
251
- return parent_name
254
+ return parent_name.removesuffix(suffix)
252
255
 
253
256
  def _get_browser_page_title(self):
254
257
  """Get the proper title for this browse level."""
@@ -298,7 +301,8 @@ class BrowserView(BrowserAnnotationsView):
298
301
  except EmptyPage:
299
302
  if page < 1 or page > paginator.num_pages:
300
303
  self._page_out_out_bounds(page, paginator.num_pages)
301
- LOG.warning(f"No {self.model.__class__.__name__}s on page {page}")
304
+ model_name = self.model.__name__ if self.model else "NO_MODEL"
305
+ LOG.warning(f"No {model_name}s on page {page}")
302
306
  model.objects.none()
303
307
 
304
308
  return qs, num_pages, total_count
@@ -329,20 +333,11 @@ class BrowserView(BrowserAnnotationsView):
329
333
  self.set_rel_prefix(self.model)
330
334
  self._set_group_instance() # Placed up here to invalidate earlier
331
335
  # Create the main query with the filters
332
- try:
333
- object_filter, search_scores = self.get_query_filters(self.model, False)
334
- except Folder.DoesNotExist:
335
- pk = self.kwargs.get("pk")
336
- self._raise_redirect(
337
- {"group": self.FOLDER_GROUP},
338
- f"folder {pk} Does not exist! Redirect to root folder.",
339
- )
340
- object_filter = Q()
341
- search_scores = {}
336
+ search_scores: dict = self.get_search_scores()
342
337
  group = self.kwargs.get("group")
343
338
 
344
- group_qs = self._get_group_queryset(object_filter, search_scores)
345
- book_qs = self._get_book_queryset(object_filter, search_scores)
339
+ group_qs = self._get_group_queryset(search_scores)
340
+ book_qs = self._get_book_queryset(search_scores)
346
341
 
347
342
  # Paginate
348
343
  group_qs, book_qs, num_pages, total_count = self._paginate(group_qs, book_qs)
@@ -377,19 +372,21 @@ class BrowserView(BrowserAnnotationsView):
377
372
  )
378
373
 
379
374
  # construct final data structure
380
- return {
381
- "up_route": up_route,
382
- "browser_title": browser_page_title,
383
- "model_group": self.model_group,
384
- "groups": group_qs,
385
- "books": book_qs,
386
- "issue_max": issue_max,
387
- "num_pages": num_pages,
388
- "total_count": total_count,
389
- "admin_flags": {"folder_view": efv_flag.on},
390
- "libraries_exist": libraries_exist,
391
- "covers_timestamp": covers_timestamp,
392
- }
375
+ return MappingProxyType(
376
+ {
377
+ "up_route": up_route,
378
+ "browser_title": browser_page_title,
379
+ "model_group": self.model_group,
380
+ "groups": group_qs,
381
+ "books": book_qs,
382
+ "issue_max": issue_max,
383
+ "num_pages": num_pages,
384
+ "total_count": total_count,
385
+ "admin_flags": {"folder_view": efv_flag.on},
386
+ "libraries_exist": libraries_exist,
387
+ "covers_timestamp": covers_timestamp,
388
+ }
389
+ )
393
390
 
394
391
  def _get_valid_top_groups(self):
395
392
  """Get valid top groups for the current settings.
@@ -398,8 +395,9 @@ class BrowserView(BrowserAnnotationsView):
398
395
  """
399
396
  valid_top_groups = []
400
397
 
398
+ show: MappingProxyType = self.params["show"] # type:ignore
401
399
  for nav_group in self._NAV_GROUPS:
402
- if self.params["show"].get(nav_group):
400
+ if show.get(nav_group):
403
401
  valid_top_groups.append(nav_group)
404
402
  # Issues is always a valid top group
405
403
  valid_top_groups += [self.COMIC_GROUP]
@@ -436,8 +434,8 @@ class BrowserView(BrowserAnnotationsView):
436
434
 
437
435
  def _raise_redirect(self, route_mask, reason, settings_mask=None):
438
436
  """Redirect the client to a valid group url."""
439
- route = deepcopy(self._DEFAULT_ROUTE)
440
- route["params"].update(route_mask)
437
+ route = deepcopy(dict(self._DEFAULT_ROUTE))
438
+ route["params"].update(route_mask) # type: ignore
441
439
  settings = deepcopy(self.params)
442
440
  if settings_mask:
443
441
  settings.update(settings_mask)
@@ -9,6 +9,7 @@ from django.db.models import (
9
9
  DateTimeField,
10
10
  F,
11
11
  FilteredRelation,
12
+ Max,
12
13
  Min,
13
14
  OuterRef,
14
15
  Q,
@@ -49,15 +50,19 @@ class BrowserAnnotationsView(BrowserOrderByView):
49
50
 
50
51
  is_opds_1_acquisition = False
51
52
 
52
- def _annotate_search_score(self, queryset, search_scores):
53
- """Annotate the search score for ordering by search score."""
53
+ def _annotate_search_score(self, queryset, search_scores, model):
54
+ """Annotate the search score for ordering by search score.
55
+
56
+ Choose the maximum matching score for the group.
57
+ """
54
58
  if self.order_key != "search_score":
55
59
  return queryset
56
60
  whens = []
61
+ prefix = "" if model == Comic else self.rel_prefix
57
62
  for pk, score in search_scores.items():
58
- when = {self.rel_prefix + "pk": pk, "then": score}
63
+ when = {prefix + "pk": pk, "then": score}
59
64
  whens.append(When(**when))
60
- annotate = {self.rel_prefix + "search_score": Case(*whens, default=0.0)}
65
+ annotate = {"search_score": Max(Case(*whens, default=0.0))}
61
66
  return queryset.annotate(**annotate)
62
67
 
63
68
  def _annotate_cover_pk(self, queryset, model):
@@ -116,14 +121,13 @@ class BrowserAnnotationsView(BrowserOrderByView):
116
121
 
117
122
  if self.kwargs.get("group") == self.FOLDER_GROUP and model == Comic:
118
123
  # File View Filename
119
- queryset = queryset.annotate(
124
+ return queryset.annotate(
120
125
  sort_name=Right(
121
126
  "path",
122
127
  StrIndex(Reverse(F("path")), Value(sep)) - 1, # type: ignore
123
128
  output_field=CharField(),
124
129
  )
125
130
  )
126
- return queryset
127
131
 
128
132
  ##################################################
129
133
  # Otherwise Remove articles from the browse name #
@@ -168,7 +172,9 @@ class BrowserAnnotationsView(BrowserOrderByView):
168
172
  pk = self.kwargs["pk"]
169
173
  if group == self.STORY_ARC_GROUP and pk:
170
174
  story_arc_pk = pk
171
- elif story_arc_pks := self.params.get("filters", {}).get("story_arcs", []):
175
+ elif story_arc_pks := self.params.get("filters", {}).get( # type: ignore
176
+ "story_arcs", []
177
+ ):
172
178
  story_arc_pk = story_arc_pks[0]
173
179
  else:
174
180
  story_arc_pk = None
@@ -261,7 +267,7 @@ class BrowserAnnotationsView(BrowserOrderByView):
261
267
 
262
268
  def annotate_common_aggregates(self, qs, model, search_scores):
263
269
  """Annotate common aggregates between browser and metadata."""
264
- qs = self._annotate_search_score(qs, search_scores)
270
+ qs = self._annotate_search_score(qs, search_scores, model)
265
271
  qs = self._annotate_child_count(qs, model)
266
272
  qs = self._annotate_page_count(qs, model)
267
273
  bm_rel = self.get_bm_rel(model)
@@ -273,5 +279,4 @@ class BrowserAnnotationsView(BrowserOrderByView):
273
279
  # cover depends on the above annotations for order-by
274
280
  qs = self._annotate_cover_pk(qs, model)
275
281
  qs = self._annotate_bookmarks(qs, model, bm_rel, bm_filter)
276
- qs = self._annotate_progress(qs)
277
- return qs
282
+ return self._annotate_progress(qs)
@@ -1,5 +1,6 @@
1
1
  """Base view for ordering the query."""
2
2
  from os import sep
3
+ from types import MappingProxyType
3
4
 
4
5
  from django.db.models import Avg, F, Max, Min, Sum, Value
5
6
  from django.db.models.functions import Reverse, Right, StrIndex
@@ -11,21 +12,25 @@ from codex.views.browser.base import BrowserBaseView
11
12
  class BrowserOrderByView(BrowserBaseView):
12
13
  """Base class for views that need ordering."""
13
14
 
14
- _ORDER_AGGREGATE_FUNCS = {
15
- "age_rating": Avg,
16
- "community_rating": Avg,
17
- "created_at": Min,
18
- "critical_rating": Avg,
19
- "date": Min,
20
- "page_count": Sum,
21
- "path": Min,
22
- "size": Sum,
23
- "updated_at": Min,
24
- "search_score": Min,
25
- "story_arc_number": Min,
26
- }
15
+ _ORDER_AGGREGATE_FUNCS = MappingProxyType(
16
+ {
17
+ "age_rating": Avg,
18
+ "community_rating": Avg,
19
+ "created_at": Min,
20
+ "critical_rating": Avg,
21
+ "date": Min,
22
+ "page_count": Sum,
23
+ "path": Min,
24
+ "size": Sum,
25
+ "updated_at": Min,
26
+ "search_score": Min,
27
+ "story_arc_number": Min,
28
+ }
29
+ )
27
30
  _SEP_VALUE = Value(sep)
28
- _ANNOTATED_ORDER_FIELDS = frozenset(("sort_name", "bookmark_updated_at"))
31
+ _ANNOTATED_ORDER_FIELDS = frozenset(
32
+ ("sort_name", "search_score", "bookmark_updated_at")
33
+ )
29
34
 
30
35
  def set_order_key(self):
31
36
  """Get the default order key for the view."""
@@ -70,7 +75,7 @@ class BrowserOrderByView(BrowserBaseView):
70
75
  func = self._get_path_query_func(self.order_key)
71
76
  elif model == Comic or self.order_key in self._ANNOTATED_ORDER_FIELDS:
72
77
  # agg_none uses group fields not comic fields.
73
- func = F(self.order_key)
78
+ func = F(self.order_key) # type: ignore
74
79
  else:
75
80
  func = self.get_aggregate_func(model, self.order_key)
76
81
  return func
@@ -82,7 +87,7 @@ class BrowserOrderByView(BrowserBaseView):
82
87
  prefix += "-"
83
88
 
84
89
  if self.order_key == "sort_name":
85
- order_fields = ("order_value", *model.ORDERING[1:])
90
+ order_fields = model.ORDERING
86
91
  elif self.order_key == "bookmark_updated_at":
87
92
  order_fields = ("order_value", "updated_at", "created_at", "pk")
88
93
  elif self.order_key == "story_arc_number" and model == Comic:
@@ -1,4 +1,7 @@
1
1
  """View for marking comics read and unread."""
2
+ from types import MappingProxyType
3
+ from typing import ClassVar, Union
4
+
2
5
  import pycountry
3
6
  from caseconverter import snakecase
4
7
  from django.db.models import QuerySet
@@ -7,6 +10,7 @@ from rest_framework.response import Response
7
10
 
8
11
  from codex.logger.logging import get_logger
9
12
  from codex.models import (
13
+ BrowserGroupModel,
10
14
  Comic,
11
15
  CreatorPerson,
12
16
  Folder,
@@ -30,21 +34,25 @@ LOG = get_logger(__name__)
30
34
  class BrowserChoicesViewBase(BrowserBaseView):
31
35
  """Get choices for filter dialog."""
32
36
 
33
- permission_classes = [IsAuthenticatedOrEnabledNonUsers]
37
+ permission_classes: ClassVar[list] = [IsAuthenticatedOrEnabledNonUsers]
34
38
 
35
39
  _CREATORS_PERSON_REL = "creators__person"
36
40
  _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
- }
41
+ _NULL_NAMED_ROW = MappingProxyType({"pk": -1, "name": "_none_"})
42
+ _BACK_REL_MAP = MappingProxyType(
43
+ {CreatorPerson: "creator__", StoryArc: "storyarcnumber__"}
44
+ )
45
+ _REL_MAP = MappingProxyType(
46
+ {
47
+ Publisher: "publisher",
48
+ Imprint: "imprint",
49
+ Series: "series",
50
+ Volume: "volume",
51
+ Comic: "pk",
52
+ Folder: "parent_folder",
53
+ StoryArc: "story_arc_numbers__story_arc",
54
+ }
55
+ )
48
56
 
49
57
  @staticmethod
50
58
  def get_field_choices_query(comic_qs, field_name):
@@ -57,13 +65,17 @@ class BrowserChoicesViewBase(BrowserBaseView):
57
65
 
58
66
  def get_m2m_field_query(self, model, comic_qs: QuerySet):
59
67
  """Get distinct m2m value objects for the relation."""
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"
64
- return (
65
- model.objects.filter(**{back_rel: comic_qs}).values("pk", "name").distinct()
66
- )
68
+ if self.model is None:
69
+ LOG.error("No model to make filter choices!")
70
+ m2m_filter = {}
71
+ else:
72
+ model_rel: str = self._REL_MAP[self.model] # type: ignore
73
+ back_rel = self._BACK_REL_MAP.get(model, "")
74
+ back_rel += "comic__"
75
+ back_rel += model_rel
76
+ back_rel += "__in"
77
+ m2m_filter = {back_rel: comic_qs}
78
+ return model.objects.filter(**m2m_filter).values("pk", "name").distinct()
67
79
 
68
80
  @staticmethod
69
81
  def does_m2m_null_exist(comic_qs, rel):
@@ -91,15 +103,18 @@ class BrowserChoicesViewBase(BrowserBaseView):
91
103
 
92
104
  def get_object(self):
93
105
  """Get the comic subquery use for the choices."""
94
- object_filter, _ = self.get_query_filters(self.model, True)
95
- return self.model.objects.filter(object_filter)
106
+ search_scores = self.get_search_scores()
107
+ object_filter = self.get_query_filters(self.model, search_scores, True)
108
+ return self.model.objects.filter(object_filter) # type: ignore
96
109
 
97
110
  def _set_model(self):
98
111
  """Set the model to query."""
99
112
  group = self.kwargs["group"]
100
113
  if group == self.ROOT_GROUP:
101
114
  group = self.params.get("top_group", "p")
102
- self.model = self.GROUP_MODEL_MAP[group]
115
+ self.model: Union[BrowserGroupModel, Comic, None] = self.GROUP_MODEL_MAP[
116
+ group
117
+ ] # type: ignore
103
118
 
104
119
 
105
120
  class BrowserChoicesAvailableView(BrowserChoicesViewBase):