Continue Reading Phase 1: Merge 'Active Loans' with 'Loan History' on 'My Books' page - #13281
Conversation
There was a problem hiding this comment.
Pull request overview
This PR implements Phase 1 of a unified “My Loans” carousel on /account/books/ by merging active loans with recent loan history, plus UI affordances (badges + return button) for active loans.
Changes:
- Extracts
get_loan_history_data(username, page)intoopenlibrary/core/lending.pyand updates legacy account endpoints to use it. - Updates
openlibrary/plugins/upstream/mybooks.pyto merge active loans + page 1 of loan history with deduping and sorting, capped to 18 items. - Extends the custom carousel card template and styling to render active-loan overlays and an inline return action; adds FastAPI tests for the merge behavior.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| static/css/legacy.css | Adds import for the new custom carousel card stylesheet. |
| static/css/components/custom-carousel-card.css | Introduces overlay badge + action-row styling for loan UI. |
| openlibrary/tests/fastapi/test_unified_loans_carousel.py | Adds tests asserting merge/dedup/sort behavior and active-loan precedence. |
| openlibrary/tests/fastapi/test_account_loans.py | Adds a regression test for null/empty loan-history API responses (currently written against the old helper location/signature). |
| openlibrary/templates/books/custom_carousel.html | Forwards book.loan into the card context for rendering. |
| openlibrary/templates/books/custom_carousel_card.html | Renders loan overlay badges and an inline return form/button for active bookreader loans. |
| openlibrary/plugins/upstream/mybooks.py | Implements merged active+history loans list creation and sorting for the My Books page. |
| openlibrary/plugins/upstream/account.py | Switches to the new core.lending.get_loan_history_data(username, page) helper. |
| openlibrary/core/lending.py | Adds the extracted get_loan_history_data helper with null-safe response handling. |
Suppressed comments (1)
openlibrary/templates/books/custom_carousel_card.html:55
- These inline styles use hard-coded hex colors; prefer existing CSS token variables (
var(--red),var(--link-blue), etc.) to stay consistent with Stylelint/project conventions and avoid embedding palette values in templates.
<span style="color: #ff8888; font-weight: bold;">$_("Not yet downloaded.")</span><br/>
<a href="$loan.get('loan_link')" style="color: #88c0ff; text-decoration: underline;">$_("Download Now")</a>
8ee10c4 to
a24b732
Compare
82dcd8f to
318679b
Compare
@Sadashii have you run this locally and visually QA'd? As is implemented, it's a bit confusing because it still says, "My Loans (20)"; we may want to switch to Continue Reading. It would be useful to have screenshots attached to the PR from testing and also any questions you have or feedback you would like. In particular, we'll want to get feedback from @lokesh and the community on the rest of the UI (e.g. the return button iconography, color, etc) |
|
There was a problem hiding this comment.
Found 4 issues across 2 rules (2 WCAG, 2 Best Practice).
Reviewed by AccessLint, which checks every pull request for accessibility issues.
There was a problem hiding this comment.
Found 4 issues across 2 rules (2 WCAG, 2 Best Practice).
Reviewed by AccessLint, which checks every pull request for accessibility issues.
There was a problem hiding this comment.
Found 4 issues across 2 rules (2 WCAG, 2 Best Practice).
Reviewed by AccessLint, which checks every pull request for accessibility issues.
There was a problem hiding this comment.
Found 1 issue across 1 rule.
Reviewed by AccessLint, which checks every pull request for accessibility issues.
…rousel - Move get_loan_history_data from account.py to openlibrary/core/lending.py and decouple from MyBooksTemplate. - Handle missing or null history/items in IA loan API response gracefully. - Merge active loans and history in mybooks.py, deduplicating by work key and prioritizing active loans over returned history. - Update custom_carousel.html and custom_carousel_card.html to render loan overlay badges (expiry, waitlist count, download links) and return buttons. - Add CSS rules for loan overlay badges, action rows, and return button styling. - Add unit tests for unified loans carousel ranking and null safety in loan history fetching.
for more information, see https://pre-commit.ci
- Rename My Loans carousel and sidebar to Loans & History - Redirect old /account/loan-history page to /account/loans via 301 Permanent Redirect - Embed paginated Loan History list under the Active Loans section - Add Active Loans (count) section header to match waitlist headers - Display cover overlay Loan Expired badges on dashboard carousel for expired loans - Support cover wrapper scaling and clean badge border positioning in CSS - Safely fetch total_results in compact and mobile carousels to prevent AttributeErrors
- Track specific carousel card action clicks separately (e.g. BookCarousel|BorrowClick|loans, BookCarousel|ReadClick|loans)
- Support dynamic {action} placeholder replacement inside LoanStatus analytics_override
- Track unified page active loans table return button clicks as LoansAndHistory|ReturnClick
- Track book details / search results return button clicks as LoanStatus|ReturnClick
for more information, see https://pre-commit.ci
4707d60 to
de55b43
Compare
ReturnForm's new `analytics_track` parameter built the whole attribute as
one pre-formatted string spliced into the tag:
$('data-ol-link-track="' + analytics_track + '"' if analytics_track else '')
`$(...)` HTML-escapes, so the quotes rendered as `"` and the attribute
came out unquoted. A browser parses the value as `"LoansAndHistory|ReturnClick"`
-- literal quote characters included -- and ol.analytics.js:65 splits it on
`|` into category `"LoansAndHistory` and action `ReturnClick"`, corrupting
both events this branch adds (LoansAndHistory|ReturnClick on the loans table,
LoanStatus|ReturnClick on book details and search results).
This is the same anti-pattern removed from four templates in 4285eb8
(issue internetarchive#13186); the escaped quotes are inert text rather than real delimiters,
so a value containing a space can also inject a new attribute. Use the fix
established there instead: an inline `$if` emitting the attribute with a
static, template-source quote the value cannot break out of.
Adds regression coverage to test_link_track_attribute_escaping.py, exercising
the real macro rather than a copy of its markup. Verified the injection test
fails against the previous version and passes against this one.
This branch folds the loan-history list into /account/loans and reduces
account_loan_history.GET to a 301, removing the only
render["account/loan_history"] call. The template itself was left on disk
with nothing referencing it.
Master has since added openlibrary/tests/test_unused_templates.py, which
fails on exactly this:
1 templates/macros are not referenced in any Python, Templetor, Jinja,
or JS source:
templetor template: openlibrary/templates/account/loan_history.html
Its markup is superseded by the new Loan History section in
account/loans.html, which renders the same OlPaginationArrows / IABook /
SearchResultsWork structure. The dropped include_ratings/StarRatings branch
was dead too -- the old call site never passed ratings.
An entry in DB_USED_EXCLUSIONS would be wrong here; the template is not
reachable from database content, it is simply dead.
…logged in
render_template() guarded the merged loans carousel with `if mb.me:` -- which
only asks whether *someone* is signed in -- and then fetched history for
`mb.username`, which is the profile in the URL, not the session:
if mb.me:
myloans = get_loans_of_user(mb.me.key) # viewer's own loans
...
history_data = get_loan_history_data(mb.username, page=1) # URL-supplied
get_loan_history_data() resolves S3 credentials for whatever username it is
handed (get_s3_keys() prefers the requester's own `s3` cookie, but falls back
to the store entry for the account it was passed -- the path taken by sessions
predating that cookie). So any signed-in visitor to /people/<someone>/books
could drive a borrow-history lookup against that patron's account.
MyBooksTemplate already computes the real ownership flag, `mb.is_my_page`;
this uses it. Nothing was rendered to the visitor either way -- mybooks.html
gates both carousels behind `$if owners_page:` -- but that guard lives in the
view layer, so the data layer was one template change away from leaking. It
also removes an external IA round-trip from every profile page view.
Scope: only the history fetch this branch introduced is gated. The pre-existing
`get_loans_of_user(mb.me.key)` call is left alone.
|
PR #13281 — Rebase + verification (Ada, on behalf of Mek) @Sadashii — this branch was 72 commits behind The rebase All 7 of your commits are preserved: same author, same author dates, same messages, in the same order. Exactly one file conflicted — I checked this by merging Fixes pushed on top
This is the same pattern removed from four templates in
Verification Docker, on the rebased branch: Not fixed — worth your call
Overall Nice piece of work — the merge logic, the active-over-returned ranking, and the redirect handling all hold up under test. The two blockers are fixed and pushed; the rest above is advisory and yours to take or leave. Human review and merge remain with Mek. |
Reported from testing.openlibrary.org:
TypeError: unsupported operand type(s) for |: 'NoneType' and 'dict'
openlibrary/core/lending.py in s3_loan_api data = s3_keys | kwargs
openlibrary/core/lending.py in get_loan_history_data
openlibrary/plugins/upstream/account.py in GET (account_loans)
get_s3_keys() is annotated `dict | None` and genuinely returns None when the
patron has no `s3` session cookie and no `s3_keys` in the account store.
get_loan_history_data() passed that straight into s3_loan_api(), which does
`s3_keys | kwargs`.
This became a page-level failure with this branch. Before it, /account/loans
never called get_loan_history_data() -- only /account/loan-history did. Folding
history into the loans page put an unguarded IA call in front of the
active-loans table, so a patron with no S3 keys now gets a 500 on a page that
previously rendered fine, and loses the loan table too, which has nothing to do
with IA history. The My Books carousel survives only because its call sits in a
bare `except Exception:` -- it just silently shows no history.
Degrade to an empty history instead, and log a warning so this is diagnosable
rather than invisible. Covered by tests: the guard short-circuits before
s3_loan_api, real keys still reach it (so this can't be "fixed" by disabling
history for everyone), and the TypeError at the s3_loan_api boundary is pinned
so the reason for the guard doesn't get lost.
…g it The My Books carousel wraps its loan-history fetch in a bare `except Exception: # noqa: BLE001` that discards the error entirely. Keeping the page alive when IA is unreachable is right -- the active loans and every other shelf should still render -- but discarding the reason is not: a missing history section becomes indistinguishable from a patron who simply has no borrow history, with nothing in the logs to tell them apart. That is exactly how the TypeError fixed in the previous commit stayed hidden. On testing, /account/loans surfaced it as a 500 while My Books rendered a clean 200 with the history silently absent and not one log line to explain it. Keep the catch, log the exception.



Closes #13272
Feature: Merges active user loans with past loan history into a unified carousel on the My Books landing page (
/account/books/).Technical
openlibrary/core/lending.py): Extractedget_loan_history_data(username, page)out ofaccount.pyintocore/lending.pyso it can be shared between the loan history page and the My Books landing page controller.openlibrary/plugins/upstream/mybooks.py): Implemented a two-pass deduplicating merge algorithm fordocs["loans"]:get_loans_of_user(), following up to 5 redirect hops to key each item by its canonical work key.get_loan_history_data(), filtering out IA-only items without Open Library editions.is_active=True), followed by timestamp descending (loaned_at/last_loan_date), capped at 18 books total.custom_carousel.html,custom_carousel_card.html,custom-carousel-card.css,carousel.css):book.loaninto the carousel card context (book2['loan']).bookreaderitems..book-cover-wrapperand configured CSS absolute positioning to scale overlays exactly to the cover image width. Added grey"Loan Expired"overlay badges for past loans./account/loanstemplate. Modified/account/loan-historyGET handler to issue a 301 Permanent Redirect to/account/loans?page={page}.loans.html. Omitted book counts on the "Loans & History" carousel dashboard title. Safely resolved missingtotal_resultslookups inmybooks.htmlto prevent AttributeErrors.$ifcompile syntax error to standard python ternary expressions.Analytics Tracked
We have integrated key user interaction points with analytics tracking using
data-ol-link-trackattributes:BookCarousel|BorrowClick|loans— Tracks when a user borrows a book directly from the dashboard carousel.BookCarousel|ReadClick|loans— Tracks when a user opens the reader for a borrowed book from the carousel.BookCarousel|JoinWaitlistClick|loans— Tracks joining a waitlist from the carousel.BookCarousel|ReturnClick|loans— Tracks returns initiated via the hover cover returns in the carousel.BookCarousel|CoverClick|loans— Tracks navigation clicks on book covers inside the carousel.LoansAndHistory|ReturnClick— Tracks return form submissions inside the active loans table on the/account/loanspage.LoanStatus|ReturnClick— Tracks return form submissions inside the genericLoanStatus.htmlmacro (e.g. details pages).MyBooksSidebar|Loans— Tracks sidebar clicks on the newly unified "Loans & History" link.Testing
http://localhost:8080/account/books/):@openlibrary.Stakeholders
@Sadashii @mekarpeles