codex 1.4.2__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-be9c2e22.33b594bc8ee7.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-be9c2e22.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-dc8c7804.7f60dfea2ea4.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-dc8c7804.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-e4b426e6.6fb67bd8d938.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-e4b426e6.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-804658a5.112a80d295b8.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-804658a5.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.3ee495a508d6.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 +26 -17
  140. codex/views/browser/browser.py +67 -53
  141. codex/views/browser/browser_annotations.py +5 -5
  142. codex/views/browser/browser_order_by.py +18 -15
  143. codex/views/browser/choices.py +37 -22
  144. codex/views/browser/filters/search.py +3 -3
  145. codex/views/browser/metadata.py +49 -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.2.dist-info → codex-1.4.3.dist-info}/METADATA +24 -28
  164. {codex-1.4.2.dist-info → codex-1.4.3.dist-info}/RECORD +167 -167
  165. {codex-1.4.2.dist-info → codex-1.4.3.dist-info}/WHEEL +1 -1
  166. codex/static_root/assets/admin-a34bbd42.9722989091b5.js +0 -41
  167. codex/static_root/assets/admin-a34bbd42.9722989091b5.js.br +0 -0
  168. codex/static_root/assets/admin-a34bbd42.9722989091b5.js.gz +0 -0
  169. codex/static_root/assets/admin-a34bbd42.js +0 -41
  170. codex/static_root/assets/admin-a34bbd42.js.br +0 -0
  171. codex/static_root/assets/admin-a34bbd42.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-be9c2e22.33b594bc8ee7.js.br +0 -0
  179. codex/static_root/assets/admin-drawer-panel-be9c2e22.33b594bc8ee7.js.gz +0 -0
  180. codex/static_root/assets/admin-drawer-panel-be9c2e22.js.br +0 -0
  181. codex/static_root/assets/admin-drawer-panel-be9c2e22.js.gz +0 -0
  182. codex/static_root/assets/browser-349d0e1c.a4b979e545a7.js +0 -1
  183. codex/static_root/assets/browser-349d0e1c.a4b979e545a7.js.br +0 -0
  184. codex/static_root/assets/browser-349d0e1c.a4b979e545a7.js.gz +0 -0
  185. codex/static_root/assets/browser-349d0e1c.js +0 -1
  186. codex/static_root/assets/browser-349d0e1c.js.br +0 -0
  187. codex/static_root/assets/browser-349d0e1c.js.gz +0 -0
  188. codex/static_root/assets/browser-dc8c7804.7f60dfea2ea4.css.br +0 -0
  189. codex/static_root/assets/browser-dc8c7804.7f60dfea2ea4.css.gz +0 -0
  190. codex/static_root/assets/browser-dc8c7804.css.br +0 -0
  191. codex/static_root/assets/browser-dc8c7804.css.gz +0 -0
  192. codex/static_root/assets/http-error-e4b426e6.6fb67bd8d938.js.br +0 -0
  193. codex/static_root/assets/http-error-e4b426e6.6fb67bd8d938.js.gz +0 -0
  194. codex/static_root/assets/http-error-e4b426e6.js.br +0 -0
  195. codex/static_root/assets/http-error-e4b426e6.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-1cd792f1.4ca9a2ab454b.js +0 -1
  199. codex/static_root/assets/main-1cd792f1.4ca9a2ab454b.js.br +0 -0
  200. codex/static_root/assets/main-1cd792f1.4ca9a2ab454b.js.gz +0 -0
  201. codex/static_root/assets/main-1cd792f1.js +0 -1
  202. codex/static_root/assets/main-1cd792f1.js.br +0 -0
  203. codex/static_root/assets/main-1cd792f1.js.gz +0 -0
  204. codex/static_root/assets/metadata-dialog-c4306743.4b1a3bd9e8be.js +0 -1
  205. codex/static_root/assets/metadata-dialog-c4306743.4b1a3bd9e8be.js.br +0 -0
  206. codex/static_root/assets/metadata-dialog-c4306743.4b1a3bd9e8be.js.gz +0 -0
  207. codex/static_root/assets/metadata-dialog-c4306743.js +0 -1
  208. codex/static_root/assets/metadata-dialog-c4306743.js.br +0 -0
  209. codex/static_root/assets/metadata-dialog-c4306743.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-804658a5.112a80d295b8.js.br +0 -0
  217. codex/static_root/assets/page-pdf-804658a5.112a80d295b8.js.gz +0 -0
  218. codex/static_root/assets/page-pdf-804658a5.js.br +0 -0
  219. codex/static_root/assets/page-pdf-804658a5.js.gz +0 -0
  220. codex/static_root/assets/reader-2df231f9.7d07a31e0fbf.css +0 -1
  221. codex/static_root/assets/reader-2df231f9.7d07a31e0fbf.css.br +0 -0
  222. codex/static_root/assets/reader-2df231f9.7d07a31e0fbf.css.gz +0 -0
  223. codex/static_root/assets/reader-2df231f9.css +0 -1
  224. codex/static_root/assets/reader-2df231f9.css.br +0 -0
  225. codex/static_root/assets/reader-2df231f9.css.gz +0 -0
  226. codex/static_root/assets/reader-75d90a0f.40ac454e2359.js +0 -1
  227. codex/static_root/assets/reader-75d90a0f.40ac454e2359.js.br +0 -0
  228. codex/static_root/assets/reader-75d90a0f.40ac454e2359.js.gz +0 -0
  229. codex/static_root/assets/reader-75d90a0f.js +0 -1
  230. codex/static_root/assets/reader-75d90a0f.js.br +0 -0
  231. codex/static_root/assets/reader-75d90a0f.js.gz +0 -0
  232. codex/static_root/manifest.3ee495a508d6.json.br +0 -0
  233. codex/static_root/manifest.3ee495a508d6.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.2.dist-info → codex-1.4.3.dist-info}/LICENSE +0 -0
  237. {codex-1.4.2.dist-info → codex-1.4.3.dist-info}/entry_points.txt +0 -0
