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
28 changes: 28 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ Most resources follow a standard pattern:
- **Create:** `POST /api/{resource}/` - Create new item (requires editor or admin)
- **Update:** `PUT/PATCH /api/{resource}/{id}/` - Update item (requires editor or admin)

### Attribution

Detail responses for arcs, characters, creators, imprints, issues, publishers, series, teams, and
universes include a read-only `attribution` list naming where descriptive content (such as `desc`)
was adapted from. If you reuse that content, credit these sources, since some licenses require it
(Wikipedia's CC BY-SA, for example). List responses don't include it.

```json
"attribution": [
{"source": "Wikipedia", "url": "https://en.wikipedia.org/wiki/Spider-Man"},
{"source": "Marvel", "url": "https://www.marvel.com/characters/spider-man-peter-parker"}
]
```

### Nested Resources

Some resources include nested actions:
Expand Down Expand Up @@ -175,6 +189,7 @@ Story arcs that span multiple issues, often across different series.
- `cv_id` - Comic Vine ID
- `gcd_id` - Grand Comics Database ID
- `resource_url` - Link to web UI
- `attribution` - Sources the descriptive content was adapted from, as a list of `{"source", "url"}` objects (empty if none). `source` is one of `Wikipedia`, `Marvel`, `DC`, or `Grand Comics Database`

**Example:**

Expand Down Expand Up @@ -232,6 +247,7 @@ Comic book characters appearing in issues.
- `cv_id` - Comic Vine ID
- `gcd_id` - Grand Comics Database ID
- `resource_url` - Link to web UI
- `attribution` - Sources the descriptive content was adapted from, as a list of `{"source", "url"}` objects (empty if none). `source` is one of `Wikipedia`, `Marvel`, `DC`, or `Grand Comics Database`

**Example:**

Expand Down Expand Up @@ -286,6 +302,7 @@ Writers, artists, and other comic book creators.
- `cv_id` - Comic Vine ID
- `gcd_id` - Grand Comics Database ID
- `resource_url` - Link to web UI
- `attribution` - Sources the descriptive content was adapted from, as a list of `{"source", "url"}` objects (empty if none). `source` is one of `Wikipedia`, `Marvel`, `DC`, or `Grand Comics Database`

**Example:**

Expand Down Expand Up @@ -426,6 +443,7 @@ The Issue endpoint supports the most comprehensive filtering options:
- `cover_hash` - Perceptual image hash
- `foc_date` - Final order cutoff date
- `resource_url` - Link to web UI
- `attribution` - Sources the descriptive content was adapted from, as a list of `{"source", "url"}` objects (empty if none). `source` is one of `Wikipedia`, `Marvel`, `DC`, or `Grand Comics Database`

**Cover Price (Write):**

Expand Down Expand Up @@ -535,6 +553,7 @@ Comic book publishers.
- `cv_id` - Comic Vine ID
- `gcd_id` - Grand Comics Database ID
- `resource_url` - Link to web UI
- `attribution` - Sources the descriptive content was adapted from, as a list of `{"source", "url"}` objects (empty if none). `source` is one of `Wikipedia`, `Marvel`, `DC`, or `Grand Comics Database`

**Country (Write):**

Expand Down Expand Up @@ -599,6 +618,7 @@ Publisher imprints (e.g., Vertigo, MAX).
- `cv_id` - Comic Vine ID
- `gcd_id` - Grand Comics Database ID
- `resource_url` - Link to web UI
- `attribution` - Sources the descriptive content was adapted from, as a list of `{"source", "url"}` objects (empty if none). `source` is one of `Wikipedia`, `Marvel`, `DC`, or `Grand Comics Database`

**Example:**

Expand Down Expand Up @@ -708,6 +728,7 @@ Comic book series.
- `genres` - Array of genre objects
- `associated` - Associated series
- `resource_url` - Link to web UI
- `attribution` - Sources the descriptive content was adapted from, as a list of `{"source", "url"}` objects (empty if none). `source` is one of `Wikipedia`, `Marvel`, `DC`, or `Grand Comics Database`

**Series Status Choices:**

Expand Down Expand Up @@ -813,6 +834,7 @@ Superhero teams and groups.
- `cv_id` - Comic Vine ID
- `gcd_id` - Grand Comics Database ID
- `resource_url` - Link to web UI
- `attribution` - Sources the descriptive content was adapted from, as a list of `{"source", "url"}` objects (empty if none). `source` is one of `Wikipedia`, `Marvel`, `DC`, or `Grand Comics Database`

**Example:**

Expand Down Expand Up @@ -864,6 +886,7 @@ Comic book universes (e.g., Earth-616, Earth-1).
- `desc` - Description
- `image` - Universe image URL
- `resource_url` - Link to web UI
- `attribution` - Sources the descriptive content was adapted from, as a list of `{"source", "url"}` objects (empty if none). `source` is one of `Wikipedia`, `Marvel`, `DC`, or `Grand Comics Database`

**Example:**

Expand Down Expand Up @@ -2548,6 +2571,11 @@ For questions, issues, or feature requests:

## Changelog

### Version 1.12

- Added read-only `attribution` field to the Arc, Character, Creator, Imprint, Issue, Publisher, Series, Team, and Universe detail endpoints: a list of `{"source", "url"}` objects naming where descriptive content was adapted from
- Added `cover_date_range_after` and `cover_date_range_before` filters to the Issue endpoint

### Version 1.11

- Added `publisher` (object with `id`/`name`) and `series_type` (object with `id`/`name`) fields to the Series list endpoint
Expand Down
11 changes: 10 additions & 1 deletion api/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@

DETAIL_CACHE_TTL = 60 * 60 * 24 * 3 # 3 days; live keys self-invalidate on write.
LIST_CACHE_TTL = 60 * 2 # 2min; bounds staleness from nested-object changes we don't chase.
# Bump when a detail response's shape changes (e.g. a field is added), so entries cached in the
# old shape aren't served for up to DETAIL_CACHE_TTL after deploy. List keys don't need this:
# LIST_CACHE_TTL is short enough to ride out.
DETAIL_CACHE_SCHEMA_VERSION = 2

_VERSION_KEY_PREFIX = "cachever"

Expand Down Expand Up @@ -112,7 +116,12 @@ def detail_cache_key(

`request` supplies origin and query params -- see `_request_digest()`.
"""
key = f"api:detail:{model_label}:{action}:{pk}:{modified.timestamp()}"
# The schema version goes after the model label, which audit_response_cache
# reads as the key's category.
key = (
f"api:detail:{model_label}:v{DETAIL_CACHE_SCHEMA_VERSION}:{action}:{pk}:"
f"{modified.timestamp()}"
)
if dependent_labels:
version_map = get_model_versions(dependent_labels)
versions = "-".join(str(version_map[lbl]) for lbl in dependent_labels)
Expand Down
9 changes: 8 additions & 1 deletion api/v1_0/serializers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
from api.v1_0.serializers.publisher import ( # noqa: I001
BasicPublisherSerializer,
PublisherListSerializer,
PublisherReadSerializer,
PublisherSerializer,
)
from api.v1_0.serializers.arc import ArcListSerializer, ArcSerializer
from api.v1_0.serializers.attribution import AttributionSerializer
from api.v1_0.serializers.arc import ArcListSerializer, ArcReadSerializer, ArcSerializer
from api.v1_0.serializers.character import (
CharacterListSerializer,
CharacterReadSerializer,
CharacterSerializer,
)
from api.v1_0.serializers.creator import (
CreatorListSerializer,
CreatorReadSerializer,
CreatorSerializer,
CreditReadSerializer,
CreditSerializer,
Expand Down Expand Up @@ -65,8 +68,10 @@

__all__ = [
"ArcListSerializer",
"ArcReadSerializer",
"ArcSerializer",
"AssociatedSeriesSerializer",
"AttributionSerializer",
"BasicImprintSerializer",
"BasicPublisherSerializer",
"CharacterListSerializer",
Expand All @@ -77,6 +82,7 @@
"CollectionRatingUpdateSerializer",
"CollectionReadSerializer",
"CreatorListSerializer",
"CreatorReadSerializer",
"CreatorSerializer",
"CreditReadSerializer",
"CreditSerializer",
Expand All @@ -92,6 +98,7 @@
"MissingIssueSerializer",
"MissingSeriesSerializer",
"PublisherListSerializer",
"PublisherReadSerializer",
"PublisherSerializer",
"RatingSerializer",
"ReadingListItemSerializer",
Expand Down
8 changes: 8 additions & 0 deletions api/v1_0/serializers/arc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from rest_framework import serializers

from api.v1_0.serializers.attribution import AttributionSerializer
from comicsdb.models import Arc


Expand Down Expand Up @@ -27,3 +28,10 @@ def update(self, instance: Arc, validated_data):
class Meta:
model = Arc
fields = ("id", "name", "desc", "image", "cv_id", "gcd_id", "resource_url", "modified")


class ArcReadSerializer(ArcSerializer):
attribution = AttributionSerializer(many=True, read_only=True)

class Meta(ArcSerializer.Meta):
fields = (*ArcSerializer.Meta.fields, "attribution")
28 changes: 28 additions & 0 deletions api/v1_0/serializers/attribution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers

from comicsdb.models import Attribution

# Fixed, untranslated names so API output doesn't vary with the request's language (and can be
# cached per object), unlike the model's translatable choice labels.
SOURCE_NAMES = {
Attribution.Source.MARVEL: "Marvel",
Attribution.Source.WIKIPEDIA: "Wikipedia",
Attribution.Source.GCD: "Grand Comics Database",
Attribution.Source.DC: "DC",
}
# Referenced by SPECTACULAR_SETTINGS["ENUM_NAME_OVERRIDES"].
SOURCE_CHOICES = list(SOURCE_NAMES.values())


class AttributionSerializer(serializers.ModelSerializer):
source = serializers.SerializerMethodField()

class Meta:
model = Attribution
fields = ("source", "url")

@extend_schema_field(serializers.ChoiceField(choices=SOURCE_CHOICES))
def get_source(self, obj: Attribution) -> str:
# Fall back to the stored code rather than 500 on a value outside the choices.
return SOURCE_NAMES.get(obj.source, obj.source)
5 changes: 5 additions & 0 deletions api/v1_0/serializers/character.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from rest_framework import serializers

from api.v1_0.serializers.attribution import AttributionSerializer
from api.v1_0.serializers.creator import CreatorListSerializer
from api.v1_0.serializers.team import TeamListSerializer
from api.v1_0.serializers.universe import UniverseListSerializer
Expand Down Expand Up @@ -77,3 +78,7 @@ class CharacterReadSerializer(CharacterSerializer):
creators = CreatorListSerializer(many=True, read_only=True)
teams = TeamListSerializer(many=True, read_only=True)
universes = UniverseListSerializer(many=True, read_only=True)
attribution = AttributionSerializer(many=True, read_only=True)

class Meta(CharacterSerializer.Meta):
fields = (*CharacterSerializer.Meta.fields, "attribution")
8 changes: 8 additions & 0 deletions api/v1_0/serializers/creator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from rest_framework import serializers

from api.v1_0.serializers.attribution import AttributionSerializer
from comicsdb.models import Creator, Credits, Role


Expand Down Expand Up @@ -41,6 +42,13 @@ class Meta:
)


class CreatorReadSerializer(CreatorSerializer):
attribution = AttributionSerializer(many=True, read_only=True)

class Meta(CreatorSerializer.Meta):
fields = (*CreatorSerializer.Meta.fields, "attribution")


class RoleSerializer(serializers.ModelSerializer):
class Meta:
model = Role
Expand Down
5 changes: 5 additions & 0 deletions api/v1_0/serializers/imprint.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from rest_framework import serializers

from api.v1_0.serializers import BasicPublisherSerializer
from api.v1_0.serializers.attribution import AttributionSerializer
from comicsdb.models import Imprint


Expand Down Expand Up @@ -40,6 +41,10 @@ class Meta:

class ImprintReadSerializer(ImprintSerializer):
publisher = BasicPublisherSerializer(read_only=True)
attribution = AttributionSerializer(many=True, read_only=True)

class Meta(ImprintSerializer.Meta):
fields = (*ImprintSerializer.Meta.fields, "attribution")


class BasicImprintSerializer(serializers.ModelSerializer):
Expand Down
3 changes: 3 additions & 0 deletions api/v1_0/serializers/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from api.v1_0.serializers import CreditReadSerializer
from api.v1_0.serializers.arc import ArcListSerializer
from api.v1_0.serializers.attribution import AttributionSerializer
from api.v1_0.serializers.character import CharacterListSerializer
from api.v1_0.serializers.genre import GenreSerializer
from api.v1_0.serializers.imprint import BasicImprintSerializer
Expand Down Expand Up @@ -331,6 +332,7 @@ class IssueReadSerializer(serializers.ModelSerializer):
series = IssueSeriesSerializer(read_only=True)
reprints = ReprintSerializer(many=True, read_only=True)
rating = RatingSerializer(read_only=True)
attribution = AttributionSerializer(many=True, read_only=True)
average_rating = serializers.DecimalField(
max_digits=3, decimal_places=2, coerce_to_string=False, read_only=True
)
Expand Down Expand Up @@ -384,4 +386,5 @@ class Meta:
"gcd_id",
"resource_url",
"modified",
"attribution",
)
8 changes: 8 additions & 0 deletions api/v1_0/serializers/publisher.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django_countries.serializers import CountryFieldMixin
from rest_framework import serializers

from api.v1_0.serializers.attribution import AttributionSerializer
from comicsdb.models import Publisher


Expand Down Expand Up @@ -42,6 +43,13 @@ class Meta:
)


class PublisherReadSerializer(PublisherSerializer):
attribution = AttributionSerializer(many=True, read_only=True)

class Meta(PublisherSerializer.Meta):
fields = (*PublisherSerializer.Meta.fields, "attribution")


class BasicPublisherSerializer(serializers.ModelSerializer):
class Meta:
model = Publisher
Expand Down
5 changes: 5 additions & 0 deletions api/v1_0/serializers/series.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from rest_framework import serializers

from api.v1_0.serializers.attribution import AttributionSerializer
from api.v1_0.serializers.genre import GenreSerializer
from api.v1_0.serializers.imprint import BasicImprintSerializer
from api.v1_0.serializers.publisher import BasicPublisherSerializer
Expand Down Expand Up @@ -116,3 +117,7 @@ class SeriesReadSerializer(SeriesSerializer):
issue_count = serializers.IntegerField(source="num_issues", read_only=True)
associated = AssociatedSeriesSerializer(many=True, read_only=True)
genres = GenreSerializer(many=True, read_only=True)
attribution = AttributionSerializer(many=True, read_only=True)

class Meta(SeriesSerializer.Meta):
fields = (*SeriesSerializer.Meta.fields, "attribution")
5 changes: 5 additions & 0 deletions api/v1_0/serializers/team.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from rest_framework import serializers

from api.v1_0.serializers.attribution import AttributionSerializer
from api.v1_0.serializers.creator import CreatorListSerializer
from api.v1_0.serializers.universe import UniverseListSerializer
from comicsdb.models import Team
Expand Down Expand Up @@ -67,3 +68,7 @@ class Meta:
class TeamReadSerializer(TeamSerializer):
creators = CreatorListSerializer(many=True, read_only=True)
universes = UniverseListSerializer(many=True, read_only=True)
attribution = AttributionSerializer(many=True, read_only=True)

class Meta(TeamSerializer.Meta):
fields = (*TeamSerializer.Meta.fields, "attribution")
5 changes: 5 additions & 0 deletions api/v1_0/serializers/universe.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from rest_framework import serializers

from api.v1_0.serializers import BasicPublisherSerializer
from api.v1_0.serializers.attribution import AttributionSerializer
from comicsdb.models import Universe


Expand Down Expand Up @@ -42,3 +43,7 @@ class Meta:

class UniverseReadSerializer(UniverseSerializer):
publisher = BasicPublisherSerializer(read_only=True)
attribution = AttributionSerializer(many=True, read_only=True)

class Meta(UniverseSerializer.Meta):
fields = (*UniverseSerializer.Meta.fields, "attribution")
Loading
Loading