codex 1.4.0a1__py3-none-any.whl → 1.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of codex might be problematic. Click here for more details.
- codex/config_default.yaml +12 -4
- codex/db_functions.py +4 -2
- codex/integrity.py +17 -6
- codex/librarian/covers/create.py +6 -8
- codex/librarian/importer/aggregate_metadata.py +75 -41
- codex/librarian/importer/clean_metadata.py +30 -7
- codex/librarian/importer/create_fks.py +154 -55
- codex/librarian/importer/deleted.py +11 -2
- codex/librarian/importer/failed_imports.py +41 -5
- codex/librarian/importer/importerd.py +34 -11
- codex/librarian/importer/link_comics.py +54 -31
- codex/librarian/importer/moved.py +55 -11
- codex/librarian/importer/query_fks.py +210 -48
- codex/librarian/importer/tasks.py +7 -7
- codex/librarian/janitor/cleanup.py +17 -5
- codex/librarian/librariand.py +10 -0
- codex/librarian/watchdog/events.py +11 -14
- codex/librarian/watchdog/observers.py +5 -1
- codex/logger/loggerd.py +7 -3
- codex/logger/logging.py +1 -1
- codex/migrations/0024_comic_gtin_comic_story_arc_number.py +24 -0
- codex/migrations/0025_add_story_arc_number.py +83 -0
- codex/models.py +21 -11
- codex/search/backend.py +1 -1
- codex/search/indexes.py +1 -1
- codex/serializers/browser.py +1 -0
- codex/serializers/metadata.py +5 -1
- codex/serializers/models.py +16 -1
- codex/serializers/opds/v1.py +1 -0
- codex/serializers/opds/v2.py +5 -2
- codex/serializers/reader.py +55 -16
- codex/settings/settings.py +1 -1
- codex/static_root/assets/admin-12749881.ef0f50bac290.js +41 -0
- codex/static_root/assets/admin-12749881.ef0f50bac290.js.br +0 -0
- codex/static_root/assets/admin-12749881.ef0f50bac290.js.gz +0 -0
- codex/static_root/assets/admin-12749881.js +41 -0
- codex/static_root/assets/admin-12749881.js.br +0 -0
- codex/static_root/assets/admin-12749881.js.gz +0 -0
- codex/static_root/assets/admin-beda768d.a614eee46307.css +1 -0
- codex/static_root/assets/admin-beda768d.a614eee46307.css.br +0 -0
- codex/static_root/assets/admin-beda768d.a614eee46307.css.gz +0 -0
- codex/static_root/assets/admin-beda768d.css +1 -0
- codex/static_root/assets/admin-beda768d.css.br +0 -0
- codex/static_root/assets/admin-beda768d.css.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css +1 -0
- codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css.br +0 -0
- codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-41c225cc.css +1 -0
- codex/static_root/assets/admin-drawer-panel-41c225cc.css.br +0 -0
- codex/static_root/assets/admin-drawer-panel-41c225cc.css.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js +1 -0
- codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-522f1e6c.js +1 -0
- codex/static_root/assets/admin-drawer-panel-522f1e6c.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-522f1e6c.js.gz +0 -0
- codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css +1 -0
- codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.br +0 -0
- codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.gz +0 -0
- codex/static_root/assets/browser-7f7d7134.css +1 -0
- codex/static_root/assets/browser-7f7d7134.css.br +0 -0
- codex/static_root/assets/browser-7f7d7134.css.gz +0 -0
- codex/static_root/assets/browser-af622672.d51aca96d64d.js +1 -0
- codex/static_root/assets/browser-af622672.d51aca96d64d.js.br +0 -0
- codex/static_root/assets/browser-af622672.d51aca96d64d.js.gz +0 -0
- codex/static_root/assets/browser-af622672.js +1 -0
- codex/static_root/assets/browser-af622672.js.br +0 -0
- codex/static_root/assets/browser-af622672.js.gz +0 -0
- codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js +1 -0
- codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.br +0 -0
- codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.gz +0 -0
- codex/static_root/assets/http-error-5e17b794.js +1 -0
- codex/static_root/assets/http-error-5e17b794.js.br +0 -0
- codex/static_root/assets/http-error-5e17b794.js.gz +0 -0
- codex/static_root/assets/main-9e76a4c3.6844a407d14c.js +1 -0
- codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.br +0 -0
- codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.gz +0 -0
- codex/static_root/assets/main-9e76a4c3.js +1 -0
- codex/static_root/assets/main-9e76a4c3.js.br +0 -0
- codex/static_root/assets/main-9e76a4c3.js.gz +0 -0
- codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js +1 -0
- codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.br +0 -0
- codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.gz +0 -0
- codex/static_root/assets/metadata-dialog-62c29ce0.js +1 -0
- codex/static_root/assets/metadata-dialog-62c29ce0.js.br +0 -0
- codex/static_root/assets/metadata-dialog-62c29ce0.js.gz +0 -0
- codex/static_root/assets/{metadata-dialog-785c4cfc.694a251cda37.css → metadata-dialog-cb306ffd.cc304996d7bb.css} +1 -1
- codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.br +0 -0
- codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.gz +0 -0
- codex/static_root/assets/{metadata-dialog-785c4cfc.css → metadata-dialog-cb306ffd.css} +1 -1
- codex/static_root/assets/metadata-dialog-cb306ffd.css.br +0 -0
- codex/static_root/assets/metadata-dialog-cb306ffd.css.gz +0 -0
- codex/static_root/assets/{page-pdf-c603e996.ab2d147c9ae1.js → page-pdf-157ba97e.613d7c2beb77.js} +61 -51
- codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.br +0 -0
- codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.gz +0 -0
- codex/static_root/assets/{page-pdf-c603e996.js → page-pdf-157ba97e.js} +61 -51
- codex/static_root/assets/page-pdf-157ba97e.js.br +0 -0
- codex/static_root/assets/page-pdf-157ba97e.js.gz +0 -0
- codex/static_root/assets/reader-36266549.0b2cf1291f27.js +1 -0
- codex/static_root/assets/reader-36266549.0b2cf1291f27.js.br +0 -0
- codex/static_root/assets/reader-36266549.0b2cf1291f27.js.gz +0 -0
- codex/static_root/assets/reader-36266549.js +1 -0
- codex/static_root/assets/reader-36266549.js.br +0 -0
- codex/static_root/assets/reader-36266549.js.gz +0 -0
- codex/static_root/assets/reader-7f004141.506eecc6954b.css +1 -0
- codex/static_root/assets/reader-7f004141.506eecc6954b.css.br +0 -0
- codex/static_root/assets/reader-7f004141.506eecc6954b.css.gz +0 -0
- codex/static_root/assets/reader-7f004141.css +1 -0
- codex/static_root/assets/reader-7f004141.css.br +0 -0
- codex/static_root/assets/reader-7f004141.css.gz +0 -0
- codex/static_root/js/choices.8c58714cf5b2.json +1 -0
- codex/static_root/js/choices.8c58714cf5b2.json.br +5 -0
- codex/static_root/js/choices.8c58714cf5b2.json.gz +0 -0
- codex/static_root/js/choices.json +1 -1
- codex/static_root/js/choices.json.br +0 -0
- codex/static_root/js/choices.json.gz +0 -0
- codex/static_root/{manifest.c0e270b2e6b6.json → manifest.d2f93a519ada.json} +32 -32
- codex/static_root/manifest.d2f93a519ada.json.br +0 -0
- codex/static_root/manifest.d2f93a519ada.json.gz +0 -0
- codex/static_root/manifest.json +32 -32
- codex/static_root/manifest.json.br +0 -0
- codex/static_root/manifest.json.gz +0 -0
- codex/static_root/staticfiles.json +1 -1
- codex/templates/headers-script-globals.html +1 -1
- codex/templates/{opds → opds_v1}/index.xml +3 -1
- codex/templates/{opds/opensearch.xml → opds_v1/opensearch_v1.xml} +1 -1
- codex/templates/search/indexes/codex/comic_text.txt +2 -2
- codex/urls/converters.py +1 -1
- codex/urls/opds/authentication.py +1 -1
- codex/urls/opds/root.py +8 -12
- codex/urls/opds/v1.py +12 -5
- codex/urls/opds/v2.py +2 -2
- codex/views/bookmark.py +2 -2
- codex/views/browser/base.py +23 -7
- codex/views/browser/browser.py +51 -41
- codex/views/browser/browser_annotations.py +159 -50
- codex/views/browser/browser_order_by.py +50 -106
- codex/views/browser/choices.py +75 -38
- codex/views/browser/filters/bookmark.py +6 -9
- codex/views/browser/filters/field.py +9 -6
- codex/views/browser/filters/group.py +12 -27
- codex/views/browser/filters/search.py +5 -10
- codex/views/browser/metadata.py +44 -19
- codex/views/download.py +1 -1
- codex/views/frontend.py +2 -3
- codex/views/mixins.py +15 -2
- codex/views/opds/const.py +8 -1
- codex/views/opds/util.py +37 -1
- codex/views/opds/v1/__init__.py +1 -1
- codex/views/opds/v1/data.py +21 -0
- codex/views/opds/v1/entry/__init__.py +1 -0
- codex/views/opds/v1/entry/data.py +23 -0
- codex/views/opds/v1/entry/entry.py +151 -0
- codex/views/opds/v1/entry/links.py +135 -0
- codex/views/opds/v1/facets.py +190 -0
- codex/views/opds/v1/feed.py +199 -0
- codex/views/opds/v1/links.py +198 -0
- codex/views/opds/{opensearch.py → v1/opensearch_v1.py} +3 -3
- codex/views/opds/v2/__init__.py +1 -1
- codex/views/opds/v2/const.py +10 -2
- codex/views/opds/v2/feed.py +82 -21
- codex/views/opds/v2/links.py +1 -1
- codex/views/opds/v2/publications.py +1 -1
- codex/views/opds/v2/top_links.py +1 -1
- codex/views/reader/page.py +6 -7
- codex/views/reader/reader.py +191 -61
- codex/views/session.py +2 -1
- {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/METADATA +10 -41
- {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/RECORD +172 -170
- codex/librarian/importer/db_ops.py +0 -251
- codex/pdf.py +0 -115
- codex/static_root/assets/admin-75c007ce.199fccf24c8d.js +0 -48
- codex/static_root/assets/admin-75c007ce.199fccf24c8d.js.br +0 -0
- codex/static_root/assets/admin-75c007ce.199fccf24c8d.js.gz +0 -0
- codex/static_root/assets/admin-75c007ce.js +0 -48
- codex/static_root/assets/admin-75c007ce.js.br +0 -0
- codex/static_root/assets/admin-75c007ce.js.gz +0 -0
- codex/static_root/assets/admin-848d48b1.5de8a0c45636.css +0 -1
- codex/static_root/assets/admin-848d48b1.5de8a0c45636.css.br +0 -0
- codex/static_root/assets/admin-848d48b1.5de8a0c45636.css.gz +0 -0
- codex/static_root/assets/admin-848d48b1.css +0 -1
- codex/static_root/assets/admin-848d48b1.css.br +0 -0
- codex/static_root/assets/admin-848d48b1.css.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-a110c068.edf187333272.js +0 -1
- codex/static_root/assets/admin-drawer-panel-a110c068.edf187333272.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-a110c068.edf187333272.js.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-a110c068.js +0 -1
- codex/static_root/assets/admin-drawer-panel-a110c068.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-a110c068.js.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css +0 -1
- codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css.br +0 -2
- codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-cce8c0aa.css +0 -1
- codex/static_root/assets/admin-drawer-panel-cce8c0aa.css.br +0 -2
- codex/static_root/assets/admin-drawer-panel-cce8c0aa.css.gz +0 -0
- codex/static_root/assets/browser-2c2380fd.8b515af7a743.js +0 -1
- codex/static_root/assets/browser-2c2380fd.8b515af7a743.js.br +0 -0
- codex/static_root/assets/browser-2c2380fd.8b515af7a743.js.gz +0 -0
- codex/static_root/assets/browser-2c2380fd.js +0 -1
- codex/static_root/assets/browser-2c2380fd.js.br +0 -0
- codex/static_root/assets/browser-2c2380fd.js.gz +0 -0
- codex/static_root/assets/browser-7325db61.css +0 -1
- codex/static_root/assets/browser-7325db61.css.br +0 -0
- codex/static_root/assets/browser-7325db61.css.gz +0 -0
- codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css +0 -1
- codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css.br +0 -0
- codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css.gz +0 -0
- codex/static_root/assets/http-error-402decbe.9ea8de1df13f.js +0 -1
- codex/static_root/assets/http-error-402decbe.9ea8de1df13f.js.br +0 -0
- codex/static_root/assets/http-error-402decbe.9ea8de1df13f.js.gz +0 -0
- codex/static_root/assets/http-error-402decbe.js +0 -1
- codex/static_root/assets/http-error-402decbe.js.br +0 -0
- codex/static_root/assets/http-error-402decbe.js.gz +0 -0
- codex/static_root/assets/main-a7f327e9.6641fe833335.js +0 -1
- codex/static_root/assets/main-a7f327e9.6641fe833335.js.br +0 -0
- codex/static_root/assets/main-a7f327e9.6641fe833335.js.gz +0 -0
- codex/static_root/assets/main-a7f327e9.js +0 -1
- codex/static_root/assets/main-a7f327e9.js.br +0 -0
- codex/static_root/assets/main-a7f327e9.js.gz +0 -0
- codex/static_root/assets/metadata-dialog-785c4cfc.694a251cda37.css.br +0 -0
- codex/static_root/assets/metadata-dialog-785c4cfc.694a251cda37.css.gz +0 -0
- codex/static_root/assets/metadata-dialog-785c4cfc.css.br +0 -0
- codex/static_root/assets/metadata-dialog-785c4cfc.css.gz +0 -0
- codex/static_root/assets/metadata-dialog-8a0bd8e1.c213b08d582f.js +0 -1
- codex/static_root/assets/metadata-dialog-8a0bd8e1.c213b08d582f.js.br +0 -0
- codex/static_root/assets/metadata-dialog-8a0bd8e1.c213b08d582f.js.gz +0 -0
- codex/static_root/assets/metadata-dialog-8a0bd8e1.js +0 -1
- codex/static_root/assets/metadata-dialog-8a0bd8e1.js.br +0 -0
- codex/static_root/assets/metadata-dialog-8a0bd8e1.js.gz +0 -0
- codex/static_root/assets/page-pdf-c603e996.ab2d147c9ae1.js.br +0 -0
- codex/static_root/assets/page-pdf-c603e996.ab2d147c9ae1.js.gz +0 -0
- codex/static_root/assets/page-pdf-c603e996.js.br +0 -0
- codex/static_root/assets/page-pdf-c603e996.js.gz +0 -0
- codex/static_root/assets/reader-c2965a5f.b011260169f7.js +0 -1
- codex/static_root/assets/reader-c2965a5f.b011260169f7.js.br +0 -0
- codex/static_root/assets/reader-c2965a5f.b011260169f7.js.gz +0 -0
- codex/static_root/assets/reader-c2965a5f.js +0 -1
- codex/static_root/assets/reader-c2965a5f.js.br +0 -0
- codex/static_root/assets/reader-c2965a5f.js.gz +0 -0
- codex/static_root/assets/reader-d8534888.2821de925986.css +0 -1
- codex/static_root/assets/reader-d8534888.2821de925986.css.br +0 -0
- codex/static_root/assets/reader-d8534888.2821de925986.css.gz +0 -0
- codex/static_root/assets/reader-d8534888.css +0 -1
- codex/static_root/assets/reader-d8534888.css.br +0 -0
- codex/static_root/assets/reader-d8534888.css.gz +0 -0
- codex/static_root/js/choices.6bfc2a3d293f.json +0 -1
- codex/static_root/js/choices.6bfc2a3d293f.json.br +0 -0
- codex/static_root/js/choices.6bfc2a3d293f.json.gz +0 -0
- codex/static_root/manifest.c0e270b2e6b6.json.br +0 -0
- codex/static_root/manifest.c0e270b2e6b6.json.gz +0 -0
- codex/urls/opds/opensearch.py +0 -18
- codex/views/opds/v1/browser.py +0 -346
- codex/views/opds/v1/entry.py +0 -278
- codex/views/opds/v1/start.py +0 -28
- codex/views/opds/v1/util.py +0 -162
- codex/views/opds/v2/start.py +0 -28
- {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/LICENSE +0 -0
- {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/WHEEL +0 -0
- {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/entry_points.txt +0 -0
codex/views/browser/choices.py
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
"""View for marking comics read and unread."""
|
|
2
2
|
import pycountry
|
|
3
3
|
from caseconverter import snakecase
|
|
4
|
+
from django.db.models import QuerySet
|
|
4
5
|
from drf_spectacular.utils import extend_schema
|
|
5
6
|
from rest_framework.response import Response
|
|
6
7
|
|
|
7
8
|
from codex.logger.logging import get_logger
|
|
8
|
-
from codex.models import
|
|
9
|
+
from codex.models import (
|
|
10
|
+
Comic,
|
|
11
|
+
CreatorPerson,
|
|
12
|
+
Folder,
|
|
13
|
+
Imprint,
|
|
14
|
+
Publisher,
|
|
15
|
+
Series,
|
|
16
|
+
StoryArc,
|
|
17
|
+
Volume,
|
|
18
|
+
)
|
|
9
19
|
from codex.serializers.browser import (
|
|
10
20
|
BrowserChoicesSerializer,
|
|
11
21
|
BrowserFilterChoicesSerializer,
|
|
@@ -22,23 +32,37 @@ class BrowserChoicesViewBase(BrowserBaseView):
|
|
|
22
32
|
|
|
23
33
|
permission_classes = [IsAuthenticatedOrEnabledNonUsers]
|
|
24
34
|
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
_CREATORS_PERSON_REL = "creators__person"
|
|
36
|
+
_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
|
+
}
|
|
27
48
|
|
|
28
49
|
@staticmethod
|
|
29
|
-
def get_field_choices_query(
|
|
50
|
+
def get_field_choices_query(comic_qs, field_name):
|
|
30
51
|
"""Get distinct values for the field."""
|
|
31
|
-
return
|
|
52
|
+
return (
|
|
53
|
+
comic_qs.exclude(**{field_name: None})
|
|
54
|
+
.values_list(field_name, flat=True)
|
|
55
|
+
.distinct()
|
|
56
|
+
)
|
|
32
57
|
|
|
33
|
-
|
|
34
|
-
def get_m2m_field_query(cls, rel, comic_qs, model):
|
|
58
|
+
def get_m2m_field_query(self, model, comic_qs: QuerySet):
|
|
35
59
|
"""Get distinct m2m value objects for the relation."""
|
|
36
|
-
|
|
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"
|
|
37
64
|
return (
|
|
38
|
-
model.objects.filter(**{
|
|
39
|
-
.prefetch_related(comic_rel)
|
|
40
|
-
.values("pk", "name")
|
|
41
|
-
.distinct()
|
|
65
|
+
model.objects.filter(**{back_rel: comic_qs}).values("pk", "name").distinct()
|
|
42
66
|
)
|
|
43
67
|
|
|
44
68
|
@staticmethod
|
|
@@ -46,12 +70,14 @@ class BrowserChoicesViewBase(BrowserBaseView):
|
|
|
46
70
|
"""Get if null values exists for an m2m field."""
|
|
47
71
|
return comic_qs.filter(**{f"{rel}__isnull": True}).exists()
|
|
48
72
|
|
|
49
|
-
|
|
50
|
-
def _get_rel_and_model(cls, field_name):
|
|
73
|
+
def _get_rel_and_model(self, field_name):
|
|
51
74
|
"""Return the relation and model for the field name."""
|
|
52
|
-
if field_name ==
|
|
53
|
-
rel =
|
|
75
|
+
if field_name == self.CREATOR_PERSON_UI_FIELD:
|
|
76
|
+
rel = self._CREATORS_PERSON_REL
|
|
54
77
|
model = CreatorPerson
|
|
78
|
+
elif field_name == self.STORY_ARC_UI_FIELD:
|
|
79
|
+
rel = self._STORY_ARC_REL
|
|
80
|
+
model = StoryArc
|
|
55
81
|
else:
|
|
56
82
|
remote_field = getattr(
|
|
57
83
|
Comic._meta.get_field(field_name), "remote_field", None
|
|
@@ -59,12 +85,21 @@ class BrowserChoicesViewBase(BrowserBaseView):
|
|
|
59
85
|
rel = field_name
|
|
60
86
|
model = remote_field.model if remote_field else None
|
|
61
87
|
|
|
88
|
+
rel = self.rel_prefix + rel
|
|
89
|
+
|
|
62
90
|
return rel, model
|
|
63
91
|
|
|
64
92
|
def get_object(self):
|
|
65
93
|
"""Get the comic subquery use for the choices."""
|
|
66
|
-
object_filter, _ = self.get_query_filters(
|
|
67
|
-
return
|
|
94
|
+
object_filter, _ = self.get_query_filters(self.model, True)
|
|
95
|
+
return self.model.objects.filter(object_filter)
|
|
96
|
+
|
|
97
|
+
def _set_model(self):
|
|
98
|
+
"""Set the model to query."""
|
|
99
|
+
group = self.kwargs["group"]
|
|
100
|
+
if group == self.ROOT_GROUP:
|
|
101
|
+
group = self.params.get("top_group", "p")
|
|
102
|
+
self.model = self.GROUP_MODEL_MAP[group]
|
|
68
103
|
|
|
69
104
|
|
|
70
105
|
class BrowserChoicesAvailableView(BrowserChoicesViewBase):
|
|
@@ -72,23 +107,20 @@ class BrowserChoicesAvailableView(BrowserChoicesViewBase):
|
|
|
72
107
|
|
|
73
108
|
serializer_class = BrowserFilterChoicesSerializer
|
|
74
109
|
|
|
75
|
-
CREATORS_PERSON_REL = "creators__person"
|
|
76
|
-
|
|
77
110
|
@classmethod
|
|
78
|
-
def _get_field_choices_count(cls,
|
|
111
|
+
def _get_field_choices_count(cls, comic_qs, field_name):
|
|
79
112
|
"""Create a pk:name object for fields without tables."""
|
|
80
|
-
return cls.get_field_choices_query(
|
|
113
|
+
return cls.get_field_choices_query(comic_qs, field_name).count()
|
|
81
114
|
|
|
82
|
-
|
|
83
|
-
def _get_m2m_field_choices_count(cls, rel, comic_qs, model):
|
|
115
|
+
def _get_m2m_field_choices_count(self, model, comic_qs, rel):
|
|
84
116
|
"""Get choices with nulls where there are nulls."""
|
|
85
|
-
count =
|
|
117
|
+
count = self.get_m2m_field_query(model, comic_qs).count()
|
|
86
118
|
|
|
87
119
|
# Detect if there are null choices.
|
|
88
120
|
# Regretabbly with another query, but doing a forward query
|
|
89
121
|
# on the comic above restricts all results to only the filtered
|
|
90
122
|
# rows. :(
|
|
91
|
-
if
|
|
123
|
+
if self.does_m2m_null_exist(comic_qs, rel):
|
|
92
124
|
count += 1
|
|
93
125
|
|
|
94
126
|
return count
|
|
@@ -97,16 +129,21 @@ class BrowserChoicesAvailableView(BrowserChoicesViewBase):
|
|
|
97
129
|
def get(self, *args, **kwargs):
|
|
98
130
|
"""Return all choices with more than one choice."""
|
|
99
131
|
self.parse_params()
|
|
132
|
+
self._set_model()
|
|
133
|
+
self.set_rel_prefix(self.model)
|
|
100
134
|
comic_qs = self.get_object()
|
|
101
135
|
|
|
102
136
|
data = {}
|
|
103
137
|
for field_name in self.serializer_class().get_fields(): # type: ignore
|
|
138
|
+
if field_name == "story_arcs" and self.model == StoryArc:
|
|
139
|
+
# don't allow filtering on story arc in story arc view.
|
|
140
|
+
continue
|
|
104
141
|
rel, m2m_model = self._get_rel_and_model(field_name)
|
|
105
142
|
|
|
106
143
|
if m2m_model:
|
|
107
|
-
count = self._get_m2m_field_choices_count(
|
|
144
|
+
count = self._get_m2m_field_choices_count(m2m_model, comic_qs, rel)
|
|
108
145
|
else:
|
|
109
|
-
count = self._get_field_choices_count(
|
|
146
|
+
count = self._get_field_choices_count(comic_qs, rel)
|
|
110
147
|
|
|
111
148
|
filters = self.params.get("filters", {})
|
|
112
149
|
data[field_name] = count > 1 or field_name in filters
|
|
@@ -120,10 +157,9 @@ class BrowserChoicesView(BrowserChoicesViewBase):
|
|
|
120
157
|
|
|
121
158
|
serializer_class = BrowserChoicesSerializer
|
|
122
159
|
|
|
123
|
-
|
|
124
|
-
def _get_field_choices(cls, field_name, comic_qs):
|
|
160
|
+
def _get_field_choices(self, comic_qs, field_name):
|
|
125
161
|
"""Create a pk:name object for fields without tables."""
|
|
126
|
-
qs =
|
|
162
|
+
qs = self.get_field_choices_query(comic_qs, field_name)
|
|
127
163
|
|
|
128
164
|
if field_name == "country":
|
|
129
165
|
lookup = pycountry.countries
|
|
@@ -139,18 +175,17 @@ class BrowserChoicesView(BrowserChoicesViewBase):
|
|
|
139
175
|
|
|
140
176
|
return choices
|
|
141
177
|
|
|
142
|
-
|
|
143
|
-
def _get_m2m_field_choices(cls, rel, comic_qs, model):
|
|
178
|
+
def _get_m2m_field_choices(self, model, comic_qs, rel):
|
|
144
179
|
"""Get choices with nulls where there are nulls."""
|
|
145
|
-
qs =
|
|
180
|
+
qs = self.get_m2m_field_query(model, comic_qs)
|
|
146
181
|
|
|
147
182
|
# Detect if there are null choices.
|
|
148
183
|
# Regretabbly with another query, but doing a forward query
|
|
149
184
|
# on the comic above restrcts all results to only the filtered
|
|
150
185
|
# rows. :(
|
|
151
|
-
if
|
|
186
|
+
if self.does_m2m_null_exist(comic_qs, rel):
|
|
152
187
|
choices = list(qs)
|
|
153
|
-
choices.append(
|
|
188
|
+
choices.append(self._NULL_NAMED_ROW)
|
|
154
189
|
else:
|
|
155
190
|
choices = qs
|
|
156
191
|
return choices
|
|
@@ -159,6 +194,8 @@ class BrowserChoicesView(BrowserChoicesViewBase):
|
|
|
159
194
|
def get(self, *args, **kwargs):
|
|
160
195
|
"""Return all choices with more than one choice."""
|
|
161
196
|
self.parse_params()
|
|
197
|
+
self._set_model()
|
|
198
|
+
self.set_rel_prefix(self.model)
|
|
162
199
|
|
|
163
200
|
field_name = snakecase(self.kwargs["field_name"])
|
|
164
201
|
|
|
@@ -166,9 +203,9 @@ class BrowserChoicesView(BrowserChoicesViewBase):
|
|
|
166
203
|
|
|
167
204
|
comic_qs = self.get_object()
|
|
168
205
|
if m2m_model:
|
|
169
|
-
choices = self._get_m2m_field_choices(
|
|
206
|
+
choices = self._get_m2m_field_choices(m2m_model, comic_qs, rel)
|
|
170
207
|
else:
|
|
171
|
-
choices = self._get_field_choices(
|
|
208
|
+
choices = self._get_field_choices(comic_qs, rel)
|
|
172
209
|
|
|
173
210
|
serializer = self.get_serializer(choices, many=True)
|
|
174
211
|
return Response(serializer.data)
|
|
@@ -9,12 +9,10 @@ class BookmarkFilterMixin:
|
|
|
9
9
|
|
|
10
10
|
_BOOKMARK_FILTERS = frozenset(set(CHOICES["bookmarkFilter"].keys()) - {"ALL"})
|
|
11
11
|
|
|
12
|
-
def get_bm_rel(self,
|
|
12
|
+
def get_bm_rel(self, model):
|
|
13
13
|
"""Create bookmark relation."""
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
bm_rel = "comic__" + bm_rel
|
|
17
|
-
return bm_rel
|
|
14
|
+
rel_prefix = self.get_rel_prefix(model) # type: ignore
|
|
15
|
+
return rel_prefix + "bookmark"
|
|
18
16
|
|
|
19
17
|
def _get_my_bookmark_filter(self, bm_rel):
|
|
20
18
|
"""Get a filter for my session or user defined bookmarks."""
|
|
@@ -27,12 +25,11 @@ class BookmarkFilterMixin:
|
|
|
27
25
|
}
|
|
28
26
|
return Q(**my_bookmarks_kwargs)
|
|
29
27
|
|
|
30
|
-
def get_bookmark_filter(self,
|
|
28
|
+
def get_bookmark_filter(self, model):
|
|
31
29
|
"""Build bookmark query."""
|
|
32
|
-
|
|
33
|
-
choice = self.params["filters"].get("bookmark", "ALL") # type: ignore
|
|
30
|
+
choice = self.params["filters"].get("bookmark", "ALL") # type: ignore
|
|
34
31
|
if choice in self._BOOKMARK_FILTERS:
|
|
35
|
-
bm_rel = self.get_bm_rel(
|
|
32
|
+
bm_rel = self.get_bm_rel(model)
|
|
36
33
|
my_bookmark_filter = self._get_my_bookmark_filter(bm_rel)
|
|
37
34
|
if choice in ("UNREAD", "IN_PROGRESS"):
|
|
38
35
|
my_not_finished_filter = my_bookmark_filter & Q(
|
|
@@ -7,18 +7,21 @@ from codex.views.session import BrowserSessionViewBase
|
|
|
7
7
|
class ComicFieldFilter(BrowserSessionViewBase):
|
|
8
8
|
"""Comic field filters."""
|
|
9
9
|
|
|
10
|
-
def _filter_by_comic_field(self, field
|
|
10
|
+
def _filter_by_comic_field(self, field):
|
|
11
11
|
"""Filter by a comic any2many attribute."""
|
|
12
12
|
filter_list = self.params["filters"].get(field) # type: ignore
|
|
13
13
|
filter_query = Q()
|
|
14
14
|
if not filter_list:
|
|
15
15
|
return filter_query
|
|
16
|
-
query_prefix = "" if is_model_comic else "comic__"
|
|
17
16
|
|
|
18
17
|
if field == self.CREATOR_PERSON_UI_FIELD:
|
|
19
|
-
rel =
|
|
18
|
+
rel = "creators__person"
|
|
19
|
+
elif field == self.STORY_ARC_UI_FIELD:
|
|
20
|
+
rel = "storyarcnumber__story_arc"
|
|
20
21
|
else:
|
|
21
|
-
rel =
|
|
22
|
+
rel = field
|
|
23
|
+
|
|
24
|
+
rel = self.rel_prefix + rel # type: ignore
|
|
22
25
|
|
|
23
26
|
for index, val in enumerate(filter_list):
|
|
24
27
|
# None values in a list don't work right so test for them separately
|
|
@@ -29,9 +32,9 @@ class ComicFieldFilter(BrowserSessionViewBase):
|
|
|
29
32
|
filter_query |= Q(**{f"{rel}__in": filter_list})
|
|
30
33
|
return filter_query
|
|
31
34
|
|
|
32
|
-
def get_comic_field_filter(self
|
|
35
|
+
def get_comic_field_filter(self):
|
|
33
36
|
"""Filter the comics based on the form filters."""
|
|
34
37
|
comic_field_filter = Q()
|
|
35
38
|
for attribute in self.FILTER_ATTRIBUTES:
|
|
36
|
-
comic_field_filter &= self._filter_by_comic_field(attribute
|
|
39
|
+
comic_field_filter &= self._filter_by_comic_field(attribute)
|
|
37
40
|
return comic_field_filter
|
|
@@ -7,35 +7,20 @@ from codex.views.mixins import GroupACLMixin
|
|
|
7
7
|
class GroupFilterMixin(GroupACLMixin):
|
|
8
8
|
"""Group Filters."""
|
|
9
9
|
|
|
10
|
-
def _get_folders_filter(self):
|
|
11
|
-
"""Get a filter for ALL parent folders not just immediate one."""
|
|
12
|
-
pk = self.kwargs.get("pk") # type: ignore
|
|
13
|
-
return Q(folders__in=[pk]) if pk else Q()
|
|
14
|
-
|
|
15
|
-
def _get_browser_group_filter(self):
|
|
16
|
-
"""Get the objects we'll be displaying."""
|
|
17
|
-
# Get the instances that are children of the group_instance
|
|
18
|
-
# And the filtered comics that are children of the group_instance
|
|
19
|
-
group_filter = Q()
|
|
20
|
-
pk = self.kwargs.get("pk") # type: ignore
|
|
21
|
-
group = self.kwargs.get("group") # type: ignore
|
|
22
|
-
if pk or group == self.FOLDER_GROUP:
|
|
23
|
-
if not pk:
|
|
24
|
-
pk = None
|
|
25
|
-
group_relation = self.GROUP_RELATION[group]
|
|
26
|
-
group_filter |= Q(**{group_relation: pk})
|
|
27
|
-
|
|
28
|
-
return group_filter
|
|
29
|
-
|
|
30
10
|
def get_group_filter(self, choices):
|
|
31
11
|
"""Get filter for the displayed group."""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
12
|
+
pk = self.kwargs.get("pk") # type: ignore
|
|
13
|
+
group = self.kwargs.get("group") # type: ignore
|
|
14
|
+
if pk:
|
|
15
|
+
group_relation = "comic__" if choices else ""
|
|
16
|
+
if choices and group == self.FOLDER_GROUP:
|
|
17
|
+
group_relation += "folders"
|
|
18
|
+
else:
|
|
19
|
+
group_relation += self.GROUP_RELATION[group]
|
|
20
|
+
group_filter = Q(**{group_relation: pk})
|
|
21
|
+
elif group == self.FOLDER_GROUP:
|
|
22
|
+
group_filter = Q(parent_folder=None)
|
|
37
23
|
else:
|
|
38
|
-
|
|
39
|
-
group_filter = self._get_browser_group_filter()
|
|
24
|
+
group_filter = Q()
|
|
40
25
|
|
|
41
26
|
return group_filter
|
|
@@ -23,19 +23,14 @@ class SearchFilterMixin:
|
|
|
23
23
|
LOG.warning("While searching:")
|
|
24
24
|
LOG.exception(exc)
|
|
25
25
|
|
|
26
|
-
def _get_search_query_filter(self, text,
|
|
26
|
+
def _get_search_query_filter(self, text, search_scores):
|
|
27
27
|
"""Get the search filter and scores."""
|
|
28
|
-
#
|
|
28
|
+
rel = self.rel_prefix + "pk__in" # type: ignore
|
|
29
29
|
self._get_search_scores(text, search_scores)
|
|
30
|
-
|
|
31
|
-
# Create query
|
|
32
|
-
prefix = ""
|
|
33
|
-
if not is_model_comic:
|
|
34
|
-
prefix = "comic__"
|
|
35
|
-
query_dict = {f"{prefix}pk__in": search_scores.keys()}
|
|
30
|
+
query_dict = {rel: search_scores.keys()}
|
|
36
31
|
return Q(**query_dict)
|
|
37
32
|
|
|
38
|
-
def get_search_filter(self
|
|
33
|
+
def get_search_filter(self):
|
|
39
34
|
"""Preparse search, search and return the filter and scores."""
|
|
40
35
|
search_filter = Q()
|
|
41
36
|
search_scores = {}
|
|
@@ -48,7 +43,7 @@ class SearchFilterMixin:
|
|
|
48
43
|
if query_string:
|
|
49
44
|
# Query haystack
|
|
50
45
|
search_filter = self._get_search_query_filter(
|
|
51
|
-
query_string,
|
|
46
|
+
query_string, search_scores
|
|
52
47
|
)
|
|
53
48
|
except Exception as exc:
|
|
54
49
|
LOG.warning(exc)
|
codex/views/browser/metadata.py
CHANGED
|
@@ -7,7 +7,7 @@ from rest_framework.exceptions import NotFound
|
|
|
7
7
|
from rest_framework.response import Response
|
|
8
8
|
|
|
9
9
|
from codex.comic_field_names import COMIC_M2M_FIELD_NAMES
|
|
10
|
-
from codex.models import AdminFlag, Comic
|
|
10
|
+
from codex.models import AdminFlag, Comic, StoryArc
|
|
11
11
|
from codex.serializers.metadata import MetadataSerializer
|
|
12
12
|
from codex.views.auth import IsAuthenticatedOrEnabledNonUsers
|
|
13
13
|
from codex.views.browser.browser_annotations import BrowserAnnotationsView
|
|
@@ -28,6 +28,7 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
28
28
|
"critical_rating",
|
|
29
29
|
"day",
|
|
30
30
|
"file_type",
|
|
31
|
+
"gtin",
|
|
31
32
|
"issue",
|
|
32
33
|
"issue_suffix",
|
|
33
34
|
"language",
|
|
@@ -56,6 +57,7 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
56
57
|
_COMIC_RELATED_VALUE_FIELDS = {"series__volume_count", "volume__issue_count"}
|
|
57
58
|
_PATH_GROUPS = ("c", "f")
|
|
58
59
|
_CREATOR_RELATIONS = ("role", "person")
|
|
60
|
+
_STORY_ARC_NUMBER_RELATIONS = ("story_arc",)
|
|
59
61
|
|
|
60
62
|
def _get_comic_value_fields(self):
|
|
61
63
|
"""Include the path field for staff."""
|
|
@@ -79,7 +81,7 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
79
81
|
# Have to use simple_qs because every annotation in the loop
|
|
80
82
|
# corrupts the the main qs
|
|
81
83
|
# If 1 variant, annotate value, otherwise None
|
|
82
|
-
full_field =
|
|
84
|
+
full_field = self.rel_prefix + field
|
|
83
85
|
|
|
84
86
|
sq = (
|
|
85
87
|
simple_qs.values("id")
|
|
@@ -105,7 +107,7 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
105
107
|
def _annotate_aggregates(self, qs):
|
|
106
108
|
"""Annotate aggregate values."""
|
|
107
109
|
if not self.is_model_comic:
|
|
108
|
-
size_func = self.get_aggregate_func("size"
|
|
110
|
+
size_func = self.get_aggregate_func(self.model, "size")
|
|
109
111
|
qs = qs.annotate(size=size_func)
|
|
110
112
|
qs = self.annotate_common_aggregates(qs, self.model, {})
|
|
111
113
|
return qs
|
|
@@ -135,18 +137,32 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
135
137
|
)
|
|
136
138
|
return qs
|
|
137
139
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
@staticmethod
|
|
141
|
+
def _get_intersection_queryset(qs, values, count_rel, comic_pks):
|
|
142
|
+
"""Create an intersection queryset."""
|
|
143
|
+
return (
|
|
144
|
+
qs.only(*values)
|
|
145
|
+
.annotate(count=Count(count_rel))
|
|
146
|
+
.order_by()
|
|
147
|
+
.filter(count=comic_pks.count())
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def _get_story_arc_intersection_queryset(cls, comic_pks):
|
|
152
|
+
"""Hoist Story Arc intersections up from StoryArcNumber."""
|
|
153
|
+
qs = StoryArc.objects.filter(storyarcnumber__comic__pk__in=comic_pks)
|
|
154
|
+
return cls._get_intersection_queryset(
|
|
155
|
+
qs,
|
|
156
|
+
("name",),
|
|
157
|
+
"storyarcnumber__comic",
|
|
158
|
+
comic_pks,
|
|
159
|
+
)
|
|
144
160
|
|
|
145
161
|
def _query_m2m_intersections(self, simple_qs):
|
|
146
162
|
"""Query the through models to figure out m2m intersections."""
|
|
147
163
|
# Speed ok, but still does a query per m2m model
|
|
148
164
|
m2m_intersections = {}
|
|
149
|
-
pk_field =
|
|
165
|
+
pk_field = self.rel_prefix + "pk"
|
|
150
166
|
comic_pks = simple_qs.values_list(pk_field, flat=True)
|
|
151
167
|
for field_name in COMIC_M2M_FIELD_NAMES:
|
|
152
168
|
model = Comic._meta.get_field(field_name).related_model
|
|
@@ -156,19 +172,28 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
156
172
|
|
|
157
173
|
intersection_qs = model.objects.filter(comic__pk__in=comic_pks)
|
|
158
174
|
if field_name == "creators":
|
|
175
|
+
# XXX This doesn't prevent an n+1 warning
|
|
159
176
|
intersection_qs = intersection_qs.select_related(
|
|
160
177
|
*self._CREATOR_RELATIONS
|
|
161
178
|
)
|
|
162
179
|
values = self._CREATOR_RELATIONS
|
|
180
|
+
elif field_name == "story_arc_numbers":
|
|
181
|
+
# XXX This doesn't prevent an n+1 warning
|
|
182
|
+
intersection_qs = intersection_qs.select_related(
|
|
183
|
+
*self._STORY_ARC_NUMBER_RELATIONS
|
|
184
|
+
)
|
|
185
|
+
values = self._STORY_ARC_NUMBER_RELATIONS
|
|
186
|
+
|
|
187
|
+
# Extra add on m2m
|
|
188
|
+
m2m_intersections[
|
|
189
|
+
"story_arcs"
|
|
190
|
+
] = self._get_story_arc_intersection_queryset(comic_pks)
|
|
163
191
|
else:
|
|
164
192
|
values = ("name",)
|
|
165
193
|
|
|
166
194
|
# order_by() is very important for grouping
|
|
167
|
-
intersection_qs = (
|
|
168
|
-
intersection_qs
|
|
169
|
-
.annotate(count=Count("comic"))
|
|
170
|
-
.order_by()
|
|
171
|
-
.filter(count=comic_pks.count())
|
|
195
|
+
intersection_qs = self._get_intersection_queryset(
|
|
196
|
+
intersection_qs, values, "comic", comic_pks
|
|
172
197
|
)
|
|
173
198
|
m2m_intersections[field_name] = intersection_qs
|
|
174
199
|
return m2m_intersections
|
|
@@ -221,7 +246,7 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
221
246
|
|
|
222
247
|
# filename
|
|
223
248
|
if self.model == Comic:
|
|
224
|
-
obj.filename =
|
|
249
|
+
obj.filename = obj.filename()
|
|
225
250
|
|
|
226
251
|
return obj
|
|
227
252
|
|
|
@@ -234,7 +259,7 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
234
259
|
if self.model is None:
|
|
235
260
|
raise NotFound(detail=f"Cannot get metadata for {self.group=}")
|
|
236
261
|
|
|
237
|
-
object_filter, _ = self.get_query_filters_without_group(self.
|
|
262
|
+
object_filter, _ = self.get_query_filters_without_group(self.model)
|
|
238
263
|
pk = self.kwargs["pk"]
|
|
239
264
|
qs = self.model.objects.filter(object_filter, pk=pk)
|
|
240
265
|
|
|
@@ -242,10 +267,8 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
242
267
|
simple_qs = qs
|
|
243
268
|
|
|
244
269
|
qs = self._annotate_values_and_fks(qs, simple_qs)
|
|
245
|
-
qs = self._annotate_for_filename(qs)
|
|
246
270
|
|
|
247
271
|
try:
|
|
248
|
-
# obj = qs.values()[0]
|
|
249
272
|
obj = qs.first()
|
|
250
273
|
if not obj:
|
|
251
274
|
reason = "Empty obj"
|
|
@@ -282,6 +305,8 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
282
305
|
self.parse_params()
|
|
283
306
|
self.group = self.kwargs["group"]
|
|
284
307
|
self._validate()
|
|
308
|
+
self.rel_prefix = self.get_rel_prefix(self.model)
|
|
309
|
+
self.set_order_key()
|
|
285
310
|
|
|
286
311
|
obj = self.get_object()
|
|
287
312
|
|
codex/views/download.py
CHANGED
|
@@ -25,7 +25,7 @@ class DownloadView(APIView, GroupACLMixin):
|
|
|
25
25
|
"""Download a comic archive."""
|
|
26
26
|
pk = kwargs.get("pk")
|
|
27
27
|
try:
|
|
28
|
-
group_acl_filter = self.get_group_acl_filter(
|
|
28
|
+
group_acl_filter = self.get_group_acl_filter(Comic)
|
|
29
29
|
comic = (
|
|
30
30
|
Comic.objects.filter(group_acl_filter)
|
|
31
31
|
.select_related(*self._DOWNLOAD_SELECT_RELATED)
|
codex/views/frontend.py
CHANGED
|
@@ -14,7 +14,6 @@ class IndexView(BrowserSessionViewBase):
|
|
|
14
14
|
|
|
15
15
|
def get(self, *args, **kwargs):
|
|
16
16
|
"""Get the app index page."""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
17
|
+
last_route = self.get_from_session("route")
|
|
18
|
+
extra_context = {"last_route": last_route}
|
|
20
19
|
return Response(extra_context)
|
codex/views/mixins.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"""A filter for group ACLS."""
|
|
2
2
|
from django.db.models import Q
|
|
3
3
|
|
|
4
|
+
from codex.models import Comic, Folder, StoryArc
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
class GroupACLMixin:
|
|
6
8
|
"""Filter group ACLS for views."""
|
|
7
9
|
|
|
8
10
|
ROOT_GROUP = "r"
|
|
9
11
|
FOLDER_GROUP = "f"
|
|
12
|
+
STORY_ARC_GROUP = "a"
|
|
10
13
|
COMIC_GROUP = "c"
|
|
11
14
|
GROUP_RELATION = {
|
|
12
15
|
"p": "publisher",
|
|
@@ -15,12 +18,22 @@ class GroupACLMixin:
|
|
|
15
18
|
"v": "volume",
|
|
16
19
|
COMIC_GROUP: "pk",
|
|
17
20
|
FOLDER_GROUP: "parent_folder",
|
|
21
|
+
STORY_ARC_GROUP: "story_arc_numbers__story_arc",
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
def
|
|
24
|
+
def get_rel_prefix(self, model):
|
|
25
|
+
"""Return the relation prfiex for most fields."""
|
|
26
|
+
prefix = ""
|
|
27
|
+
if model != Comic:
|
|
28
|
+
if model == StoryArc:
|
|
29
|
+
prefix += "storyarcnumber__"
|
|
30
|
+
prefix += "comic__"
|
|
31
|
+
return prefix
|
|
32
|
+
|
|
33
|
+
def get_group_acl_filter(self, model):
|
|
21
34
|
"""Generate the group acl filter for comics."""
|
|
22
35
|
# The rel prefix
|
|
23
|
-
prefix =
|
|
36
|
+
prefix = self.get_rel_prefix(model) if model != Folder else ""
|
|
24
37
|
groups_rel = f"{prefix}library__groups"
|
|
25
38
|
|
|
26
39
|
# Libraries with no groups are always visible
|
codex/views/opds/const.py
CHANGED
|
@@ -15,6 +15,7 @@ class Rel:
|
|
|
15
15
|
IMAGE = "http://opds-spec.org/image"
|
|
16
16
|
STREAM = "http://vaemendis.net/opds-pse/stream"
|
|
17
17
|
SORT_NEW = "http://opds-spec.org/sort/new"
|
|
18
|
+
POPULAR = "http://opds-spec.org/sort/popular"
|
|
18
19
|
FEATURED = "http://opds-spec.org/featured"
|
|
19
20
|
SELF = "self"
|
|
20
21
|
START = "start"
|
|
@@ -40,7 +41,6 @@ class MimeType:
|
|
|
40
41
|
ENTRY_CATALOG = ";".join((ATOM, "type=entry", _PROFILE_CATALOG))
|
|
41
42
|
AUTHENTICATION = "application/opds-authentication+json"
|
|
42
43
|
OPENSEARCH = "application/opensearchdescription+xml"
|
|
43
|
-
DOWNLOAD = "application/zip" # PocketBooks needs app/zip
|
|
44
44
|
STREAM = "image/jpeg"
|
|
45
45
|
OPDS_JSON = "application/opds+json"
|
|
46
46
|
OPDS_PUB = "application/opds-publication+json"
|
|
@@ -57,4 +57,11 @@ class MimeType:
|
|
|
57
57
|
"CBT": "application/vnd.comicbook+tar",
|
|
58
58
|
"PDF": "application/pdf",
|
|
59
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
|
+
}
|
|
60
67
|
OCTET = "application/octet-stream"
|