Skip to content

Commit

Permalink
Merge pull request internetarchive#8375 from anujamerwade/7311/featur…
Browse files Browse the repository at this point in the history
…e/loan-history

7311/feature/loan history code for rendering loan_history page
  • Loading branch information
mekarpeles authored Feb 14, 2024
2 parents c506c1b + 939a621 commit 72593ef
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 8 deletions.
32 changes: 30 additions & 2 deletions openlibrary/core/lending.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Module for providing core functionality of lending on Open Library.
"""
from typing import Literal, TypedDict, cast
from typing import TYPE_CHECKING, Literal, TypedDict, cast

import web
import datetime
Expand All @@ -21,6 +21,11 @@
from . import ia
from . import helpers as h


if TYPE_CHECKING:
from openlibrary.plugins.upstream.models import Edition


logger = logging.getLogger(__name__)

S3_LOAN_URL = 'https://%s/services/loans/loan/'
Expand Down Expand Up @@ -504,13 +509,36 @@ def get_availability_of_ocaid(ocaid):
return get_availability('identifier', [ocaid])


def get_availability_of_ocaids(ocaids: list[str]) -> dict:
def get_availability_of_ocaids(ocaids: list[str]) -> dict[str, AvailabilityStatusV2]:
"""
Retrieves availability based on ocaids/archive.org identifiers
"""
return get_availability('identifier', ocaids)


def get_items_and_add_availability(ocaids: list[str]) -> dict[str, "Edition"]:
"""
Get Editions from OCAIDs and attach their availabiliity.
Returns a dict of the form: `{"ocaid1": edition1, "ocaid2": edition2, ...}`
"""
ocaid_availability = get_availability_of_ocaids(ocaids=ocaids)
editions = web.ctx.site.get_many(
[
f"/books/{item.get('openlibrary_edition')}"
for item in ocaid_availability.values()
if item.get('openlibrary_edition')
]
)

# Attach availability
for edition in editions:
if edition.ocaid in ocaids:
edition.availability = ocaid_availability.get(edition.ocaid)

return {edition.ocaid: edition for edition in editions if edition.ocaid}


def is_loaned_out(identifier):
"""Returns True if the given identifier is loaned out.
Expand Down
26 changes: 26 additions & 0 deletions openlibrary/macros/IABook.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
$def with (doc, ia_base_url="https://archive.org")
$ ocaid = doc.get('ocaid')
<li class="searchResultItem">
<span class="bookcover">
$ cover = "%s/services/img/%s" % (ia_base_url, ocaid)
<a href="$ia_base_url/details/$ocaid">
<img itemprop="image"
src="$cover"
alt="$_('Cover of: %(title)s', title=ocaid)"
title="$_('Cover of: %(title)s', title=ocaid)"
/>
</a>
</span>

<div class="details">
<div class="resultTitle">
<h3 itemprop="name" class="booktitle">
<a itemprop="url" href="$ia_base_url/details/$ocaid" class="results">Borrowed from Internet Archive: $ocaid</a>
</h3>
</div>
</div>
$# The following (empty) <div> is to aid in formatting the layout.
<div class="searchResultItemCTA">
</div>
</li>

9 changes: 9 additions & 0 deletions openlibrary/macros/Pager_loanhistory.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
$def with (page, show_next=True)
<div class="clearfix"></div>
<div class="pagination">
$if page != 1:
<a href="$changequery(page=None)" class="ChoosePage">&laquo;&nbsp;$_('First')</a>
<a href="$changequery(page=page-1)" class="ChoosePage">&lt;&nbsp;$_('Previous')</a>
$if show_next:
<a href="$changequery(page=page+1)" class="ChoosePage">$_('Next')&nbsp;&gt;</a>
</div>
123 changes: 122 additions & 1 deletion openlibrary/plugins/upstream/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import json
import logging
import re
from typing import Any
from typing import Any, Final
from collections.abc import Callable
from collections.abc import Iterable, Mapping

Expand All @@ -25,6 +25,10 @@
from openlibrary.core import helpers as h, lending
from openlibrary.core.booknotes import Booknotes
from openlibrary.core.bookshelves import Bookshelves
from openlibrary.core.lending import (
get_items_and_add_availability,
s3_loan_api,
)
from openlibrary.core.observations import Observations
from openlibrary.core.ratings import Ratings
from openlibrary.plugins.recaptcha import recaptcha
Expand All @@ -41,9 +45,12 @@
from openlibrary.plugins.upstream import borrow, forms, utils
from openlibrary.utils.dateutil import elapsed_time


