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
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
"""Base view for metadata annotations."""
|
|
2
|
+
from os.path import sep
|
|
3
|
+
|
|
2
4
|
from django.db.models import (
|
|
3
5
|
BooleanField,
|
|
4
6
|
Case,
|
|
7
|
+
CharField,
|
|
5
8
|
Count,
|
|
6
9
|
DateTimeField,
|
|
7
10
|
F,
|
|
11
|
+
FilteredRelation,
|
|
12
|
+
Min,
|
|
8
13
|
OuterRef,
|
|
9
14
|
Q,
|
|
10
15
|
Subquery,
|
|
@@ -13,10 +18,9 @@ from django.db.models import (
|
|
|
13
18
|
When,
|
|
14
19
|
)
|
|
15
20
|
from django.db.models.fields import PositiveSmallIntegerField
|
|
16
|
-
from django.db.models.functions import Least
|
|
21
|
+
from django.db.models.functions import Least, Lower, Reverse, Right, StrIndex, Substr
|
|
17
22
|
|
|
18
|
-
from codex.models import Comic
|
|
19
|
-
from codex.views.browser.base import BrowserBaseView
|
|
23
|
+
from codex.models import Comic
|
|
20
24
|
from codex.views.browser.browser_order_by import BrowserOrderByView
|
|
21
25
|
|
|
22
26
|
|
|
@@ -24,31 +28,36 @@ class BrowserAnnotationsView(BrowserOrderByView):
|
|
|
24
28
|
"""Base class for views that need special metadata annotations."""
|
|
25
29
|
|
|
26
30
|
_ONE_INTEGERFIELD = Value(1, PositiveSmallIntegerField())
|
|
27
|
-
|
|
28
|
-
GROUP_MODEL_MAP = {
|
|
29
|
-
BrowserBaseView.ROOT_GROUP: None,
|
|
30
|
-
"p": Publisher,
|
|
31
|
-
"i": Imprint,
|
|
32
|
-
"s": Series,
|
|
33
|
-
"v": Volume,
|
|
34
|
-
BrowserBaseView.COMIC_GROUP: Comic,
|
|
35
|
-
BrowserBaseView.FOLDER_GROUP: Folder,
|
|
36
|
-
}
|
|
31
|
+
_NONE_INTEGERFIELD = Value(None, PositiveSmallIntegerField())
|
|
37
32
|
_NONE_DATETIMEFIELD = Value(None, DateTimeField())
|
|
33
|
+
_ARTICLES = frozenset(
|
|
34
|
+
("a", "an", "the") # en # noqa RUF005
|
|
35
|
+
+ ("un", "unos", "unas", "el", "los", "la", "las") # es
|
|
36
|
+
+ ("un", "une", "le", "les", "la", "les", "l'") # fr
|
|
37
|
+
+ ("o", "a", "os") # pt
|
|
38
|
+
# pt "as" conflicts with English
|
|
39
|
+
+ ("der", "dem", "des", "das") # de
|
|
40
|
+
# de: "den & die conflict with English
|
|
41
|
+
+ ("il", "lo", "gli", "la", "le", "l'") # it
|
|
42
|
+
# it: "i" conflicts with English
|
|
43
|
+
+ ("de", "het", "een") # nl
|
|
44
|
+
+ ("en", "ett") # sw
|
|
45
|
+
+ ("en", "ei", "et") # no
|
|
46
|
+
+ ("en", "et") # da
|
|
47
|
+
+ ("el", "la", "els", "les", "un", "una", "uns", "unes", "na") # ct
|
|
48
|
+
)
|
|
38
49
|
|
|
39
50
|
is_opds_1_acquisition = False
|
|
40
51
|
|
|
41
|
-
def _annotate_search_score(self, queryset,
|
|
52
|
+
def _annotate_search_score(self, queryset, search_scores):
|
|
42
53
|
"""Annotate the search score for ordering by search score."""
|
|
43
|
-
order_key
|
|
44
|
-
if order_key != "search_score":
|
|
54
|
+
if self.order_key != "search_score":
|
|
45
55
|
return queryset
|
|
46
|
-
prefix = "comic__" if not is_model_comic else ""
|
|
47
56
|
whens = []
|
|
48
57
|
for pk, score in search_scores.items():
|
|
49
|
-
when = {
|
|
58
|
+
when = {self.rel_prefix + "pk": pk, "then": score}
|
|
50
59
|
whens.append(When(**when))
|
|
51
|
-
annotate = {
|
|
60
|
+
annotate = {self.rel_prefix + "search_score": Case(*whens, default=0.0)}
|
|
52
61
|
return queryset.annotate(**annotate)
|
|
53
62
|
|
|
54
63
|
def _annotate_cover_pk(self, queryset, model):
|
|
@@ -60,28 +69,134 @@ class BrowserAnnotationsView(BrowserOrderByView):
|
|
|
60
69
|
else:
|
|
61
70
|
# This creates two subqueries. It would be better condensed into one.
|
|
62
71
|
# but there's no way to annotate an object or multiple values.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
cover_pk = Subquery(
|
|
72
|
+
cover_qs = queryset.filter(pk=OuterRef("pk"))
|
|
73
|
+
cover_qs = self.add_order_by(cover_qs, model)
|
|
74
|
+
cover_pk = Subquery(cover_qs.values(self.rel_prefix + "pk")[:1])
|
|
66
75
|
return queryset.annotate(cover_pk=cover_pk)
|
|
67
76
|
|
|
68
|
-
def _annotate_page_count(self, qs):
|
|
77
|
+
def _annotate_page_count(self, qs, model):
|
|
69
78
|
"""Hoist up total page_count of children."""
|
|
70
79
|
# Used for sorting and progress
|
|
71
|
-
|
|
80
|
+
if model == Comic:
|
|
81
|
+
return qs
|
|
82
|
+
|
|
83
|
+
page_count_sum = Sum(self.rel_prefix + "page_count", distinct=True)
|
|
72
84
|
return qs.annotate(page_count=page_count_sum)
|
|
73
85
|
|
|
74
|
-
def
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
def _annotate_child_count(self, qs, model):
|
|
87
|
+
"""Annotate Child Count."""
|
|
88
|
+
self.kwargs.get("group")
|
|
89
|
+
if model == Comic:
|
|
90
|
+
child_count_sum = self._ONE_INTEGERFIELD
|
|
91
|
+
else:
|
|
92
|
+
child_count_sum = Count(self.rel_prefix + "pk", distinct=True)
|
|
93
|
+
qs = qs.annotate(child_count=child_count_sum)
|
|
94
|
+
if model != Comic:
|
|
95
|
+
# XXX Extra filter for empty groups
|
|
96
|
+
qs = qs.filter(child_count__gt=0)
|
|
97
|
+
return qs
|
|
98
|
+
|
|
99
|
+
def _annotate_bookmark_updated_at(self, qs, bm_rel, bm_filter):
|
|
100
|
+
"""Annotate bookmark_updated_at."""
|
|
101
|
+
if not self.is_opds_1_acquisition and self.order_key != "bookmark_updated_at":
|
|
102
|
+
return qs
|
|
103
|
+
|
|
104
|
+
updated_at_rel = f"{bm_rel}__updated_at"
|
|
105
|
+
bookmark_updated_at_aggregate = Min(
|
|
106
|
+
updated_at_rel,
|
|
107
|
+
default=self._NONE_DATETIMEFIELD,
|
|
108
|
+
filter=bm_filter,
|
|
109
|
+
)
|
|
110
|
+
return qs.annotate(bookmark_updated_at=bookmark_updated_at_aggregate)
|
|
111
|
+
|
|
112
|
+
def _annotate_sort_name(self, queryset, model):
|
|
113
|
+
"""Sort groups by name ignoring articles."""
|
|
114
|
+
if self.order_key != "sort_name":
|
|
115
|
+
return queryset
|
|
116
|
+
|
|
117
|
+
if self.kwargs.get("group") == self.FOLDER_GROUP and model == Comic:
|
|
118
|
+
# File View Filename
|
|
119
|
+
queryset = queryset.annotate(
|
|
120
|
+
sort_name=Right(
|
|
121
|
+
"path",
|
|
122
|
+
StrIndex(Reverse(F("path")), Value(sep)) - 1, # type: ignore
|
|
123
|
+
output_field=CharField(),
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
return queryset
|
|
127
|
+
|
|
128
|
+
##################################################
|
|
129
|
+
# Otherwise Remove articles from the browse name #
|
|
130
|
+
##################################################
|
|
131
|
+
|
|
132
|
+
# first_space_index
|
|
133
|
+
first_field = model.ORDERING[0]
|
|
134
|
+
queryset = queryset.annotate(
|
|
135
|
+
first_space_index=StrIndex(first_field, Value(" "))
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# lowercase_first_word
|
|
139
|
+
lowercase_first_word = Lower(
|
|
140
|
+
Substr(first_field, 1, length=(F("first_space_index") - 1)) # type: ignore
|
|
141
|
+
)
|
|
142
|
+
queryset = queryset.annotate(
|
|
143
|
+
lowercase_first_word=Case(
|
|
144
|
+
When(Q(first_space_index__gt=0), then=lowercase_first_word)
|
|
145
|
+
),
|
|
146
|
+
default=Value(""),
|
|
147
|
+
)
|
|
78
148
|
|
|
149
|
+
# sort_name
|
|
150
|
+
return queryset.annotate(
|
|
151
|
+
sort_name=Case(
|
|
152
|
+
When(
|
|
153
|
+
lowercase_first_word__in=self._ARTICLES,
|
|
154
|
+
then=Substr(
|
|
155
|
+
first_field, F("first_space_index") + 1 # type: ignore
|
|
156
|
+
),
|
|
157
|
+
),
|
|
158
|
+
default=first_field,
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def _annotate_story_arc_number(self, qs):
|
|
163
|
+
if self.order_key != "story_arc_number":
|
|
164
|
+
return qs
|
|
165
|
+
|
|
166
|
+
# Get story_arc__pk
|
|
167
|
+
group = self.kwargs["group"]
|
|
168
|
+
pk = self.kwargs["pk"]
|
|
169
|
+
if group == self.STORY_ARC_GROUP and pk:
|
|
170
|
+
story_arc_pk = pk
|
|
171
|
+
elif story_arc_pks := self.params.get("filters", {}).get("story_arcs", []):
|
|
172
|
+
story_arc_pk = story_arc_pks[0]
|
|
173
|
+
else:
|
|
174
|
+
story_arc_pk = None
|
|
175
|
+
|
|
176
|
+
# If we have one annotate it.
|
|
177
|
+
if story_arc_pk:
|
|
178
|
+
san_rel = self.rel_prefix + "story_arc_numbers"
|
|
179
|
+
rel = f"{san_rel}"
|
|
180
|
+
condition = Q(**{f"{san_rel}__story_arc": story_arc_pk})
|
|
181
|
+
qs = qs.annotate(
|
|
182
|
+
selected_story_arc_number=FilteredRelation(rel, condition=condition),
|
|
183
|
+
story_arc_number=F("selected_story_arc_number__number"),
|
|
184
|
+
)
|
|
185
|
+
else:
|
|
186
|
+
qs = qs.annotate(story_arc_number=self._NONE_INTEGERFIELD)
|
|
187
|
+
return qs
|
|
188
|
+
|
|
189
|
+
def _annotate_order_value(self, qs, model):
|
|
190
|
+
"""Annotate a main key for sorting."""
|
|
191
|
+
order_func = self.get_order_value(model)
|
|
192
|
+
return qs.annotate(order_value=order_func)
|
|
193
|
+
|
|
194
|
+
def _annotate_bookmarks(self, qs, model, bm_rel, bm_filter):
|
|
195
|
+
"""Hoist up bookmark annoations."""
|
|
79
196
|
page_rel = f"{bm_rel}__page"
|
|
80
197
|
finished_rel = f"{bm_rel}__finished"
|
|
81
|
-
updated_at_rel = f"{bm_rel}__updated_at"
|
|
82
|
-
bookmark_updated_at = None
|
|
83
198
|
|
|
84
|
-
if
|
|
199
|
+
if model == Comic:
|
|
85
200
|
# Hoist up the bookmark and finished states
|
|
86
201
|
bookmark_page = Sum(
|
|
87
202
|
page_rel,
|
|
@@ -95,14 +210,12 @@ class BrowserAnnotationsView(BrowserOrderByView):
|
|
|
95
210
|
filter=bm_filter,
|
|
96
211
|
output_field=BooleanField(),
|
|
97
212
|
)
|
|
98
|
-
if self.is_opds_1_acquisition:
|
|
99
|
-
bookmark_updated_at = F(updated_at_rel)
|
|
100
213
|
else:
|
|
101
214
|
# Aggregate bookmark and finished states
|
|
102
215
|
bookmark_page = Sum(
|
|
103
216
|
Case(
|
|
104
217
|
When(**{bm_rel: None}, then=0),
|
|
105
|
-
When(**{finished_rel: True}, then="
|
|
218
|
+
When(**{finished_rel: True}, then=f"{self.rel_prefix}page_count"),
|
|
106
219
|
default=page_rel,
|
|
107
220
|
output_field=PositiveSmallIntegerField(),
|
|
108
221
|
),
|
|
@@ -130,16 +243,10 @@ class BrowserAnnotationsView(BrowserOrderByView):
|
|
|
130
243
|
output_field=BooleanField(),
|
|
131
244
|
)
|
|
132
245
|
|
|
133
|
-
|
|
246
|
+
return qs.annotate(
|
|
134
247
|
page=bookmark_page,
|
|
135
248
|
finished=finished_aggregate,
|
|
136
249
|
)
|
|
137
|
-
if bookmark_updated_at:
|
|
138
|
-
qs = qs.annotate(
|
|
139
|
-
bookmark_updated_at=bookmark_updated_at,
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
return qs
|
|
143
250
|
|
|
144
251
|
@staticmethod
|
|
145
252
|
def _annotate_progress(queryset):
|
|
@@ -154,15 +261,17 @@ class BrowserAnnotationsView(BrowserOrderByView):
|
|
|
154
261
|
|
|
155
262
|
def annotate_common_aggregates(self, qs, model, search_scores):
|
|
156
263
|
"""Annotate common aggregates between browser and metadata."""
|
|
157
|
-
|
|
158
|
-
qs = self.
|
|
264
|
+
qs = self._annotate_search_score(qs, search_scores)
|
|
265
|
+
qs = self._annotate_child_count(qs, model)
|
|
266
|
+
qs = self._annotate_page_count(qs, model)
|
|
267
|
+
bm_rel = self.get_bm_rel(model)
|
|
268
|
+
bm_filter = self._get_my_bookmark_filter(bm_rel)
|
|
269
|
+
qs = self._annotate_bookmark_updated_at(qs, bm_rel, bm_filter)
|
|
270
|
+
qs = self._annotate_sort_name(qs, model)
|
|
271
|
+
qs = self._annotate_story_arc_number(qs)
|
|
272
|
+
qs = self._annotate_order_value(qs, model)
|
|
273
|
+
# cover depends on the above annotations for order-by
|
|
159
274
|
qs = self._annotate_cover_pk(qs, model)
|
|
160
|
-
|
|
161
|
-
child_count_sum = self._ONE_INTEGERFIELD
|
|
162
|
-
else:
|
|
163
|
-
qs = self._annotate_page_count(qs)
|
|
164
|
-
child_count_sum = Count("comic__pk", distinct=True)
|
|
165
|
-
qs = qs.annotate(child_count=child_count_sum)
|
|
166
|
-
qs = self._annotate_bookmarks(qs, is_model_comic)
|
|
275
|
+
qs = self._annotate_bookmarks(qs, model, bm_rel, bm_filter)
|
|
167
276
|
qs = self._annotate_progress(qs)
|
|
168
277
|
return qs
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""Base view for ordering the query."""
|
|
2
2
|
from os import sep
|
|
3
3
|
|
|
4
|
-
from django.db.models import Avg,
|
|
5
|
-
from django.db.models.functions import
|
|
4
|
+
from django.db.models import Avg, F, Max, Min, Sum, Value
|
|
5
|
+
from django.db.models.functions import Reverse, Right, StrIndex
|
|
6
6
|
|
|
7
|
-
from codex.models import Comic, Folder
|
|
7
|
+
from codex.models import Comic, Folder, StoryArc
|
|
8
8
|
from codex.views.browser.base import BrowserBaseView
|
|
9
9
|
|
|
10
10
|
|
|
@@ -22,33 +22,18 @@ class BrowserOrderByView(BrowserBaseView):
|
|
|
22
22
|
"size": Sum,
|
|
23
23
|
"updated_at": Min,
|
|
24
24
|
"search_score": Min,
|
|
25
|
+
"story_arc_number": Min,
|
|
25
26
|
}
|
|
26
27
|
_SEP_VALUE = Value(sep)
|
|
27
|
-
|
|
28
|
-
("a", "an", "the") # en
|
|
29
|
-
+ ("un", "unos", "unas", "el", "los", "la", "las") # es
|
|
30
|
-
+ ("un", "une", "le", "les", "la", "les", "l'") # fr
|
|
31
|
-
+ ("o", "a", "os") # pt
|
|
32
|
-
# pt "as" conflicts with English
|
|
33
|
-
+ ("der", "dem", "des", "das") # de
|
|
34
|
-
# de: "den & die conflict with English
|
|
35
|
-
+ ("il", "lo", "gli", "la", "le", "l'") # it
|
|
36
|
-
# it: "i" conflicts with English
|
|
37
|
-
+ ("de", "het", "een") # nl
|
|
38
|
-
+ ("en", "ett") # sw
|
|
39
|
-
+ ("en", "ei", "et") # no
|
|
40
|
-
+ ("en", "et") # da
|
|
41
|
-
+ ("el", "la", "els", "les", "un", "una", "uns", "unes", "na") # ct
|
|
42
|
-
)
|
|
43
|
-
NONE_CHARFIELD = Value(None, CharField())
|
|
28
|
+
_ANNOTATED_ORDER_FIELDS = frozenset(("sort_name", "bookmark_updated_at"))
|
|
44
29
|
|
|
45
|
-
def
|
|
30
|
+
def set_order_key(self):
|
|
46
31
|
"""Get the default order key for the view."""
|
|
47
32
|
order_key = self.params.get("order_by")
|
|
48
33
|
if not order_key:
|
|
49
34
|
group = self.kwargs.get("group")
|
|
50
35
|
order_key = "path" if group == self.FOLDER_GROUP else "sort_name"
|
|
51
|
-
|
|
36
|
+
self.order_key = order_key
|
|
52
37
|
|
|
53
38
|
@classmethod
|
|
54
39
|
def _get_path_query_func(cls, field):
|
|
@@ -57,97 +42,56 @@ class BrowserOrderByView(BrowserBaseView):
|
|
|
57
42
|
field, StrIndex(Reverse(field), cls._SEP_VALUE) - 1 # type: ignore
|
|
58
43
|
)
|
|
59
44
|
|
|
60
|
-
def get_aggregate_func(self,
|
|
61
|
-
"""
|
|
62
|
-
|
|
45
|
+
def get_aggregate_func(self, model, field):
|
|
46
|
+
"""Order by aggregate."""
|
|
47
|
+
# get agg_func
|
|
48
|
+
agg_func = self._ORDER_AGGREGATE_FUNCS[field]
|
|
49
|
+
if agg_func == Min and self.params.get("order_reverse"):
|
|
50
|
+
agg_func = Max
|
|
51
|
+
|
|
52
|
+
# get full_field
|
|
53
|
+
self.kwargs.get("group")
|
|
54
|
+
if model == StoryArc and field == "story_arc_number":
|
|
55
|
+
full_field = "storyarcnumber__number"
|
|
56
|
+
else:
|
|
57
|
+
if self.order_key == "story_arc_number":
|
|
58
|
+
field = "story_arc_numbers__number"
|
|
59
|
+
full_field = self.rel_prefix + field
|
|
60
|
+
if field == "path":
|
|
61
|
+
full_field = self._get_path_query_func(full_field)
|
|
62
|
+
|
|
63
|
+
return agg_func(full_field)
|
|
63
64
|
|
|
65
|
+
def get_order_value(self, model):
|
|
66
|
+
"""Get a complete function for aggregating an attribute."""
|
|
64
67
|
# Determine order func
|
|
65
|
-
if
|
|
66
|
-
# use default sorting.
|
|
67
|
-
func = self.NONE_CHARFIELD
|
|
68
|
-
elif field == "path" and model in (Comic, Folder):
|
|
68
|
+
if self.order_key == "path" and model in (Comic, Folder):
|
|
69
69
|
# special path sorting.
|
|
70
|
-
func = self._get_path_query_func(
|
|
71
|
-
elif model == Comic:
|
|
70
|
+
func = self._get_path_query_func(self.order_key)
|
|
71
|
+
elif model == Comic or self.order_key in self._ANNOTATED_ORDER_FIELDS:
|
|
72
72
|
# agg_none uses group fields not comic fields.
|
|
73
|
-
func = F(
|
|
73
|
+
func = F(self.order_key)
|
|
74
74
|
else:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
# get agg_func
|
|
78
|
-
agg_func = self._ORDER_AGGREGATE_FUNCS[field]
|
|
79
|
-
if agg_func == Min and self.params.get("order_reverse"):
|
|
80
|
-
agg_func = Max
|
|
81
|
-
|
|
82
|
-
# get full_field
|
|
83
|
-
full_field = "comic__" + field
|
|
84
|
-
if field == "path":
|
|
85
|
-
full_field = self._get_path_query_func(full_field)
|
|
86
|
-
|
|
87
|
-
func = agg_func(full_field)
|
|
75
|
+
func = self.get_aggregate_func(model, self.order_key)
|
|
88
76
|
return func
|
|
89
77
|
|
|
90
|
-
|
|
91
|
-
def _order_without_articles(cls, queryset, model):
|
|
92
|
-
"""Sort groups by name ignoring articles."""
|
|
93
|
-
# first_space_index
|
|
94
|
-
first_field = model.ORDERING[0]
|
|
95
|
-
queryset = queryset.annotate(
|
|
96
|
-
first_space_index=StrIndex(first_field, Value(" "))
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
# lowercase_first_word
|
|
100
|
-
lowercase_first_word = Lower(
|
|
101
|
-
Substr(first_field, 1, length=(F("first_space_index") - 1)) # type: ignore
|
|
102
|
-
)
|
|
103
|
-
queryset = queryset.annotate(
|
|
104
|
-
lowercase_first_word=Case(
|
|
105
|
-
When(Q(first_space_index__gt=0), then=lowercase_first_word)
|
|
106
|
-
),
|
|
107
|
-
default=Value(""),
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
# sort_name
|
|
111
|
-
queryset = queryset.annotate(
|
|
112
|
-
sort_name=Case(
|
|
113
|
-
When(
|
|
114
|
-
lowercase_first_word__in=cls._ARTICLES,
|
|
115
|
-
then=Substr(
|
|
116
|
-
first_field, F("first_space_index") + 1 # type: ignore
|
|
117
|
-
),
|
|
118
|
-
),
|
|
119
|
-
default=first_field,
|
|
120
|
-
)
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
# final ordering
|
|
124
|
-
ordering = ("sort_name", *model.ORDERING[1:])
|
|
125
|
-
return queryset, ordering
|
|
126
|
-
|
|
127
|
-
def get_order_by(self, model, queryset, for_cover_pk=False):
|
|
78
|
+
def add_order_by(self, queryset, model):
|
|
128
79
|
"""Create the order_by list."""
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
group = self.kwargs.get("group")
|
|
140
|
-
if group != self.FOLDER_GROUP:
|
|
141
|
-
# special annotations for ordering in browser mode
|
|
142
|
-
queryset, ordering = self._order_without_articles(queryset, model)
|
|
143
|
-
else:
|
|
144
|
-
ordering = model.ORDERING
|
|
80
|
+
prefix = ""
|
|
81
|
+
if self.params.get("order_reverse"):
|
|
82
|
+
prefix += "-"
|
|
83
|
+
|
|
84
|
+
if self.order_key == "sort_name":
|
|
85
|
+
order_fields = ("order_value", *model.ORDERING[1:])
|
|
86
|
+
elif self.order_key == "bookmark_updated_at":
|
|
87
|
+
order_fields = ("order_value", "updated_at", "created_at", "pk")
|
|
88
|
+
elif self.order_key == "story_arc_number" and model == Comic:
|
|
89
|
+
order_fields = ("order_value", "date", *model.ORDERING)
|
|
145
90
|
else:
|
|
146
|
-
|
|
147
|
-
|
|
91
|
+
order_fields = ("order_value", *model.ORDERING)
|
|
92
|
+
|
|
93
|
+
order_by = []
|
|
94
|
+
for field in order_fields:
|
|
95
|
+
order_by.append(prefix + field)
|
|
148
96
|
|
|
149
|
-
|
|
150
|
-
neg_prefix = "-" if self.params.get("order_reverse") else ""
|
|
151
|
-
prefix = neg_prefix + comic_rel
|
|
152
|
-
ordering = (prefix + field for field in ordering)
|
|
153
|
-
return queryset.order_by(*ordering)
|
|
97
|
+
return queryset.order_by(*order_by)
|