codex 1.6.0a6__py3-none-any.whl → 1.6.0a8__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/exceptions.py +1 -0
- codex/integrity.py +4 -4
- codex/librarian/importer/{aggregate_metadata.py → aggregate.py} +56 -86
- codex/librarian/importer/cache.py +180 -0
- codex/librarian/importer/const.py +15 -6
- codex/librarian/importer/create_comics.py +102 -17
- codex/librarian/importer/create_covers.py +98 -0
- codex/librarian/importer/create_fks.py +73 -170
- codex/librarian/importer/deleted.py +59 -46
- codex/librarian/importer/{clean_metadata.py → extract.py} +33 -4
- codex/librarian/importer/failed_imports.py +41 -62
- codex/librarian/importer/importer.py +96 -0
- codex/librarian/importer/importerd.py +71 -395
- codex/librarian/importer/init.py +265 -0
- codex/librarian/importer/link_comics.py +26 -81
- codex/librarian/importer/link_covers.py +73 -0
- codex/librarian/importer/moved.py +72 -96
- codex/librarian/importer/query_covers.py +52 -0
- codex/librarian/importer/query_fks.py +128 -172
- codex/librarian/importer/status.py +2 -39
- codex/librarian/importer/tasks.py +22 -13
- codex/librarian/janitor/cleanup.py +2 -2
- codex/librarian/janitor/janitor.py +11 -1
- codex/librarian/librariand.py +8 -2
- codex/migrations/0027_import_order_and_covers.py +345 -0
- codex/models/comic.py +1 -2
- codex/models/groups.py +3 -2
- codex/models/paths.py +0 -1
- codex/permissions.py +1 -1
- codex/search/query.py +25 -2
- codex/search/writing.py +1 -1
- codex/serializers/admin.py +1 -2
- codex/serializers/browser/filters.py +8 -101
- codex/serializers/browser/mixins.py +2 -3
- codex/serializers/browser/page.py +2 -2
- codex/serializers/browser/settings.py +4 -3
- codex/serializers/choices.py +10 -1
- codex/serializers/fields.py +123 -0
- codex/serializers/mtime.py +25 -0
- codex/serializers/opds/v2.py +7 -11
- codex/serializers/opds/v2_unused.py +5 -3
- codex/serializers/reader.py +5 -2
- codex/serializers/route.py +7 -3
- codex/settings/settings.py +2 -2
- codex/static_root/assets/VCheckbox-DI-DgMb4.8eeb1bd3e18b.js +1 -0
- codex/static_root/assets/VCheckbox-DI-DgMb4.8eeb1bd3e18b.js.br +0 -0
- codex/static_root/assets/VCheckbox-DI-DgMb4.8eeb1bd3e18b.js.gz +0 -0
- codex/static_root/assets/VCheckbox-DI-DgMb4.js +1 -0
- codex/static_root/assets/VCheckbox-DI-DgMb4.js.br +0 -0
- codex/static_root/assets/VCheckbox-DI-DgMb4.js.gz +0 -0
- codex/static_root/assets/{VCheckboxBtn-m4IbdjEe.f0c4a814ded8.js → VCheckboxBtn-B8Z4NA-t.3d8921f98ad9.js} +1 -1
- codex/static_root/assets/VCheckboxBtn-B8Z4NA-t.3d8921f98ad9.js.br +0 -0
- codex/static_root/assets/VCheckboxBtn-B8Z4NA-t.3d8921f98ad9.js.gz +0 -0
- codex/static_root/assets/{VCheckboxBtn-m4IbdjEe.js → VCheckboxBtn-B8Z4NA-t.js} +1 -1
- codex/static_root/assets/VCheckboxBtn-B8Z4NA-t.js.br +0 -0
- codex/static_root/assets/VCheckboxBtn-B8Z4NA-t.js.gz +0 -0
- codex/static_root/assets/VCombobox-D1_0lCri.6ef85a29c2dc.js +1 -0
- codex/static_root/assets/VCombobox-D1_0lCri.6ef85a29c2dc.js.br +0 -0
- codex/static_root/assets/VCombobox-D1_0lCri.6ef85a29c2dc.js.gz +0 -0
- codex/static_root/assets/VCombobox-D1_0lCri.js +1 -0
- codex/static_root/assets/VCombobox-D1_0lCri.js.br +0 -0
- codex/static_root/assets/VCombobox-D1_0lCri.js.gz +0 -0
- codex/static_root/assets/VDialog-opmV0YZz.d77216e97599.js +1 -0
- codex/static_root/assets/VDialog-opmV0YZz.d77216e97599.js.br +0 -0
- codex/static_root/assets/VDialog-opmV0YZz.d77216e97599.js.gz +0 -0
- codex/static_root/assets/VDialog-opmV0YZz.js +1 -0
- codex/static_root/assets/VDialog-opmV0YZz.js.br +0 -0
- codex/static_root/assets/VDialog-opmV0YZz.js.gz +0 -0
- codex/static_root/assets/VExpansionPanels-DX2dhUf5.ecc2ebfa62ec.js +1 -0
- codex/static_root/assets/VExpansionPanels-DX2dhUf5.ecc2ebfa62ec.js.br +0 -0
- codex/static_root/assets/VExpansionPanels-DX2dhUf5.ecc2ebfa62ec.js.gz +0 -0
- codex/static_root/assets/VExpansionPanels-DX2dhUf5.js +1 -0
- codex/static_root/assets/VExpansionPanels-DX2dhUf5.js.br +0 -0
- codex/static_root/assets/VExpansionPanels-DX2dhUf5.js.gz +0 -0
- codex/static_root/assets/VRadioGroup-BGa6_q5a.d1a3ad7266bd.js +1 -0
- codex/static_root/assets/VRadioGroup-BGa6_q5a.d1a3ad7266bd.js.br +0 -0
- codex/static_root/assets/VRadioGroup-BGa6_q5a.d1a3ad7266bd.js.gz +0 -0
- codex/static_root/assets/VRadioGroup-BGa6_q5a.js +1 -0
- codex/static_root/assets/VRadioGroup-BGa6_q5a.js.br +0 -0
- codex/static_root/assets/VRadioGroup-BGa6_q5a.js.gz +0 -0
- codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css +1 -0
- codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css.br +0 -0
- codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css.gz +0 -0
- codex/static_root/assets/VSelect-ARDhJiQK.css +1 -0
- codex/static_root/assets/VSelect-ARDhJiQK.css.br +0 -0
- codex/static_root/assets/VSelect-ARDhJiQK.css.gz +0 -0
- codex/static_root/assets/VSelect-sIYW_-ud.7d40da0ec3ff.js +1 -0
- codex/static_root/assets/VSelect-sIYW_-ud.7d40da0ec3ff.js.br +0 -0
- codex/static_root/assets/VSelect-sIYW_-ud.7d40da0ec3ff.js.gz +0 -0
- codex/static_root/assets/VSelect-sIYW_-ud.js +1 -0
- codex/static_root/assets/VSelect-sIYW_-ud.js.br +0 -0
- codex/static_root/assets/VSelect-sIYW_-ud.js.gz +0 -0
- codex/static_root/assets/VSelectionControl-C8DP9rgp.ef80e8e3dfc1.js +1 -0
- codex/static_root/assets/VSelectionControl-C8DP9rgp.ef80e8e3dfc1.js.br +0 -0
- codex/static_root/assets/VSelectionControl-C8DP9rgp.ef80e8e3dfc1.js.gz +0 -0
- codex/static_root/assets/VSelectionControl-C8DP9rgp.js +1 -0
- codex/static_root/assets/VSelectionControl-C8DP9rgp.js.br +0 -0
- codex/static_root/assets/VSelectionControl-C8DP9rgp.js.gz +0 -0
- codex/static_root/assets/VSlideGroup-BRYhkLH8.3534ec94b2f1.js +1 -0
- codex/static_root/assets/VSlideGroup-BRYhkLH8.3534ec94b2f1.js.br +0 -0
- codex/static_root/assets/VSlideGroup-BRYhkLH8.3534ec94b2f1.js.gz +0 -0
- codex/static_root/assets/VSlideGroup-BRYhkLH8.js +1 -0
- codex/static_root/assets/VSlideGroup-BRYhkLH8.js.br +0 -0
- codex/static_root/assets/VSlideGroup-BRYhkLH8.js.gz +0 -0
- codex/static_root/assets/{VTable-C0gAriFq.fa6e4b60d4ef.js → VTable-D1esKtlw.52165930e3b9.js} +1 -1
- codex/static_root/assets/VTable-D1esKtlw.52165930e3b9.js.br +0 -0
- codex/static_root/assets/VTable-D1esKtlw.52165930e3b9.js.gz +0 -0
- codex/static_root/assets/{VTable-C0gAriFq.js → VTable-D1esKtlw.js} +1 -1
- codex/static_root/assets/VTable-D1esKtlw.js.br +0 -0
- codex/static_root/assets/VTable-D1esKtlw.js.gz +0 -0
- codex/static_root/assets/VTextField-BnGscKcY.54032adf3a4b.js +1 -0
- codex/static_root/assets/VTextField-BnGscKcY.54032adf3a4b.js.br +0 -0
- codex/static_root/assets/VTextField-BnGscKcY.54032adf3a4b.js.gz +0 -0
- codex/static_root/assets/VTextField-BnGscKcY.js +1 -0
- codex/static_root/assets/VTextField-BnGscKcY.js.br +0 -0
- codex/static_root/assets/VTextField-BnGscKcY.js.gz +0 -0
- codex/static_root/assets/{VWindowItem-CFhKCj49.b2458002a937.js → VWindowItem-B0KokcAk.ea94224aecb9.js} +1 -1
- codex/static_root/assets/VWindowItem-B0KokcAk.ea94224aecb9.js.br +0 -0
- codex/static_root/assets/VWindowItem-B0KokcAk.ea94224aecb9.js.gz +0 -0
- codex/static_root/assets/{VWindowItem-CFhKCj49.js → VWindowItem-B0KokcAk.js} +1 -1
- codex/static_root/assets/VWindowItem-B0KokcAk.js.br +0 -0
- codex/static_root/assets/VWindowItem-B0KokcAk.js.gz +0 -0
- codex/static_root/assets/admin-Y0bL21AB.42d4f0d68163.js +1 -0
- codex/static_root/assets/admin-Y0bL21AB.42d4f0d68163.js.br +0 -0
- codex/static_root/assets/admin-Y0bL21AB.42d4f0d68163.js.gz +0 -0
- codex/static_root/assets/admin-Y0bL21AB.js +1 -0
- codex/static_root/assets/admin-Y0bL21AB.js.br +0 -0
- codex/static_root/assets/admin-Y0bL21AB.js.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-DNngWUEM.4e992e3fe86e.js +30 -0
- codex/static_root/assets/admin-drawer-panel-DNngWUEM.4e992e3fe86e.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-DNngWUEM.4e992e3fe86e.js.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-DNngWUEM.js +30 -0
- codex/static_root/assets/admin-drawer-panel-DNngWUEM.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-DNngWUEM.js.gz +0 -0
- codex/static_root/assets/{admin-drawer-panel-BHdNPwzF.bd2c1d6148bb.css → admin-drawer-panel-o87BwD38.ce39427c8201.css} +1 -1
- codex/static_root/assets/admin-drawer-panel-o87BwD38.ce39427c8201.css.br +0 -0
- codex/static_root/assets/{admin-drawer-panel-BHdNPwzF.bd2c1d6148bb.css.gz → admin-drawer-panel-o87BwD38.ce39427c8201.css.gz} +0 -0
- codex/static_root/assets/{admin-drawer-panel-BHdNPwzF.css → admin-drawer-panel-o87BwD38.css} +1 -1
- codex/static_root/assets/admin-drawer-panel-o87BwD38.css.br +0 -0
- codex/static_root/assets/{admin-drawer-panel-BHdNPwzF.css.gz → admin-drawer-panel-o87BwD38.css.gz} +0 -0
- codex/static_root/assets/browser-CMAtBCLx.17a277ce43a9.css +1 -0
- codex/static_root/assets/browser-CMAtBCLx.17a277ce43a9.css.br +0 -0
- codex/static_root/assets/browser-CMAtBCLx.17a277ce43a9.css.gz +0 -0
- codex/static_root/assets/browser-CMAtBCLx.css +1 -0
- codex/static_root/assets/browser-CMAtBCLx.css.br +0 -0
- codex/static_root/assets/browser-CMAtBCLx.css.gz +0 -0
- codex/static_root/assets/browser-DoJk8uvb.2c97a3858932.js +1 -0
- codex/static_root/assets/browser-DoJk8uvb.2c97a3858932.js.br +0 -0
- codex/static_root/assets/browser-DoJk8uvb.2c97a3858932.js.gz +0 -0
- codex/static_root/assets/browser-DoJk8uvb.js +1 -0
- codex/static_root/assets/browser-DoJk8uvb.js.br +0 -0
- codex/static_root/assets/browser-DoJk8uvb.js.gz +0 -0
- codex/static_root/assets/{change-password-dialog-JYM9aAR9.js → change-password-dialog-CH4xbjJa.b749aa6ea07e.js} +1 -1
- codex/static_root/assets/change-password-dialog-CH4xbjJa.b749aa6ea07e.js.br +0 -0
- codex/static_root/assets/change-password-dialog-CH4xbjJa.b749aa6ea07e.js.gz +0 -0
- codex/static_root/assets/{change-password-dialog-JYM9aAR9.f71d378f4fa6.js → change-password-dialog-CH4xbjJa.js} +1 -1
- codex/static_root/assets/change-password-dialog-CH4xbjJa.js.br +0 -0
- codex/static_root/assets/change-password-dialog-CH4xbjJa.js.gz +0 -0
- codex/static_root/assets/{confirm-dialog-ZAZ6P1Rc.515858dd21e8.js → confirm-dialog-DkU0twE6.a20006aa5ba6.js} +1 -1
- codex/static_root/assets/confirm-dialog-DkU0twE6.a20006aa5ba6.js.br +0 -0
- codex/static_root/assets/confirm-dialog-DkU0twE6.a20006aa5ba6.js.gz +0 -0
- codex/static_root/assets/{confirm-dialog-ZAZ6P1Rc.js → confirm-dialog-DkU0twE6.js} +1 -1
- codex/static_root/assets/confirm-dialog-DkU0twE6.js.br +0 -0
- codex/static_root/assets/confirm-dialog-DkU0twE6.js.gz +0 -0
- codex/static_root/assets/copy-to-clipboard-DoUvDJVx.bec27f144aca.js +1 -0
- codex/static_root/assets/copy-to-clipboard-DoUvDJVx.bec27f144aca.js.br +0 -0
- codex/static_root/assets/copy-to-clipboard-DoUvDJVx.js +1 -0
- codex/static_root/assets/copy-to-clipboard-DoUvDJVx.js.br +0 -0
- codex/static_root/assets/datetime-column-BfW_j_3Q.6a614321a435.js +1 -0
- codex/static_root/assets/datetime-column-BfW_j_3Q.6a614321a435.js.br +0 -0
- codex/static_root/assets/datetime-column-BfW_j_3Q.6a614321a435.js.gz +0 -0
- codex/static_root/assets/datetime-column-BfW_j_3Q.js +1 -0
- codex/static_root/assets/datetime-column-BfW_j_3Q.js.br +0 -0
- codex/static_root/assets/datetime-column-BfW_j_3Q.js.gz +0 -0
- codex/static_root/assets/datetime-column-DeCthByU.1f3bf499e063.css +1 -0
- codex/static_root/assets/datetime-column-DeCthByU.css +1 -0
- codex/static_root/assets/filter-DjxN78cK.35673f6e49b1.js +1 -0
- codex/static_root/assets/filter-DjxN78cK.35673f6e49b1.js.br +0 -0
- codex/static_root/assets/filter-DjxN78cK.35673f6e49b1.js.gz +0 -0
- codex/static_root/assets/filter-DjxN78cK.js +1 -0
- codex/static_root/assets/filter-DjxN78cK.js.br +0 -0
- codex/static_root/assets/filter-DjxN78cK.js.gz +0 -0
- codex/static_root/assets/{flag-tab-Ci_XcDT0.a5ea640a08d6.js → flag-tab-D4ZNSbMR.e763a9937701.js} +1 -1
- codex/static_root/assets/flag-tab-D4ZNSbMR.e763a9937701.js.br +0 -0
- codex/static_root/assets/flag-tab-D4ZNSbMR.e763a9937701.js.gz +0 -0
- codex/static_root/assets/{flag-tab-Ci_XcDT0.js → flag-tab-D4ZNSbMR.js} +1 -1
- codex/static_root/assets/flag-tab-D4ZNSbMR.js.br +0 -0
- codex/static_root/assets/flag-tab-D4ZNSbMR.js.gz +0 -0
- codex/static_root/assets/{group-tab-DFma2R-E.879c37b15bbd.js → group-tab-DucpNoBs.a6f37ae940c0.js} +1 -1
- codex/static_root/assets/group-tab-DucpNoBs.a6f37ae940c0.js.br +0 -0
- codex/static_root/assets/group-tab-DucpNoBs.a6f37ae940c0.js.gz +0 -0
- codex/static_root/assets/{group-tab-DFma2R-E.js → group-tab-DucpNoBs.js} +1 -1
- codex/static_root/assets/group-tab-DucpNoBs.js.br +0 -0
- codex/static_root/assets/group-tab-DucpNoBs.js.gz +0 -0
- codex/static_root/assets/http-error-Cj0hTq-c.062a88ddd4dc.js +1 -0
- codex/static_root/assets/http-error-Cj0hTq-c.062a88ddd4dc.js.br +0 -0
- codex/static_root/assets/http-error-Cj0hTq-c.062a88ddd4dc.js.gz +0 -0
- codex/static_root/assets/http-error-Cj0hTq-c.js +1 -0
- codex/static_root/assets/http-error-Cj0hTq-c.js.br +0 -0
- codex/static_root/assets/http-error-Cj0hTq-c.js.gz +0 -0
- codex/static_root/assets/{library-tab-V8DUGoHN.js → library-tab-CzksdS3Q.566acdb45dec.js} +1 -1
- codex/static_root/assets/library-tab-CzksdS3Q.566acdb45dec.js.br +0 -0
- codex/static_root/assets/library-tab-CzksdS3Q.566acdb45dec.js.gz +0 -0
- codex/static_root/assets/{library-tab-V8DUGoHN.809c548ff347.js → library-tab-CzksdS3Q.js} +1 -1
- codex/static_root/assets/library-tab-CzksdS3Q.js.br +0 -0
- codex/static_root/assets/library-tab-CzksdS3Q.js.gz +0 -0
- codex/static_root/assets/main-DX-k2724.f73d08e9ad64.js +32 -0
- codex/static_root/assets/main-DX-k2724.f73d08e9ad64.js.br +0 -0
- codex/static_root/assets/main-DX-k2724.f73d08e9ad64.js.gz +0 -0
- codex/static_root/assets/main-DX-k2724.js +32 -0
- codex/static_root/assets/main-DX-k2724.js.br +0 -0
- codex/static_root/assets/main-DX-k2724.js.gz +0 -0
- codex/static_root/assets/{main-sK7HC5h4.css → main-Ztp855PW.css} +1 -1
- codex/static_root/assets/main-Ztp855PW.css.br +0 -0
- codex/static_root/assets/{main-sK7HC5h4.css.gz → main-Ztp855PW.css.gz} +0 -0
- codex/static_root/assets/{main-sK7HC5h4.6f04ddfc5f1a.css → main-Ztp855PW.d46d718fac45.css} +1 -1
- codex/static_root/assets/main-Ztp855PW.d46d718fac45.css.br +0 -0
- codex/static_root/assets/{main-sK7HC5h4.6f04ddfc5f1a.css.gz → main-Ztp855PW.d46d718fac45.css.gz} +0 -0
- codex/static_root/assets/pagination-toolbar-CQcR0a3T.7a2d33ed8c39.js +1 -0
- codex/static_root/assets/pagination-toolbar-CQcR0a3T.7a2d33ed8c39.js.br +0 -0
- codex/static_root/assets/pagination-toolbar-CQcR0a3T.7a2d33ed8c39.js.gz +0 -0
- codex/static_root/assets/pagination-toolbar-CQcR0a3T.js +1 -0
- codex/static_root/assets/pagination-toolbar-CQcR0a3T.js.br +0 -0
- codex/static_root/assets/pagination-toolbar-CQcR0a3T.js.gz +0 -0
- codex/static_root/assets/pagination-toolbar-DWdp_kmz.css +1 -0
- codex/static_root/assets/pagination-toolbar-DWdp_kmz.css.br +0 -0
- codex/static_root/assets/pagination-toolbar-DWdp_kmz.css.gz +0 -0
- codex/static_root/assets/pagination-toolbar-DWdp_kmz.f98f47e87612.css +1 -0
- codex/static_root/assets/pagination-toolbar-DWdp_kmz.f98f47e87612.css.br +0 -0
- codex/static_root/assets/pagination-toolbar-DWdp_kmz.f98f47e87612.css.gz +0 -0
- codex/static_root/assets/pdf-doc-GkMk4o5L.ef24c859a7a4.js +305 -0
- codex/static_root/assets/pdf-doc-GkMk4o5L.ef24c859a7a4.js.br +0 -0
- codex/static_root/assets/pdf-doc-GkMk4o5L.ef24c859a7a4.js.gz +0 -0
- codex/static_root/assets/pdf-doc-GkMk4o5L.js +305 -0
- codex/static_root/assets/pdf-doc-GkMk4o5L.js.br +0 -0
- codex/static_root/assets/pdf-doc-GkMk4o5L.js.gz +0 -0
- codex/static_root/assets/reader-CCCfpM3x.1553e45bd775.css +1 -0
- codex/static_root/assets/reader-CCCfpM3x.1553e45bd775.css.br +0 -0
- codex/static_root/assets/reader-CCCfpM3x.1553e45bd775.css.gz +0 -0
- codex/static_root/assets/reader-CCCfpM3x.css +1 -0
- codex/static_root/assets/reader-CCCfpM3x.css.br +0 -0
- codex/static_root/assets/reader-CCCfpM3x.css.gz +0 -0
- codex/static_root/assets/reader-Cl6mmQB_.7b9e7591954f.js +2 -0
- codex/static_root/assets/reader-Cl6mmQB_.7b9e7591954f.js.br +0 -0
- codex/static_root/assets/reader-Cl6mmQB_.7b9e7591954f.js.gz +0 -0
- codex/static_root/assets/reader-Cl6mmQB_.js +2 -0
- codex/static_root/assets/reader-Cl6mmQB_.js.br +0 -0
- codex/static_root/assets/reader-Cl6mmQB_.js.gz +0 -0
- codex/static_root/assets/relation-chips-CZ8VvR4y.d4232f3c2518.js +1 -0
- codex/static_root/assets/relation-chips-CZ8VvR4y.d4232f3c2518.js.br +0 -0
- codex/static_root/assets/relation-chips-CZ8VvR4y.d4232f3c2518.js.gz +0 -0
- codex/static_root/assets/relation-chips-CZ8VvR4y.js +1 -0
- codex/static_root/assets/relation-chips-CZ8VvR4y.js.br +0 -0
- codex/static_root/assets/relation-chips-CZ8VvR4y.js.gz +0 -0
- codex/static_root/assets/settings-drawer-DyS0A2pN.17a98928589f.js +2 -0
- codex/static_root/assets/settings-drawer-DyS0A2pN.17a98928589f.js.br +0 -0
- codex/static_root/assets/settings-drawer-DyS0A2pN.17a98928589f.js.gz +0 -0
- codex/static_root/assets/settings-drawer-DyS0A2pN.js +2 -0
- codex/static_root/assets/settings-drawer-DyS0A2pN.js.br +0 -0
- codex/static_root/assets/settings-drawer-DyS0A2pN.js.gz +0 -0
- codex/static_root/assets/stats-tab-R3Ekobfm.70fa09a67518.js +1 -0
- codex/static_root/assets/stats-tab-R3Ekobfm.70fa09a67518.js.br +0 -0
- codex/static_root/assets/stats-tab-R3Ekobfm.70fa09a67518.js.gz +0 -0
- codex/static_root/assets/stats-tab-R3Ekobfm.js +1 -0
- codex/static_root/assets/stats-tab-R3Ekobfm.js.br +0 -0
- codex/static_root/assets/stats-tab-R3Ekobfm.js.gz +0 -0
- codex/static_root/assets/task-tab-BhLDK_7Q.28ff51b3ed01.js +1 -0
- codex/static_root/assets/task-tab-BhLDK_7Q.28ff51b3ed01.js.br +0 -0
- codex/static_root/assets/task-tab-BhLDK_7Q.28ff51b3ed01.js.gz +0 -0
- codex/static_root/assets/task-tab-BhLDK_7Q.js +1 -0
- codex/static_root/assets/task-tab-BhLDK_7Q.js.br +0 -0
- codex/static_root/assets/task-tab-BhLDK_7Q.js.gz +0 -0
- codex/static_root/assets/to-case-BR2frjFx.87de5d7a0503.js +1 -0
- codex/static_root/assets/to-case-BR2frjFx.js +1 -0
- codex/static_root/assets/unauthorized-BbXjAe5l.d48912d34a68.js +1 -0
- codex/static_root/assets/unauthorized-BbXjAe5l.d48912d34a68.js.br +0 -0
- codex/static_root/assets/unauthorized-BbXjAe5l.d48912d34a68.js.gz +0 -0
- codex/static_root/assets/unauthorized-BbXjAe5l.js +1 -0
- codex/static_root/assets/unauthorized-BbXjAe5l.js.br +0 -0
- codex/static_root/assets/unauthorized-BbXjAe5l.js.gz +0 -0
- codex/static_root/assets/user-tab-DxITIl2d.d79d7f062930.js +1 -0
- codex/static_root/assets/user-tab-DxITIl2d.d79d7f062930.js.br +0 -0
- codex/static_root/assets/user-tab-DxITIl2d.d79d7f062930.js.gz +0 -0
- codex/static_root/assets/user-tab-DxITIl2d.js +1 -0
- codex/static_root/assets/user-tab-DxITIl2d.js.br +0 -0
- codex/static_root/assets/user-tab-DxITIl2d.js.gz +0 -0
- codex/static_root/js/choices-admin.d790df01c20a.json +1 -0
- codex/static_root/js/choices-admin.d790df01c20a.json.br +0 -0
- codex/static_root/js/choices-admin.d790df01c20a.json.gz +0 -0
- codex/static_root/js/choices-admin.json +1 -1
- codex/static_root/js/choices-admin.json.br +0 -0
- codex/static_root/js/choices-admin.json.gz +0 -0
- codex/static_root/js/choices.a0aaaa6c7ef7.json +1 -0
- codex/static_root/js/choices.a0aaaa6c7ef7.json.br +0 -0
- codex/static_root/js/choices.a0aaaa6c7ef7.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.16d52597ef50.json +629 -0
- codex/static_root/manifest.16d52597ef50.json.br +0 -0
- codex/static_root/manifest.16d52597ef50.json.gz +0 -0
- codex/static_root/manifest.json +275 -288
- codex/static_root/manifest.json.br +0 -0
- codex/static_root/manifest.json.gz +0 -0
- codex/static_root/staticfiles.json +1 -1
- codex/urls/api/auth.py +2 -1
- codex/urls/api/browser.py +13 -4
- codex/urls/api/reader.py +3 -9
- codex/urls/api/v3.py +2 -0
- codex/urls/opds/binary.py +1 -1
- codex/util.py +10 -0
- codex/views/admin/api_key.py +2 -6
- codex/views/admin/auth.py +28 -0
- codex/views/admin/flag.py +2 -7
- codex/views/admin/group.py +2 -6
- codex/views/admin/library.py +7 -20
- codex/views/admin/stats.py +7 -66
- codex/views/admin/tasks.py +24 -11
- codex/views/admin/user.py +3 -9
- codex/views/auth.py +30 -53
- codex/views/bookmark.py +7 -11
- codex/views/browser/{browser_annotations.py → annotations.py} +90 -181
- codex/views/browser/base.py +23 -88
- codex/views/browser/breadcrumbs.py +202 -0
- codex/views/browser/browser.py +64 -342
- codex/views/browser/choices.py +22 -54
- codex/views/browser/cover.py +177 -0
- codex/views/browser/filters/annotations.py +24 -23
- codex/views/browser/filters/bookmark.py +9 -13
- codex/views/browser/filters/field.py +5 -5
- codex/views/browser/filters/group.py +18 -10
- codex/views/browser/filters/search.py +30 -18
- codex/views/browser/metadata.py +22 -31
- codex/views/browser/{browser_order_by.py → order_by.py} +2 -6
- codex/views/browser/paginate.py +106 -0
- codex/views/browser/{session.py → settings.py} +4 -3
- codex/views/browser/title.py +48 -0
- codex/views/browser/validate.py +158 -0
- codex/views/const.py +109 -0
- codex/views/download.py +5 -8
- codex/views/frontend.py +4 -2
- codex/views/mtime.py +72 -0
- codex/views/opds/auth.py +17 -0
- codex/views/opds/authentication_v1.py +46 -45
- codex/views/opds/binary.py +5 -10
- codex/views/opds/const.py +13 -0
- codex/views/opds/urls.py +6 -4
- codex/views/opds/util.py +4 -12
- codex/views/opds/v1/entry/links.py +18 -33
- codex/views/opds/v1/facets.py +7 -7
- codex/views/opds/v1/feed.py +52 -40
- codex/views/opds/v1/links.py +7 -5
- codex/views/opds/v1/opensearch_v1.py +2 -6
- codex/views/opds/v2/feed.py +22 -23
- codex/views/opds/v2/links.py +4 -2
- codex/views/opds/v2/publications.py +34 -54
- codex/views/opds/v2/top_links.py +2 -2
- codex/views/public.py +36 -0
- codex/views/reader/arcs.py +101 -0
- codex/views/reader/books.py +199 -0
- codex/views/reader/page.py +3 -7
- codex/views/reader/reader.py +56 -301
- codex/views/reader/{session.py → settings.py} +4 -2
- codex/views/session.py +77 -103
- codex/views/settings.py +34 -0
- codex/views/template.py +3 -3
- codex/views/util.py +63 -0
- codex/views/version.py +2 -2
- {codex-1.6.0a6.dist-info → codex-1.6.0a8.dist-info}/METADATA +19 -14
- {codex-1.6.0a6.dist-info → codex-1.6.0a8.dist-info}/RECORD +373 -362
- codex/librarian/importer/covers.py +0 -47
- codex/librarian/importer/update_comics.py +0 -85
- codex/migrations/0027_sort_name.py +0 -139
- codex/migrations/0028_custom_covers.py +0 -166
- codex/migrations/0029_choices_adminflag_and_timestamp.py +0 -44
- codex/static_root/assets/VCheckbox-BS5fdM4M.a5d446e4aeb3.js +0 -1
- codex/static_root/assets/VCheckbox-BS5fdM4M.a5d446e4aeb3.js.br +0 -0
- codex/static_root/assets/VCheckbox-BS5fdM4M.a5d446e4aeb3.js.gz +0 -0
- codex/static_root/assets/VCheckbox-BS5fdM4M.js +0 -1
- codex/static_root/assets/VCheckbox-BS5fdM4M.js.br +0 -0
- codex/static_root/assets/VCheckbox-BS5fdM4M.js.gz +0 -0
- codex/static_root/assets/VCheckboxBtn-m4IbdjEe.f0c4a814ded8.js.br +0 -0
- codex/static_root/assets/VCheckboxBtn-m4IbdjEe.f0c4a814ded8.js.gz +0 -0
- codex/static_root/assets/VCheckboxBtn-m4IbdjEe.js.br +0 -0
- codex/static_root/assets/VCheckboxBtn-m4IbdjEe.js.gz +0 -0
- codex/static_root/assets/VCombobox-jg7SI5mn.af66f18b4c54.js +0 -1
- codex/static_root/assets/VCombobox-jg7SI5mn.af66f18b4c54.js.br +0 -0
- codex/static_root/assets/VCombobox-jg7SI5mn.af66f18b4c54.js.gz +0 -0
- codex/static_root/assets/VCombobox-jg7SI5mn.js +0 -1
- codex/static_root/assets/VCombobox-jg7SI5mn.js.br +0 -0
- codex/static_root/assets/VCombobox-jg7SI5mn.js.gz +0 -0
- codex/static_root/assets/VDialog-DiixpkQL.f84fd1fd1f84.js +0 -1
- codex/static_root/assets/VDialog-DiixpkQL.f84fd1fd1f84.js.br +0 -0
- codex/static_root/assets/VDialog-DiixpkQL.f84fd1fd1f84.js.gz +0 -0
- codex/static_root/assets/VDialog-DiixpkQL.js +0 -1
- codex/static_root/assets/VDialog-DiixpkQL.js.br +0 -0
- codex/static_root/assets/VDialog-DiixpkQL.js.gz +0 -0
- codex/static_root/assets/VExpansionPanels-Db5mPFue.56d0ad094a62.js +0 -1
- codex/static_root/assets/VExpansionPanels-Db5mPFue.56d0ad094a62.js.br +0 -0
- codex/static_root/assets/VExpansionPanels-Db5mPFue.56d0ad094a62.js.gz +0 -0
- codex/static_root/assets/VExpansionPanels-Db5mPFue.js +0 -1
- codex/static_root/assets/VExpansionPanels-Db5mPFue.js.br +0 -0
- codex/static_root/assets/VExpansionPanels-Db5mPFue.js.gz +0 -0
- codex/static_root/assets/VRadioGroup-DULWiMSI.4ae535e01d2b.js +0 -1
- codex/static_root/assets/VRadioGroup-DULWiMSI.4ae535e01d2b.js.br +0 -0
- codex/static_root/assets/VRadioGroup-DULWiMSI.4ae535e01d2b.js.gz +0 -0
- codex/static_root/assets/VRadioGroup-DULWiMSI.js +0 -1
- codex/static_root/assets/VRadioGroup-DULWiMSI.js.br +0 -0
- codex/static_root/assets/VRadioGroup-DULWiMSI.js.gz +0 -0
- codex/static_root/assets/VSelect-BnoCfCLY.4d50fd11c77c.js +0 -1
- codex/static_root/assets/VSelect-BnoCfCLY.4d50fd11c77c.js.br +0 -0
- codex/static_root/assets/VSelect-BnoCfCLY.4d50fd11c77c.js.gz +0 -0
- codex/static_root/assets/VSelect-BnoCfCLY.js +0 -1
- codex/static_root/assets/VSelect-BnoCfCLY.js.br +0 -0
- codex/static_root/assets/VSelect-BnoCfCLY.js.gz +0 -0
- codex/static_root/assets/VSelect-MGVSeLgr.7cabd30bc5e4.css +0 -1
- codex/static_root/assets/VSelect-MGVSeLgr.7cabd30bc5e4.css.br +0 -0
- codex/static_root/assets/VSelect-MGVSeLgr.7cabd30bc5e4.css.gz +0 -0
- codex/static_root/assets/VSelect-MGVSeLgr.css +0 -1
- codex/static_root/assets/VSelect-MGVSeLgr.css.br +0 -0
- codex/static_root/assets/VSelect-MGVSeLgr.css.gz +0 -0
- codex/static_root/assets/VSelectionControl-BSL0qeL9.5b5835d286df.js +0 -1
- codex/static_root/assets/VSelectionControl-BSL0qeL9.5b5835d286df.js.br +0 -0
- codex/static_root/assets/VSelectionControl-BSL0qeL9.5b5835d286df.js.gz +0 -0
- codex/static_root/assets/VSelectionControl-BSL0qeL9.js +0 -1
- codex/static_root/assets/VSelectionControl-BSL0qeL9.js.br +0 -0
- codex/static_root/assets/VSelectionControl-BSL0qeL9.js.gz +0 -0
- codex/static_root/assets/VSlideGroup-BQtz8h9p.ff97dbf209e1.js +0 -1
- codex/static_root/assets/VSlideGroup-BQtz8h9p.ff97dbf209e1.js.br +0 -0
- codex/static_root/assets/VSlideGroup-BQtz8h9p.ff97dbf209e1.js.gz +0 -0
- codex/static_root/assets/VSlideGroup-BQtz8h9p.js +0 -1
- codex/static_root/assets/VSlideGroup-BQtz8h9p.js.br +0 -0
- codex/static_root/assets/VSlideGroup-BQtz8h9p.js.gz +0 -0
- codex/static_root/assets/VTable-C0gAriFq.fa6e4b60d4ef.js.br +0 -0
- codex/static_root/assets/VTable-C0gAriFq.fa6e4b60d4ef.js.gz +0 -0
- codex/static_root/assets/VTable-C0gAriFq.js.br +0 -0
- codex/static_root/assets/VTable-C0gAriFq.js.gz +0 -0
- codex/static_root/assets/VTextField-Da_Hs7ls.c295f24c251e.js +0 -1
- codex/static_root/assets/VTextField-Da_Hs7ls.c295f24c251e.js.br +0 -0
- codex/static_root/assets/VTextField-Da_Hs7ls.c295f24c251e.js.gz +0 -0
- codex/static_root/assets/VTextField-Da_Hs7ls.js +0 -1
- codex/static_root/assets/VTextField-Da_Hs7ls.js.br +0 -0
- codex/static_root/assets/VTextField-Da_Hs7ls.js.gz +0 -0
- codex/static_root/assets/VWindowItem-CFhKCj49.b2458002a937.js.br +0 -0
- codex/static_root/assets/VWindowItem-CFhKCj49.b2458002a937.js.gz +0 -0
- codex/static_root/assets/VWindowItem-CFhKCj49.js.br +0 -0
- codex/static_root/assets/VWindowItem-CFhKCj49.js.gz +0 -0
- codex/static_root/assets/admin-BpGFsvOH.d7193c78d0dd.js +0 -1
- codex/static_root/assets/admin-BpGFsvOH.d7193c78d0dd.js.br +0 -0
- codex/static_root/assets/admin-BpGFsvOH.d7193c78d0dd.js.gz +0 -0
- codex/static_root/assets/admin-BpGFsvOH.js +0 -1
- codex/static_root/assets/admin-BpGFsvOH.js.br +0 -0
- codex/static_root/assets/admin-BpGFsvOH.js.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-BHdNPwzF.bd2c1d6148bb.css.br +0 -0
- codex/static_root/assets/admin-drawer-panel-BHdNPwzF.css.br +0 -0
- codex/static_root/assets/admin-drawer-panel-C34Ze3-5.113c50cdf2cc.js +0 -30
- codex/static_root/assets/admin-drawer-panel-C34Ze3-5.113c50cdf2cc.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-C34Ze3-5.113c50cdf2cc.js.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-C34Ze3-5.js +0 -30
- codex/static_root/assets/admin-drawer-panel-C34Ze3-5.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-C34Ze3-5.js.gz +0 -0
- codex/static_root/assets/browser-CRPvEdqv.f355a71f4380.js +0 -1
- codex/static_root/assets/browser-CRPvEdqv.f355a71f4380.js.br +0 -0
- codex/static_root/assets/browser-CRPvEdqv.f355a71f4380.js.gz +0 -0
- codex/static_root/assets/browser-CRPvEdqv.js +0 -1
- codex/static_root/assets/browser-CRPvEdqv.js.br +0 -0
- codex/static_root/assets/browser-CRPvEdqv.js.gz +0 -0
- codex/static_root/assets/browser-Zqc9cR6T.b7d6ddeff130.css +0 -1
- codex/static_root/assets/browser-Zqc9cR6T.b7d6ddeff130.css.br +0 -0
- codex/static_root/assets/browser-Zqc9cR6T.b7d6ddeff130.css.gz +0 -0
- codex/static_root/assets/browser-Zqc9cR6T.css +0 -1
- codex/static_root/assets/browser-Zqc9cR6T.css.br +0 -0
- codex/static_root/assets/browser-Zqc9cR6T.css.gz +0 -0
- codex/static_root/assets/change-password-dialog-JYM9aAR9.f71d378f4fa6.js.br +0 -0
- codex/static_root/assets/change-password-dialog-JYM9aAR9.f71d378f4fa6.js.gz +0 -0
- codex/static_root/assets/change-password-dialog-JYM9aAR9.js.br +0 -0
- codex/static_root/assets/change-password-dialog-JYM9aAR9.js.gz +0 -0
- codex/static_root/assets/confirm-dialog-ZAZ6P1Rc.515858dd21e8.js.br +0 -0
- codex/static_root/assets/confirm-dialog-ZAZ6P1Rc.515858dd21e8.js.gz +0 -0
- codex/static_root/assets/confirm-dialog-ZAZ6P1Rc.js.br +0 -0
- codex/static_root/assets/confirm-dialog-ZAZ6P1Rc.js.gz +0 -0
- codex/static_root/assets/datetime-column-BT1MSP82.9617f6b94ebf.js +0 -1
- codex/static_root/assets/datetime-column-BT1MSP82.9617f6b94ebf.js.br +0 -0
- codex/static_root/assets/datetime-column-BT1MSP82.9617f6b94ebf.js.gz +0 -0
- codex/static_root/assets/datetime-column-BT1MSP82.js +0 -1
- codex/static_root/assets/datetime-column-BT1MSP82.js.br +0 -0
- codex/static_root/assets/datetime-column-BT1MSP82.js.gz +0 -0
- codex/static_root/assets/datetime-column-Dg_RxTVM.1fa7c5cbead7.css +0 -1
- codex/static_root/assets/datetime-column-Dg_RxTVM.1fa7c5cbead7.css.br +0 -0
- codex/static_root/assets/datetime-column-Dg_RxTVM.css +0 -1
- codex/static_root/assets/datetime-column-Dg_RxTVM.css.br +0 -0
- codex/static_root/assets/filter-frSNsexx.eab1570bd32d.js +0 -1
- codex/static_root/assets/filter-frSNsexx.eab1570bd32d.js.br +0 -0
- codex/static_root/assets/filter-frSNsexx.eab1570bd32d.js.gz +0 -0
- codex/static_root/assets/filter-frSNsexx.js +0 -1
- codex/static_root/assets/filter-frSNsexx.js.br +0 -0
- codex/static_root/assets/filter-frSNsexx.js.gz +0 -0
- codex/static_root/assets/flag-tab-Ci_XcDT0.a5ea640a08d6.js.br +0 -0
- codex/static_root/assets/flag-tab-Ci_XcDT0.a5ea640a08d6.js.gz +0 -0
- codex/static_root/assets/flag-tab-Ci_XcDT0.js.br +0 -0
- codex/static_root/assets/flag-tab-Ci_XcDT0.js.gz +0 -0
- codex/static_root/assets/group-tab-DFma2R-E.879c37b15bbd.js.br +0 -0
- codex/static_root/assets/group-tab-DFma2R-E.879c37b15bbd.js.gz +0 -0
- codex/static_root/assets/group-tab-DFma2R-E.js.br +0 -0
- codex/static_root/assets/group-tab-DFma2R-E.js.gz +0 -0
- codex/static_root/assets/http-error-B3fQGxpt.9efc29496e30.js +0 -1
- codex/static_root/assets/http-error-B3fQGxpt.9efc29496e30.js.br +0 -0
- codex/static_root/assets/http-error-B3fQGxpt.9efc29496e30.js.gz +0 -0
- codex/static_root/assets/http-error-B3fQGxpt.js +0 -1
- codex/static_root/assets/http-error-B3fQGxpt.js.br +0 -0
- codex/static_root/assets/http-error-B3fQGxpt.js.gz +0 -0
- codex/static_root/assets/index-DUjeNg4x.36d50c91b2c1.js +0 -1
- codex/static_root/assets/index-DUjeNg4x.36d50c91b2c1.js.br +0 -0
- codex/static_root/assets/index-DUjeNg4x.36d50c91b2c1.js.gz +0 -0
- codex/static_root/assets/index-DUjeNg4x.js +0 -1
- codex/static_root/assets/index-DUjeNg4x.js.br +0 -0
- codex/static_root/assets/index-DUjeNg4x.js.gz +0 -0
- codex/static_root/assets/library-tab-V8DUGoHN.809c548ff347.js.br +0 -0
- codex/static_root/assets/library-tab-V8DUGoHN.809c548ff347.js.gz +0 -0
- codex/static_root/assets/library-tab-V8DUGoHN.js.br +0 -0
- codex/static_root/assets/library-tab-V8DUGoHN.js.gz +0 -0
- codex/static_root/assets/main-Cavvqkyp.a9c4df906406.js +0 -32
- codex/static_root/assets/main-Cavvqkyp.a9c4df906406.js.br +0 -0
- codex/static_root/assets/main-Cavvqkyp.a9c4df906406.js.gz +0 -0
- codex/static_root/assets/main-Cavvqkyp.js +0 -32
- codex/static_root/assets/main-Cavvqkyp.js.br +0 -0
- codex/static_root/assets/main-Cavvqkyp.js.gz +0 -0
- codex/static_root/assets/main-sK7HC5h4.6f04ddfc5f1a.css.br +0 -0
- codex/static_root/assets/main-sK7HC5h4.css.br +0 -0
- codex/static_root/assets/pagination-toolbar-ClZui9Zf.697d7ae11c21.css +0 -1
- codex/static_root/assets/pagination-toolbar-ClZui9Zf.697d7ae11c21.css.br +0 -0
- codex/static_root/assets/pagination-toolbar-ClZui9Zf.697d7ae11c21.css.gz +0 -0
- codex/static_root/assets/pagination-toolbar-ClZui9Zf.css +0 -1
- codex/static_root/assets/pagination-toolbar-ClZui9Zf.css.br +0 -0
- codex/static_root/assets/pagination-toolbar-ClZui9Zf.css.gz +0 -0
- codex/static_root/assets/pagination-toolbar-Dg9tH1Ju.b2eb1e0c6985.js +0 -1
- codex/static_root/assets/pagination-toolbar-Dg9tH1Ju.b2eb1e0c6985.js.br +0 -0
- codex/static_root/assets/pagination-toolbar-Dg9tH1Ju.b2eb1e0c6985.js.gz +0 -0
- codex/static_root/assets/pagination-toolbar-Dg9tH1Ju.js +0 -1
- codex/static_root/assets/pagination-toolbar-Dg9tH1Ju.js.br +0 -0
- codex/static_root/assets/pagination-toolbar-Dg9tH1Ju.js.gz +0 -0
- codex/static_root/assets/pdf-doc-4PKLN-Z3.62d8de4d1fd8.js +0 -35
- codex/static_root/assets/pdf-doc-4PKLN-Z3.62d8de4d1fd8.js.br +0 -0
- codex/static_root/assets/pdf-doc-4PKLN-Z3.62d8de4d1fd8.js.gz +0 -0
- codex/static_root/assets/pdf-doc-4PKLN-Z3.js +0 -35
- codex/static_root/assets/pdf-doc-4PKLN-Z3.js.br +0 -0
- codex/static_root/assets/pdf-doc-4PKLN-Z3.js.gz +0 -0
- codex/static_root/assets/reader-C39uGGxn.67b0f324cd5e.js +0 -2
- codex/static_root/assets/reader-C39uGGxn.67b0f324cd5e.js.br +0 -0
- codex/static_root/assets/reader-C39uGGxn.67b0f324cd5e.js.gz +0 -0
- codex/static_root/assets/reader-C39uGGxn.js +0 -2
- codex/static_root/assets/reader-C39uGGxn.js.br +0 -0
- codex/static_root/assets/reader-C39uGGxn.js.gz +0 -0
- codex/static_root/assets/reader-N2Ei_y2K.8c0cdc0d513f.css +0 -1
- codex/static_root/assets/reader-N2Ei_y2K.8c0cdc0d513f.css.br +0 -0
- codex/static_root/assets/reader-N2Ei_y2K.8c0cdc0d513f.css.gz +0 -0
- codex/static_root/assets/reader-N2Ei_y2K.css +0 -1
- codex/static_root/assets/reader-N2Ei_y2K.css.br +0 -0
- codex/static_root/assets/reader-N2Ei_y2K.css.gz +0 -0
- codex/static_root/assets/relation-chips-CAC5ytqH.f205d920ffea.js +0 -1
- codex/static_root/assets/relation-chips-CAC5ytqH.f205d920ffea.js.br +0 -0
- codex/static_root/assets/relation-chips-CAC5ytqH.f205d920ffea.js.gz +0 -0
- codex/static_root/assets/relation-chips-CAC5ytqH.js +0 -1
- codex/static_root/assets/relation-chips-CAC5ytqH.js.br +0 -0
- codex/static_root/assets/relation-chips-CAC5ytqH.js.gz +0 -0
- codex/static_root/assets/settings-drawer-CT73ieiV.fac6f69e86ff.js +0 -2
- codex/static_root/assets/settings-drawer-CT73ieiV.fac6f69e86ff.js.br +0 -0
- codex/static_root/assets/settings-drawer-CT73ieiV.fac6f69e86ff.js.gz +0 -0
- codex/static_root/assets/settings-drawer-CT73ieiV.js +0 -2
- codex/static_root/assets/settings-drawer-CT73ieiV.js.br +0 -0
- codex/static_root/assets/settings-drawer-CT73ieiV.js.gz +0 -0
- codex/static_root/assets/stats-tab-C6qAnV6-.f08c46b5f141.js +0 -1
- codex/static_root/assets/stats-tab-C6qAnV6-.f08c46b5f141.js.br +0 -0
- codex/static_root/assets/stats-tab-C6qAnV6-.f08c46b5f141.js.gz +0 -0
- codex/static_root/assets/stats-tab-C6qAnV6-.js +0 -1
- codex/static_root/assets/stats-tab-C6qAnV6-.js.br +0 -0
- codex/static_root/assets/stats-tab-C6qAnV6-.js.gz +0 -0
- codex/static_root/assets/task-tab-JcG2vvgt.eed6e03db487.js +0 -1
- codex/static_root/assets/task-tab-JcG2vvgt.eed6e03db487.js.br +0 -0
- codex/static_root/assets/task-tab-JcG2vvgt.eed6e03db487.js.gz +0 -0
- codex/static_root/assets/task-tab-JcG2vvgt.js +0 -1
- codex/static_root/assets/task-tab-JcG2vvgt.js.br +0 -0
- codex/static_root/assets/task-tab-JcG2vvgt.js.gz +0 -0
- codex/static_root/assets/to-case-kUm8NPpW.5b3369f511b8.js +0 -1
- codex/static_root/assets/to-case-kUm8NPpW.js +0 -1
- codex/static_root/assets/unauthorized-28bmiwAU.c74825b5c16f.js +0 -1
- codex/static_root/assets/unauthorized-28bmiwAU.c74825b5c16f.js.br +0 -0
- codex/static_root/assets/unauthorized-28bmiwAU.c74825b5c16f.js.gz +0 -0
- codex/static_root/assets/unauthorized-28bmiwAU.js +0 -1
- codex/static_root/assets/unauthorized-28bmiwAU.js.br +0 -0
- codex/static_root/assets/unauthorized-28bmiwAU.js.gz +0 -0
- codex/static_root/assets/user-tab-BOq4BMMc.b64ac15dd772.js +0 -1
- codex/static_root/assets/user-tab-BOq4BMMc.b64ac15dd772.js.br +0 -0
- codex/static_root/assets/user-tab-BOq4BMMc.b64ac15dd772.js.gz +0 -0
- codex/static_root/assets/user-tab-BOq4BMMc.js +0 -1
- codex/static_root/assets/user-tab-BOq4BMMc.js.br +0 -0
- codex/static_root/assets/user-tab-BOq4BMMc.js.gz +0 -0
- codex/static_root/js/choices-admin.ef1ff3a8b9da.json +0 -1
- codex/static_root/js/choices-admin.ef1ff3a8b9da.json.br +0 -0
- codex/static_root/js/choices-admin.ef1ff3a8b9da.json.gz +0 -0
- codex/static_root/js/choices.23fedac2b9ba.json +0 -1
- codex/static_root/js/choices.23fedac2b9ba.json.br +0 -0
- codex/static_root/js/choices.23fedac2b9ba.json.gz +0 -0
- codex/static_root/manifest.ad9da9714e46.json +0 -642
- codex/static_root/manifest.ad9da9714e46.json.br +0 -0
- codex/static_root/manifest.ad9da9714e46.json.gz +0 -0
- codex/views/browser/browser_breadcrumbs.py +0 -175
- codex/views/browser/const.py +0 -3
- codex/views/cover.py +0 -78
- {codex-1.6.0a6.dist-info → codex-1.6.0a8.dist-info}/LICENSE +0 -0
- {codex-1.6.0a6.dist-info → codex-1.6.0a8.dist-info}/WHEEL +0 -0
- {codex-1.6.0a6.dist-info → codex-1.6.0a8.dist-info}/entry_points.txt +0 -0
codex/views/browser/browser.py
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
"""Views for browsing comic library."""
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from math import ceil, floor, log10
|
|
5
|
-
from os import sep
|
|
6
|
-
from pathlib import Path
|
|
3
|
+
from math import floor, log10
|
|
7
4
|
from types import MappingProxyType
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
9
6
|
|
|
10
|
-
from django.core.paginator import EmptyPage, Paginator
|
|
11
7
|
from django.db.models import Max
|
|
12
8
|
from drf_spectacular.utils import extend_schema
|
|
13
9
|
from rest_framework.response import Response
|
|
14
10
|
|
|
15
|
-
from codex.exceptions import SeeOtherRedirectError
|
|
16
11
|
from codex.logger.logging import get_logger
|
|
17
12
|
from codex.models import (
|
|
18
13
|
AdminFlag,
|
|
@@ -27,10 +22,13 @@ from codex.models import (
|
|
|
27
22
|
Volume,
|
|
28
23
|
)
|
|
29
24
|
from codex.serializers.browser.page import BrowserPageSerializer
|
|
30
|
-
from codex.
|
|
31
|
-
from codex.views.
|
|
32
|
-
from codex.views.
|
|
33
|
-
|
|
25
|
+
from codex.util import max_none
|
|
26
|
+
from codex.views.browser.title import BrowserTitleView
|
|
27
|
+
from codex.views.const import (
|
|
28
|
+
COMIC_GROUP,
|
|
29
|
+
FOLDER_GROUP,
|
|
30
|
+
STORY_ARC_GROUP,
|
|
31
|
+
)
|
|
34
32
|
|
|
35
33
|
if TYPE_CHECKING:
|
|
36
34
|
from django.db.models.query import QuerySet
|
|
@@ -39,10 +37,9 @@ if TYPE_CHECKING:
|
|
|
39
37
|
LOG = get_logger(__name__)
|
|
40
38
|
|
|
41
39
|
|
|
42
|
-
class BrowserView(
|
|
40
|
+
class BrowserView(BrowserTitleView):
|
|
43
41
|
"""Browse comics with a variety of filters and sorts."""
|
|
44
42
|
|
|
45
|
-
permission_classes: ClassVar[list] = [IsAuthenticatedOrEnabledNonUsers] # type: ignore
|
|
46
43
|
serializer_class = BrowserPageSerializer
|
|
47
44
|
|
|
48
45
|
ADMIN_FLAG_VALUE_KEY_MAP = MappingProxyType(
|
|
@@ -51,7 +48,7 @@ class BrowserView(BrowserBreadcrumbsView):
|
|
|
51
48
|
AdminFlag.FlagChoices.IMPORT_METADATA.value: "import_metadata",
|
|
52
49
|
}
|
|
53
50
|
)
|
|
54
|
-
|
|
51
|
+
TARGET = "browser"
|
|
55
52
|
_GROUP_INSTANCE_SELECT_RELATED: MappingProxyType[
|
|
56
53
|
type[BrowserGroupModel], tuple[str | None, ...]
|
|
57
54
|
] = MappingProxyType(
|
|
@@ -65,45 +62,43 @@ class BrowserView(BrowserBreadcrumbsView):
|
|
|
65
62
|
StoryArc: (None,),
|
|
66
63
|
}
|
|
67
64
|
)
|
|
68
|
-
DEFAULT_ROUTE = MappingProxyType(
|
|
69
|
-
{
|
|
70
|
-
"name": "browser",
|
|
71
|
-
"params": deepcopy(DEFAULTS["breadcrumbs"][0]),
|
|
72
|
-
}
|
|
73
|
-
)
|
|
74
|
-
_OPDS_M2M_RELS = (
|
|
75
|
-
"characters",
|
|
76
|
-
"genres",
|
|
77
|
-
"locations",
|
|
78
|
-
"series_groups",
|
|
79
|
-
"story_arc_numbers",
|
|
80
|
-
"story_arc_numbers__story_arc",
|
|
81
|
-
"tags",
|
|
82
|
-
"teams",
|
|
83
|
-
"contributors",
|
|
84
|
-
"contributors__role",
|
|
85
|
-
"contributors__person",
|
|
86
|
-
)
|
|
87
|
-
TARGET = "browser"
|
|
88
65
|
|
|
89
66
|
def __init__(self, *args, **kwargs):
|
|
90
67
|
"""Set params for the type checker."""
|
|
91
68
|
super().__init__(*args, **kwargs)
|
|
92
69
|
self.is_opds_2_acquisition = False
|
|
93
70
|
self.group_query: QuerySet = Comic.objects.none()
|
|
94
|
-
self.group_instance: BrowserGroupModel | None = None
|
|
95
71
|
self.valid_nav_groups: tuple[str, ...] = ()
|
|
96
72
|
|
|
97
|
-
def
|
|
98
|
-
"""
|
|
99
|
-
|
|
100
|
-
if
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
73
|
+
def _set_group_query_and_instance(self):
|
|
74
|
+
"""Create group_class instance."""
|
|
75
|
+
pks = self.kwargs.get("pks")
|
|
76
|
+
if pks and self.group_class:
|
|
77
|
+
try:
|
|
78
|
+
select_related: tuple[str | None, ...] = (
|
|
79
|
+
self._GROUP_INSTANCE_SELECT_RELATED[self.group_class]
|
|
80
|
+
)
|
|
81
|
+
self.group_query = self.group_class.objects.select_related(
|
|
82
|
+
*select_related
|
|
83
|
+
).filter(pk__in=pks)
|
|
84
|
+
except self.group_class.DoesNotExist:
|
|
85
|
+
group = self.kwargs.get("group")
|
|
86
|
+
page = self.kwargs.get("page")
|
|
87
|
+
if group == "r" and not pks and page == 1:
|
|
88
|
+
self.group_query = self.group_class.objects.none()
|
|
89
|
+
else:
|
|
90
|
+
reason = f"{group}__in={pks} does not exist!"
|
|
91
|
+
self.raise_redirect(reason, route_mask={"group": group})
|
|
92
|
+
elif self.group_class:
|
|
93
|
+
self.group_query = self.group_class.objects.none()
|
|
94
|
+
else:
|
|
95
|
+
self.group_query = Comic.objects.none()
|
|
96
|
+
self.group_instance = self.group_query.first()
|
|
97
|
+
|
|
98
|
+
def init_request(self):
|
|
99
|
+
"""Initialize request."""
|
|
100
|
+
super().init_request()
|
|
101
|
+
self._set_group_query_and_instance()
|
|
107
102
|
|
|
108
103
|
def get_model_group(self):
|
|
109
104
|
"""Get the group of the models to browse."""
|
|
@@ -111,21 +106,22 @@ class BrowserView(BrowserBreadcrumbsView):
|
|
|
111
106
|
# A valid nav group or 'c'
|
|
112
107
|
# the child of the current nav group or 'c'
|
|
113
108
|
group = self.kwargs["group"]
|
|
114
|
-
if group ==
|
|
109
|
+
if group == FOLDER_GROUP:
|
|
115
110
|
return group
|
|
116
|
-
if group ==
|
|
111
|
+
if group == STORY_ARC_GROUP:
|
|
117
112
|
pks = self.kwargs.get("pks")
|
|
118
|
-
return
|
|
113
|
+
return COMIC_GROUP if pks else group
|
|
119
114
|
if group == self.valid_nav_groups[-1]:
|
|
120
115
|
# special case for lowest valid group
|
|
121
|
-
return
|
|
116
|
+
return COMIC_GROUP
|
|
122
117
|
return self.valid_nav_groups[self.valid_nav_groups.index(group) + 1]
|
|
123
118
|
|
|
119
|
+
################
|
|
120
|
+
# MAIN QUERIES #
|
|
121
|
+
################
|
|
124
122
|
def _get_common_queryset(self, model):
|
|
125
123
|
"""Create queryset common to group & books."""
|
|
126
|
-
|
|
127
|
-
qs = model.objects.filter(object_filter)
|
|
128
|
-
qs = self.filter_by_annotations(qs, model)
|
|
124
|
+
qs = self.get_filtered_queryset(model)
|
|
129
125
|
count_group_by = self.get_group_by(model)
|
|
130
126
|
count = qs.group_by(count_group_by).count()
|
|
131
127
|
if count:
|
|
@@ -161,167 +157,16 @@ class BrowserView(BrowserBreadcrumbsView):
|
|
|
161
157
|
count = 0
|
|
162
158
|
return qs, count
|
|
163
159
|
|
|
164
|
-
def
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if pks and self.group_class:
|
|
168
|
-
try:
|
|
169
|
-
select_related: tuple[str | None, ...] = (
|
|
170
|
-
self._GROUP_INSTANCE_SELECT_RELATED[self.group_class]
|
|
171
|
-
)
|
|
172
|
-
self.group_query = self.group_class.objects.select_related(
|
|
173
|
-
*select_related
|
|
174
|
-
).filter(pk__in=pks)
|
|
175
|
-
except self.group_class.DoesNotExist:
|
|
176
|
-
group = self.kwargs.get("group")
|
|
177
|
-
page = self.kwargs.get("page")
|
|
178
|
-
if group == "r" and not pks and page == 1:
|
|
179
|
-
self.group_query = self.group_class.objects.none()
|
|
180
|
-
else:
|
|
181
|
-
reason = f"{group}__in={pks} does not exist!"
|
|
182
|
-
self._raise_redirect(reason, route_mask={"group": group})
|
|
183
|
-
elif self.group_class:
|
|
184
|
-
self.group_query = self.group_class.objects.none()
|
|
185
|
-
else:
|
|
186
|
-
self.group_query = Comic.objects.none()
|
|
187
|
-
self.group_instance = self.group_query.first()
|
|
188
|
-
|
|
189
|
-
def _get_root_group_name(self):
|
|
190
|
-
if not self.model:
|
|
191
|
-
reason = "No model set in browser"
|
|
192
|
-
raise ValueError(reason)
|
|
193
|
-
plural = self.model._meta.verbose_name_plural
|
|
194
|
-
if not plural:
|
|
195
|
-
reason = f"No plural name for {self.model}"
|
|
196
|
-
raise ValueError(reason)
|
|
197
|
-
return plural.capitalize()
|
|
198
|
-
|
|
199
|
-
def _get_folder_parent_name(self):
|
|
200
|
-
"""Get title for a folder browser page."""
|
|
201
|
-
group_instance: Folder = self.group_instance # type: ignore
|
|
202
|
-
if not group_instance.path:
|
|
203
|
-
return ""
|
|
204
|
-
|
|
205
|
-
parent_path = Path(group_instance.path).parent
|
|
206
|
-
if not self.is_admin():
|
|
207
|
-
try:
|
|
208
|
-
# remove library path for not admins
|
|
209
|
-
library_path_parent = Path(group_instance.library.path).parent
|
|
210
|
-
parent_path = parent_path.relative_to(library_path_parent)
|
|
211
|
-
except ValueError:
|
|
212
|
-
parent_path = ""
|
|
213
|
-
|
|
214
|
-
parent_path_str = str(parent_path)
|
|
215
|
-
parent_path_str = parent_path_str.removeprefix(".")
|
|
216
|
-
if parent_path_str and parent_path_str[0] != sep:
|
|
217
|
-
parent_path_str = sep + parent_path_str
|
|
218
|
-
return parent_path_str
|
|
219
|
-
|
|
220
|
-
def _get_browser_page_title(self):
|
|
221
|
-
"""Get the proper title for this browse level."""
|
|
222
|
-
group_count = 0
|
|
223
|
-
group_name = ""
|
|
224
|
-
pks = self.kwargs.get("pks")
|
|
225
|
-
if not pks:
|
|
226
|
-
group_name = self._get_root_group_name()
|
|
227
|
-
else:
|
|
228
|
-
group_instance = self.group_instance
|
|
229
|
-
if group_instance:
|
|
230
|
-
if isinstance(group_instance, Volume):
|
|
231
|
-
group_count = group_instance.series.volume_count
|
|
232
|
-
elif isinstance(group_instance, Comic):
|
|
233
|
-
group_count = group_instance.volume.issue_count
|
|
234
|
-
group_name = group_instance.name
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
"group_name": group_name,
|
|
238
|
-
"group_count": group_count,
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
def _check_page_in_bounds(self, total_count):
|
|
242
|
-
"""Redirect page out of bounds."""
|
|
243
|
-
pks = self.kwargs.get("pks")
|
|
244
|
-
page = self.kwargs.get("page", 1)
|
|
245
|
-
num_pages = ceil(total_count / MAX_OBJ_PER_PAGE)
|
|
246
|
-
if (not pks and page == 1) or (page >= 1 and page <= num_pages):
|
|
247
|
-
# Don't redirect if on the root page for the group.
|
|
248
|
-
# Or page within valid range.
|
|
249
|
-
return num_pages
|
|
250
|
-
|
|
251
|
-
# Adjust route mask for redirect
|
|
252
|
-
group = self.kwargs.get("group")
|
|
253
|
-
if num_pages and page > 1:
|
|
254
|
-
# Try page 1 first.
|
|
255
|
-
new_page = num_pages if num_pages and page > num_pages else 1
|
|
256
|
-
pks = pks if pks else "0"
|
|
257
|
-
else:
|
|
258
|
-
# Redirect to root for the group.
|
|
259
|
-
pks = "0"
|
|
260
|
-
new_page = 1
|
|
261
|
-
|
|
262
|
-
reason = f"{page=} does not exist!"
|
|
263
|
-
route_changes = {"group": group, "pks": pks, "page": new_page}
|
|
264
|
-
return self._raise_redirect(reason, route_mask=route_changes)
|
|
265
|
-
|
|
266
|
-
def _paginate_section(self, model, qs, page):
|
|
267
|
-
"""Paginate a group or Comic section."""
|
|
268
|
-
orphans = 0 if self.model_group == "f" or self.params.get("q") else 5
|
|
269
|
-
paginator = Paginator(qs, MAX_OBJ_PER_PAGE, orphans=orphans)
|
|
270
|
-
try:
|
|
271
|
-
qs = paginator.page(page).object_list
|
|
272
|
-
except EmptyPage:
|
|
273
|
-
if model != Folder:
|
|
274
|
-
model_name = self.model.__name__ if self.model else "NO_MODEL"
|
|
275
|
-
LOG.warning(f"No {model_name}s on page {page}")
|
|
276
|
-
qs = model.objects.none()
|
|
277
|
-
|
|
278
|
-
return qs
|
|
279
|
-
|
|
280
|
-
def _paginate_groups(self, group_qs, total_group_count):
|
|
281
|
-
"""Paginate the group object list before books."""
|
|
282
|
-
if total_group_count:
|
|
283
|
-
page = self.kwargs.get("page", 1)
|
|
284
|
-
page_group_qs = self._paginate_section(self.model, group_qs, page)
|
|
160
|
+
def _get_page_mtime(self, group_qs):
|
|
161
|
+
if self.group_instance:
|
|
162
|
+
page_updated_at_max = self.group_instance.updated_at
|
|
285
163
|
else:
|
|
286
|
-
|
|
287
|
-
return page_group_qs
|
|
164
|
+
page_updated_at_max = group_qs.aggregate(max=Max("updated_at"))["max"]
|
|
288
165
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if page_obj_count >= MAX_OBJ_PER_PAGE:
|
|
292
|
-
# No books for this page
|
|
293
|
-
page_book_qs = Comic.objects.none()
|
|
294
|
-
else:
|
|
295
|
-
group_remainder = total_group_count % MAX_OBJ_PER_PAGE
|
|
296
|
-
if page_obj_count:
|
|
297
|
-
# There are books after the groups on the same page
|
|
298
|
-
# Manually add books without the paginator
|
|
299
|
-
book_limit = MAX_OBJ_PER_PAGE - group_remainder
|
|
300
|
-
page_book_qs = book_qs[:book_limit]
|
|
301
|
-
else:
|
|
302
|
-
# There are books after the groups on a new page
|
|
303
|
-
if group_remainder:
|
|
304
|
-
book_offset = MAX_OBJ_PER_PAGE - group_remainder
|
|
305
|
-
page_book_qs = book_qs[book_offset:]
|
|
306
|
-
else:
|
|
307
|
-
page_book_qs = book_qs
|
|
166
|
+
agg_func = self.get_bookmark_updated_at_aggregate(self.model, True)
|
|
167
|
+
page_bookmark_updated_at = group_qs.aggregate(max=agg_func)["max"]
|
|
308
168
|
|
|
309
|
-
|
|
310
|
-
book_only_page = self.kwargs.get("page", 1) - num_group_and_mixed_pages
|
|
311
|
-
page_book_qs = self._paginate_section(
|
|
312
|
-
Comic, page_book_qs, book_only_page
|
|
313
|
-
)
|
|
314
|
-
return page_book_qs
|
|
315
|
-
|
|
316
|
-
def _paginate(self, group_qs, book_qs, group_count, book_count):
|
|
317
|
-
"""Paginate the queryset into a group and book object lists."""
|
|
318
|
-
num_pages = self._check_page_in_bounds(group_count + book_count)
|
|
319
|
-
page_group_qs = self._paginate_groups(group_qs, group_count)
|
|
320
|
-
page_group_count = page_group_qs.count()
|
|
321
|
-
page_book_qs = self._paginate_books(book_qs, group_count, page_group_count)
|
|
322
|
-
page_book_count = page_book_qs.count()
|
|
323
|
-
|
|
324
|
-
return page_group_qs, page_book_qs, num_pages, page_group_count, page_book_count
|
|
169
|
+
return max_none(page_updated_at_max, page_bookmark_updated_at)
|
|
325
170
|
|
|
326
171
|
@staticmethod
|
|
327
172
|
def _get_zero_pad(book_qs):
|
|
@@ -337,10 +182,11 @@ class BrowserView(BrowserBreadcrumbsView):
|
|
|
337
182
|
def _get_group_and_books(self):
|
|
338
183
|
"""Create the main queries with filters, annotation and pagination."""
|
|
339
184
|
group_qs, group_count = self._get_group_queryset()
|
|
185
|
+
mtime = self._get_page_mtime(group_qs)
|
|
340
186
|
book_qs, book_count = self._get_book_queryset()
|
|
341
187
|
# Paginate
|
|
342
|
-
group_qs, book_qs, num_pages, page_group_count, page_book_count = (
|
|
343
|
-
|
|
188
|
+
group_qs, book_qs, num_pages, page_group_count, page_book_count = self.paginate(
|
|
189
|
+
group_qs, book_qs, group_count, book_count
|
|
344
190
|
)
|
|
345
191
|
if page_group_count:
|
|
346
192
|
group_qs = self.annotate_card_aggregates(group_qs, self.model)
|
|
@@ -353,19 +199,18 @@ class BrowserView(BrowserBreadcrumbsView):
|
|
|
353
199
|
# print(group_qs.explain())
|
|
354
200
|
# print(group_qs.query)
|
|
355
201
|
|
|
356
|
-
recovered_group_list = self.re_cover_multi_groups(group_qs)
|
|
357
202
|
total_count = page_group_count + page_book_count
|
|
358
|
-
return
|
|
203
|
+
return group_qs, book_qs, num_pages, total_count, zero_pad, mtime
|
|
359
204
|
|
|
360
205
|
def get_object(self):
|
|
361
206
|
"""Validate settings and get the querysets."""
|
|
362
|
-
|
|
207
|
+
group_qs, book_qs, num_pages, total_count, zero_pad, mtime = (
|
|
363
208
|
self._get_group_and_books()
|
|
364
209
|
)
|
|
365
210
|
|
|
366
211
|
# get additional context
|
|
367
212
|
parent_breadcrumbs = self.get_parent_breadcrumbs()
|
|
368
|
-
title = self.
|
|
213
|
+
title = self.get_browser_page_title()
|
|
369
214
|
# needs to happen after pagination
|
|
370
215
|
# runs obj list query twice :/
|
|
371
216
|
libraries_exist = Library.objects.filter(covers_only=False).exists()
|
|
@@ -376,141 +221,18 @@ class BrowserView(BrowserBreadcrumbsView):
|
|
|
376
221
|
"breadcrumbs": parent_breadcrumbs,
|
|
377
222
|
"title": title,
|
|
378
223
|
"model_group": self.model_group,
|
|
379
|
-
"groups":
|
|
224
|
+
"groups": group_qs,
|
|
380
225
|
"books": book_qs,
|
|
381
226
|
"zero_pad": zero_pad,
|
|
382
227
|
"num_pages": num_pages,
|
|
383
228
|
"total_count": total_count,
|
|
384
229
|
"admin_flags": self.admin_flags,
|
|
385
230
|
"libraries_exist": libraries_exist,
|
|
231
|
+
"mtime": mtime,
|
|
386
232
|
}
|
|
387
233
|
)
|
|
388
234
|
|
|
389
|
-
|
|
390
|
-
"""Get valid top groups for the current settings.
|
|
391
|
-
|
|
392
|
-
Valid top groups are determined by the Browser Settings.
|
|
393
|
-
"""
|
|
394
|
-
valid_top_groups = []
|
|
395
|
-
|
|
396
|
-
show = self.params["show"]
|
|
397
|
-
for nav_group, allowed in show.items():
|
|
398
|
-
if allowed:
|
|
399
|
-
valid_top_groups.append(nav_group)
|
|
400
|
-
# Issues is always a valid top group
|
|
401
|
-
valid_top_groups += [self.COMIC_GROUP]
|
|
402
|
-
|
|
403
|
-
return valid_top_groups
|
|
404
|
-
|
|
405
|
-
def _validate_top_group(self, valid_top_groups):
|
|
406
|
-
nav_group = self.kwargs.get("group")
|
|
407
|
-
top_group = self.params.get("top_group")
|
|
408
|
-
if top_group not in valid_top_groups:
|
|
409
|
-
reason = f"top_group {top_group} not in valid nav groups, changed to "
|
|
410
|
-
if nav_group in valid_top_groups:
|
|
411
|
-
valid_top_group = nav_group
|
|
412
|
-
reason += "nav group: "
|
|
413
|
-
else:
|
|
414
|
-
valid_top_group = valid_top_groups[0]
|
|
415
|
-
reason += "first valid top group "
|
|
416
|
-
reason += valid_top_group
|
|
417
|
-
pks = self.kwargs.get("pks", ())
|
|
418
|
-
page = self.kwargs["page"]
|
|
419
|
-
route = {"group": nav_group, "pks": pks, "page": page}
|
|
420
|
-
settings_mask = {"top_group": valid_top_group}
|
|
421
|
-
self._raise_redirect(reason, route, settings_mask)
|
|
422
|
-
|
|
423
|
-
def set_valid_browse_nav_groups(self, valid_top_groups):
|
|
424
|
-
"""Get valid nav groups for the current settings.
|
|
425
|
-
|
|
426
|
-
Valid nav groups are the top group and below that are also
|
|
427
|
-
enabled in browser settings.
|
|
428
|
-
|
|
429
|
-
May always navigate to root 'r' nav group.
|
|
430
|
-
"""
|
|
431
|
-
top_group = self.params["top_group"]
|
|
432
|
-
nav_group = self.kwargs["group"]
|
|
433
|
-
valid_nav_groups = [self.ROOT_GROUP]
|
|
434
|
-
|
|
435
|
-
for possible_index, possible_nav_group in enumerate(valid_top_groups):
|
|
436
|
-
if top_group == possible_nav_group:
|
|
437
|
-
# all the nav groups past this point,
|
|
438
|
-
# 'c' is obscured by the web reader url, but valid for opds
|
|
439
|
-
tail_top_groups = valid_top_groups[possible_index:]
|
|
440
|
-
valid_nav_groups += tail_top_groups
|
|
441
|
-
|
|
442
|
-
if nav_group not in valid_nav_groups:
|
|
443
|
-
reason = (
|
|
444
|
-
f"Nav group {nav_group} unavailable, "
|
|
445
|
-
f"redirect to {self.ROOT_GROUP}"
|
|
446
|
-
)
|
|
447
|
-
self._raise_redirect(reason)
|
|
448
|
-
break
|
|
449
|
-
self.valid_nav_groups = tuple(valid_nav_groups)
|
|
450
|
-
|
|
451
|
-
def _validate_folder_settings(self):
|
|
452
|
-
"""Check that all the view variables for folder mode are set right."""
|
|
453
|
-
# Check folder view admin flag
|
|
454
|
-
if not self.admin_flags["folder_view"]:
|
|
455
|
-
reason = "folder view disabled"
|
|
456
|
-
valid_top_groups = self._get_valid_browse_top_groups()
|
|
457
|
-
settings_mask = {"top_group": valid_top_groups[0]}
|
|
458
|
-
self._raise_redirect(reason, settings_mask=settings_mask)
|
|
459
|
-
|
|
460
|
-
valid_top_groups = (self.FOLDER_GROUP,)
|
|
461
|
-
self._validate_top_group(valid_top_groups)
|
|
462
|
-
self.valid_nav_groups = valid_top_groups
|
|
463
|
-
|
|
464
|
-
def _validate_browser_group_settings(self):
|
|
465
|
-
"""Check that all the view variables for browser mode are set right."""
|
|
466
|
-
# Validate Browser top_group
|
|
467
|
-
# Change top_group if its not in the valid top groups
|
|
468
|
-
valid_top_groups = self._get_valid_browse_top_groups()
|
|
469
|
-
self._validate_top_group(valid_top_groups)
|
|
470
|
-
|
|
471
|
-
# Validate pks
|
|
472
|
-
nav_group = self.kwargs["group"]
|
|
473
|
-
pks = self.kwargs["pks"]
|
|
474
|
-
if nav_group == self.ROOT_GROUP and (pks and 0 not in pks):
|
|
475
|
-
# r never has pks
|
|
476
|
-
reason = f"Redirect r with {pks=} to pks 0"
|
|
477
|
-
self._raise_redirect(reason)
|
|
478
|
-
|
|
479
|
-
# Validate Browser nav_group
|
|
480
|
-
# Redirect if nav group is wrong
|
|
481
|
-
self.set_valid_browse_nav_groups(valid_top_groups)
|
|
482
|
-
|
|
483
|
-
def _validate_story_arc_settings(self):
|
|
484
|
-
"""Validate story arc settings."""
|
|
485
|
-
valid_top_groups = (self.STORY_ARC_GROUP,)
|
|
486
|
-
self._validate_top_group(valid_top_groups)
|
|
487
|
-
self.valid_nav_groups = valid_top_groups
|
|
488
|
-
|
|
489
|
-
def validate_settings(self):
|
|
490
|
-
"""Validate group and top group settings."""
|
|
491
|
-
group = self.kwargs["group"]
|
|
492
|
-
if group == self.FOLDER_GROUP:
|
|
493
|
-
self._validate_folder_settings()
|
|
494
|
-
elif group == self.STORY_ARC_GROUP:
|
|
495
|
-
self._validate_story_arc_settings()
|
|
496
|
-
else:
|
|
497
|
-
self._validate_browser_group_settings()
|
|
498
|
-
|
|
499
|
-
# Validate order
|
|
500
|
-
if self.order_key == "filename" and not self.admin_flags["folder_view"]:
|
|
501
|
-
pks = self.kwargs["pks"]
|
|
502
|
-
page = self.kwargs["page"]
|
|
503
|
-
route_changes = {"group": group, "pks": pks, "page": page}
|
|
504
|
-
reason = "order by filename not allowed by admin flag."
|
|
505
|
-
settings_mask = {"order_by": "sort_name"}
|
|
506
|
-
self._raise_redirect(reason, route_changes, settings_mask)
|
|
507
|
-
|
|
508
|
-
def init_request(self):
|
|
509
|
-
"""Initialize request."""
|
|
510
|
-
super().init_request()
|
|
511
|
-
self._set_group_query_and_instance()
|
|
512
|
-
|
|
513
|
-
@extend_schema(request=BrowserBreadcrumbsView.input_serializer_class)
|
|
235
|
+
@extend_schema(request=BrowserTitleView.input_serializer_class)
|
|
514
236
|
def get(self, *_args, **_kwargs):
|
|
515
237
|
"""Get browser settings."""
|
|
516
238
|
self.init_request()
|
codex/views/browser/choices.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""View for marking comics read and unread."""
|
|
2
2
|
|
|
3
3
|
from types import MappingProxyType
|
|
4
|
-
from typing import ClassVar
|
|
5
4
|
|
|
6
5
|
import pycountry
|
|
7
6
|
from caseconverter import snakecase
|
|
@@ -13,12 +12,7 @@ from codex.logger.logging import get_logger
|
|
|
13
12
|
from codex.models import (
|
|
14
13
|
Comic,
|
|
15
14
|
ContributorPerson,
|
|
16
|
-
Folder,
|
|
17
|
-
Imprint,
|
|
18
|
-
Publisher,
|
|
19
|
-
Series,
|
|
20
15
|
StoryArc,
|
|
21
|
-
Volume,
|
|
22
16
|
)
|
|
23
17
|
from codex.models.named import IdentifierType
|
|
24
18
|
from codex.serializers.browser.choices import CHOICES_SERIALIZER_CLASS_MAP
|
|
@@ -27,7 +21,6 @@ from codex.serializers.browser.filters import (
|
|
|
27
21
|
CharListField,
|
|
28
22
|
)
|
|
29
23
|
from codex.serializers.models.pycountry import PyCountrySerializer
|
|
30
|
-
from codex.views.auth import IsAuthenticatedOrEnabledNonUsers
|
|
31
24
|
from codex.views.browser.filters.annotations import (
|
|
32
25
|
BrowserAnnotationsFilterView,
|
|
33
26
|
)
|
|
@@ -50,37 +43,21 @@ _FIELD_TO_REL_MODEL_MAP = MappingProxyType(
|
|
|
50
43
|
),
|
|
51
44
|
}
|
|
52
45
|
)
|
|
46
|
+
_BACK_REL_MAP = MappingProxyType(
|
|
47
|
+
{
|
|
48
|
+
ContributorPerson: "contributor__",
|
|
49
|
+
StoryArc: "storyarcnumber__",
|
|
50
|
+
IdentifierType: "identifier__",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
_NULL_NAMED_ROW = MappingProxyType({"pk": -1, "name": "_none_"})
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
class BrowserChoicesViewBase(BrowserAnnotationsFilterView):
|
|
56
57
|
"""Get choices for filter dialog."""
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
_CONTRIBUTORS_PERSON_REL = "contributors__person"
|
|
61
|
-
_STORY_ARC_REL = "story_arc_numbers__story_arc"
|
|
62
|
-
_IDENTIFIERS_REL = "identifiers__identifier_type"
|
|
63
|
-
_NULL_NAMED_ROW = MappingProxyType({"pk": -1, "name": "_none_"})
|
|
64
|
-
_BACK_REL_MAP = MappingProxyType(
|
|
65
|
-
{
|
|
66
|
-
ContributorPerson: "contributor__",
|
|
67
|
-
StoryArc: "storyarcnumber__",
|
|
68
|
-
IdentifierType: "identifier__",
|
|
69
|
-
}
|
|
70
|
-
)
|
|
71
|
-
_REL_MAP = MappingProxyType(
|
|
72
|
-
{
|
|
73
|
-
Publisher: "publisher",
|
|
74
|
-
Imprint: "imprint",
|
|
75
|
-
Series: "series",
|
|
76
|
-
Volume: "volume",
|
|
77
|
-
Comic: "pk",
|
|
78
|
-
Folder: "parent_folder",
|
|
79
|
-
StoryArc: "story_arc_numbers__story_arc",
|
|
80
|
-
IdentifierType: "identifiers__identifier_type",
|
|
81
|
-
}
|
|
82
|
-
)
|
|
83
|
-
_SERIALIZER_MANY = False
|
|
59
|
+
SERIALIZER_MANY = False
|
|
60
|
+
TARGET = "choices"
|
|
84
61
|
|
|
85
62
|
@staticmethod
|
|
86
63
|
def get_field_choices_query(comic_qs, field_name):
|
|
@@ -93,21 +70,13 @@ class BrowserChoicesViewBase(BrowserAnnotationsFilterView):
|
|
|
93
70
|
|
|
94
71
|
def get_m2m_field_query(self, model, comic_qs: QuerySet):
|
|
95
72
|
"""Get distinct m2m value objects for the relation."""
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
m2m_filter = {}
|
|
99
|
-
else:
|
|
100
|
-
model_rel: str = self._REL_MAP[self.model] # type: ignore
|
|
101
|
-
back_rel = self._BACK_REL_MAP.get(model, "")
|
|
102
|
-
back_rel += "comic__"
|
|
103
|
-
back_rel += model_rel
|
|
104
|
-
back_rel += "__in"
|
|
105
|
-
m2m_filter = {back_rel: comic_qs}
|
|
73
|
+
back_rel = _BACK_REL_MAP.get(model, "")
|
|
74
|
+
m2m_filter = {f"{back_rel}comic__in": comic_qs}
|
|
106
75
|
return model.objects.filter(**m2m_filter).values("pk", "name").distinct()
|
|
107
76
|
|
|
108
77
|
@staticmethod
|
|
109
78
|
def does_m2m_null_exist(comic_qs, rel):
|
|
110
|
-
"""Get if
|
|
79
|
+
"""Get if comics exist in the filter without values exists for an m2m field."""
|
|
111
80
|
return comic_qs.filter(**{f"{rel}__isnull": True}).exists()
|
|
112
81
|
|
|
113
82
|
def get_rel_and_model(self, field_name):
|
|
@@ -122,22 +91,18 @@ class BrowserChoicesViewBase(BrowserAnnotationsFilterView):
|
|
|
122
91
|
rel = field_name
|
|
123
92
|
model = remote_field.model if remote_field else None
|
|
124
93
|
|
|
125
|
-
rel = self.rel_prefix + rel
|
|
126
|
-
|
|
127
94
|
return rel, model
|
|
128
95
|
|
|
129
96
|
def get_object(self):
|
|
130
97
|
"""Get the comic subquery use for the choices."""
|
|
131
|
-
|
|
132
|
-
qs = self.model.objects.filter(object_filter) # type: ignore
|
|
133
|
-
return self.filter_by_annotations(qs, self.model, binary=True)
|
|
98
|
+
return self.get_filtered_queryset(Comic)
|
|
134
99
|
|
|
135
100
|
@extend_schema(request=BrowserAnnotationsFilterView.input_serializer_class)
|
|
136
101
|
def get(self, *_args, **_kwargs):
|
|
137
102
|
"""Return choices."""
|
|
138
103
|
self.init_request()
|
|
139
104
|
obj = self.get_object()
|
|
140
|
-
serializer = self.get_serializer(obj, many=self.
|
|
105
|
+
serializer = self.get_serializer(obj, many=self.SERIALIZER_MANY)
|
|
141
106
|
return Response(serializer.data)
|
|
142
107
|
|
|
143
108
|
|
|
@@ -170,6 +135,7 @@ class BrowserChoicesAvailableView(BrowserChoicesViewBase):
|
|
|
170
135
|
def get_object(self):
|
|
171
136
|
"""Get choice counts."""
|
|
172
137
|
qs = super().get_object()
|
|
138
|
+
filters = self.params.get("filters", {})
|
|
173
139
|
data = {}
|
|
174
140
|
for field_name in self.serializer_class().get_fields(): # type: ignore
|
|
175
141
|
if field_name == "story_arcs" and self.model == StoryArc:
|
|
@@ -182,9 +148,9 @@ class BrowserChoicesAvailableView(BrowserChoicesViewBase):
|
|
|
182
148
|
else:
|
|
183
149
|
count = self._get_field_choices_count(qs, rel)
|
|
184
150
|
|
|
185
|
-
filters = self.params.get("filters", {})
|
|
186
151
|
try:
|
|
187
|
-
|
|
152
|
+
is_filter_set = bool(filters.get(field_name))
|
|
153
|
+
flag = is_filter_set or count > 1 # type: ignore
|
|
188
154
|
except TypeError:
|
|
189
155
|
flag = False
|
|
190
156
|
data[field_name] = flag
|
|
@@ -195,7 +161,9 @@ class BrowserChoicesView(BrowserChoicesViewBase):
|
|
|
195
161
|
"""Get choices for filter dialog."""
|
|
196
162
|
|
|
197
163
|
# serializer_class = Dynamic class determined in get()
|
|
198
|
-
|
|
164
|
+
SERIALIZER_MANY = True
|
|
165
|
+
# HACK get_serializer(data, many=True) requires this to be set for the debug API view
|
|
166
|
+
queryset = Comic.objects.none()
|
|
199
167
|
|
|
200
168
|
def _get_field_choices(self, comic_qs, field_name):
|
|
201
169
|
"""Create a pk:name object for fields without tables."""
|
|
@@ -225,7 +193,7 @@ class BrowserChoicesView(BrowserChoicesViewBase):
|
|
|
225
193
|
# rows. :(
|
|
226
194
|
if self.does_m2m_null_exist(comic_qs, rel):
|
|
227
195
|
choices = list(qs)
|
|
228
|
-
choices.append(
|
|
196
|
+
choices.append(_NULL_NAMED_ROW)
|
|
229
197
|
else:
|
|
230
198
|
choices = qs
|
|
231
199
|
return choices
|