logger = logging.getLogger("openlibrary.account")

CONFIG_IA_DOMAIN: Final = config.get('ia_base_url', 'https://archive.org')
USERNAME_RETRIES = 3
RESULTS_PER_PAGE: Final = 25

# XXX: These need to be cleaned up
send_verification_email = accounts.send_verification_email
Expand Down Expand Up @@ -1077,6 +1084,48 @@ def GET(self):
return delegate.RawText(json.dumps({"loans": loans}))


class account_loan_history(delegate.page):
path = "/account/loan-history"

@require_login
def GET(self):
i = web.input(page=1)
page = int(i.page)
user = accounts.get_current_user()
username = user['key'].split('/')[-1]
mb = MyBooksTemplate(username, key='loan_history')
loan_history_data = get_loan_history_data(page=page, mb=mb)
template = render['account/loan_history'](
docs=loan_history_data['docs'],
current_page=page,
show_next=loan_history_data['show_next'],
ia_base_url=CONFIG_IA_DOMAIN,
)
return mb.render(header_title=_("Loan History"), template=template)


class account_loan_history_json(delegate.page):
encoding = "json"
path = "/account/loan-history"

@require_login
def GET(self):
i = web.input(page=1)
page = int(i.page)
user = accounts.get_current_user()
username = user['key'].split('/')[-1]
mb = MyBooksTemplate(username, key='loan_history')
loan_history_data = get_loan_history_data(page=page, mb=mb)
# Ensure all `docs` are `dicts`, as some are `Edition`s.
loan_history_data['docs'] = [
loan.dict() if not isinstance(loan, dict) else loan
for loan in loan_history_data['docs']
]
web.header('Content-Type', 'application/json')

return delegate.RawText(json.dumps({"loans_history": loan_history_data}))


class account_waitlist(delegate.page):
path = "/account/waitlist"

Expand Down Expand Up @@ -1140,3 +1189,75 @@ def process_goodreads_csv(i):
else:
books_wo_isbns[_book['Book Id']] = _book
return books, books_wo_isbns


def get_loan_history_data(page: int, mb: "MyBooksTemplate") -> dict[str, Any]:
"""
Retrieve IA loan history data for page `page` of the patron's history.
This will use a patron's S3 keys to query the IA loan history API,
get the IA IDs, get the OLIDs if available, and and then convert this
into editions and IA-only items for display in the loan history.
This returns both editions and IA-only items because the loan history API
includes items that are not in Open Library, and displaying only IA
items creates pagination and navigation issues. For further discussion,
see https://github.com/internetarchive/openlibrary/pull/8375.
"""
if not (account := OpenLibraryAccount.get(username=mb.username)):
raise render.notfound(
"Account for not found for %s" % mb.username, create=False
)
s3_keys = web.ctx.site.store.get(account._key).get('s3_keys')
limit = RESULTS_PER_PAGE
offset = page * limit - limit
loan_history = s3_loan_api(
s3_keys=s3_keys,
action='user_borrow_history',
limit=limit + 1,
offset=offset,
newest=True,
).json()['history']['items']

# We request limit+1 to see if there is another page of history to display,
# and then pop the +1 off if it's present.
show_next = len(loan_history) == limit + 1
if show_next:
loan_history.pop()

ocaids = [loan_record['identifier'] for loan_record in loan_history]
loan_history_map = {
loan_record['identifier']: loan_record for loan_record in loan_history
}

# Get editions and attach their loan history.
editions_map = get_items_and_add_availability(ocaids=ocaids)
for edition in editions_map.values():
edition_loan_history = loan_history_map.get(edition.get('ocaid'))
edition['last_loan_date'] = (
edition_loan_history.get('updatedate') if edition_loan_history else ''
)

# Create 'placeholders' dicts for items in the Internet Archive loan history,
# but absent from Open Library, and then add loan history.
# ia_only['loan'] isn't set because `LoanStatus.html` reads it as a current
# loan. No apparenty way to distinguish between current and past loans with
# this API call.
ia_only_loans = [{'ocaid': ocaid} for ocaid in ocaids if ocaid not in editions_map]
for ia_only_loan in ia_only_loans:
loan_data = loan_history_map[ia_only_loan['ocaid']]
ia_only_loan['last_loan_date'] = loan_data.get('updatedate', '')
# Determine the macro to load for loan-history items only.
ia_only_loan['ia_only'] = True # type: ignore[typeddict-unknown-key]