codex/views/admin/user.py CHANGED
@@ -1,4 +1,6 @@
1
1
  """Admin User ViewSet."""
2
+ from typing import ClassVar
3
+
2
4
  from django.contrib.auth.models import User
3
5
  from django.contrib.auth.password_validation import validate_password
4
6
  from django.core.cache import cache
@@ -21,7 +23,7 @@ LOG = get_logger(__name__)
21
23
  class AdminUserViewSet(ModelViewSet):
22
24
  """User ViewSet."""
23
25
 
24
- permission_classes = [IsAdminUser]
26
+ permission_classes: ClassVar[list] = [IsAdminUser]
25
27
  queryset = User.objects.prefetch_related("groups").defer(
26
28
  "first_name", "last_name", "email"
27
29
  )
@@ -60,7 +62,7 @@ class AdminUserViewSet(ModelViewSet):
60
62
  class AdminUserChangePasswordView(GenericAPIView):
61
63
  """Special View to hash user password."""
62
64
 
63
- permission_classes = [IsAdminUser]
65
+ permission_classes: ClassVar[list] = [IsAdminUser]
64
66
  serializer_class = UserChangePasswordSerializer
65
67
 
66
68
  def put(self, request, *args, **kwargs):
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,7 +62,7 @@ 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, search_scores):
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
68
  object_filter &= self.get_search_filter(model, search_scores)
