Conversation
| <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', '')> |
There was a problem hiding this comment.
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" /> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| $# How can I detect if this is a series... | ||
| $if 1: |
There was a problem hiding this comment.
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.
| $# 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: |
| $:render_template("lib/not_logged") | ||
| $:note | ||
| $if alert_message: | ||
| <p class="alert $alert_class">$alert_message</p> |
There was a problem hiding this comment.
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.
| 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())) | ||
|
|
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
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.
| $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 |
There was a problem hiding this comment.
The comment on line 4 incorrectly states "author dict" when this is a series autocomplete template. This should say "series dict" instead.
| $# :param str dict_path: form path for the author dict | |
| $# :param str dict_path: form path for the series dict |
| @@ -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 | |||
There was a problem hiding this comment.
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.
| $# :param str name_path: form path for the user-visible author name | |
| $# :param str name_path: form path for the user-visible series name |
| <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" /> |
There was a problem hiding this comment.
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.
| <input name="$dict_path--$i--position" value="$series.position" /> | |
| <input name="$dict_path--$i--position" type="text" value="$series.position" aria-label="$_('Series position')" /> |
|
|
||
| $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'), ': ') |
There was a problem hiding this comment.
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.
| <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'), ': ') |
| if TYPE_CHECKING: | ||
| from openlibrary.core.lists.model import Series, SeriesDict | ||
|
|
There was a problem hiding this comment.
Import of 'Series' is not used.
Import of 'SeriesDict' is not used.
| if TYPE_CHECKING: | |
| from openlibrary.core.lists.model import Series, SeriesDict |
mekarpeles
left a comment
There was a problem hiding this comment.
data flow / approach lgtm, editing should be discussed w/ community
|
|
||
| web.ctx.site.save_many( | ||
| records_to_save, | ||
| action="lists", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Remaining:
Technical
Testing
Screenshot
Stakeholders
@mheiman