Skip to content

Experiment with Series#11678

Draft
cdrini wants to merge 7 commits intointernetarchive:masterfrom
cdrini:feature/series3
Draft

Experiment with Series#11678
cdrini wants to merge 7 commits intointernetarchive:masterfrom
cdrini:feature/series3

Conversation

@cdrini
Copy link
Collaborator

@cdrini cdrini commented Jan 7, 2026

Remaining:

  • Failing unit tests
  • Failing accessibility tests
  • Put behind librarian only flag

Technical

Testing

Screenshot

Stakeholders

@mheiman

Copy link

@accesslint accesslint bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are accessibility issues in these changes.

<div class="input ac-input mia__input">
$# detect-missing-i18n-skip-line
<div class="mia__reorder">≡</div>
<input class="ac-input__visible" name="$name_path--$i" type="text" value="$series.name" $cond(readonly, 'readonly', '')>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this element is missing an accessible name or label. That makes it hard for people using screen readers or voice control to use the control.

<div class="mia__reorder">≡</div>
<input class="ac-input__visible" name="$name_path--$i" type="text" value="$series.name" $cond(readonly, 'readonly', '')>
<input class="ac-input__value" name="$dict_path--$i--series--key" type="hidden" value="$series.key" />
<input name="$dict_path--$i--position" value="$series.position" />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this element is missing an accessible name or label. That makes it hard for people using screen readers or voice control to use the control.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds experimental Series support to Open Library, allowing works to be organized into series (e.g., Harry Potter, The Sword of Truth). Series are implemented as a special type of list with additional metadata for position tracking and are stored using the same OL###L identifier scheme as lists.

Key changes include:

  • Series data model integration with works, using bidirectional edges stored on work objects
  • Solr indexing for series fields (series_key, series_name, series_position) to enable search
  • UI components for creating, editing, and displaying series on work/edition pages

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
openlibrary/core/models.py Adds WorkSeriesEdge types, ListSeedMetadata, and series helper methods to Work class
openlibrary/core/lists/model.py Implements Series class extending List, refactors seed metadata handling
openlibrary/core/schema.py Comments out series sequence schema (OL%dS format)
openlibrary/core/processors/readableurls.py Adds URL processor for /series/ paths
openlibrary/solr/updater/work.py Fetches and indexes series data in work Solr documents
openlibrary/solr/updater/list.py Updates list updater to handle series type and list_type field
openlibrary/plugins/openlibrary/lists.py Adds series CRUD endpoints, implements work-series edge synchronization
openlibrary/plugins/upstream/addbook.py Adds series creation support and doc overloads
openlibrary/plugins/upstream/code.py Adds series to editable keys regex
openlibrary/plugins/worksearch/schemes/works.py Adds series fields to work search scheme
openlibrary/plugins/worksearch/schemes/lists.py Updates list search universe to include series
openlibrary/plugins/worksearch/code.py Populates series data in work search results
openlibrary/plugins/worksearch/autocomplete.py Adds series_autocomplete endpoint
openlibrary/templates/type/series/*.html Symlinks to list templates for series views
openlibrary/templates/type/list/view_body.html Updates edit permission logic to allow series editing
openlibrary/templates/type/list/edit.html Adds position field with temporary detection logic
openlibrary/templates/type/edition/title_and_author.html Displays primary series above work title
openlibrary/templates/books/series-autocomplete.html Series autocomplete input component
openlibrary/templates/books/edit.html Adds Work Series fieldset and refactors alert messages
openlibrary/macros/SearchResultsWork.html Displays series info in search results, fixes dict access patterns
openlibrary/plugins/openlibrary/js/index.js Initializes series autocomplete on edit pages
openlibrary/plugins/openlibrary/js/edit.js Implements initSeriesMultiInputAutocomplete function
openlibrary/plugins/openlibrary/code.py Calls register_types() for series type registration
static/css/components/*.less Adds styling for series display
conf/solr/conf/managed-schema.xml Defines series_key, series_name, series_position Solr fields
Makefile Adds series reindexing to reindex-solr target
openlibrary/i18n/messages.pot Adds internationalization strings for series features
openlibrary/catalog/utils/query.py Adds ?_raw=true parameter for list/series queries
openlibrary/tests/solr/updater/test_work.py Updates test helper to accept series parameter
Comments suppressed due to low confidence (1)

openlibrary/plugins/openlibrary/lists.py:444

  • The lists_delete endpoint only handles /lists/ paths but doesn't support /series/ paths. Series should also be deletable. Consider updating the path regex to include series or creating a separate delete endpoint for series.
class lists_delete(delegate.page):
    path = r"((?:/people/[^/]+)?/lists/OL\d+L)/delete"

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +58 to +59
$# How can I detect if this is a series...
$if 1:
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition "$if 1:" with the comment "How can I detect if this is a series..." suggests unfinished logic. This will always show the position field for all lists, not just series. Consider implementing proper series detection or creating a flag to distinguish between list and series types.

Suggested change
$# How can I detect if this is a series...
$if 1:
$# Show position only for series lists, or when an existing seed already has a position.
$ is_series = getattr(lst, 'is_series', False)
$ has_position = bool(jsdef_get(seed, 'position'))
$ show_position = is_series or has_position
$if show_position:

Copilot uses AI. Check for mistakes.
$:render_template("lib/not_logged")
$:note
$if alert_message:
<p class="alert $alert_class">$alert_message</p>
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alert message is now correctly escaped since it's no longer wrapped in HTML tags. However, the message contains HTML (e.g., <b> tags) which was previously safe because it was inside a pre-formatted string. Now with $alert_message, the HTML will be automatically escaped by the template engine. This needs to use $:alert_message for HTML rendering, or the internationalized strings should avoid HTML markup.

Copilot uses AI. Check for mistakes.
Comment on lines +358 to +390
if list_type == 'series':
# Don't save seeds on this record
thing_json['seeds'] = []
# Edit the works to add series edges
work_key_to_list_seed = {
seed['thing']['key']: seed for seed in list_record.get_annotated_seeds()
}
works = cast(list[Work], web.ctx.site.get_many(list(work_key_to_list_seed)))
for work in works:
list_seed = work_key_to_list_seed[work.key]
work_series_edge = work.find_series_edge(list_key)
already_has_series = bool(work_series_edge)

if not work_series_edge:
# Note: The type is actually WorkSeriesEdgeDict, but internally inside infogami
# these behave sort of the same, since a full `Thing` object is replaced with
# a ThingReference (eg `{'key': '/works/OL123W'}`) when saved to the DB.
work_series_edge = cast(
WorkSeriesEdgeDB, {'series': {'key': list_key}}
)

# Update the edge with any metadata from the list seed
update_list_seed_metadata(work_series_edge, list_seed)

if not already_has_series:
if not work.series:
work.series = []

work.series.append(work_series_edge)

# Cast is needed since the infogami code isn't typed yet
records_to_save.append(cast(dict, work.dict()))

Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When editing a series, the code doesn't handle the removal of works from the series. If a work was previously part of the series but is no longer in the list of seeds, the series edge will remain on that work. This could lead to stale series memberships that are difficult to clean up. Consider comparing the current seeds with the new seeds and removing series edges from works that are no longer included.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +30
<div class="series-line">
<a href="$primary_series.series.url()">$primary_series.series.name</a>$cond(primary_series.position, ':')
$if primary_series.position:
$_("Book %s", primary_series.position)
</div>
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing closing </div> tag for the series-line div. The div is opened on line 26 but never closed, which could cause HTML structure issues.

Copilot uses AI. Check for mistakes.
$def with (series, name_path, dict_path, readonly=False)
$# :param list[dict] series: e.g. {name: str, key: str}
$# :param str name_path: form path for the user-visible author name
$# :param str dict_path: form path for the author dict
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 4 incorrectly states "author dict" when this is a series autocomplete template. This should say "series dict" instead.

Suggested change
$# :param str dict_path: form path for the author dict
$# :param str dict_path: form path for the series dict

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,30 @@
$def with (series, name_path, dict_path, readonly=False)
$# :param list[dict] series: e.g. {name: str, key: str}
$# :param str name_path: form path for the user-visible author name
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 3 incorrectly states "user-visible author name" when this template is for series autocomplete. This should say "user-visible series name" instead.

Suggested change
$# :param str name_path: form path for the user-visible author name
$# :param str name_path: form path for the user-visible series name

Copilot uses AI. Check for mistakes.
<div class="mia__reorder">≡</div>
<input class="ac-input__visible" name="$name_path--$i" type="text" value="$series.name" $cond(readonly, 'readonly', '')>
<input class="ac-input__value" name="$dict_path--$i--series--key" type="hidden" value="$series.key" />
<input name="$dict_path--$i--position" value="$series.position" />
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The position input field lacks a type attribute and label for accessibility. It should have type="text" and be associated with a label or have an aria-label for screen reader users.

Suggested change
<input name="$dict_path--$i--position" value="$series.position" />
<input name="$dict_path--$i--position" type="text" value="$series.position" aria-label="$_('Series position')" />

Copilot uses AI. Check for mistakes.

$if series_edge:
<span class="bookseries" itemprop="isPartOf" itemscope itemtype="https://schema.org/Series">
<a href="/series/$series_edge['series']['key']/$urlsafe(series_edge['series'].get('name'))" itemprop="name url">$series_edge['series'].get('name')</a>$cond(series_edge.get('position'), ': ')
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The series key needs to be split to extract just the ID portion before constructing the URL. The key format is '/series/OL123L' but the URL path should be '/series/OL123L/name'. Currently, this will produce URLs like '/series//series/OL123L/name' which is incorrect.

Suggested change
<a href="/series/$series_edge['series']['key']/$urlsafe(series_edge['series'].get('name'))" itemprop="name url">$series_edge['series'].get('name')</a>$cond(series_edge.get('position'), ': ')
$ series_key = series_edge['series']['key'].split('/')[-1]
<a href="/series/$series_key/$urlsafe(series_edge['series'].get('name'))" itemprop="name url">$series_edge['series'].get('name')</a>$cond(series_edge.get('position'), ': ')

Copilot uses AI. Check for mistakes.
]

def get_work_position(position: str | None) -> float:
position_float = 9999.0
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to 'position_float' is unnecessary as it is redefined before this value is used.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +46
if TYPE_CHECKING:
from openlibrary.core.lists.model import Series, SeriesDict

Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'Series' is not used.
Import of 'SeriesDict' is not used.

Suggested change
if TYPE_CHECKING:
from openlibrary.core.lists.model import Series, SeriesDict

Copilot uses AI. Check for mistakes.
Copy link
Member

@mekarpeles mekarpeles left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data flow / approach lgtm, editing should be discussed w/ community


web.ctx.site.save_many(
records_to_save,
action="lists",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

action needs to be updated to something like edit-series when the record is a series.

Ideally, the action would be edit-lists for the list case. Many of the action values throughout the codebase are incorrect or misleading -- I'd like to fix all of those, including this lists action, by a single PR in the near future.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same is true for the transaction that saves a new series. You can probably name the action something like create-series or something.

I wouldn't be surprised if new lists are being saved using action=lists (or worse, create). If so, please ignore the temptation to fix the action.

@RayBB RayBB mentioned this pull request Feb 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants