Repository: internetarchive/openlibrary

Issue:
List Widget: Asynchronously fetch patron lists
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
### Describe the problem that you'd like solved
<!-- A clear and concise description of what you want to happen. -->
If a list widget is present on an Open Library page, the page will not load until the list widget has been rendered.  If a patron has lists with a large amount of entries, the list widget can take upwards of 30 seconds to render.  This makes the site painfully slow for such patrons, as list widgets are present on every book page.

### Proposal & Constraints
<!-- What is the proposed solution / implementation? Is there a precedent of this approach succeeding elsewhere? -->
Let's asynchronously fetch the lists that are displayed in our list widget (highlighted below).
![list_of_lists](https://user-images.githubusercontent.com/28732543/180500694-cecb2a24-4073-40b3-b64c-c71b23a2925a.png)

The highlighted portion can initially be some type of loading animation, which can be replaced with the list names when they are available.

<!-- Which suggestions or requirements should be considered for how feature needs to appear or be implemented? -->

### Additional context
<!-- Add any other context or screenshots about the feature request here. -->

### Stakeholders
<!-- @ tag stakeholders of this bug -->
@seabelis




Discussion:
Once we have a solution to this issue, if we can get me, @seabelis and yourself (@jimchamp) to test on testing v. prod (load times with debug=true) this would be great

Resolving patch:
diff --git a/openlibrary/plugins/openlibrary/lists.py b/openlibrary/plugins/openlibrary/lists.py
--- a/openlibrary/plugins/openlibrary/lists.py
+++ b/openlibrary/plugins/openlibrary/lists.py
@@ -13,6 +13,7 @@
 from openlibrary.core import formats, cache
 from openlibrary.core.lists.model import ListMixin
 import openlibrary.core.helpers as h
+from openlibrary.i18n import gettext as _
 from openlibrary.utils import dateutil
 from openlibrary.plugins.upstream import spamcheck
 from openlibrary.plugins.upstream.account import MyBooksTemplate
@@ -28,6 +29,90 @@ def GET(self):
         return render_template("lists/home")
 
 
+@public
+def get_seed_info(doc):
+    """Takes a thing, determines what type it is, and returns a seed summary"""
+    if doc.key.startswith("/subjects/"):
+        seed = doc.key.split("/")[-1]
+        if seed.split(":")[0] not in ("place", "person", "time"):
+            seed = f"subject:{seed}"
+        seed = seed.replace(",", "_").replace("__", "_")
+        seed_type = "subject"
+        title = doc.name
+    else:
+        seed = {"key": doc.key}
+        if doc.key.startswith("/authors/"):
+            seed_type = "author"
+            title = doc.get('name', 'name missing')
+        elif doc.key.startswith("/works"):
+            seed_type = "work"
+            title = doc.get("title", "untitled")
+        else:
+            seed_type = "edition"
+            title = doc.get("title", "untitled")
+    return {
+        "seed": seed,
+        "type": seed_type,
+        "title": web.websafe(title),
+        "remove_dialog_html": _(
+            'Are you sure you want to remove <strong>%(title)s</strong> from your list?',
+            title=web.websafe(title),
+        ),
+    }
+
+
+@public
+def get_list_data(list, seed, include_cover_url=True):
+    d = web.storage(
+        {"name": list.name or "", "key": list.key, "active": list.has_seed(seed)}
+    )
+    if include_cover_url:
+        cover = list.get_cover() or list.get_default_cover()
+        d['cover_url'] = cover and cover.url("S") or "/images/icons/avatar_book-sm.png"
+        if 'None' in d['cover_url']:
+            d['cover_url'] = "/images/icons/avatar_book-sm.png"
+    owner = list.get_owner()
+    d['owner'] = web.storage(displayname=owner.displayname or "", key=owner.key)
+    return d
+
+
+@public
+def get_user_lists(seed_info):
+    user = get_current_user()
+    user_lists = user.get_lists(sort=True)
+    seed = seed_info['seed']
+    return [get_list_data(list, seed) for list in user_lists]
+
+
+class lists_partials(delegate.page):
+    path = "/lists/partials"
+
+    def GET(self):
+        i = web.input(key=None)
+
+        user = get_current_user()
+        doc = self.get_doc(i.key)
+        seed_info = get_seed_info(doc)
+        user_lists = get_user_lists(seed_info)
+
+        dropper = render_template('lists/dropper_lists', user_lists)
+        active = render_template(
+            'lists/active_lists', user_lists, user['key'], seed_info
+        )
+
+        partials = {
+            'dropper': str(dropper),
+            'active': str(active),
+        }
+
+        return delegate.RawText(json.dumps(partials), content_type="application/json")
+
+    def get_doc(self, key):
+        if key.startswith("/subjects/"):
+            return subjects.get_subject(key)
+        return web.ctx.site.get(key)
+
+
 class lists(delegate.page):
     """Controller for displaying lists of a seed or lists of a person."""
 


Test patch: