Repository navigation
Expose resource attribution in the REST API - #654
Merged
bpepple merged 14 commits intoOct 16, 2026
Merged
Conversation
Add a read-only `attribution` list (source label + URL) to the detail responses for arcs, characters, creators, imprints, issues, publishers, series, teams, and universes, so API consumers can credit where descriptive content came from. List responses are unchanged. - Add AttributionSerializer, returning `source` as its display label - Add Arc/Creator/Publisher read serializers for retrieve - Prefetch `attribution` on each retrieve queryset - Bump the parent's `modified` and cache version when an Attribution is saved or deleted, so cached detail responses and Last-Modified stay current - Document the field in api/README.md
Replace the endpoint query-count comparison, which passed with or without the prefetch, with a check that the retrieve queryset returns objects whose attribution is already loaded (zero queries on access).
5 tasks
Add an id tiebreaker to Attribution.Meta.ordering so an object's attributions come back in creation order. Before, Postgres could return them in any order, so the same data could serialize differently after a cache miss. Serialize `source` from a fixed map of English names instead of the translatable choice label. The detail cache key doesn't include the language, so a translated label could be cached and then served to clients in other languages. The values are the ones the README already documents, and the OpenAPI schema now lists them as an enum.
- Look up the parent model via the cached ContentType.objects.get_for_id() instead of the content_type FK, avoiding a query per save/delete. - Skip the post_delete bump when the delete cascades from the parent (detected via the signal's `origin`), avoiding no-op UPDATEs and cache bumps when deleting series, issues, etc. - Ignore raw (loaddata) saves, and record the previous parent in pre_save so reassigning an attribution also bumps the old parent.
- Skip the parent bump for attribution parents without a `modified` field, instead of raising FieldError. - Add attribution_parent_saved_separately() and use it around the attribution formset saves in the create/update views, whose own parent save already bumps `modified` and the cache version. Move the parent save inside the transaction in the update views so it commits together with the attribution changes. - Add AttributionRetrieveMixin to prefetch `attribution` on retrieve, replacing the per-viewset prefetches.
- Redirect directly from AttributionCreateMixin and IssueCreate instead of calling super().form_valid(), which re-ran form.save() and wrote a second history record for every new object. - Bump each parent at most once per attribution queryset delete. - Name the attribution source enum AttributionSourceEnum in the OpenAPI schema rather than the generic SourceEnum.
- Remove AttributionRetrieveMixin: retrieve serializes a single object, so prefetching attribution saved no queries. - Revert attribution_parent_saved_separately(): it saved one cheap UPDATE per formset row but every save path had to remember to use it (the admin inlines didn't), and forgetting was silent. - Fall back to the stored source code in AttributionSerializer instead of raising KeyError on a value outside SOURCE_NAMES.
Record (content_type_id, object_id) in Attribution.from_db() and after each save, and compare against it in post_save to detect reassignment. This replaces the pre_save handler, which ran an extra SELECT before every update of an existing attribution.
AttributionUpdateMixin.form_valid() only called form.save(commit=False) inside transaction.atomic() and left the real save to super().form_valid(), which ran after the block closed. If that save failed, the attribution rows and the parent `modified` bump stayed committed, but the parent edit was lost. Save the form, including its M2M fields, inside the transaction and redirect directly, the same way AttributionCreateMixin does.
Nothing reassigns an attribution to a different parent: the formset and admin inline fix the parent, and move_pub_to_imprint, the only code that moved attributions, has been removed. Remove Attribution.from_db() and loaded_parent, and only bump the current parent on save.
Record the new attribution field on detail endpoints, and the cover_date_range filter from #628, which never got an entry.
Detail keys depend only on pk and `modified`, so responses cached before a shape change, such as the new `attribution` field, were served for up to DETAIL_CACHE_TTL after deploy. Add DETAIL_CACHE_SCHEMA_VERSION to the key so bumping it orphans entries in the old shape.
bpepple
deleted the
651-expose-resource-attribution-source-url-in-the-rest-api
branch
October 16, 2026 14:48
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #651
Summary
Detail responses for arcs, characters, creators, imprints, issues, publishers, series, teams, and universes now include a read-only
attributionlist. Each entry gives the source name and the URL that the descriptive content was adapted from. API consumers that reusedesccan now credit the source, which some licenses require (Wikipedia's CC BY-SA, for example).The list is empty when there's no attribution. List responses and create/update responses are unchanged. This is an additive, non-breaking change.
Changes
AttributionSerializer(sourceandurl), nested read-only on each detail serializer.sourceuses fixed English names, so the output doesn't change with the request's language and can be cached per object. It's declared in the schema asAttributionSourceEnum. Arc, Creator, and Publisher had no separate read serializer, so this addsArcReadSerializer,CreatorReadSerializer, andPublisherReadSerializerand uses them forretrieve.Attributiongets a stable default ordering (migration0060), so the list order is deterministic.post_save/post_deletehandlers onAttributionupdate the parent'smodifiedfield and bump its cache version. Adding, editing, or deleting an attribution (from the web form, an admin inline, or a merge) therefore invalidates the cached detail response and updatesLast-Modified. A queryset delete bumps each parent once, and a cascade delete from the parent is skipped.DETAIL_CACHE_SCHEMA_VERSION(api/cache.py). Without it, responses cached before this deploy, which have noattributionfield, would be served for up toDETAIL_CACHE_TTL(3 days). Bump the version whenever a detail response's shape changes.IssueCreate) no longer save the parent twice.AttributionUpdateMixinnow saves the parent inside the same transaction as the attribution formset, so a failed parent save can't leave attribution changes committed without it.move_pub_to_imprintmanagement command.api/README.mdhas a new Attribution section under Common Patterns, the field is listed for each of the nine resources, and there's a Version 1.12 changelog entry. That entry also covers thecover_date_rangefilter from Add cover_date_range filter to the issue API #628, which never got one.Testing
New
tests/comicsdb/test_api_attribution.py:Attribution.Sourcehas an API name, the schema names the enum, and an unknown stored code falls back to the raw value instead of erroring.If-Modified-Sincereturns 200 after a change, and the parent'smodifiedand cache version are bumped. This also covers queryset deletes (each parent bumped once), cascade deletes, raw fixture loads, and parents without amodifiedfield.Also added: bumping
DETAIL_CACHE_SCHEMA_VERSIONturns a cached hit into a miss, andaudit_response_cachestill categorizes versioned detail keys by model.Full suite passes.
manage.py spectacular --validatereports no errors, and theAttributioncomponent appears in the generated schema.Deployment
The detail cache key version means old cached detail responses are never served again, but they stay in Redis until their TTL expires (up to 3 days). To free that memory right away, run this in
manage.py shellafter deploying:It only deletes old-format keys. The detail cache starts cold after the deploy either way, so expect a short rise in database load while it warms back up.
Known limitations