Repository: internetarchive/openlibrary

Issue:
Book Donations Flow
<!-- What problem are we solving? What does the experience look like today? What are the symptoms? -->

By piloting book sponsorship, we've learned a lot about what patrons are looking for in a book donation flow (compared to our vanilla process of emailing the Archive, checking if a title is available, and mailing the book to our physical archive -- as described in https://openlibrary.org/bookdrive.

A benefit of the sponsorship flow is it helps patrons identify which titles may not yet be in the controlled digital lending library. One cost, however, which has become especially evident due to additional challenges posed by the pandemic, is that sponsorship poses logistic challenges and constraints which do not exist within our vanilla book donation flow.

After stakeholder discussions, we've decided to try temporarily simplifying the book contribution process with a v2 of this program that tries to find a happy medium between vanilla donation and an integration which both (a) helps patrons discover which books may be useful for the library and (b) simplifies / streamlines / guides patrons through the process of donating a copy of a book.

This opens up possibilities because such a revised flow also enabled patrons who may already own the book to donate it to the library without being required to purchase an additional copy (a concern we hear raised regularly). This proposal also comes after learning our physical collections team has their own process for handing book donations and there may be an opportunity for us to leverage and re-purpose our existing processes and technology to converge on a single flow which accommodates both teams and ultimately results in a better outcome.

The initial goals of the sponsorship program were to give patrons a voice, increase representation and diversity of our materials, and simplify/streamline the process of making certain qualifying titles available through the library. In the spirit of continuing to make the process as simple as possible (both for patrons and also for us, in terms of managing multiple programs) we think it makes most sense to try only this simpler flow for some time (rather than maintain multiple systems which may work differently and require different support).

## Proposal

The general idea of the new donation flow is to show a "Donate" button where the Sponsorship button used to be on any qualifying titles. Instead of eating all of the logistic challenges of the sponsorship program (even though this may be most convenient for patrons), we'd like to test instead showing a popup with instructions to help patrons either donate an existing copy of the book they own, or to purchase a copy to donate to the library's address.

### Stakeholders
<!-- @ tag stakeholders of this bug -->
@bfalling @cdrini 


Discussion:
I think that makes more sense, as the money shouldn't go towards books when it can go towards other things. Also, people want to get rid of their books all the time - especially in the pandemic. Logistically, buying may not be as guaranteed for getting the desired book compared to donations. I mean, maybe there are rare books even that aren't even available to buy! It also allows books to get into the OL faster, as we don't have to wait until there's enough money. It might even be cheaper, as I bet some people may just buy a book on their own for really cheap and send it out.

Plus, from an environmental perspective - too much paper!

One day I really hope it'll come to a point where no one has to physically send over a book but rather be able to be the controlled lender where they are - so people can treat borrowing books like they would in person - and that's safer/healthier too. I mean, that would even go better with the idea of an open library. I guess there'd have to be a way to verify the person still has the physical book - but I bet there's a way.

Just glad that better efficiency's been figured out.

Resolving patch:
diff --git a/openlibrary/core/sponsorships.py b/openlibrary/core/sponsorships.py
--- a/openlibrary/core/sponsorships.py
+++ b/openlibrary/core/sponsorships.py
@@ -45,25 +45,7 @@ def get_sponsored_editions(user):
     contact_id = get_contact_id_by_username(archive_id)
     return get_sponsorships_by_contact_id(contact_id) if contact_id else []
 
-def get_sponsorable_editions():
-    """This should move to an infogami type so any admin can add editions
-    to the list. This will need to be paginated.
-    """
-    try:
-        candidates = web.ctx.site.get('/sponsorship/books').get('editions', [])
-    except AttributeError:
-        candidates = []
-
-    eligible_editions = []
-    for key in candidates:
-        ed = web.ctx.site.get(key)
-        ed.eligibility = qualifies_for_sponsorship(ed)
-        if ed.eligibility.get('is_eligible'):
-            eligible_editions.append(ed)
-    return eligible_editions
-
-
-def do_we_want_it(isbn, work_id):
+def do_we_want_it(isbn):
     """
     Returns True if we don't have this edition (or other editions of
     the same work), if the isbn has not been promised to us, has not
@@ -74,10 +56,6 @@ def do_we_want_it(isbn, work_id):
     :rtype: (bool, list)
     :return: bool answer to do-we-want-it, list of matching books
     """
-    availability = lending.get_work_availability(work_id)  # checks all editions
-    if availability and availability.get(work_id, {}).get('status', 'error') != 'error':
-        return False, availability
-
     # We don't have any of these work's editions available to borrow
     # Let's confirm this edition hasn't already been sponsored or promised
     params = {
@@ -96,7 +74,7 @@ def do_we_want_it(isbn, work_id):
     return False, []
 
 @public
-def qualifies_for_sponsorship(edition, scan_only=False, patron=None):
+def qualifies_for_sponsorship(edition, scan_only=False, donate_only=False, patron=None):
     """
     :param edition edition: An infogami book edition
     :rtype: dict
@@ -142,24 +120,28 @@ def qualifies_for_sponsorship(edition, scan_only=False, patron=None):
         }
         return resp
 
-    work_id = work.key.split("/")[-1]
+    work_id = edition.works[0].key.split("/")[-1]
     edition_id = edition.key.split('/')[-1]
-    dwwi, matches = do_we_want_it(edition.isbn, work_id)
+    dwwi, matches = do_we_want_it(edition.isbn)
     if dwwi:
-        bwb_price = get_betterworldbooks_metadata(edition.isbn).get('price_amt')
-        if bwb_price or scan_only:
-            num_pages = int(edition_data['number_of_pages'])
-            scan_price_cents = SETUP_COST_CENTS + (PAGE_COST_CENTS * num_pages)
-            book_cost_cents = int(float(bwb_price) * 100) if not scan_only else 0
-            total_price_cents = scan_price_cents + book_cost_cents
-            resp['price'] = {
-                'book_cost_cents': book_cost_cents,
-                'scan_price_cents': scan_price_cents,
-                'total_price_cents': total_price_cents,
-                'total_price_display': '${:,.2f}'.format(
-                    total_price_cents / 100.
-                ),
-            }
+        num_pages = int(edition_data['number_of_pages'])
+        bwb_price = None
+        if not donate_only:
+            if not scan_only:
+                bwb_price = get_betterworldbooks_metadata(edition.isbn).get('price_amt')
+            if scan_only or bwb_price:
+                scan_price_cents = SETUP_COST_CENTS + (PAGE_COST_CENTS * num_pages)
+                book_cost_cents = int(float(bwb_price) * 100) if not scan_only else 0
+                total_price_cents = scan_price_cents + book_cost_cents
+                resp['price'] = {
+                    'book_cost_cents': book_cost_cents,
+                    'scan_price_cents': scan_price_cents,
+                    'total_price_cents': total_price_cents,
+                    'total_price_display': '${:,.2f}'.format(
+                        total_price_cents / 100.
+                    ),
+                }
+        if donate_only or scan_only or bwb_price:
             resp['is_eligible'] = eligibility_check(edition, patron=patron)
     else:
         resp['error'] = {
@@ -172,12 +154,7 @@ def qualifies_for_sponsorship(edition, scan_only=False, patron=None):
     })
     resp.update({
         'edition': edition_data,
-        'sponsor_url': lending.config_ia_domain + '/donate?' + urlencode({
-            'campaign': 'pilot',
-            'type': 'sponsorship',
-            'context': 'ol',
-            'isbn': edition.isbn
-        })
+        'sponsor_url': 'https://openlibrary.org/bookdrive',
     })
     return resp
 
diff --git a/openlibrary/plugins/openlibrary/home.py b/openlibrary/plugins/openlibrary/home.py
--- a/openlibrary/plugins/openlibrary/home.py
+++ b/openlibrary/plugins/openlibrary/home.py
@@ -11,7 +11,6 @@
 
 from openlibrary.core import admin, cache, ia, lending, \
     helpers as h
-from openlibrary.core.sponsorships import get_sponsorable_editions
 from openlibrary.utils import dateutil
 from openlibrary.plugins.upstream.utils import get_blog_feeds
 from openlibrary.plugins.worksearch import search, subjects
@@ -104,19 +103,6 @@ def get_featured_subjects():
     return dict([(subject_name, subjects.get_subject('/subjects/' + subject_name, sort='edition_count'))
                  for subject_name in FEATURED_SUBJECTS])
 
-
-def get_cachable_sponsorable_editions():
-    if 'env' not in web.ctx:
-        delegate.fakeload()
-
-    return [format_book_data(ed) for ed in get_sponsorable_editions()]
-
-@public
-def get_cached_sponsorable_editions():
-    return storify(cache.memcache_memoize(
-        get_cachable_sponsorable_editions, "books.sponsorable_editions",
-        timeout=dateutil.HOUR_SECS)())
-
 @public
 def get_cached_featured_subjects():
     return cache.memcache_memoize(


Test patch:
diff --git a/openlibrary/plugins/openlibrary/tests/test_home.py b/openlibrary/plugins/openlibrary/tests/test_home.py
--- a/openlibrary/plugins/openlibrary/tests/test_home.py
+++ b/openlibrary/plugins/openlibrary/tests/test_home.py
@@ -95,9 +95,8 @@ def spoofed_generic_carousel(*args, **kwargs):
                 "cover_url": ""
             }]
 
-        monkeypatch.setattr(home, 'get_cachable_sponsorable_editions', lambda: [])
         html = six.text_type(render_template("home/index", stats=stats, test=True))
-        headers = ["Books We Love", "Recently Returned", "Kids", "Books to Sponsor",
+        headers = ["Books We Love", "Recently Returned", "Kids",
                    "Thrillers", "Romance", "Classic Books", "Textbooks"]
         for h in headers:
             assert h in html
diff --git a/openlibrary/tests/core/test_sponsors.py b/openlibrary/tests/core/test_sponsors.py
--- a/openlibrary/tests/core/test_sponsors.py
+++ b/openlibrary/tests/core/test_sponsors.py
@@ -21,44 +21,38 @@ def test_get_sponsored_editions(self, monkeypatch):
 
     def test_do_we_want_it(self, monkeypatch, mock_site):
         isbn = '0123456789'
-        work_id = 'OL1W'
-        mock_availability__reject = {
-            'OL1W': {
-                'status': 'open'
-            }
-        }
-        mock_availability__need = {
-            'OL1W': {
-                'status': 'error'
-            }
-        }
-
-        # We already have an edition for this work ...
-        monkeypatch.setattr(sponsorships.lending, 'get_work_availability',
-                            lambda work_id: mock_availability__reject)
-        dwwi, availability = do_we_want_it(isbn, work_id)
-        assert dwwi == False
-        assert availability == mock_availability__reject
 
         # Simulating exception / API call failure ...
-        monkeypatch.setattr(sponsorships.lending, 'get_work_availability',
-                            lambda work_id: mock_availability__need)
-        monkeypatch.setattr(sponsorships.requests, 'get', lambda url, **kwargs: {'invalid': 'json'})
-        dwwi, matches = do_we_want_it(isbn, work_id)
+        monkeypatch.setattr(
+            sponsorships.requests,
+            'get',
+            lambda url,
+            **kwargs: {
+                'invalid': 'json'
+            }
+        )
+        dwwi, matches = do_we_want_it(isbn)
         assert dwwi == False
         assert matches == []
 
-        # Check archive.org promise items, previous sponsorships ...
-        monkeypatch.setattr(sponsorships.lending, 'get_work_availability',
-                            lambda work_id: mock_availability__need)
-        monkeypatch.setattr(sponsorships.requests, 'get', lambda url, **kwargs: None)
-        dwwi, matches = do_we_want_it(isbn, work_id)
+        monkeypatch.setattr(
+            sponsorships.requests,
+            'get',
+            lambda url,
+            **kwargs: None
+        )
+        dwwi, matches = do_we_want_it(isbn)
         assert dwwi == False
         assert matches == []
 
         # We need a copy ...
         r = storage({'json': lambda: {"response": 1}})
-        monkeypatch.setattr(sponsorships.requests, 'get', lambda url, **kwargs: r)
-        dwwi, matches = do_we_want_it(isbn, work_id)
+        monkeypatch.setattr(
+            sponsorships.requests,
+            'get',
+            lambda url,
+            **kwargs: r
+        )
+        dwwi, matches = do_we_want_it(isbn)
         assert dwwi == True
         assert matches == []