feat: show alternatives banner on unavailable book pages - #12910
Conversation
7fe996d to
d74ca35
Compare
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR improves in-page navigation and availability messaging on edition pages by enabling smooth scrolling, adjusting anchor offsets, and surfacing lending-state-driven banners/sections.
Changes:
- Add
get_lending_stateto resolve user-facing lending/availability state (with new tests) and expose it to templates. - Update edition templates/macros to pass lending state into
EditionNavBarand reposition the Related Books carousel as a tab section target. - Improve scrolling UX (smooth scroll + updated
scroll-padding-top) and adjust carousel layout CSS; update pending-action banner rendering & i18n strings.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| static/css/page-book.css | Adjusts scroll padding offsets for anchor navigation across breakpoints. |
| static/css/components/work.css | Tweaks carousel container margin for related books layout. |
| static/css/base/common.css | Enables smooth scrolling with reduced-motion fallback. |
| openlibrary/core/lending.py | Adds get_lending_state helper for templates and other call sites. |
| openlibrary/tests/core/test_lending.py | Adds test coverage for get_lending_state. |
| openlibrary/plugins/openlibrary/code.py | Exposes get_lending_state as a template global. |
| openlibrary/templates/type/edition/view.html | Computes lending state once and wires it into EditionNavBar; relocates Related Books section. |
| openlibrary/macros/EditionNavBar.html | Adds conditional availability banner linking to related books. |
| openlibrary/templates/account/view.html | Switches pending-action banner to <ol-banner> output. |
| openlibrary/plugins/upstream/mybooks.py | Refactors translated pending-action message to use placeholders around an HTML link. |
| openlibrary/i18n/messages.pot | Updates extracted strings and source references for i18n. |
| AGENTS.md | Documents i18n guidance for link placeholders inside a single translatable string. |
c6c6a3f to
d74ca35
Compare
|
Note to self, a follow up PR on ol banner has to be merged first with some additions to be made here, do not merge this before that. |
|
@RayBB need your inputs on the extracted get_lending_state function (it is slightly drifted) and about the urgency of migrating LoanStatus macro to utilize this. Drift: |
lokesh
left a comment
There was a problem hiding this comment.
@Sadashii
Can you check on the following issues:
1 ) Persistence working correctly when X clicked in banner
Two distinct dismissal paths existed and only one survives the swap:
- Click-through (clicking "Continue"): the inline script at account/view.html:31-33 clears the pending_action cookie. β
Still works β .pending-action-link is inside the server-rendered message HTML,
captured into ol-banner's light-DOM _content, so the selector still matches. - Explicit dismiss (X button): OLD site/banner with cookie_duration_days=1 β legacy dismiss β /hide_banner sets pending_action=1 β server stops rendering for a day. NEW ol-banner fires
ol-banner-dismiss with empty id β listener returns early β no cookie written β the original pending_action JSON cookie is untouched β banner re-renders on the next page load. β Regression.
2 ) Analytics double-fire on the related-books carousel
The ping is emitted per-carousel, but the experiment wants per-section discovery. Two carousels under one #related-work-carousel anchor β two pings.
Impact β worse than a flat 2Γ: Both placeholders live adjacent in .related-books with rootMargin: '200px', so they intersect near-simultaneously and almost always both fire. But the multiplier is
inconsistent:
- work with subjects + authors β 2 pings
- work with only one β 1 ping
So FromBanner / ScrolledDownAvailable / ScrolledDownUnavailable are inflated by a variable factor that correlates with the book's metadata richness. That corrupts not just absolute counts but the
banner-vs-natural ratio the experiment exists to measure (richer books may skew toward one cohort).
Fix: track once per section. Simplest is a module-scoped guard:
// top of module
let relatedBooksTracked = false;
// in the block
if (!relatedBooksTracked && (config.key === 'related-subjects-carousel' || config.key === 'related-authors-carousel')) {
relatedBooksTracked = true;
...
}
** 3 ) Broken dismiss-tracking selector **
Root cause: class renamed across the component swap. querySelector('.page-banner--dismissable-close') now returns null, the if (dismiss) guard is false, and the PreserveIntent|Dismiss attribute is
never attached. Dismiss analytics silently lost.
Fix: stop trying to decorate the button. Listen for the component's own event (it bubbles, composed: true) and send the ping directly:
container.addEventListener('ol-banner-dismiss', function() {
if (window.archive_analytics && window.archive_analytics.ol_send_event_ping) {
window.archive_analytics.ol_send_event_ping({ category: 'PreserveIntent', action: 'Dismiss' });
}
});
|
@Sadashii I'm not super familiar with In terms of the loanstatus macro. If you mean converting it to Jinja, we certainly could but I wouldn't let you block on it. I think it's moderately difficult compared to the other templates and we haven't converted anything like that yet. Let me know how I can be helpful! |
|
Thanks for addressing the previous comments. Here are some additional items that came up in the automatic review that need a look over:
|
|
@Sadashii I see what you're asking. Since basically all of openlibrary/macros/EditionNavBar.html is being touched, and it doesn't make any network calls I would love if you converted it to Jinja! That would be a great step in the right direction and good to get more people understanding it. #13077 has the most recent example of how we did it (you might need to merge master or rebase). Summary is you should be able to:
Don't spent too much time on it if you get stuck but it would be very easy for AI to convert that particular macro. What do you think? |
Sounds good, will update this and LoanStatus macro to jinja in a seperate PR. Had Antigravity take a look and should be a clean change. |
|
One question and one comment.
In the future we might want to consider a lighter contextual treatment. For example, an empty-state near the Read/Borrow CTA "Not available to read on OL - see similar available books" One more thing: I just noticed that the "You might also like" carousel only includes books that are available to read. Which is surprising, though not necessarily a bad thing. But we should at minimum update the heading to something like "Available books like this one" |
|
β¦hive#12743) Recommend alternative books via a warning banner when a book edition is unavailable (preview_only, checked out, waitlist, or locate physical states). To avoid code duplication and keep template markup clean, the availability state resolution is extracted to Python, and the responsive banner logic is encapsulated inside the EditionNavBar component. - Add `get_lending_state` helper to lending.py and register it as template global. - Update EditionNavBar.html to conditionally render the banner wrapped in responsive divs, avoiding display-override conflicts. - Delegate banner logic in view.html to EditionNavBar and clean up unused data-workid carousel attribute. - Refactor get_pending_action_banner in mybooks.py and update AGENTS.md to follow i18n best practices (unified strings with format placeholders). - Add unit tests for get_lending_state to test_lending.py.
β¦get_lending_state
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
β¦er tracking attribution
β¦dules Concurrently building 'js', 'css', and 'components' targets in parallel via concurrently causes a race condition because each target lists node_modules as a prerequisite in the Makefile, which executes npm ci when the directory is out of date. On fresh checkout on GitHub Actions, package-lock.json and package.json timestamps are newer than the restored node_modules cache, leading all parallel processes to concurrently run npm ci (which deletes and reinstalls node_modules), causing arbitrary MODULE_NOT_FOUND errors. Touching node_modules right after cache restore/install updates its modification timestamp to the current time, making it newer than package-lock.json. This tells make that the directory is already up-to-date, preventing concurrent npm ci execution.
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
β¦rk/db calls - Refactor get_lending_state in lending.py to prioritize cheap checks and run db queries lazily. - Compute lending state once at the top-level book page and pass it down to EditionNavBar, databarWork, and edition-sort. - Refactor LoanStatus macro to accept pre-computed lending_state, skipping get_lending_state and lazy loading loans/waitlists only when needed. - Remove unused variables (my_turn_to_borrow) and cleanup dead code. - Update RelatedWorksCarousel header to 'Available books like this one' to match search query filtering.
for more information, see https://pre-commit.ci
β¦rebase conflict resolution A manual conflict resolution while rebasing onto master reordered dict entries in setup_template_globals(), which caused a later commit's removal of the get_lending_state template global (superseded by precomputed lending_state) to silently not reapply. Also includes ruff import-order autofixes surfaced by the rebase.
79439e7 to
0c878c4
Compare
|
π This branch was rebased onto current Why the conflict happened Your branch had drifted 17 commits behind There was also a small One real (non-generated) conflict: Tips to avoid this proactively:
No action needed on your end β the branch is up to date with β PAM (Open Library's Project AI Manager) |
for more information, see https://pre-commit.ci
openlibrary-bot
left a comment
There was a problem hiding this comment.
Follow-up review after the rebase (all prior review threads are marked resolved β I independently re-verified each against the current code):
- β
get_book_provider(doc).short_namefix inlending.pyβ confirmed, calls the function and reads the attribute off the returned provider, not off the function itself. - β
waiting_loannow drivesis_waitingβ"waitlist"β confirmed used, not dead. - β
test_get_lending_state_partnermocksshort_name, matching the corrected implementation. - β
No
!importantremains anywhere in this PR's CSS diff. - β
lazy-carousel.jsmatchesdata-ol-link-track="OpenRelatedBooks|BannerClick"specifically (fixes nav-bar-click misattribution) and guards on a module-scopedrelatedBooksTrackedflag (fixes the double-fire). - β
account/view.html'sol-banner-dismisshandler unconditionally clears thepending_actioncookie β the old "empty id β early return, cookie never cleared" regression is gone. - β
banner-analytics.jslistens forol-banner-dismissdirectly (e.detail.dismissId || e.target.id) instead of the renamed.page-banner--dismissable-closeselector, and is wired up viainitOlBannerDismissals()injs/index.jsgated onol-banner[dismissible]. - β
Single
get_lending_state()call per book page, threaded down throughEditionNavBar/databarWork/editions_datatableβ not recomputed per row (editions_datatable only passes it for the highlighted/current edition row, correctly leaving other rows to resolve their own state). - β
get_lending_stateuses@public, no manualTemplate.globalsregistration needed.
One unrelated discovery while reading the surrounding code β flagging separately on LoanStatus.html, not blocking this PR.
| $if not waiting_loan: | ||
| $ book_provider = get_book_provider(doc) | ||
| $else: | ||
| $ book_provider = get_book_provider.ia |
There was a problem hiding this comment.
Pre-existing bug, not introduced by this PR (this line is unchanged context in the diff β same on master): get_book_provider.ia reads an .ia attribute off the function object get_book_provider, not off a provider instance. That attribute doesn't exist β there's no get_book_provider.ia = ... anywhere in book_providers.py. The module-level singleton you likely want is ia_provider (openlibrary/book_providers.py:752), or call get_book_provider_by_name("ia").
This branch ($else: under $if not waiting_loan:) is reachable whenever lending_state == 'waitlist' and the user has a real active waiting_loan β at that point this line raises AttributeError: 'function' object has no attribute 'ia'.
Flagging here because this PR's get_lending_state refactor sits right next to it, and @Sadashii's comment above mentions a follow-up PR to migrate LoanStatus onto get_lending_state as the single source of truth β worth fixing as part of that follow-up rather than in this PR.
The CheckWorldcat rename's pot entries drifted from master (which merged internetarchive#12910 in the meantime); regenerated from the final rebased tree so the two are consistent.
β¦.pot after rebasing onto master internetarchive#12910 merged in the meantime and split the old single is_lendable branch into separate borrowable/waitlist/checkedout branches, each duplicating the $if secondary_action: BookPreview(...) line. Applied this PR's BookPreview -> PreviewSearchInside swap to all three, matching the single occurrence it replaced pre-split. Regenerated messages.pot from the final rebased tree rather than hand-merging.
The CheckWorldcat rename's pot entries drifted from master (which merged internetarchive#12910 in the meantime); regenerated from the final rebased tree so the two are consistent.
β¦.pot after rebasing onto master internetarchive#12910 merged in the meantime and split the old single is_lendable branch into separate borrowable/waitlist/checkedout branches, each duplicating the $if secondary_action: BookPreview(...) line. Applied this PR's BookPreview -> PreviewSearchInside swap to all three, matching the single occurrence it replaced pre-split. Regenerated messages.pot from the final rebased tree rather than hand-merging.
* feat: Migrate Book Preview & Search Inside to ol-dialog
- Migrates the Book Preview and Search Inside modals from legacy jQuery Colorbox to the new <ol-dialog> Lit component.
- Extracted the shared book preview dialog markup to a new macros/BookPreviewFloater.html template to prevent markup duplication.
- Updated templates (BookPreview.html, PreviewSearchInside.html, and home/index.html) to call BookPreviewFloater.
- Made the book preview iframe size dynamic and responsive using CSS calc() in buttonCta.css to eliminate vertical scrollbars inside the modal.
- Improved accessibility by adding an iframe title, form search role/aria-label, and dynamically updating the search trigger's aria-expanded attributes in JS.
- Cleaned up dialog.js by extracting redundant show/hide form actions into a shared collapseSearchForm helper.
- Standardized templates to use the correct ('...') i18n syntax.
- Regenerated openlibrary/i18n/messages.pot to compile the new 'See more about this book on Archive.org' localization string.
* Improve accessibility of preview and search inside elements
* fix(rebase): resolve LoanStatus.html conflict and regenerate messages.pot after rebasing onto master
#12910 merged in the meantime and split the old single is_lendable
branch into separate borrowable/waitlist/checkedout branches, each
duplicating the $if secondary_action: BookPreview(...) line. Applied
this PR's BookPreview -> PreviewSearchInside swap to all three,
matching the single occurrence it replaced pre-split. Regenerated
messages.pot from the final rebased tree rather than hand-merging.
---------
Co-authored-by: Michael E. Karpeles (Mek) <michael.karpeles@gmail.com>
Closes #12743
This PR adds a warning banner recommending available alternative books on the Book Edition page when a book is in
preview_only,checkedout,waitlist, orlocatephysical states.Technical
@publicfunctionget_lending_stateinlending.py.code.py.<ol-banner>inside theEditionNavBarmacro, wrapped within a responsivedivusing.desktop-only/.mobile-onlyCSS utility classes to avoid layout conflicts.view.htmllayout by delegating the banner rendering to the navbar component, and removed the unuseddata-workidcarousel attribute.get_pending_action_bannerinmybooks.pyand updatedAGENTS.mdto follow i18n best practices (combining localized segments into single formatted strings with placeholders).test_lending.py.data-lending-stateattribute to#contentBodyand visibility/tracking hooks inlazy-carousel.jsto instrument discovery of recommended books.Analytics & Experiment Tracking
Added tracking for the discovery of the recommended books carousel under the category
OpenRelatedBooksto compare natural discovery vs banner-driven discovery:OpenRelatedBooks|BannerClick: Tracked when a user clicks the link on the warning banner to jump to the recommendations.OpenRelatedBooks|FromBanner: Tracked when the related books carousel lazy-loads/is discovered after the user clicked the banner link (URL hash is#related-work-carousel).OpenRelatedBooks|ScrolledDownAvailable: Tracked when the related books carousel lazy-loads/is discovered by scrolling down normally when the book is available.OpenRelatedBooks|ScrolledDownUnavailable: Tracked when the related books carousel lazy-loads/is discovered by scrolling down normally when the book is unavailable (in one of the 4 warning states).Testing
uv run --with-requirements requirements_test.txt pytest openlibrary/tests/core/test_lending.pyuv run --with-requirements requirements_test.txt pytest openlibrary/tests/test_templates.pypre-commit run --files openlibrary/core/lending.py openlibrary/plugins/openlibrary/code.py openlibrary/templates/type/edition/view.html openlibrary/tests/core/test_lending.py openlibrary/macros/EditionNavBar.html openlibrary/plugins/openlibrary/js/lazy-carousel.jsScreenshot
Stakeholders
@mekarpeles