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
@@ -3,6 +3,7 @@ from django.db.models import Q
3
3
  from haystack.query import SearchQuerySet
4
4
 
5
5
  from codex.logger.logging import get_logger
6
+ from codex.models import Comic
6
7
 
7
8
  LOG = get_logger(__name__)
8
9
 
@@ -10,8 +11,17 @@ LOG = get_logger(__name__)
10
11
  class SearchFilterMixin:
11
12
  """Search Filters Methods."""
12
13
 
13
- def _get_search_scores(self, text, search_scores):
14
+ def get_search_scores(self) -> dict:
14
15
  """Perform the search and return the scores as a dict."""
16
+ search_scores = {}
17
+ text = self.params.get("q", "") # type: ignore
18
+ if not text:
19
+ # for opds 2
20
+ text = self.params.get("query", "") # type: ignore
21
+ text = text.strip()
22
+ if not text:
23
+ return search_scores
24
+
15
25
  sqs = SearchQuerySet().auto_query(text)
16
26
  comic_scores = sqs.values("pk", "score")
17
27
  try:
@@ -22,30 +32,23 @@ class SearchFilterMixin:
22
32
  except Exception as exc:
23
33
  LOG.warning("While searching:")
24
34
  LOG.exception(exc)
35
+ return search_scores
25
36
 
26
- def _get_search_query_filter(self, text, search_scores):
37
+ def _get_search_query_filter(self, model, search_scores: dict):
27
38
  """Get the search filter and scores."""
28
- rel = self.rel_prefix + "pk__in" # type: ignore
29
- self._get_search_scores(text, search_scores)
39
+ prefix = "" if model == Comic else self.rel_prefix # type: ignore
40
+ rel = prefix + "pk__in"
30
41
  query_dict = {rel: search_scores.keys()}
31
42
  return Q(**query_dict)
32
43
 
33
- def get_search_filter(self):
44
+ def get_search_filter(self, model, search_scores: dict):
34
45
  """Preparse search, search and return the filter and scores."""
35
46
  search_filter = Q()
36
- search_scores = {}
37
47
  try:
38
- query_string = self.params.get("q", "") # type: ignore
39
- if not query_string:
40
- # for opds 2
41
- query_string = self.params.get("query", "") # type: ignore
42
-
43
- if query_string:
48
+ if search_scores:
44
49
  # Query haystack
45
- search_filter = self._get_search_query_filter(
46
- query_string, search_scores
47
- )
50
+ search_filter = self._get_search_query_filter(model, search_scores)
48
51
  except Exception as exc:
49
52
  LOG.warning(exc)
50
53
 
51
- return search_filter, search_scores
54
+ return search_filter
@@ -1,5 +1,7 @@
1
1
  """View for marking comics read and unread."""
2
2
  from copy import copy
3
+ from types import MappingProxyType
4
+ from typing import ClassVar
3
5
 
4
6
  from django.db.models import Count, F, IntegerField, Subquery, Value
5
7
  from drf_spectacular.utils import extend_schema
@@ -16,45 +18,53 @@ from codex.views.browser.browser_annotations import BrowserAnnotationsView
16
18
  class MetadataView(BrowserAnnotationsView):
17
19
  """Comic metadata."""
18
20
 
19
- permission_classes = [IsAuthenticatedOrEnabledNonUsers]
21
+ permission_classes: ClassVar[list] = [IsAuthenticatedOrEnabledNonUsers]
20
22
  serializer_class = MetadataSerializer
21
23
 
22
24
  # DO NOT USE BY ITSELF. USE _get_comic_value_fields() instead.
23
- _COMIC_VALUE_FIELDS = {
24
- "age_rating",
25
- "comments",
26
- "community_rating",
27
- "country",
28
- "critical_rating",
29
- "day",
30
- "file_type",
31
- "gtin",
32
- "issue",
33
- "issue_suffix",
34
- "language",
35
- "month",
36
- "notes",
37
- "original_format",
38
- "read_ltr",
39
- "scan_info",
40
- "summary",
41
- "web",
42
- "year",
43
- }
44
- _ADMIN_OR_FILE_VIEW_ENABLED_COMIC_VALUE_FIELDS = {"path"}
45
- _COMIC_VALUE_FIELDS_CONFLICTING = {
46
- "name",
47
- "updated_at",
48
- "created_at",
49
- }
25
+ _COMIC_VALUE_FIELDS = frozenset(
26
+ {
27
+ "age_rating",
28
+ "comments",
29
+ "community_rating",
30
+ "country",
31
+ "critical_rating",
32
+ "day",
33
+ "file_type",
34
+ "gtin",
35
+ "issue",
36
+ "issue_suffix",
37
+ "language",
38
+ "month",
39
+ "notes",
40
+ "original_format",
41
+ "read_ltr",
42
+ "scan_info",
43
+ "summary",
44
+ "web",
45
+ "year",
46
+ }
47
+ )
48
+ _ADMIN_OR_FILE_VIEW_ENABLED_COMIC_VALUE_FIELDS = frozenset({"path"})
49
+ _COMIC_VALUE_FIELDS_CONFLICTING = frozenset(
50
+ {
51
+ "name",
52
+ "updated_at",
53
+ "created_at",
54
+ }
55
+ )
50
56
  _COMIC_VALUE_FIELDS_CONFLICTING_PREFIX = "conflict_"
51
- _COMIC_FK_FIELDS_MAP = {
52
- "p": "publisher",
53
- "i": "imprint",
54
- "s": "series",
55
- "v": "volume",
56
- }
57
- _COMIC_RELATED_VALUE_FIELDS = {"series__volume_count", "volume__issue_count"}
57
+ _COMIC_FK_FIELDS_MAP = MappingProxyType(
58
+ {
59
+ "p": "publisher",
60
+ "i": "imprint",
61
+ "s": "series",
62
+ "v": "volume",
63
+ }
64
+ )
65
+ _COMIC_RELATED_VALUE_FIELDS = frozenset(
66
+ {"series__volume_count", "volume__issue_count"}
67
+ )
58
68
  _PATH_GROUPS = ("c", "f")
59
69
  _CREATOR_RELATIONS = ("role", "person")
60
70
  _STORY_ARC_NUMBER_RELATIONS = ("story_arc",)
@@ -109,8 +119,7 @@ class MetadataView(BrowserAnnotationsView):
109
119
  if not self.is_model_comic:
110
120
  size_func = self.get_aggregate_func(self.model, "size")
111
121
  qs = qs.annotate(size=size_func)
112
- qs = self.annotate_common_aggregates(qs, self.model, {})
113
- return qs
122
+ return self.annotate_common_aggregates(qs, self.model, {})
114
123
 
115
124
  def _annotate_values_and_fks(self, qs, simple_qs):
116
125
  """Annotate comic values and comic foreign key values."""
@@ -211,7 +220,7 @@ class MetadataView(BrowserAnnotationsView):
211
220
  def _highlight_current_group(self, obj):
212
221
  """Values for highlighting the current group."""
213
222
  obj.group = self.group
214
- if not self.is_model_comic:
223
+ if self.model and not self.is_model_comic:
215
224
  # move the name of the group to the correct field
216
225
  group_field = self.model.__name__.lower()
217
226
  group_obj = {"pk": obj.pk, "name": obj.name}
@@ -259,7 +268,8 @@ class MetadataView(BrowserAnnotationsView):
259
268
  if self.model is None:
260
269
  raise NotFound(detail=f"Cannot get metadata for {self.group=}")
261
270
 
262
- object_filter, _ = self.get_query_filters_without_group(self.model)
271
+ search_scores: dict = self.get_search_scores()
272
+ object_filter = self.get_query_filters_without_group(self.model, search_scores)
263
273
  pk = self.kwargs["pk"]
264
274
  qs = self.model.objects.filter(object_filter, pk=pk)
265
275
 
@@ -279,8 +289,7 @@ class MetadataView(BrowserAnnotationsView):
279
289
  ) from exc
280
290
 
281
291
  m2m_intersections = self._query_m2m_intersections(simple_qs)
282
- obj = self._copy_annotations_into_comic_fields(obj, m2m_intersections)
283
- return obj
292
+ return self._copy_annotations_into_comic_fields(obj, m2m_intersections)
284
293
 
285
294
  def _is_enabled_folder_view(self):
286
295
  if self._efv_flag is None:
codex/views/cover.py CHANGED
@@ -1,4 +1,6 @@
1
1
  """Comic cover thumbnail view."""
2
+ from typing import ClassVar
3
+
2
4
  from django.http import HttpResponse
3
5
  from drf_spectacular.types import OpenApiTypes
4
6
  from drf_spectacular.utils import extend_schema
@@ -31,7 +33,7 @@ class WEBPRenderer(BaseRenderer):
31
33
  class CoverView(APIView, GroupACLMixin):
32
34
  """ComicCover View."""
33
35
 
34
- permission_classes = [IsAuthenticatedOrEnabledNonUsers]
36
+ permission_classes: ClassVar[list] = [IsAuthenticatedOrEnabledNonUsers]
35
37
  renderer_classes = (WEBPRenderer,)
36
38
  content_type = "image/webp"
37
39
 
codex/views/download.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Download a comic archive."""
2
2
  from pathlib import Path
3
+ from typing import ClassVar
3
4
 
4
5
  from django.http import FileResponse, Http404
5
6
  from drf_spectacular.types import OpenApiTypes
@@ -14,7 +15,7 @@ from codex.views.mixins import GroupACLMixin
14
15
  class DownloadView(APIView, GroupACLMixin):
15
16
  """Return the comic archive file as an attachment."""
16
17
 
17
- permission_classes = [IsAuthenticatedOrEnabledNonUsers]
18
+ permission_classes: ClassVar[list] = [IsAuthenticatedOrEnabledNonUsers]
18
19
  content_type = "application/vnd.comicbook+zip"
19
20
 
20
21
  _DOWNLOAD_SELECT_RELATED = ("series", "volume")
@@ -36,7 +37,8 @@ class DownloadView(APIView, GroupACLMixin):
36
37
  reason = f"Comic {pk} not not found."
37
38
  raise Http404(reason) from err
38
39
 
39
- comic_file = Path(comic.path).open("rb")
40
+ # FileResponse requires file handle not be closed in this method.
41
+ comic_file = Path(comic.path).open("rb") # noqa: SIM115
40
42
  return FileResponse(
41
43
  comic_file,
42
44
  as_attachment=True,
codex/views/frontend.py CHANGED
@@ -1,4 +1,6 @@
1
1
  """Frontend views."""
2
+ from typing import ClassVar
3
+
2
4
  from rest_framework.renderers import TemplateHTMLRenderer
3
5
  from rest_framework.response import Response
4
6
 
@@ -8,9 +10,8 @@ from codex.views.session import BrowserSessionViewBase
8
10
  class IndexView(BrowserSessionViewBase):
9
11
  """The main app."""
10
12
 
11
- renderer_classes = [TemplateHTMLRenderer]
13
+ renderer_classes: ClassVar[list] = [TemplateHTMLRenderer]
12
14
  template_name = "index.html"
13
- main_urls = {}
14
15
 
15
16
  def get(self, *args, **kwargs):
16
17
  """Get the app index page."""
codex/views/mixins.py CHANGED
@@ -1,4 +1,6 @@
1
1
  """A filter for group ACLS."""
2
+ from types import MappingProxyType
3
+
2
4
  from django.db.models import Q
3
5
 
4
6
  from codex.models import Comic, Folder, StoryArc
@@ -11,15 +13,17 @@ class GroupACLMixin:
11
13
  FOLDER_GROUP = "f"
12
14
  STORY_ARC_GROUP = "a"
13
15
  COMIC_GROUP = "c"
14
- GROUP_RELATION = {
15
- "p": "publisher",
16
- "i": "imprint",
17
- "s": "series",
18
- "v": "volume",
19
- COMIC_GROUP: "pk",
20
- FOLDER_GROUP: "parent_folder",
21
- STORY_ARC_GROUP: "story_arc_numbers__story_arc",
22
- }
16
+ GROUP_RELATION = MappingProxyType(
17
+ {
18
+ "p": "publisher",
19
+ "i": "imprint",
20
+ "s": "series",
21
+ "v": "volume",
22
+ COMIC_GROUP: "pk",
23
+ FOLDER_GROUP: "parent_folder",
24
+ STORY_ARC_GROUP: "story_arc_numbers__story_arc",
25
+ }
26
+ )
23
27
 
24
28
  def get_rel_prefix(self, model):
25
29
  """Return the relation prfiex for most fields."""
@@ -1,4 +1,6 @@
1
1
  """OPDS Authentication 1.0."""
2
+ from types import MappingProxyType
3
+
2
4
  from django.contrib.staticfiles.storage import staticfiles_storage
3
5
  from django.urls import reverse_lazy
4
6
  from rest_framework.generics import GenericAPIView
@@ -12,47 +14,49 @@ class OPDSAuthentication1View(GenericAPIView):
12
14
 
13
15
  serializer_class = OPDSAuthentication1Serializer
14
16
 
15
- DOC = {
16
- "id": reverse_lazy("opds:authentication:v1"),
17
- "title": "Codex",
18
- "description": "Codex OPDS Syndication",
19
- "links": [
20
- {
21
- "rel": "logo",
22
- "href": staticfiles_storage.url("img/logo.svg"),
23
- "type": "image/svg+xml",
24
- "width": 512,
25
- "height": 512,
26
- },
27
- {
28
- "rel": "help",
29
- "href": "https://github.com/ajslater/codex",
30
- "type": "text/html",
31
- },
32
- {
33
- "rel": "register",
34
- "href": reverse_lazy("app:start"),
35
- "type": "text/html",
36
- },
37
- ],
38
- "authentication": [
39
- {
40
- "type": "http://opds-spec.org/auth/basic",
41
- "labels": {"login": "Username", "password": "Password"},
42
- },
43
- {
44
- # XXX Out of spec type
45
- "type": "cookie",
46
- "links": [
47
- {
48
- "rel": "authenticate",
49
- "href": reverse_lazy("app:start"),
50
- "type": "text/html",
51
- }
52
- ],
53
- },
54
- ],
55
- }
17
+ DOC = MappingProxyType(
18
+ {
19
+ "id": reverse_lazy("opds:authentication:v1"),
20
+ "title": "Codex",
21
+ "description": "Codex OPDS Syndication",
22
+ "links": [
23
+ {
24
+ "rel": "logo",
25
+ "href": staticfiles_storage.url("img/logo.svg"),
26
+ "type": "image/svg+xml",
27
+ "width": 512,
28
+ "height": 512,
29
+ },
30
+ {
31
+ "rel": "help",
32
+ "href": "https://github.com/ajslater/codex",
33
+ "type": "text/html",
34
+ },
35
+ {
36
+ "rel": "register",
37
+ "href": reverse_lazy("app:start"),
38
+ "type": "text/html",
39
+ },
40
+ ],
41
+ "authentication": [
42
+ {
43
+ "type": "http://opds-spec.org/auth/basic",
44
+ "labels": {"login": "Username", "password": "Password"},
45
+ },
46
+ {
47
+ # XXX Out of spec type
48
+ "type": "cookie",
49
+ "links": [
50
+ {
51
+ "rel": "authenticate",
52
+ "href": reverse_lazy("app:start"),
53
+ "type": "text/html",
54
+ }
55
+ ],
56
+ },
57
+ ],
58
+ }
59
+ )
56
60
 
57
61
  def get(self, *args, **kwargs):
58
62
  """Fill in the authentication dict."""
codex/views/opds/const.py CHANGED
@@ -1,5 +1,8 @@
1
1
  """OPDS Common consts."""
2
2
 
3
+ from collections.abc import Mapping
4
+ from types import MappingProxyType
5
+
3
6
  BLANK_TITLE = "Unknown"
4
7
  FALSY = {"", "false", "0"}
5
8
  AUTHOR_ROLES = {"Writer"}
@@ -51,17 +54,21 @@ class MimeType:
51
54
  HTML = "text/html"
52
55
  AUTH_BASIC = "http://opds-spec.org/auth/basic"
53
56
  COOKIE = "cookie"
54
- FILE_TYPE_MAP = {
55
- "CBZ": "application/vnd.comicbook+zip",
56
- "CBR": "application/vnd.comicbook+rar",
57
- "CBT": "application/vnd.comicbook+tar",
58
- "PDF": "application/pdf",
59
- }
60
- SIMPLE_FILE_TYPE_MAP = {
61
- # PocketBooks needs app/zip
62
- "CBZ": "application/zip",
63
- "CBR": "application/x-rar-compressed",
64
- "CBT": "application/x-tar",
65
- "PDF": "application/pdf",
66
- }
57
+ FILE_TYPE_MAP: Mapping[str, str] = MappingProxyType(
58
+ {
59
+ "CBZ": "application/vnd.comicbook+zip",
60
+ "CBR": "application/vnd.comicbook+rar",
61
+ "CBT": "application/vnd.comicbook+tar",
62
+ "PDF": "application/pdf",
63
+ }
64
+ )
65
+ SIMPLE_FILE_TYPE_MAP: Mapping[str, str] = MappingProxyType(
66
+ {
67
+ # PocketBooks needs app/zip
68
+ "CBZ": "application/zip",
69
+ "CBR": "application/x-rar-compressed",
70
+ "CBT": "application/x-tar",
71
+ "PDF": "application/pdf",
72
+ }
73
+ )
67
74
  OCTET = "application/octet-stream"
@@ -1,4 +1,5 @@
1
1
  """OPDS v1 Entry Data classes."""
2
+ from collections.abc import Mapping
2
3
  from dataclasses import dataclass
3
4
 
4
5
 
@@ -20,4 +21,4 @@ class OPDS1EntryData:
20
21
  acquisition_groups: frozenset
21
22
  issue_max: int
22
23
  metadata: bool
23
- mime_type_map: dict
24
+ mime_type_map: Mapping[str, str]
@@ -1,5 +1,6 @@
1
1
  """OPDS v1 Facets methods."""
2
2
  from dataclasses import dataclass
3
+ from types import MappingProxyType
3
4
 
4
5
  from django.urls import reverse
5
6
 
@@ -83,7 +84,7 @@ class FacetsMixin(BrowserView):
83
84
  skip_order_facets = False
84
85
  acquisition_groups = frozenset()
85
86
  mime_type_map = MimeType.FILE_TYPE_MAP
86
- obj = {}
87
+ obj = MappingProxyType({})
87
88
 
88
89
  def _facet(self, kwargs, facet_group, facet_title, new_query_params):
89
90
  href = reverse("opds:v1:feed", kwargs=kwargs)
@@ -1,5 +1,6 @@
1
1
  """OPDS v1 feed."""
2
2
  from datetime import datetime, timezone
3
+ from typing import TYPE_CHECKING, Any
3
4
 
4
5
  from drf_spectacular.utils import extend_schema
5
6
  from rest_framework.authentication import BasicAuthentication, SessionAuthentication
@@ -24,6 +25,9 @@ from codex.views.opds.v1.links import (
24
25
  )
25
26
  from codex.views.template import CodexXMLTemplateView
26
27
 
28
+ if TYPE_CHECKING:
29
+ from collections.abc import Mapping
30
+
27
31
  LOG = get_logger(__name__)
28
32
 
29
33
 
@@ -77,7 +81,10 @@ class OPDS1FeedView(CodexXMLTemplateView, LinksMixin):
77
81
  """Create the feed title."""
78
82
  result = ""
79
83
  try:
80
- if browser_title := self.obj.get("browser_title"):
84
+ browser_title: Mapping[str, Any] = self.obj.get(
85
+ "browser_title"
86
+ ) # type: ignore
87
+ if browser_title:
81
88
  parent_name = browser_title.get("parent_name", "All")
82
89
  if not parent_name and self.kwargs.get("pk") == 0:
83
90
  parent_name = "All"
@@ -95,7 +102,7 @@ class OPDS1FeedView(CodexXMLTemplateView, LinksMixin):
95
102
  """Hack in feed update time from cover timestamp."""
96
103
  try:
97
104
  if ts := self.obj.get("covers_timestamp"):
98
- return datetime.fromtimestamp(ts, tz=timezone.utc)
105
+ return datetime.fromtimestamp(ts, tz=timezone.utc) # type: ignore
99
106
  except Exception as exc:
100
107
  LOG.exception(exc)
101
108
 
@@ -120,12 +127,12 @@ class OPDS1FeedView(CodexXMLTemplateView, LinksMixin):
120
127
  def _get_entries_section(self, key, metadata):
121
128
  """Get entries by key section."""
122
129
  entries = []
123
- issue_max = self.obj.get("issue_max", 0)
130
+ issue_max: int = self.obj.get("issue_max", 0) # type: ignore
124
131
  data = OPDS1EntryData(
125
132
  self.acquisition_groups, issue_max, metadata, self.mime_type_map
126
133
  )
127
134
  if objs := self.obj.get(key):
128
- for obj in objs:
135
+ for obj in objs: # type: ignore
129
136
  entries += [OPDS1Entry(obj, self.request.query_params, data)]
130
137
  return entries
131
138
 
@@ -1,6 +1,8 @@
1
1
  """OPDS v1 Links methods."""
2
2
  from collections import defaultdict
3
+ from collections.abc import Mapping
3
4
  from dataclasses import dataclass
5
+ from types import MappingProxyType
4
6
  from typing import Union
5
7
 
6
8
  from comicbox.metadata.comic_json import json
@@ -20,18 +22,18 @@ LOG = get_logger(__name__)
20
22
  class TopRoutes:
21
23
  """Routes for top groups."""
22
24
 
23
- PUBLISHER = {"group": "p", "pk": 0, "page": 1}
24
- SERIES = {"group": "s", "pk": 0, "page": 1}
25
- FOLDER = {"group": "f", "pk": 0, "page": 1}
26
- ROOT = {"group": "r", "pk": 0, "page": 1}
27
- STORY_ARC = {"group": "a", "pk": 0, "page": 1}
25
+ PUBLISHER = MappingProxyType({"group": "p", "pk": 0, "page": 1})
26
+ SERIES = MappingProxyType({"group": "s", "pk": 0, "page": 1})
27
+ FOLDER = MappingProxyType({"group": "f", "pk": 0, "page": 1})
28
+ ROOT = MappingProxyType({"group": "r", "pk": 0, "page": 1})
29
+ STORY_ARC = MappingProxyType({"group": "a", "pk": 0, "page": 1})
28
30
 
29
31
 
30
32
  @dataclass
31
33
  class TopLink:
32
34
  """A non standard root link when facets are unsupported."""
33
35
 
34
- kwargs: dict
36
+ kwargs: Mapping
35
37
  rel: str
36
38
  mime_type: str
37
39
  query_params: defaultdict[str, Union[str, bool, int]]
@@ -12,6 +12,6 @@ class OpenSearch1View(CodexXMLTemplateView):
12
12
  """OpenSearchView."""
13
13
 
14
14
  authentication_classes = (BasicAuthentication, SessionAuthentication)
15
- permission_classes = [IsAuthenticatedOrEnabledNonUsers]
15
+ permission_classes = (IsAuthenticatedOrEnabledNonUsers,)
16
16
  template_name = "opds_v1/opensearch_v1.xml"
17
17
  content_type = "application/xml"
@@ -189,7 +189,8 @@ class OPDS2FeedView(PublicationMixin, TopLinksMixin):
189
189
 
190
190
  self.is_aq_feed = browser_page.get("model_group") == "c"
191
191
 
192
- datetime.fromtimestamp(browser_page["covers_timestamp"], tz=timezone.utc)
192
+ ts: int = browser_page["covers_timestamp"] # type: ignore
193
+ datetime.fromtimestamp(ts, tz=timezone.utc)
193
194
  self.num_pages = browser_page["num_pages"]
194
195
  number_of_items = browser_page["total_count"]
195
196
  title = self._title(browser_page.get("browser_title"))
@@ -1,4 +1,5 @@
1
1
  """Publication Methods for OPDS v2.0 feed."""
2
+ from types import MappingProxyType
2
3
  from urllib.parse import quote_plus
3
4
 
4
5
  from django.contrib.staticfiles.storage import staticfiles_storage
@@ -13,18 +14,20 @@ from codex.views.opds.v2.links import HrefData, LinkData, LinksMixin
13
14
  class PublicationMixin(LinksMixin):
14
15
  """Publication Methods for OPDS 2.0 feed."""
15
16
 
16
- _MD_CONTRIBUTOR_MAP = {
17
- "author": AUTHOR_ROLES,
18
- # "translator": {},
19
- "editor": {"Editor"},
20
- "artist": {"CoverArtist"},
21
- # "illustrator": {},
22
- "letterer": {"Letterer"},
23
- "penciller": {"Penciller"},
24
- "colorist": {"Colorist"},
25
- "inker": {"Inker"},
26
- }
27
- _CONTRIBUTOR_ROLES = {x for s in _MD_CONTRIBUTOR_MAP.values() for x in s}
17
+ _MD_CONTRIBUTOR_MAP = MappingProxyType(
18
+ {
19
+ "author": AUTHOR_ROLES,
20
+ # "translator": {},
21
+ "editor": {"Editor"},
22
+ "artist": {"CoverArtist"},
23
+ # "illustrator": {},
24
+ "letterer": {"Letterer"},
25
+ "penciller": {"Penciller"},
26
+ "colorist": {"Colorist"},
27
+ "inker": {"Inker"},
28
+ }
29
+ )
30
+ _CONTRIBUTOR_ROLES = frozenset({x for s in _MD_CONTRIBUTOR_MAP.values() for x in s})
28
31
  is_opds_metadata = False
29
32
 
30
33
  def _get_big_image_link(self, obj, cover_pk):
@@ -30,7 +30,7 @@ class IgnoreClientContentNegotiation(BaseContentNegotiation):
30
30
  class ReaderPageView(BookmarkBaseView):
31
31
  """Display a comic page from the archive itself."""
32
32
 
33
- X_MOZ_PRE_HEADERS = {"prefetch", "preload", "prerender", "subresource"}
33
+ X_MOZ_PRE_HEADERS = frozenset({"prefetch", "preload", "prerender", "subresource"})
34
34
  content_type = "image/jpeg"
35
35
  content_negotiation_class = IgnoreClientContentNegotiation # type: ignore
36
36