Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ services:
image: "${OLIMAGE:-oldev:latest}"
environment:
- OL_CONFIG=${OL_CONFIG:-/openlibrary/conf/openlibrary.yml}
- GUNICORN_OPTS=${GUNICORN_OPTS:- --reload --workers 4 --timeout 180}
- GUNICORN_OPTS=${GUNICORN_OPTS:- --reload --workers 1 --timeout 180}
command: docker/ol-web-start.sh
ports:
- ${WEB_PORT:-8080}:8080
Expand Down
2 changes: 1 addition & 1 deletion conf/openlibrary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ debug: True
coverstore_url: http://covers:7075
# URL to use inside HTML files; must be publicly accessible from
# a client's browser
coverstore_public_url: http://localhost:7075
coverstore_public_url: https://covers.openlibrary.org

state_dir: var/run

Expand Down
2 changes: 2 additions & 0 deletions conf/solr/conf/managed-schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@
<field name="author_name" type="text_international" multiValued="true"/>
<field name="author_alternative_name" type="text_international" multiValued="true"/>
<field name="author_facet" type="string" stored="false" multiValued="true"/>
<field name="series_key" type="string" multiValued="true"/>
<field name="series_name" type="text_en_splitting" multiValued="true"/>
<field name="subject" type="text_en_splitting" multiValued="true"/>
<field name="subject_facet" type="string" stored="false" multiValued="true"/>
<field name="subject_key" type="string" stored="false" multiValued="true"/>
Expand Down
154 changes: 133 additions & 21 deletions openlibrary/core/lists/model.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Helper functions used by the List model.
"""
from functools import cached_property
from typing import TypedDict, cast
from typing import Generic, Literal, TypeVar, TypedDict, cast

import web
import logging
Expand All @@ -22,7 +22,7 @@
logger = logging.getLogger("openlibrary.lists.model")


class SeedDict(TypedDict):
class ThingReferenceDict(TypedDict):
key: ThingKey


Expand All @@ -34,7 +34,72 @@ class SeedDict(TypedDict):
"""


class List(Thing):
class ListAnnotations(TypedDict):
pass


class UserListAnnotations(ListAnnotations):
notes: str


class SeriesAnnotations(ListAnnotations):
position: str
primary: bool


class AnnotatedSeedDict(TypedDict):
"""
This is the most common example of this, when it appears as a JSON-compatible
dict.

E.g:
```json
{
"thing": { "key": "/works/OL1234W" },
"notes": "Hello"
}
```
"""

thing: ThingReferenceDict


class AnnotatedSeed(TypedDict):
"""
This is what we get back from the database. This is not JSON-compatible, since
it contains Thing objects.

E.g.:
```
{
"thing": <Thing: /works/OL1234W>,
"notes": "Hello"
}
```

Note: the `Thing` is usually lazy; so it won't fetch the thing from the db
unless you reference a field on it.
"""

thing: Thing


class AnnotatedSeedThing(Thing):
"""
This is what we get back from the db. Infobase converts every sub-object
into a `Thing` instance, even if they don't have a key! So e.g. a `List`
from the db will have seeds that are `AnnotatedSeedThing`s, not direct
`dict`s.
"""

key: Literal[None] # type: ignore
_data: AnnotatedSeed


TAnnotations = TypeVar("TAnnotations", bound=ListAnnotations)


class List(Thing, Generic[TAnnotations]):
"""Class to represent /type/list objects in OL.

List contains the following properties, theoretically:
Expand All @@ -48,7 +113,7 @@ class List(Thing):
description: str | None
"""Detailed description of the list (markdown)"""

seeds: list[Thing | SeedSubjectString]
seeds: list[Thing | SeedSubjectString | AnnotatedSeedThing]
"""Members of the list. Either references or subject strings."""

def url(self, suffix="", **params):
Expand All @@ -75,15 +140,9 @@ def get_tags(self):
"""
return [web.storage(name=t, url=self.key + "/tags/" + t) for t in self.tags]

def add_seed(self, seed: Thing | SeedDict | SeedSubjectString):
"""
Adds a new seed to this list.
def add_seed(self, seed: Thing | ThingReferenceDict | SeedSubjectString):
"""Adds a new seed to this list."""

seed can be:
- a `Thing`: author, edition or work object
- a key dict: {"key": "..."} for author, edition or work objects
- a string: for a subject
"""
if isinstance(seed, dict):
seed = Thing(self._site, seed['key'], None)

Expand All @@ -94,10 +153,14 @@ def add_seed(self, seed: Thing | SeedDict | SeedSubjectString):
self.seeds.append(seed)
return True

def remove_seed(self, seed: Thing | SeedDict | SeedSubjectString):
def remove_seed(
self,
seed: Thing | ThingReferenceDict | SeedSubjectString | AnnotatedSeedDict,
):
"""Removes a seed for the list."""
if isinstance(seed, dict):
seed = Thing(self._site, seed['key'], None)
key = seed.get('key') or seed.get('thing', {}).get('key')
seed = Thing(self._site, key, None)

