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
4 changes: 3 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ border-radius: 128px;
- Features
- Series years, status and summary in the metadata pane.
- Browser title shows series years and a Tags button.
- Tag editor edits the MetronInfo series start year.
- Tag editor edits series years, status and summary. End year, status and
summary are saved in Codex only.
- Online tag match picker shows series and issue details.

## v2.5.5

Expand Down
2 changes: 2 additions & 0 deletions codex/choices/choices_to_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
IDENTIFIER_TYPE_BY_CODEX_NAME,
IDENTIFIER_TYPES,
LANGUAGES,
SERIES_STATUSES,
)

_DEFAULTS = MappingProxyType(
Expand Down Expand Up @@ -81,6 +82,7 @@
"reader-defaults.json": READER_DEFAULTS,
"reader-map.json": READER_CHOICES,
"search-map.json": SEARCH_FIELDS,
"series-statuses.json": SERIES_STATUSES,
"tagging-estimate.json": TAGGING_ESTIMATE,
}
)
Expand Down
8 changes: 8 additions & 0 deletions codex/choices/tagging.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from comicbox.formats.metron_info.transform import MetronInfoTransform

from codex.choices.reader import READER_CHOICES
from codex.models.choices import SeriesStatusChoices
from codex.models.identifier import IdentifierType, to_comicbox_id_type

# comicbox transform per codex write-format id.
Expand Down Expand Up @@ -84,6 +85,13 @@ def _vuetify_choices(pairs: Iterable[tuple[str, str]]) -> tuple[MappingProxyType
{id_type.value: to_comicbox_id_type(id_type.value) for id_type in IdentifierType}
)

# A series' publication status (Ongoing, Completed, ...). No format codex
# writes holds it, so the editor saves it to the database only and offers
# it under every format.
SERIES_STATUSES = _vuetify_choices(
(str(label), str(value)) for value, label in SeriesStatusChoices.choices
)


# ---------------------------------------------------------------------------
# Per-format field support, derived from the comicbox transforms
Expand Down
22 changes: 22 additions & 0 deletions codex/serializers/admin/tagging.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from codex.librarian.onlinetag.credential_validator import KNOWN_SOURCES
from codex.models import ComicboxTaggingDefaults
from codex.models.choices import SeriesStatusChoices
from codex.serializers.models.base import BaseModelSerializer


Expand Down Expand Up @@ -55,6 +56,27 @@ class TagWriteRequestSerializer(Serializer):
rename = BooleanField(required=False, default=None)


class SeriesAttrsRequestSerializer(Serializer):
"""
A series' Codex-only attributes, edited from the tag editor.

No format codex writes holds a series' end year, status or summary, so
these go straight to the Series row. Only the keys sent change; null or
"" clears one.
"""

collection = CharField()
pks = ListField(child=CharField())
# MetronInfo's StartYear bounds, so the pair stays comparable.
end_year = IntegerField(
required=False, allow_null=True, min_value=1000, max_value=9999
)
status = ChoiceField(
choices=SeriesStatusChoices.choices, required=False, allow_blank=True
)
summary = CharField(required=False, allow_blank=True)


class OnlineTagStartSerializer(Serializer):
"""Serializer for starting an online tagging session."""

Expand Down
6 changes: 6 additions & 0 deletions codex/urls/api/v4/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
from codex.views.admin.tagging_validate import AdminTaggingValidateView
from codex.views.admin.tagwrite import (
AdminParseIdentifierURLView,
AdminSeriesAttrsView,
AdminTagWritePreflightView,
AdminTagWriteView,
)
Expand Down Expand Up @@ -323,6 +324,11 @@
AdminTagWriteView.as_view(),
name="tag_write",
),
path(
"series-attrs",
AdminSeriesAttrsView.as_view(),
name="series_attrs",
),
path(
"folders",
AdminFolderListView.as_view(),
Expand Down
45 changes: 43 additions & 2 deletions codex/views/admin/tagwrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
from functools import cached_property
from pathlib import Path
from types import MappingProxyType
from typing import override
from typing import Final, override

from django.utils import timezone
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
from rest_framework.status import HTTP_202_ACCEPTED
Expand All @@ -19,15 +20,21 @@
)
from codex.librarian.scribe.tasks import BulkTagWriteTask
from codex.models.admin import ComicboxTaggingDefaults
from codex.models.collections import Series
from codex.models.comic import Comic
from codex.serializers.admin.tagging import TagWriteRequestSerializer
from codex.serializers.admin.tagging import (
SeriesAttrsRequestSerializer,
TagWriteRequestSerializer,
)
from codex.views.admin.auth import AdminAPIView
from codex.views.admin.identifier_parse import parse_identifier_url
from codex.views.browser.filters.filter import BrowserFilterView

#: Cap on how many per-comic rename previews the preflight builds — each opens
#: an archive, so a huge multi-selection can't stall the request.
_FILENAME_PREVIEW_LIMIT = 100
#: Series attributes the tag editor saves to the database only.
_SERIES_ATTRS: Final = ("end_year", "status", "summary")


class FilteredComicPksView(BrowserFilterView):
Expand Down Expand Up @@ -248,3 +255,37 @@ def post(self, request):
},
status=HTTP_202_ACCEPTED,
)


class AdminSeriesAttrsView(FilteredComicPksView):
"""
POST a series' end year, status or summary straight to the database.

No format codex writes can hold them, so unlike a tag write nothing
touches the comic files. Re-imports leave the values standing, because
the importer ignores a series attribute a file doesn't carry.
"""

def post(self, request):
"""Update the one series the selected comics belong to."""
serializer = SeriesAttrsRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
attrs = {key: data[key] for key in _SERIES_ATTRS if key in data}
if not attrs:
return Response({"detail": "No series attributes sent."}, status=400)

comic_pks = self.resolve_comic_pks(data["collection"], data["pks"])
if not comic_pks:
return Response({"detail": "No comics matched."}, status=400)
series_pks = tuple(
Series.objects.filter(comic__pk__in=comic_pks)
.values_list("pk", flat=True)
.distinct()
)
if len(series_pks) != 1:
return Response({"detail": "Edit one series at a time."}, status=400)

series_pk = series_pks[0]
Series.objects.filter(pk=series_pk).update(**attrs, updated_at=timezone.now())
return Response({"detail": "Series details saved.", "series": series_pk})
Loading