@@ -59,7 +70,7 @@ class BrowserBaseView(
59
70
  object_filter &= self.get_comic_field_filter()
60
71
  return object_filter
61
72
 
62
- def get_query_filters(self, model, search_scores, choices=False):
73
+ def get_query_filters(self, model, search_scores: dict, choices=False):
63
74
  """Return the main object filter and the one for aggregates."""
64
75
  object_filter = self.get_query_filters_without_group(model, search_scores)
65
76
  object_filter &= self.get_group_filter(choices)
@@ -78,13 +89,11 @@ class BrowserBaseView(
78
89
  parsed_val = val
79
90
 
80
91
  result[key] = parsed_val
81
- result = underscoreize(result, **api_settings.JSON_UNDERSCOREIZE)
82
-
83
- return result
92
+ return underscoreize(result, **api_settings.JSON_UNDERSCOREIZE)
84
93
 
85
94
  def parse_params(self):
86
95
  """Validate sbmitted settings and apply them over the session settings."""
87
- self.params = deepcopy(self.SESSION_DEFAULTS)
96
+ self.params = deepcopy(dict(self.SESSION_DEFAULTS))
88
97
  if self.request.method == "GET":
89
98
  data = self._parse_query_params(self.request.GET)
90
99
  elif hasattr(self.request, "data"):
@@ -1,6 +1,7 @@
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
7
  from django.db.models import F, Max, Value
@@ -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,8 +135,11 @@ 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, search_scores):
138
+ def _get_group_queryset(self, search_scores: dict):
131
139
  """Create group queryset."""
140
+ if not self.model:
141
+ reason = "Model not set in browser."
142
+ raise ValueError(reason)
132
143
  if self.model != Comic:
133
144
  object_filter = self.get_query_filters(self.model, search_scores, False)
134
145
  qs = self.model.objects.filter(object_filter)
@@ -138,7 +149,7 @@ class BrowserView(BrowserAnnotationsView):
138
149
  qs = self.model.objects.none()
139
150
  return qs
140
151
 
141
- def _get_book_queryset(self, search_scores):
152
+ def _get_book_queryset(self, search_scores: dict):
142
153
  """Create book queryset."""
143
154
  if self.model in (Comic, Folder):
144
155
  object_filter = self.get_query_filters(Comic, search_scores, False)
@@ -175,20 +186,20 @@ class BrowserView(BrowserAnnotationsView):
175
186
  def _set_group_instance(self):
176
187
  """Create group_class instance."""
177
188
  pk = self.kwargs.get("pk")
178
- self.group_instance: Optional[
179
- Union[Folder, Publisher, Imprint, Series, Volume, StoryArc]
180
- ] = None
181
- if not pk:
182
- return
183
- try:
184
- select_related = self._GROUP_INSTANCE_SELECT_RELATED[self.group_class]
185
- self.group_instance = self.group_class.objects.select_related(
186
- *select_related
187
- ).get(pk=pk)
188
- except self.group_class.DoesNotExist:
189
- group = self.kwargs.get("group")
190
- reason = f"{group}={pk} does not exist!"
191
- 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
192
203
 
193
204
  def _get_browse_up_route(self):
194
205
  """Get the up route from the first valid ancestor."""
@@ -240,8 +251,7 @@ class BrowserView(BrowserAnnotationsView):
240
251
  # remove library path for not admins
241
252
  parent_name = parent_name.removeprefix(prefix)
242
253
  suffix = "/" + group_instance.name
243
- parent_name = parent_name.removesuffix(suffix)
244
- return parent_name
254
+ return parent_name.removesuffix(suffix)
245
255
 
246
256
  def _get_browser_page_title(self):
247
257
  """Get the proper title for this browse level."""
@@ -291,7 +301,8 @@ class BrowserView(BrowserAnnotationsView):
291
301
  except EmptyPage:
292
302
  if page < 1 or page > paginator.num_pages:
293
303
  self._page_out_out_bounds(page, paginator.num_pages)
294
- 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}")
295
306
  model.objects.none()
296
307
 
297
308
  return qs, num_pages, total_count
@@ -322,7 +333,7 @@ class BrowserView(BrowserAnnotationsView):
322
333
  self.set_rel_prefix(self.model)
323
334
  self._set_group_instance() # Placed up here to invalidate earlier
324
335
  # Create the main query with the filters
325
- search_scores = self.get_search_scores()
336
+ search_scores: dict = self.get_search_scores()
326
337
  group = self.kwargs.get("group")
327
338
 
328
339
  group_qs = self._get_group_queryset(search_scores)
@@ -361,19 +372,21 @@ class BrowserView(BrowserAnnotationsView):
361
372
  )
362
373
 
363
374
  # construct final data structure
364
- return {
365
- "up_route": up_route,
366
- "browser_title": browser_page_title,
367
- "model_group": self.model_group,
368
- "groups": group_qs,
369
- "books": book_qs,
370
- "issue_max": issue_max,
371
- "num_pages": num_pages,
372
- "total_count": total_count,
373
- "admin_flags": {"folder_view": efv_flag.on},
374
- "libraries_exist": libraries_exist,
375
- "covers_timestamp": covers_timestamp,
376
- }
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
+ )
377
390
 
378
391
  def _get_valid_top_groups(self):
379
392
  """Get valid top groups for the current settings.
@@ -382,8 +395,9 @@ class BrowserView(BrowserAnnotationsView):
382
395
  """
383
396
  valid_top_groups = []
384
397
 
398
+ show: MappingProxyType = self.params["show"] # type:ignore
385
399
  for nav_group in self._NAV_GROUPS:
386
- if self.params["show"].get(nav_group):
400
+ if show.get(nav_group):
387
401
  valid_top_groups.append(nav_group)
388
402
  # Issues is always a valid top group
389
403
  valid_top_groups += [self.COMIC_GROUP]