if (index := self._index_of_seed(seed)) >= 0:
self.seeds.pop(index)
Expand All @@ -117,7 +180,7 @@ def __repr__(self):
return f"<List: {self.key} ({self.name!r})>"

def _get_seed_strings(self) -> list[SeedSubjectString | ThingKey]:
return [seed if isinstance(seed, str) else seed.key for seed in self.seeds]
return [Seed(self, seed).key for seed in self.seeds]

@cached_property
def last_update(self):
Expand Down Expand Up @@ -352,7 +415,7 @@ def get_subject_type(s):
def get_seeds(self, sort=False, resolve_redirects=False) -> list['Seed']:
seeds: list['Seed'] = []
for s in self.seeds:
seed = Seed(self, s)
seed = Seed.from_db(self, s)
max_checks = 10
while resolve_redirects and seed.type == 'redirect' and max_checks:
seed = Seed(self, web.ctx.site.get(seed.document.location))
Expand All @@ -364,7 +427,7 @@ def get_seeds(self, sort=False, resolve_redirects=False) -> list['Seed']:

return seeds

def has_seed(self, seed: SeedDict | SeedSubjectString) -> bool:
def has_seed(self, seed: ThingReferenceDict | SeedSubjectString) -> bool:
if isinstance(seed, dict):
seed = seed['key']
return seed in self._get_seed_strings()
Expand Down Expand Up @@ -402,16 +465,58 @@ class Seed:

value: Thing | SeedSubjectString

def __init__(self, list: List, value: Thing | SeedSubjectString):
notes: str | None = None

def __init__(
self,
list: List,
value: Thing | SeedSubjectString | AnnotatedSeed,
):
self._list = list
self._type = None

self.value = value
if isinstance(value, str):
self.key = value
self.value = value
self._type = "subject"
elif isinstance(value, dict):
# AnnotatedSeed
self.key = value['thing'].key
self.value = value['thing']
self.notes = value['notes']
else:
self.key = value.key
self.value = value

@staticmethod
def from_db(list: List, seed: Thing | SeedSubjectString) -> 'Seed':
if isinstance(seed, str):
return Seed(list, seed)
elif isinstance(seed, Thing):
if seed.key is None:
return Seed(list, cast(AnnotatedSeed, seed._data))
else:
return Seed(list, seed)

@staticmethod
def from_json(
list: List,
seed_json: SeedSubjectString | ThingReferenceDict | AnnotatedSeedDict,
):
if isinstance(seed_json, dict):
if 'thing' in seed_json:
seed_json = cast(AnnotatedSeedDict, seed_json)
thing = Thing(list._site, seed_json['thing']['key'], None)
return Seed(
list,
{
'thing': thing,
'notes': seed_json['notes'],
},
)
elif 'key' in seed_json:
return Seed(list, seed_json['key'])
return Seed(list, seed_json)

@cached_property
def document(self) -> Subject | Thing:
Expand Down Expand Up @@ -527,16 +632,23 @@ def get_removed_seed(self):
if removed and len(removed) == 1:
return self.get_seed(removed[0])

def get_list(self):
def get_list(self) -> List:
return self.get_changes()[0]

def get_seed(self, seed):
"""Returns the seed object."""
if isinstance(seed, dict):
seed = self._site.get(seed['key'])
return Seed(self.get_list(), seed)
return Seed.from_db(self.get_list(), seed)


class Series(List):
pass


def register_models():
client.register_thing_class('/type/list', List)
client.register_changeset_class('lists', ListChangeset)

client.register_thing_class('/type/series', Series)
client.register_changeset_class('series', ListChangeset)
4 changes: 2 additions & 2 deletions openlibrary/macros/LoanStatus.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
$ work_key = work_key or (doc.get('works') and doc.works[0].key)

$ waiting_loan_start_time = time()
$ waiting_loan = check_loan_status and ocaid and ctx.user and ctx.user.get_user_waiting_loans(ocaid, use_cache=True)
$ waiting_loan = None # check_loan_status and ocaid and ctx.user and ctx.user.get_user_waiting_loans(ocaid, use_cache=True)
$ waiting_loan_total_time = time() - waiting_loan_start_time
$ my_turn_to_borrow = waiting_loan and waiting_loan['status'] == 'available' and waiting_loan['position'] == 1

Expand All @@ -34,7 +34,7 @@

$# Checks to see if patron has actively loan / waitlist for this book
$ get_loan_for_start_time = time()
$ user_loan = doc.get('loan') or (check_loan_status and ocaid and ctx.user and ctx.user.get_loan_for(ocaid, use_cache=True))
$ user_loan = None # doc.get('loan') or (check_loan_status and ocaid and ctx.user and ctx.user.get_loan_for(ocaid, use_cache=True))
$ get_loan_for_total_time = time() - get_loan_for_start_time

$ is_edition = doc.key.split('/')[1] == 'books'
Expand Down
Loading