editions_and_ia_loans = list(editions_map.values()) + ia_only_loans
editions_and_ia_loans.sort(
key=lambda item: item.get('last_loan_date', ''), reverse=True
)

return {
'docs': editions_and_ia_loans,
'show_next': show_next,
'limit': limit,
'page': page,
}
7 changes: 6 additions & 1 deletion openlibrary/plugins/upstream/mybooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@

from typing import Final, Literal

from infogami import config
from infogami.utils import delegate
from infogami.utils.view import public, safeint, render

from openlibrary.i18n import gettext as _

from openlibrary import accounts
from openlibrary.accounts.model import OpenLibraryAccount
from openlibrary.utils import extract_numeric_id_from_olid
from openlibrary.utils.dateutil import current_year
from openlibrary.core.booknotes import Booknotes
from openlibrary.core.bookshelves import Bookshelves
from openlibrary.core.lending import add_availability, get_loans_of_user
from openlibrary.core.lending import (
add_availability,
get_loans_of_user,
)
from openlibrary.core.observations import Observations, convert_observation_ids
from openlibrary.core.sponsorships import get_sponsored_editions
from openlibrary.core.models import LoggedBooksData
Expand Down
21 changes: 21 additions & 0 deletions openlibrary/templates/account/loan_history.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
$def with (docs, current_page, include_ratings=False, ratings=[], show_next=False, ia_base_url="")
<div class="mybooks-list">

$if len(docs) > 0:
$:macros.Pager_loanhistory(page=current_page, show_next=show_next)
<ul class="list-books">
$if docs:
$# enumerate because using zip() will result in empty iterator when no ratings are passed, and ratings are only used on already-read.
$for idx, doc in enumerate(docs):
$if doc.get('ia_only'):
$:macros.IABook(doc=doc, ia_base_url=ia_base_url)
$else:
$ star_rating = macros.StarRatings(doc, redir_url='/account/books/already-read', id=idx+1, rating=ratings[idx]) if include_ratings else None
$:macros.SearchResultsWork(doc, availability=doc.get('availability'), rating=star_rating)
</ul>
$:macros.Pager_loanhistory(page=current_page, show_next=show_next)
$else:
<ul class="list-books">
<p>$_("No loans found in your borrow history.")</p>
<p>$:_('<a href="/search">Search for a book</a> to borrow.')</p>
</ul>
2 changes: 1 addition & 1 deletion openlibrary/templates/account/mybooks.html
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ <h1 class="details-title">My Stats</h1>
</li>
<li>
<div class="carousel-section-header">
<a class="external-link" data-ol-link-track="MyBooksSidebar|LoanHistory" href="https://archive.org/account/?tab=loans#loans-history">
<a data-ol-link-track="MyBooksSidebar|LoanHistory" href="/account/loan-history">
$_('Loan History')
<img class="icon-link__image li-count" src="/static/images/icons/right-chevron.svg">
</a>
Expand Down
6 changes: 4 additions & 2 deletions openlibrary/templates/account/sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
<li>
<a class="li-title-desktop" href="/account/loans" data-ol-link-track="MyBooksSidebar|Loans" $('class=selected' if key == 'loans' else '')>$_('My Loans')</a>
</li>
<li><a class="li-title-desktop" data-ol-link-track="MyBooksSidebar|LoanHistory" href="https://archive.org/account/?tab=loans#loans-history">$_('Loan History')</a></li>
</ul>
<li>
<a class="li-title-desktop" data-ol-link-track="MyBooksSidebar|LoanHistory" href="/account/loan-history">$_('Loan History')</a>
</li>
</ul>
$if public or owners_page:
<ul class="sidebar-section">
<li class="section-header">$_('Reading Log')</li>
Expand Down
1 change: 1 addition & 0 deletions openlibrary/templates/books/mybooks_breadcrumb_select.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
$if mb.is_my_page:
$ options += [
$ (_("Loans"), "/account/loans"),
$ (_("Loan History"), "/account/loan-history"),
$ (_("Notes"), url_prefix + "/books/notes"),
$ (_("Reviews"), url_prefix + "/books/observations"),
$ (_("Imports and Exports"), "/account/import")
Expand Down
2 changes: 1 addition & 1 deletion static/css/components/mybooks-list.less
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
margin-bottom: 28px;
}

overflow-wrap: break-word;
overflow-wrap: anywhere;
}

.mybooks-list--clean {
Expand Down

0 comments on commit 72593ef

Please sign in to comment.