Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \
Expand Down
5 changes: 5 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
@@ -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

Expand Down
31 changes: 9 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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 <host path to config>/config:/config ajslater/codex
```

## Configure Codex
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions codex/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
12 changes: 7 additions & 5 deletions codex/librarian/librariand.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Library process worker for background tasks."""
import logging
import platform
import time

from multiprocessing import Pool
Expand Down Expand Up @@ -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():
Expand Down
4 changes: 1 addition & 3 deletions codex/serializers/browse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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


Expand All @@ -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):
Expand Down
13 changes: 13 additions & 0 deletions codex/templates/admin/codex/adminflag/change_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends "admin/change_list.html" %}

{% block footer %}
<div id="footer">
<h2>Admin Flags</h2>
Administration Flags that effect the operation of the entire site.
<h3>Enable Registration</h3>
By default users bookmarks and preferenrces are saved in an anonymous browser session. But users can create a username and password to save their bookmarks in between browsers. You may disable this feature.

<h3>Enable Folder View</h3>
By default, codex provides a "Folder View" which mimics the directory heirarchy of the libraries that you've added to Codex. You may disable this feature. The database style browser view is always available.
</div>
{% endblock %}
26 changes: 26 additions & 0 deletions codex/templates/admin/codex/library/change_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% extends "admin/change_list.html" %}

{% block footer %}
<div id="footer">
<h2>Libraries</h2>
<p>
A Library is a path where Codex can recursively search for comic archives. Codex will try to import comics from a Library immediately.
</p><p>
You may navigate back to the browse view by clicking the "view site" link in the upper right of this Admin Panel.
</p><p>
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.
</p>
<h3>Actions</h3>
You may manually scan a library for changes. Forcibly re-import everything in a library or delete a library.
<h3>Enable Watch</h3>
Codex watches the filesystem for changes to the libaray and imports new comics, re-imports changed comics and deletes missing comics. You may disable this feature for each Library.
<h3>Enable Scan Cron</h3>
Codex periodically scans each library for new, changed or missing comics. You may disable this feature for each Library.
<h3>Scan Frequency</h3>
This field controls how long Codex waits between automatic scans.
<h3>Last Scan</h3>
A timestamp telling you when the last time this library was scanned.
<h3>Scan In Progress</h3>
This is checked by Codex when a scan is run to prevent multiple scans running on a Library at the same time. In unusual circumstances it can be left checked even though no scan is running. You can see scans running on the console output. You may manually uncheck this to unblock scanning if it was left checked improperly.
</div>
{% endblock %}
6 changes: 6 additions & 0 deletions codex/urls_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,6 +38,11 @@
#
# Browser
path("browse/<str:group>/<int:pk>", BrowseView.as_view(), name="browse_objects",),
path(
"browse/<str:group>/<int:pk>/choices/<str:choice_type>",
BrowseChoiceView.as_view(),
name="browse_choices",
),
path(
"browse/<str:browse_type>/<int:pk>/mark_read",
UserBookmarkFinishedView.as_view(),
Expand Down
115 changes: 115 additions & 0 deletions codex/views/browse_base.py
Original file line number Diff line number Diff line change
@@ -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
43 changes: 43 additions & 0 deletions codex/views/browse_choices.py
Original file line number Diff line number Diff line change
@@ -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)
Loading