diff --git a/Dockerfile b/Dockerfile index aeb53977a..7a9123c5a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ ARG BASE_VERSION FROM ajslater/python-alpine:$BASE_VERSION AS codex-install +ARG PKG_VERSION +LABEL version python${BASE_VERSION}_codex-${PKG_VERSION} RUN echo "**** install system wheel building packages ****" && \ apk add --no-cache \ diff --git a/NEWS b/NEWS index c09fbb74d..f862c6124 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,8 @@ +v0.5.2 +* Lazy load filter choices when the menu opens +* Fix multiprocessing for Windows +* Documentation moved into admin panel + v0.5.1 * Minor bugfixes. Rebuild for pypi diff --git a/README.md b/README.md index ff1e594bf..e473c9d63 100644 --- a/README.md +++ b/README.md @@ -86,37 +86,23 @@ and then navigate to [http://localhost:9810/](http://localhost:9810/) #### Change the Admin password -The first thing you need to do is to log in as an Administrator. +The first thing you need to do is to log in as an Administrator and change the admin password. -- Log in with the three dots menu in the upper right of the browse view. The default administator username/password is admin/admin. +- Log in with the **⋮** menu in the upper right of the browse view. The default administator username/password is admin/admin. - Navigate to the Admin Panel by selecting it from under the three dots menu after you have logged in. - Navigate to the Users panel. - Select the `admin` user. - Change the admin password using the tiny "this form" link in the password section. - You may also change the admin user's name or anything else. -#### Adding Libraries +#### Adding Comic Libraries -The first thing you need to do is to log in as an Administrator and add one or more comic libraries. +The second thing you should do is log in as an Administrator and add one or more comic libraries. -- Log in with any superuser (such as the default adimin account) using the three dots menu in the upper right of the browse view. +- Log in with any superuser (such as the default adimin account) using the **⋮** menu in the upper right of the browse view. - Navigate to the Admin Panel by selecting it from under the three dots menu after you have logged in. - Navigate to the Codex API Librarys (sic) on the Admin Panel - Add a Library with the "ADD LIBRARY +" button in the upper right. - -- A Library is just a path where Codex can recursively search for comic archives. - -- Codex will try to import comics from a Library immediately. -- Navigate back to the browse view by clicking the "view site" link in the upper right of the Admin Panel. -- Wait for comics to appear. It may take some time to import large comic libraries and Codex will only issue a browser refresh websocket notification when the entire library has loaded. Check the console if you want to be assured its doing something. - -#### Admin Flags - -##### Disable Registration - -By default users bookmarks and preferenrces are saved in an anonymous brower session. But users can create a username & password to save their bookmarks in between browsers. You may disable this ability in the Admin Flags section of the Admin Panel. - -##### Disable Folder View - -By default, codex provides a "Folder View" which mimics the directory heirarchy of the libraries that you've added to Codex. If you wish to disable this view for all users, you may do so in the Admin Flags. The database style browser view is always available. ##### Reset the admin password. @@ -126,10 +112,10 @@ If you forget all your superuser passwords, you may restore the original default CODEX_RESET_ADMIN=1 codex ``` -or if using docker: +or, if using Docker: ```sh -docker run -e CODEX_RESET_ADMIN=1 codex +docker run -e CODEX_RESET_ADMIN=1 -v /config:/config ajslater/codex ``` ## Configure Codex @@ -199,7 +185,8 @@ You will have to contact your administrator to reset your password if you forget - Edit & write metadata for one comic at a time. - Add more Filters & Sort options (e.g. filter by story_arc, location) -- Fix Admin Pages Styling to match the rest of Codex. +- [OPDS](https://en.wikipedia.org/wiki/Open_Publication_Distribution_System) +- Dark Admin Panel Styling. - Upgrade Vue to version 3 ## Out of Scope diff --git a/codex/admin.py b/codex/admin.py index b4fa997f0..ee6685f52 100644 --- a/codex/admin.py +++ b/codex/admin.py @@ -61,13 +61,13 @@ def scan(self, request, queryset): """Scan for new comics.""" self._scan(request, queryset, False) - scan.short_description = "Scan for new comics since last scan" + scan.short_description = "Scan for changes" def force_scan(self, request, queryset): """Scan all comics.""" self._scan(request, queryset, True) - force_scan.short_description = "Rescan all comics" + force_scan.short_description = "Re-import all comics" def _on_change(self, obj, created=False): """Events for when the library has changed.""" diff --git a/codex/librarian/librariand.py b/codex/librarian/librariand.py index a289fb131..7c32b9fab 100644 --- a/codex/librarian/librariand.py +++ b/codex/librarian/librariand.py @@ -1,5 +1,6 @@ """Library process worker for background tasks.""" import logging +import platform import time from multiprocessing import Pool @@ -57,11 +58,12 @@ SHUTDOWN_MSG = "shutdown" MAX_WS_ATTEMPTS = 4 librarian_proc = None -# XXX Fixes QUEUE sharing with default spawn start method. The spawn -# method is also very very slow. Use fork and the -# OBJC_DISABLE_INITIALIZE_FORK_SAFETY environment variable for macOS. -# https://bugs.python.org/issue40106 -set_start_method("fork", force=True) +if platform.system() == "Darwin": + # XXX Fixes QUEUE sharing with default spawn start method. The spawn + # method is also very very slow. Use fork and the + # OBJC_DISABLE_INITIALIZE_FORK_SAFETY environment variable for macOS. + # https://bugs.python.org/issue40106 + set_start_method("fork", force=True) def get_websocket(): diff --git a/codex/serializers/browse.py b/codex/serializers/browse.py index 1183e1467..bc2df2237 100644 --- a/codex/serializers/browse.py +++ b/codex/serializers/browse.py @@ -11,7 +11,6 @@ from rest_framework.serializers import Serializer from codex.choices.static import CHOICES -from codex.serializers.vuetify import VueIntChoiceSerializer VUE_MODEL_NULL_CODES = (-1, 0) @@ -114,8 +113,6 @@ class BrowseRouteSerializer(Serializer): class BrowserFormChoicesSerializer(Serializer): """These choices change with browse context.""" - decade = VueIntChoiceSerializer(many=True, read_only=True) # noqa: N815 - characters = VueIntChoiceSerializer(many=True, read_only=True) # noqa: N815 enableFolderView = BooleanField(read_only=True) # noqa: N815 @@ -133,6 +130,7 @@ class BrowseListSerializer(Serializer): child=BrowseComicSerializer(read_only=True), allow_empty=True, read_only=True, ) formChoices = BrowserFormChoicesSerializer(read_only=True) # noqa: N815 + librariesExist = BooleanField(read_only=True) # noqa: N815 class BrowserOpenedSerializer(Serializer): diff --git a/codex/templates/admin/codex/adminflag/change_list.html b/codex/templates/admin/codex/adminflag/change_list.html new file mode 100644 index 000000000..8b47d2597 --- /dev/null +++ b/codex/templates/admin/codex/adminflag/change_list.html @@ -0,0 +1,13 @@ +{% extends "admin/change_list.html" %} + +{% block footer %} + +{% endblock %} diff --git a/codex/templates/admin/codex/library/change_list.html b/codex/templates/admin/codex/library/change_list.html new file mode 100644 index 000000000..7431603e3 --- /dev/null +++ b/codex/templates/admin/codex/library/change_list.html @@ -0,0 +1,26 @@ +{% extends "admin/change_list.html" %} + +{% block footer %} + +{% endblock %} diff --git a/codex/urls_api.py b/codex/urls_api.py index f754baab9..b115ec3d7 100644 --- a/codex/urls_api.py +++ b/codex/urls_api.py @@ -19,6 +19,7 @@ from codex.views.auth import LogoutView from codex.views.auth import RegisterView from codex.views.auth import UserView +from codex.views.browse_choices import BrowseChoiceView from codex.views.browser import BrowseView from codex.views.metadata import ComicDownloadView from codex.views.metadata import ComicMetadataView @@ -37,6 +38,11 @@ # # Browser path("browse//", BrowseView.as_view(), name="browse_objects",), + path( + "browse///choices/", + BrowseChoiceView.as_view(), + name="browse_choices", + ), path( "browse///mark_read", UserBookmarkFinishedView.as_view(), diff --git a/codex/views/browse_base.py b/codex/views/browse_base.py new file mode 100644 index 000000000..3fcbb72cd --- /dev/null +++ b/codex/views/browse_base.py @@ -0,0 +1,115 @@ +"""Views for browsing comic library.""" +import logging + +from django.db.models import Q +from rest_framework.views import APIView + +from codex.models import Folder +from codex.views.mixins import SessionMixin + + +# from codex.views.mixins import UserBookmarkMixin + +LOG = logging.getLogger(__name__) + + +class BrowseBaseView(APIView, SessionMixin): + """Browse comics with a variety of filters and sorts.""" + + FOLDER_GROUP = "f" + GROUP_RELATION = { + "p": "publisher", + "i": "imprint", + "s": "series", + "v": "volume", + "c": "comic", + } + FILTER_ATTRIBUTES = ("decade", "characters") + + def filter_by_comic_many_attribute(self, attribute): + """Filter by a comic any2many attribute.""" + filter_list = self.params["filters"].get(attribute) + filter_query = Q() + if filter_list: + # None values in a list don't work right so test for them + # seperately + for index, val in enumerate(filter_list): + if val is None: + del filter_list[index] + filter_query |= Q(**{f"comic__{attribute}": None}) + if filter_list: + filter_query |= Q(**{f"comic__{attribute}__in": filter_list}) + return filter_query + + def get_comic_attribute_filter(self): + """Filter the comics based on the form filters.""" + comic_attribute_filter = Q() + for attribute in self.FILTER_ATTRIBUTES: + comic_attribute_filter &= self.filter_by_comic_many_attribute(attribute) + return comic_attribute_filter + + def get_bookmark_filter(self): + """Build bookmark query.""" + choice = self.params["filters"].get("bookmark", "ALL") + + bookmark_filter = Q() + if choice in ("UNREAD", "IN_PROGRESS",): + bookmark_filter &= ( + Q(comic__userbookmark__finished=False) + | Q(comic__userbookmark=None) + | Q(comic__userbookmark__finished=None) + ) + if choice == "IN_PROGRESS": + bookmark_filter &= Q(comic__userbookmark__bookmark__gt=0) + return bookmark_filter + + def get_folders_filter(self): + """Filters for ALL parent folders not just immediate one.""" + pk = self.kwargs.get("pk") + if pk: + folders_filter = Q(folder__in=[pk]) + else: + folders_filter = Q() + + return folders_filter + + def get_parent_folder_filter(self): + """Create the folder and comic object lists.""" + pk = self.kwargs.get("pk") + if pk: + self.host_folder = Folder.objects.select_related("parent_folder").get(pk=pk) + else: + self.host_folder = None + + return Q(parent_folder=self.host_folder) + + def get_browse_container_filter(self): + """Get the objects we'll be displaying.""" + # Get the instances that are children of the group_instance + # And the filtered comics that are children of the group_instance + pk = self.kwargs.get("pk") + if pk: + group = self.kwargs.get("group") + group_relation = self.GROUP_RELATION[group] + container_filter = Q(**{group_relation: pk}) + else: + container_filter = Q() + + return container_filter + + def get_query_filters(self, choices=False): + """Return the main object filter and the one for aggregates.""" + if self.kwargs.get("group") == self.FOLDER_GROUP: + if choices: + object_filter = self.get_folders_filter() + else: + object_filter = self.get_parent_folder_filter() + else: + object_filter = self.get_browse_container_filter() + + bookmark_filter_join = self.get_bookmark_filter() + comic_attribute_filter = self.get_comic_attribute_filter() + aggregate_filter = bookmark_filter_join & comic_attribute_filter + object_filter &= aggregate_filter + + return object_filter, aggregate_filter diff --git a/codex/views/browse_choices.py b/codex/views/browse_choices.py new file mode 100644 index 000000000..86110a97c --- /dev/null +++ b/codex/views/browse_choices.py @@ -0,0 +1,43 @@ +"""View for marking comics read and unread.""" +import logging + +from django.http import Http404 +from rest_framework.response import Response + +import codex.choices.model + +from codex.models import Comic +from codex.serializers.vuetify import VueIntChoiceSerializer +from codex.views.browse_base import BrowseBaseView + + +LOG = logging.getLogger(__name__) + + +class BrowseChoiceView(BrowseBaseView): + """Get choices for filter dialog.""" + + def get(self, request, *args, **kwargs): + """Get choices for filter dialog.""" + choice_type = self.kwargs.get("choice_type") + + self.params = self.get_session(self.BROWSE_KEY) + + filters, _ = self.get_query_filters(choices=True) + + choices_comic_list = ( + Comic.objects.filter(filters) + .only(choice_type) + .prefetch_related(choice_type) + ) + + class_name = f"{choice_type.capitalize()}FilterChoice" + try: + choices_class = getattr(codex.choices.model, class_name) + except AttributeError as exc: + LOG.error(exc) + raise Http404(f"Filter for {choice_type} not found") + choices = choices_class.get_vue_choices(choices_comic_list) + + serializer = VueIntChoiceSerializer(choices, many=True, read_only=True) + return Response(serializer.data) diff --git a/codex/views/browser.py b/codex/views/browser.py index 39de467ed..818c7631d 100644 --- a/codex/views/browser.py +++ b/codex/views/browser.py @@ -19,34 +19,31 @@ from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import ValidationError from rest_framework.response import Response -from rest_framework.views import APIView from stringcase import snakecase -from codex.choices.model import CharactersFilterChoice -from codex.choices.model import DecadeFilterChoice from codex.librarian.queue import QUEUE from codex.librarian.queue import ComicCoverCreateTask from codex.models import AdminFlag from codex.models import Comic from codex.models import Folder from codex.models import Imprint +from codex.models import Library from codex.models import Publisher from codex.models import Series from codex.models import Volume from codex.serializers.browse import BrowseListSerializer from codex.serializers.browse import BrowserOpenedSerializer from codex.serializers.browse import BrowserSettingsSerializer -from codex.views.mixins import SessionMixin +from codex.views.browse_base import BrowseBaseView from codex.views.mixins import UserBookmarkMixin LOG = logging.getLogger(__name__) -class BrowseView(APIView, SessionMixin, UserBookmarkMixin): +class BrowseView(BrowseBaseView, UserBookmarkMixin): """Browse comics with a variety of filters and sorts.""" - FOLDER_GROUP = "f" COMIC_GROUP = "c" GROUP_MODEL = bidict( { @@ -56,7 +53,7 @@ class BrowseView(APIView, SessionMixin, UserBookmarkMixin): "s": Series, "v": Volume, COMIC_GROUP: Comic, - FOLDER_GROUP: Folder, + BrowseBaseView.FOLDER_GROUP: Folder, } ) GROUP_SHOW_FLAGS = { @@ -67,14 +64,6 @@ class BrowseView(APIView, SessionMixin, UserBookmarkMixin): "s": "v", "v": "c", } - GROUP_RELATION = { - "p": "publisher", - "i": "imprint", - "s": "series", - "v": "volume", - "c": "comic", - } - FILTER_ATTRIBUTES = ("decade", "characters") SORT_AGGREGATE_FUNCS = { "user_rating": Avg, "critical_rating": Avg, @@ -136,43 +125,6 @@ def get_model_group(self): return self.COMIC_GROUP - def filter_by_comic_many_attribute(self, attribute): - """Filter by a comic any2many attribute.""" - filter_list = self.params["filters"].get(attribute) - filter_query = Q() - if filter_list: - # None values in a list don't work right so test for them - # seperately - for index, val in enumerate(filter_list): - if val is None: - del filter_list[index] - filter_query |= Q(**{f"comic__{attribute}": None}) - if filter_list: - filter_query |= Q(**{f"comic__{attribute}__in": filter_list}) - return filter_query - - def get_comic_attribute_filter(self): - """Filter the comics based on the form filters.""" - comic_attribute_filter = Q() - for attribute in self.FILTER_ATTRIBUTES: - comic_attribute_filter &= self.filter_by_comic_many_attribute(attribute) - return comic_attribute_filter - - def get_bookmark_filter(self): - """Build bookmark query.""" - choice = self.params["filters"].get("bookmark", "ALL") - - bookmark_filter = Q() - if choice in ("UNREAD", "IN_PROGRESS",): - bookmark_filter &= ( - Q(comic__userbookmark__finished=False) - | Q(comic__userbookmark=None) - | Q(comic__userbookmark__finished=None) - ) - if choice == "IN_PROGRESS": - bookmark_filter &= Q(comic__userbookmark__bookmark__gt=0) - return bookmark_filter - def get_order_by(self, order_key, model=None): """ Create the order_by list. @@ -238,7 +190,7 @@ def annotate_and_sort(self, obj_list, model, aggregate_filter): pages_read = Sum("comic__userbookmark__bookmark", filter=ub_filter) obj_list = obj_list.annotate(bookmark=pages_read) # Annote progress - obj_list = UserBookmarkMixin.annotate_progress(obj_list) + obj_list = self.annotate_progress(obj_list) # Select comics for the children by an outer ref for annotation # Order the decendent comics by the sort argumentst @@ -273,26 +225,6 @@ def annotate_and_sort(self, obj_list, model, aggregate_filter): obj_list = obj_list.order_by(*order_by) return obj_list - def get_parent_folder_filter(self): - """Create the folder and comic object lists.""" - pk = self.kwargs.get("pk") - if pk: - self.host_folder = Folder.objects.select_related("parent_folder").get(pk=pk) - else: - self.host_folder = None - - return Q(parent_folder=self.host_folder) - - def get_folders_filter(self): - """Create the folder and comic object lists.""" - pk = self.kwargs.get("pk") - if pk: - folders_filter = Q(folder__in=[pk]) - else: - folders_filter = Q() - - return folders_filter - def set_browse_model(self): """Set the model for the browse list.""" group = self.kwargs.get("group") @@ -301,49 +233,14 @@ def set_browse_model(self): model_group = self.get_model_group() self.model = self.GROUP_MODEL[model_group] - def get_browse_container_filter(self): - """Get the objects we'll be displaying.""" - # Get the instances that are children of the group_instance - # And the filtered comics that are children of the group_instance - pk = self.kwargs.get("pk") - if pk > 0: - group = self.kwargs.get("group") - group_relation = self.GROUP_RELATION[group] - container_filter = Q(**{group_relation: pk}) - else: - container_filter = Q() - - return container_filter - - def get_query_filters(self, container_filter): - """Return the main object filter and the one for aggregates.""" - bookmark_filter_join = self.get_bookmark_filter() - comic_attribute_filter = self.get_comic_attribute_filter() - aggregate_filter = bookmark_filter_join & comic_attribute_filter - object_filter = container_filter & aggregate_filter - return object_filter, aggregate_filter - - def set_choices_comic_list(self, filters): - """Set the choices comic list for getting choices later.""" - self.choices_comic_list = ( - Comic.objects.filter(filters) - .only(*self.CHOICES_RELATIONS) - .prefetch_related(*self.CHOICES_RELATIONS) - ) - def get_folder_queryset(self): """Create folder queryset.""" - object_filter, aggregate_filter = self.get_query_filters( - self.get_parent_folder_filter() - ) + object_filter, aggregate_filter = self.get_query_filters() # Create the main queries with filters folder_list = Folder.objects.filter(object_filter) comic_list = Comic.objects.filter(object_filter) - # Create a query for comics used by the model filters - self.set_choices_comic_list(aggregate_filter & self.get_folders_filter()) - # add annotations for display and sorting # and the comic_cover which uses a sort folder_list = self.annotate_and_sort(folder_list, Folder, aggregate_filter) @@ -356,12 +253,8 @@ def get_browse_queryset(self): self.set_browse_model() # Create the main query with the filters - object_filter, aggregate_filter = self.get_query_filters( - self.get_browse_container_filter() - ) + object_filter, aggregate_filter = self.get_query_filters() obj_list = self.model.objects.filter(object_filter) - # for the model filter choices that don't need annotations - self.set_choices_comic_list(object_filter) # obj_list filtering done # Add annotations for display and sorting @@ -459,25 +352,18 @@ def get_context(self): else: up_route = {} - decade_filter_choices = DecadeFilterChoice.get_vue_choices( - self.choices_comic_list - ) - characters_filter_choices = CharactersFilterChoice.get_vue_choices( - self.choices_comic_list - ) - efv_flag = AdminFlag.objects.only("on").get(name=AdminFlag.ENABLE_FOLDER_VIEW) + libraries_exist = Library.objects.exists() + print(f"{libraries_exist=}") + context = { "upRoute": up_route, "browseTitle": browse_title, "containerList": self.container_list, "comicList": self.comic_list, - "formChoices": { - "decade": decade_filter_choices, - "characters": characters_filter_choices, - "enableFolderView": efv_flag.on, - }, + "formChoices": {"enableFolderView": efv_flag.on}, + "librariesExist": libraries_exist, } return context diff --git a/docker-env b/docker-env index 4106658fc..1cb412c68 100644 --- a/docker-env +++ b/docker-env @@ -1,6 +1,6 @@ REPO=ajslater/codex IMAGE=ajslater/codex BASE_VERSION=latest -PKG_VERSION=v0.5.1 +PKG_VERSION=v0.5.2 PUID=501 PGID=20 diff --git a/frontend/package.json b/frontend/package.json index 97b977445..f096c569d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "codex", - "version": "0.5.1", + "version": "0.5.2", "private": true, "description": "ui for codex api", "scripts": { diff --git a/frontend/src/api/browser.js b/frontend/src/api/browser.js index 5249c71a5..078a5c9a3 100644 --- a/frontend/src/api/browser.js +++ b/frontend/src/api/browser.js @@ -15,6 +15,10 @@ const getBrowseObjects = ({ group, pk, settings }) => { return ajax("put", `${BROWSE_BASE}/${group}/${pk}`, settings); }; +const getBrowseChoices = ({ group, pk, choice_type }) => { + return ajax("get", `${BROWSE_BASE}/${group}/${pk}/choices/${choice_type}`); +}; + const setMarkRead = ({ group, pk, finished }) => { return ajax("patch", `${BROWSE_BASE}/${group}/${pk}/mark_read`, { finished, @@ -52,6 +56,7 @@ export const getCoverSrc = (coverPath) => export default { getBrowseOpened, getBrowseObjects, + getBrowseChoices, setMarkRead, getSocket, }; diff --git a/frontend/src/browser.vue b/frontend/src/browser.vue index c8fbd8d08..b81316624 100644 --- a/frontend/src/browser.vue +++ b/frontend/src/browser.vue @@ -143,9 +143,12 @@ + +

No comics found for these filters.

+
-

No libraries with comics in them have been added to Codex yet.

+

No libraries have been added to Codex yet.

Go to the admin panel and add a comic library.

@@ -203,6 +206,7 @@ export default { comicList: (state) => state.comicList, filterMode: (state) => state.filterMode, browseLoaded: (state) => state.browseLoaded, + librariesExist: (state) => state.librariesExist, itemsExist: (state) => state.containerList.length + state.comicList.length > 0, }), @@ -349,7 +353,7 @@ export default { // On click, reselect the current value to close the menu. const item = this.$refs.filterSelect.selectedItems[0]; this.$refs.filterSelect.selectItem(item); - this.$store.dispatch("browser/setFilterMode", "base"); + this.$store.dispatch("browser/setFilterMode", { mode: "base" }); }, reload: function () { this.$store.dispatch("browser/browseOpened", this.$route.params); diff --git a/frontend/src/components/filter-sub-menu.vue b/frontend/src/components/filter-sub-menu.vue index 1a8fac6a5..8fbb7d8ef 100644 --- a/frontend/src/components/filter-sub-menu.vue +++ b/frontend/src/components/filter-sub-menu.vue @@ -115,7 +115,11 @@ export default { }, methods: { setFilterMode(mode) { - this.$store.dispatch("browser/setFilterMode", mode); + this.$store.dispatch("browser/setFilterMode", { + group: this.$route.params.group, + pk: this.$route.params.pk, + mode, + }); this.query = ""; }, }, diff --git a/frontend/src/store/modules/browser.js b/frontend/src/store/modules/browser.js index 2579c89cf..299668783 100644 --- a/frontend/src/store/modules/browser.js +++ b/frontend/src/store/modules/browser.js @@ -44,8 +44,8 @@ const state = { formChoices: { // determined by api bookmark: FORM_CHOICES.bookmarkFilter, - decade: [], - characters: [], + decade: null, + characters: null, sort: FORM_CHOICES.sort, settingsGroup: FORM_CHOICES.settingsGroup, show: { @@ -57,6 +57,7 @@ const state = { comicList: [], filterMode: "base", browseLoaded: false, + librariesExist: null, }; const isRootGroupEnabled = (state, rootGroup) => { @@ -106,12 +107,19 @@ const mutations = { enableFolderView: data.formChoices.enableFolderView, }; for (let key of DYNAMIC_FILTERS) { - state.formChoices[key] = Object.freeze(data.formChoices[key]); + // Reset this every browse so the lazy loader knows to refresh it. + state.formChoices[key] = null; } state.browseTitle = data.browseTitle; state.routes.up = Object.freeze(data.upRoute); state.containerList = Object.freeze(data.containerList); state.comicList = Object.freeze(data.comicList); + state.librariesExist = data.librariesExist; + }, + setBrowseChoice(state, data) { + const key = data.key; + delete data.key; + state.formChoices[key] = Object.freeze(data); }, setFilterMode(state, mode) { state.filterMode = mode; @@ -305,7 +313,19 @@ const actions = { await API.setMarkRead(data); dispatch("getBrowseObjects"); }, - setFilterMode({ commit }, mode) { + async setFilterMode({ commit, state }, { group, pk, mode }) { + if (mode && mode !== "base" && state.formChoices[mode] == null) { + await API.getBrowseChoices({ group, pk, choice_type: mode }) + .then((response) => { + response.data.key = mode; + return commit("setBrowseChoice", response.data); + }) + .catch((error) => { + console.error("ERROR", error); + console.error("ERROR.RESPONSE", error.response); + console.error("couldn't get choices for", mode); + }); + } commit("setFilterMode", mode); }, clearFilters({ commit, dispatch, getters }) { diff --git a/poetry.lock b/poetry.lock index 7406d836c..344c1c362 100644 --- a/poetry.lock +++ b/poetry.lock @@ -31,7 +31,7 @@ version = "3.2.10" tests = ["pytest", "pytest-asyncio"] [[package]] -category = "main" +category = "dev" description = "Atomic file writes." marker = "sys_platform == \"win32\"" name = "atomicwrites" @@ -40,7 +40,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "1.4.0" [[package]] -category = "main" +category = "dev" description = "Classes Without Boilerplate" name = "attrs" optional = false @@ -142,7 +142,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "7.1.2" [[package]] -category = "main" +category = "dev" description = "Cross-platform colored terminal text." marker = "sys_platform == \"win32\" or python_version > \"3.4\"" name = "colorama" @@ -351,7 +351,7 @@ version = "0.18.2" category = "dev" description = "Lightweight in-process concurrent programming" name = "greenlet" -optional = true +optional = false python-versions = "*" version = "0.4.16" @@ -429,7 +429,7 @@ python-versions = "*" version = "5.2.0" [[package]] -category = "main" +category = "dev" description = "Read metadata from Python packages" marker = "python_version < \"3.8\"" name = "importlib-metadata" @@ -445,7 +445,7 @@ docs = ["sphinx", "rst.linker"] testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] [[package]] -category = "main" +category = "dev" description = "iniconfig: brain-dead simple config-ini parsing" name = "iniconfig" optional = false @@ -469,7 +469,7 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"] category = "dev" description = "An autocompletion tool for Python that can be used for text editors." name = "jedi" -optional = true +optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "0.17.2" @@ -484,7 +484,7 @@ testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] category = "dev" description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7 and 3.8 programs." name = "libcst" -optional = true +optional = false python-versions = ">=3.6" version = "0.3.10" @@ -522,7 +522,7 @@ version = "0.6.1" category = "dev" description = "Generating type annotations from sampled production types" name = "monkeytype" -optional = true +optional = false python-versions = ">=3.6" version = "20.5.0" @@ -531,7 +531,7 @@ libcst = ">=0.3.5" mypy-extensions = "*" [[package]] -category = "main" +category = "dev" description = "More routines for operating on iterables, beyond itertools" name = "more-itertools" optional = false @@ -542,7 +542,7 @@ version = "8.4.0" category = "dev" description = "MessagePack (de)serializer." name = "msgpack" -optional = true +optional = false python-versions = "*" version = "1.0.0" @@ -550,7 +550,7 @@ version = "1.0.0" category = "dev" description = "Optional static typing for Python" name = "mypy" -optional = true +optional = false python-versions = ">=3.5" version = "0.782" @@ -566,7 +566,7 @@ dmypy = ["psutil (>=4.0)"] category = "dev" description = "Experimental type system extensions for programs checked with the mypy typechecker." name = "mypy-extensions" -optional = true +optional = false python-versions = "*" version = "0.4.3" @@ -574,7 +574,7 @@ version = "0.4.3" category = "dev" description = "Transition packgage for pynvim" name = "neovim" -optional = true +optional = false python-versions = "*" version = "0.3.1" @@ -582,7 +582,7 @@ version = "0.3.1" pynvim = ">=0.3.1" [[package]] -category = "main" +category = "dev" description = "Core utilities for Python packages" name = "packaging" optional = false @@ -605,7 +605,7 @@ version = "1.16.0" category = "dev" description = "A Python Parser" name = "parso" -optional = true +optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "0.7.1" @@ -648,7 +648,7 @@ python-versions = ">=3.5" version = "7.2.0" [[package]] -category = "main" +category = "dev" description = "plugin and hook calling mechanisms for python" name = "pluggy" optional = false @@ -672,7 +672,7 @@ python-versions = "*" version = "1.3.0" [[package]] -category = "main" +category = "dev" description = "library with cross-python path, ini-parsing, io, code, log facilities" name = "py" optional = false @@ -723,7 +723,7 @@ version = "0.3.10" category = "dev" description = "Python client to neovim" name = "pynvim" -optional = true +optional = false python-versions = "*" version = "0.4.1" @@ -736,7 +736,7 @@ pyuv = ["pyuv (>=1.0.0)"] test = ["pytest (>=3.4.0)"] [[package]] -category = "main" +category = "dev" description = "Python parsing module" name = "pyparsing" optional = false @@ -753,7 +753,7 @@ python-versions = "*" version = "2.1" [[package]] -category = "main" +category = "dev" description = "pytest: simple powerful testing with Python" name = "pytest" optional = false @@ -811,7 +811,7 @@ pytest = ">=4.6" testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] [[package]] -category = "main" +category = "dev" description = "A Django plugin for pytest." name = "pytest-django" optional = false @@ -856,7 +856,7 @@ dev = ["mock"] category = "dev" description = "pytest-monkeytype: Generate Monkeytype annotations from your pytest tests." name = "pytest-monkeytype" -optional = true +optional = false python-versions = ">=3.6" version = "1.1.0" @@ -876,7 +876,7 @@ version = "2020.1" category = "dev" description = "YAML parser and emitter for Python" name = "pyyaml" -optional = true +optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "5.3.1" @@ -989,7 +989,7 @@ version = "3.7.4.3" category = "dev" description = "Runtime inspection utilities for typing module." name = "typing-inspect" -optional = true +optional = false python-versions = "*" version = "0.6.0" @@ -1061,7 +1061,7 @@ version = "0.15.0" h11 = ">=0.8.1" [[package]] -category = "main" +category = "dev" description = "Backport of pathlib-compatible object wrapper for zip files" marker = "python_version < \"3.8\"" name = "zipp" @@ -1073,12 +1073,8 @@ version = "3.1.0" docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] -[extras] -mypy = [] -neovim = [] - [metadata] -content-hash = "f3647e3e05bf115f671638277f058e46a680b69a23f0589f640868f3590511f9" +content-hash = "460cbb7a2e1554d2be091162147e66c7c2232d8d92afe490b669e705b6139760" lock-version = "1.0" python-versions = "^3.7" diff --git a/pyproject.toml b/pyproject.toml index 8ba5cd9f2..9c0aaf597 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = ["poetry.masonry.api"] [tool.poetry] name = "codex" -version = "0.5.1" +version = "0.5.2" description = "A comic archive web server." license = "GPL-2.0-only" authors = ["AJ Slater "] @@ -45,7 +45,6 @@ watchdog = "^0.10" whitenoise = { extras = ["brotli"], version = "^5.0" } websocket_client = "^0.57.0" comicbox = "^0.1.0" -pytest-django = "^3.9.0" [tool.poetry.dev-dependencies] black = "^19.10b0" @@ -55,23 +54,20 @@ django-debug-toolbar = "^2.2" flake8 = "^3.7" flake8-black = "^0.2.0" flake8-bugbear = "^20.1" +jedi = "^0.17.1" +monkeytype = "^20" +mypy = "^0.782" +neovim = "^0.3.1" pep8-naming = "^0.11.1" pytest = "^6.0" pytest-black = "^0.3.10" pytest-cov = "^2.8" +pytest-django = "^3.9.0" pytest-flake8 = "^1.0" pytest-isort = "^1.1" -pytest-monkeytype = { version = "^1.0", optional = true } -mypy = { version = "^0.782", optional = true } -monkeytype = { version = "^20", optional = true } +pytest-monkeytype = "^1.0" radon = "^4.1" vulture = "^2.0" -neovim = { version = "^0.3.1", optional = true } -jedi = { version = "^0.17.1", optional = true } - -[tool.poetry.extras] -neovim = ["neovim", "jedi"] -mypy = ["mypy", "monkeytype", "pytest-monkeytype"] [tool.poetry.scripts] codex = "codex.run:main"