@@ -420,8 +434,8 @@ class BrowserView(BrowserAnnotationsView):
420
434
 
421
435
  def _raise_redirect(self, route_mask, reason, settings_mask=None):
422
436
  """Redirect the client to a valid group url."""
423
- route = deepcopy(self._DEFAULT_ROUTE)
424
- route["params"].update(route_mask)
437
+ route = deepcopy(dict(self._DEFAULT_ROUTE))
438
+ route["params"].update(route_mask) # type: ignore
425
439
  settings = deepcopy(self.params)
426
440
  if settings_mask:
427
441
  settings.update(settings_mask)
@@ -121,14 +121,13 @@ class BrowserAnnotationsView(BrowserOrderByView):
121
121
 
122
122
  if self.kwargs.get("group") == self.FOLDER_GROUP and model == Comic:
123
123
  # File View Filename
124
- queryset = queryset.annotate(
124
+ return queryset.annotate(
125
125
  sort_name=Right(
126
126
  "path",
127
127
  StrIndex(Reverse(F("path")), Value(sep)) - 1, # type: ignore
128
128
  output_field=CharField(),
129
129
  )
130
130
  )
131
- return queryset
132
131
 
133
132
  ##################################################
134
133
  # Otherwise Remove articles from the browse name #
@@ -173,7 +172,9 @@ class BrowserAnnotationsView(BrowserOrderByView):
173
172
  pk = self.kwargs["pk"]
174
173
  if group == self.STORY_ARC_GROUP and pk:
175
174
  story_arc_pk = pk
176
- 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
+ ):
177
178
  story_arc_pk = story_arc_pks[0]
178
179
  else:
179
180
  story_arc_pk = None
@@ -278,5 +279,4 @@ class BrowserAnnotationsView(BrowserOrderByView):
278
279
  # cover depends on the above annotations for order-by
279
280
  qs = self._annotate_cover_pk(qs, model)
280
281
  qs = self._annotate_bookmarks(qs, model, bm_rel, bm_filter)
281
- qs = self._annotate_progress(qs)
282
- 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,19 +12,21 @@ 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
31
  _ANNOTATED_ORDER_FIELDS = frozenset(
29
32
  ("sort_name", "search_score", "bookmark_updated_at")
@@ -72,7 +75,7 @@ class BrowserOrderByView(BrowserBaseView):
72
75
  func = self._get_path_query_func(self.order_key)
73
76
  elif model == Comic or self.order_key in self._ANNOTATED_ORDER_FIELDS:
74
77
  # agg_none uses group fields not comic fields.
75
- func = F(self.order_key)
78
+ func = F(self.order_key) # type: ignore
76
79
  else:
77
80
  func = self.get_aggregate_func(model, self.order_key)
78
81
  return func
@@ -84,7 +87,7 @@ class BrowserOrderByView(BrowserBaseView):
84
87
  prefix += "-"
85
88
 
86
89
  if self.order_key == "sort_name":
87
- order_fields = ("order_value", *model.ORDERING[1:])
90
+ order_fields = model.ORDERING
88
91
  elif self.order_key == "bookmark_updated_at":
89
92
  order_fields = ("order_value", "updated_at", "created_at", "pk")
90
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):
@@ -11,7 +11,7 @@ LOG = get_logger(__name__)
11
11
  class SearchFilterMixin:
12
12
  """Search Filters Methods."""
13
13
 
14
- def get_search_scores(self):
14
+ def get_search_scores(self) -> dict:
15
15
  """Perform the search and return the scores as a dict."""
16
16
  search_scores = {}
17
17
  text = self.params.get("q", "") # type: ignore
@@ -34,14 +34,14 @@ class SearchFilterMixin:
34
34
  LOG.exception(exc)
35
35
  return search_scores
36
36
 
37
- def _get_search_query_filter(self, model, search_scores):
37
+ def _get_search_query_filter(self, model, search_scores: dict):
38
38
  """Get the search filter and scores."""
39
39
  prefix = "" if model == Comic else self.rel_prefix # type: ignore
40
40
  rel = prefix + "pk__in"
41
41
  query_dict = {rel: search_scores.keys()}
42
42
  return Q(**query_dict)
43
43
 
44
- def get_search_filter(self, model, search_scores):
44
+ def get_search_filter(self, model, search_scores: dict):
45
45
  """Preparse search, search and return the filter and scores."""
46
46
  search_filter = Q()
47
47
  try: