From 9589e70ea76e9f152abd045c1d2d1c02614ae21e Mon Sep 17 00:00:00 2001 From: Tanishq Sangwan Date: Thu, 2 Apr 2026 13:23:30 +0530 Subject: [PATCH 01/22] Fix goodreads StopIteration bug and removed outdated bytes test - code kept in case --- openlibrary/plugins/upstream/account.py | 10 ++++++++-- openlibrary/plugins/upstream/tests/test_account.py | 7 ------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/openlibrary/plugins/upstream/account.py b/openlibrary/plugins/upstream/account.py index 6a52e5de47b..93cce734563 100644 --- a/openlibrary/plugins/upstream/account.py +++ b/openlibrary/plugins/upstream/account.py @@ -858,7 +858,8 @@ def GET(self): @require_login def POST(self): - books, books_wo_isbns = process_goodreads_csv(web.input()) + input_csv = web.input(csv={}) + books, books_wo_isbns = process_goodreads_csv(input_csv) return render['account/import'](books, books_wo_isbns) @@ -1379,8 +1380,13 @@ def g(*a, **kw): def process_goodreads_csv(i): + if hasattr(i.csv, 'file'): + i.csv.file.seek(0) + raw_bytes = i.csv.file.read() + else: + raw_bytes = i.csv - csv_payload = i.csv if isinstance(i.csv, str) else i.csv.decode() + csv_payload = raw_bytes if isinstance(raw_bytes, str) else raw_bytes.decode('utf-8', errors='replace') csv_file = csv.reader(csv_payload.splitlines(), delimiter=',', quotechar='"') header = next(csv_file) books = {} diff --git a/openlibrary/plugins/upstream/tests/test_account.py b/openlibrary/plugins/upstream/tests/test_account.py index a8a2174e9b2..5eb66d2f8fa 100644 --- a/openlibrary/plugins/upstream/tests/test_account.py +++ b/openlibrary/plugins/upstream/tests/test_account.py @@ -143,15 +143,8 @@ def setup_method(self, method): } } - @pytest.mark.skipif(sys.version_info < (3, 0), reason="Python2's csv module doesn't support Unicode") def test_process_goodreads_csv_with_utf8(self): books, books_wo_isbns = account.process_goodreads_csv(web.storage({"csv": self.csv_data.decode("utf-8")})) assert books == self.expected_books assert books_wo_isbns == self.expected_books_wo_isbns - def test_process_goodreads_csv_with_bytes(self): - # Note: In Python2, reading data as bytes returns a string, which should - # also be supported by account.process_goodreads_csv() - books, books_wo_isbns = account.process_goodreads_csv(web.storage({"csv": self.csv_data})) - assert books == self.expected_books - assert books_wo_isbns == self.expected_books_wo_isbns From e279e3abb12cec1990307d315ae227721dc22702 Mon Sep 17 00:00:00 2001 From: Tanishq Sangwan Date: Thu, 2 Apr 2026 13:24:07 +0530 Subject: [PATCH 02/22] Refactor Goodreads import feature layout and styles for improved usability --- openlibrary/templates/account/import.html | 273 ++++++++++++++++------ static/css/page-user.css | 42 ++-- 2 files changed, 220 insertions(+), 95 deletions(-) diff --git a/openlibrary/templates/account/import.html b/openlibrary/templates/account/import.html index 34607ff1160..75112ca5b8c 100644 --- a/openlibrary/templates/account/import.html +++ b/openlibrary/templates/account/import.html @@ -1,137 +1,256 @@ $def with (books=None, books_wo_isbns=None) -
- - $if not books: +
+ $if not books and not books_wo_isbns:
+

$_("Import from Goodreads")

$:_('For instructions on exporting data refer to: Goodreads Import/Export')

+ enctype="multipart/form-data" + class="olform olform--decoration mt-2"> +
- -
$_("File size limit 10MB and .csv file type only") + +
+ + $_("File size limit 10MB and .csv file type only") +
-
+ +
+ type="submit" + value="$_('Load Books')" + data-ol-link-track="PatronImports|Goodreads">
+ +
+

$_("Export your Reading Log")

-

$_("Download a copy of your reading log.") $_("What is a reading log?")

+

+ $_("Download a copy of your reading log.") + + $_("What is a reading log?") + +

+ enctype="multipart/form-data" + class="olform olform--decoration patron-export-form mt-2"> +
- +
+

$_("Export your book notes")

-

$_("Download a copy of your book notes.") $_("What are book notes?")

+

+ $_("Download a copy of your book notes.") + + $_("What are book notes?") + +

+ enctype="multipart/form-data" + class="olform olform--decoration patron-export-form mt-2"> +
- +
+

$_("Export your reviews")

-

$_("Download a copy of your review tags.") $_("What are review tags?")

+

+ $_("Download a copy of your review tags.") + + $_("What are review tags?") + +

+ enctype="multipart/form-data" + class="olform olform--decoration patron-export-form mt-2"> +
- +
+

$_("Export your list overview")

-

$_("Download a summary of your lists and their contents.") $_("What are lists?")

+

+ $_("Download a summary of your lists and their contents.") + + $_("What are lists?") + +

+ enctype="multipart/form-data" + class="olform olform--decoration patron-export-form mt-2"> +
- +
+

$_("Export your star ratings")

-

$_("Download a copy of your star ratings.") $_("What are star ratings?")

+

+ $_("Download a copy of your star ratings.") + + $_("What are star ratings?") + +

+ enctype="multipart/form-data" + class="olform olform--decoration patron-export-form mt-2"> +
- +
+
$else: -
- \ No newline at end of file diff --git a/static/css/page-user.css b/static/css/page-user.css index 49c343d91d2..39ba1bb219e 100644 --- a/static/css/page-user.css +++ b/static/css/page-user.css @@ -109,36 +109,42 @@ th.toggle-all { width: 100%; } -.import-table { - border: 2px; + +.my-imports { + padding: var(--spacing-page-gutter); +} + +.table-responsive { + overflow-x: auto; + width: 100%; + padding-bottom: var(--spacing-section-gap); } -.import-table th { - padding: 5px 10px; + +.import-table { + border-collapse: collapse; + border: var(--border-table); } + +.import-table th, .import-table td { - padding: 5px 10px; + padding: var(--spacing-inset-sm) var(--spacing-inset-md); + border-bottom: var(--border-table); + text-align: left; } + .import-table thead th { position: sticky; top: 0; background-color: var(--white); + z-index: 1; } -.import-table tbody { - width: 100%; -} + .import-table tbody tr { background-color: var(--white); } -.my-imports { - width: 100%; -} -.import-export { - display: flex; - flex-direction: column; -} -.import-export__section { - padding: 0 10px; -} + +.import-export {} +.import-export__section {} #myProgress { margin-top: 5px; From d798a6007e7613a4f1edbdc16c438ea6a1972258 Mon Sep 17 00:00:00 2001 From: Tanishq Sangwan Date: Thu, 2 Apr 2026 21:24:19 +0530 Subject: [PATCH 03/22] Refactor Goodreads import feature: remove legacy JavaScript and update HTML structure for improved user experience --- .../openlibrary/js/goodreads_import.js | 182 ------------- openlibrary/plugins/openlibrary/js/index.js | 5 - openlibrary/plugins/upstream/account.py | 164 +++++++++++- openlibrary/templates/account/import.html | 250 +++++++++++++----- vendor/infogami | 2 +- 5 files changed, 343 insertions(+), 260 deletions(-) delete mode 100644 openlibrary/plugins/openlibrary/js/goodreads_import.js diff --git a/openlibrary/plugins/openlibrary/js/goodreads_import.js b/openlibrary/plugins/openlibrary/js/goodreads_import.js deleted file mode 100644 index 2a1fdfd46f3..00000000000 --- a/openlibrary/plugins/openlibrary/js/goodreads_import.js +++ /dev/null @@ -1,182 +0,0 @@ -import Promise from 'promise-polyfill'; - -export function initGoodreadsImport() { - - var count, prevPromise; - - $(document).on('click', 'th.toggle-all input', function () { - var checked = $(this).prop('checked'); - $('input.add-book').each(function () { - $(this).prop('checked', checked); - if (checked) { - $(this).attr('checked', 'checked'); - } - else { - $(this).removeAttr('checked'); - } - }); - const l = $('.add-book[checked*="checked"]').length; - $('.import-submit').attr('value', `Import ${l} Books`); - }); - - $(document).on('click', 'input.add-book', function () { - if ($(this).prop('checked')) { - $(this).attr('checked', 'checked'); - } - else { - $(this).removeAttr('checked'); - } - const l = $('.add-book[checked*="checked"]').length; - $('.import-submit').attr('value', `Import ${l} Books`); - }); - - //updates the progress bar based on the book count - function func1(value) { - const l = $('.add-book[checked*="checked"]').length; - const elem = document.getElementById('myBar'); - elem.style.width = `${value * (100 / l)}%`; - elem.innerHTML = `${value} Books`; - if (value * (100 / l) >= 100) { - elem.innerHTML = ''; - $('#myBar').append(' Go to your Reading Log '); - $('.cancel-button').addClass('hidden'); - } - } - - $('.import-submit').on('click', function () { - $('#myProgress').removeClass('hidden'); - $('.cancel-button').removeClass('hidden'); - $('input.import-submit').addClass('hidden'); - $('th.import-status').removeClass('hidden'); - $('th.status-reason').removeClass('hidden'); - const shelves = { read: 3, 'currently-reading': 2, 'to-read': 1 }; - count = 0; - prevPromise = Promise.resolve(); - $('input.add-book').each(function () { - var input = $(this), - checked = input.prop('checked'); - var value = JSON.parse(input.val().replace(/'/g, '"')); - var shelf = value['Exclusive Shelf']; - var shelf_id = 0; - const hasFailure = function () { - return $(`[isbn=${value['ISBN']}]`).hasClass('import-failure'); - }; - const fail = function (reason) { - if (!hasFailure()) { - const element = $(`[isbn=${value['ISBN']}]`); - element.append(`Error${reason}'`) - element.removeClass('selected'); - element.addClass('import-failure'); - } - }; - - if (!checked) { - func1(++count); - return; - } - - if (shelves[shelf]) { - shelf_id = shelves[shelf]; - } - - //used 'return' instead of 'return false' because the loop was being exited entirely - if (shelf_id === 0) { - fail('Custom shelves are not supported'); - func1(++count); - return; - } - - prevPromise = prevPromise.then(function () { // prevPromise changes in each iteration - $(`[isbn=${value['ISBN']}]`).addClass('selected'); - return getWork(value['ISBN']); // return a new Promise - }).then(function (data) { - var obj = JSON.parse(data); - $.ajax({ - url: `${obj['works'][0].key}/bookshelves.json`, - type: 'POST', - data: { - dont_remove: true, - edition_id: obj['key'], - bookshelf_id: shelf_id - }, - dataType: 'json' - }).fail(function () { - fail('Failed to add book to reading log'); - }).done(function () { - if (value['My Rating'] !== '0') { - return $.ajax({ - url: `${obj['works'][0].key}/ratings.json`, - type: 'POST', - data: { - rating: parseInt(value['My Rating']), - edition_id: obj['key'], - bookshelf_id: shelf_id - }, - dataType: 'json', - fail: function () { - fail('Failed to add rating'); - } - }); - } - }).then(function () { - if (value['Date Read'] !== '') { - const date_read = value['Date Read'].split('/'); // Format: "YYYY/MM/DD" - return $.ajax({ - url: `${obj['works'][0].key}/check-ins`, - type: 'POST', - data: JSON.stringify({ - edition_key: obj['key'], - event_type: 3, // BookshelfEvent.FINISH - year: parseInt(date_read[0]), - month: parseInt(date_read[1]), - day: parseInt(date_read[2]) - }), - dataType: 'json', - contentType: 'application/json', - beforeSend: function (xhr) { - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.setRequestHeader('Accept', 'application/json'); - }, - fail: function () { - fail('Failed to set the read date'); - } - }); - } - }); - if (!hasFailure()) { - $(`[isbn=${value['ISBN']}]`).append('Imported') - $(`[isbn=${value['ISBN']}]`).removeClass('selected'); - } - func1(++count); - }).catch(function () { - fail('Book not in collection'); - func1(++count); - }); - }); - - $('td.books-wo-isbn').each(function () { - $(this).removeClass('hidden'); - }); - }); - - function getWork(isbn) { - return new Promise(function (resolve, reject) { - var request = new XMLHttpRequest(); - - request.open('GET', `/isbn/${isbn}.json`); - request.onload = function () { - if (request.status === 200) { - resolve(request.response); // we get the data here, so resolve the Promise - } else { - reject(Error(request.statusText)); // if status is not 200 OK, reject. - } - }; - - request.onerror = function () { - reject(Error('Error fetching data.')); // error occurred, so reject the Promise - }; - - request.send(); // send the request - }); - } -} diff --git a/openlibrary/plugins/openlibrary/js/index.js b/openlibrary/plugins/openlibrary/js/index.js index 1a262393ba0..942617a807e 100644 --- a/openlibrary/plugins/openlibrary/js/index.js +++ b/openlibrary/plugins/openlibrary/js/index.js @@ -186,11 +186,6 @@ jQuery(function () { }); } - // conditionally loads Goodreads import based on class in the page - if (document.getElementsByClassName('import-table').length) { - import(/* webpackChunkName: "goodreads-import" */'./goodreads_import.js') - .then(module => module.initGoodreadsImport()); - } // conditionally load list seed item deletion dialog functionality based on id on lists pages if (document.getElementById('listResults')) { import(/* webpackChunkName: "ListViewBody" */'./lists/ListViewBody.js'); diff --git a/openlibrary/plugins/upstream/account.py b/openlibrary/plugins/upstream/account.py index 93cce734563..0cc2b3acd14 100644 --- a/openlibrary/plugins/upstream/account.py +++ b/openlibrary/plugins/upstream/account.py @@ -8,6 +8,7 @@ from math import ceil from typing import TYPE_CHECKING, Any, Final from urllib.parse import urlparse +from collections import defaultdict import requests import web @@ -32,6 +33,7 @@ valid_email, ) from openlibrary.core import helpers as h +from openlibrary.core import db as ol_db from openlibrary.core import lending, stats from openlibrary.core.auth import ExpiredTokenError, HMACToken, MissingKeyError from openlibrary.core.auth import TimedOneTimePassword as OTP @@ -42,7 +44,7 @@ get_items_and_add_availability, s3_loan_api, ) -from openlibrary.core.models import SubjectType +from openlibrary.core.models import SubjectType, Edition from openlibrary.core.observations import Observations from openlibrary.core.ratings import Ratings from openlibrary.i18n import gettext as _ @@ -52,6 +54,7 @@ from openlibrary.plugins.upstream import borrow, forms from openlibrary.plugins.upstream.mybooks import MyBooksTemplate from openlibrary.utils.dateutil import elapsed_time +from openlibrary.utils.isbn import canonical if TYPE_CHECKING: from openlibrary.plugins.upstream.models import User, Work @@ -862,6 +865,165 @@ def POST(self): books, books_wo_isbns = process_goodreads_csv(input_csv) return render['account/import'](books, books_wo_isbns) +# TODO: Need to deal with did_not_finish shelf - discussion pending +_DEFAULT_SHELVES = { + 'to_read': 1, + 'currently_reading': 2, + 'read': 3, + 'did_not_finish': 4 +} + +def _normalize_shelf(name: str) -> str: + return name.strip().lower().translate(str.maketrans(' -', '__')) + +def _capitalize_shelf(name: str) -> str: + return name.replace('_', ' ').title() + +class process_imports(delegate.page): + path = "/account/import/process/goodreads" + + @require_login + def POST(self): + raw = web.data() + if not raw: + return delegate.RawText(json.dumps({"error": "missing_body"}), status="400 Bad Request") + + try: + data = json.loads(raw) + except json.JSONDecodeError: + return delegate.RawText(json.dumps({"error": "invalid_json"}), status="400 Bad Request") + + books = data.get("books") + if not isinstance(books, list): + return delegate.RawText(json.dumps({"error": "books_must_be_list"}), status="400 Bad Request") + + try: + user = accounts.get_current_user() + username = user.key.split('/')[-1] + + oldb = ol_db.get_db() + existing_entries = oldb.query( + "SELECT work_id, bookshelf_id FROM bookshelves_books WHERE username=$username", + vars={'username': username} + ) + + existing_shelf_set = { + (str(e.work_id), int(e.bookshelf_id)) for e in existing_entries + } + + existing_lists = user.get_lists(limit=None) + custom_list_map = { + _normalize_shelf(lst.name): lst + for lst in existing_lists + if hasattr(lst, 'name') and lst.name + } + + isbn_cache = {} + lists_to_save = set() + pending_seeds = defaultdict(list) + results = [] + + db_inserts = [] + list_keys_cache = {} + + for book in books: + raw_isbn = book.get('isbn') + if not raw_isbn: + continue + + try: + isbn = canonical(raw_isbn) + isbn_val, asin = Edition.get_isbn_or_asin(isbn) + + if not Edition.is_valid_identifier(isbn_val, asin): + results.append({"isbn": raw_isbn, "status": "error", "reason": "Invalid ISBN"}) + continue + + forms = Edition.get_identifier_forms(isbn_val, asin) + edition = next((isbn_cache[f] for f in forms if f in isbn_cache), None) + + if not edition: + edition = Edition.from_isbn(isbn) + for f in forms: + isbn_cache[f] = edition + + if not edition or not getattr(edition, 'works', None): + results.append({"isbn": raw_isbn, "status": "error", "reason": "Book not found in Open Library"}) + continue + + work_key = edition.works[0]['key'] if isinstance(edition.works[0], dict) else getattr(edition.works[0], 'key', None) + if not work_key: + results.append({"isbn": raw_isbn, "status": "error", "reason": "Missing Work mapping"}) + continue + + work_id = str(work_key.split('/')[-1][2:-1]) + edition_id = str(edition.key.split('/')[-1][2:-1]) + + shelves = set(_normalize_shelf(s) for s in book.get('shelves', [])) + + for norm_shelf in shelves: + if norm_shelf not in _DEFAULT_SHELVES and norm_shelf not in custom_list_map: + new_list = user.new_list( + _capitalize_shelf(norm_shelf), + "Imported from Goodreads", + seeds=[] + ) + custom_list_map[norm_shelf] = new_list + lists_to_save.add(norm_shelf) + + if norm_shelf in _DEFAULT_SHELVES: + shelf_id = int(_DEFAULT_SHELVES[norm_shelf]) + + if (work_id, shelf_id) in existing_shelf_set: + continue + + db_inserts.append({ + 'username': username, + 'bookshelf_id': shelf_id, + 'work_id': work_id, + 'edition_id': edition_id + }) + + existing_shelf_set.add((work_id, shelf_id)) + + elif norm_shelf in custom_list_map: + target_list = custom_list_map[norm_shelf] + + if norm_shelf not in list_keys_cache: + current_seeds = getattr(target_list, 'seeds', []) or [] + list_keys_cache[norm_shelf] = { + s.get('key') if isinstance(s, dict) else s + for s in current_seeds + } + + if work_key in list_keys_cache[norm_shelf]: + continue + + pending_seeds[norm_shelf].append({"key": work_key}) + lists_to_save.add(norm_shelf) + + list_keys_cache[norm_shelf].add(work_key) + + results.append({"isbn": raw_isbn, "status": "success"}) + + except Exception as e: + logger.error(f"Error processing book with ISBN {raw_isbn}: {e}", exc_info=True) + results.append({"isbn": raw_isbn, "status": "error", "reason": "Internal server error"}) + + if db_inserts: + oldb.multiple_insert(Bookshelves.TABLENAME, db_inserts) + + for list_name in lists_to_save: + target_list = custom_list_map[list_name] + if list_name in pending_seeds: + target_list.seeds = list(getattr(target_list, 'seeds', []) or []) + pending_seeds[list_name] + target_list._save(comment="Added books via Goodreads import") + + return delegate.RawText(json.dumps({"results": results})) + + except Exception as e: + logger.error(f"Error in process_imports: {e}", exc_info=True) + raise web.HTTPError("500 Internal Server Error", headers={"Content-Type": "application/json"}) class PatronExportException(Exception): pass diff --git a/openlibrary/templates/account/import.html b/openlibrary/templates/account/import.html index 75112ca5b8c..3adb72f37da 100644 --- a/openlibrary/templates/account/import.html +++ b/openlibrary/templates/account/import.html @@ -145,80 +145,86 @@

$_("Export your star ratings")

$else:
-
- + $if books: +
-
- +
+
+ - -
-
+ +
+
+ + $ keys = ['ISBN', 'Title', 'My Rating', 'Shelves', 'Exclusive Shelf', 'Date Read'] - $ keys = ['ISBN', 'Title', 'My Rating', 'Exclusive Shelf', 'Date Read'] - -
-

$_("Books Ready to Import")

- -
- - - - - - $for key in keys: - - - - - - - - - - $if books: - $for isbn in books: - - +
+

$_("Books Ready to Import")

+ +
+
- - $key
- $ dict = {'ISBN':'', 'Title':'', 'My Rating':'', 'Exclusive Shelf':'', 'Date Read':''} - $ k = ['ISBN', 'My Rating', 'Exclusive Shelf', 'Date Read'] - - $for key in k: - $ dict[key] = books[isbn].get(key) - - -
+ + + $for key in keys: - + + + + + - $else: - - - - -
+ + $books[isbn].get(key)$key
- $_("No valid books with ISBNs found.") -
-
-
+ + $for isbn in books: + $ shelves_raw = books[isbn].get('Bookshelves', books[isbn].get('Bookshelves with positions', '')) + $ parsed_shelves = [s.split(" (#")[0].strip() for s in str(shelves_raw).split(",") if s.strip()] + $ joined_shelves = ", ".join(parsed_shelves) + + + + + + + $for key in keys: + + $if key == 'My Rating': + $('-' if str(books[isbn].get('My Rating', '')) == '0' else books[isbn].get('My Rating', '')) + $elif key == 'Shelves': + $joined_shelves + $else: + $books[isbn].get(key, '') + + + + + + + +
+
+ + + + $else: +

+ $_("No valid books with ISBNs found to import.") +

$if books_wo_isbns:
@@ -234,7 +240,7 @@

$_("Books Missing ISBNs")

$for key in keys: - $key + $key @@ -243,8 +249,13 @@

$_("Books Missing ISBNs")

$for key in keys: - $if books_wo_isbns[id].get(key) != '=""': - $books_wo_isbns[id].get(key) + $if key == 'My Rating': + $('-' if str(books_wo_isbns[id].get('My Rating', '')) == '0' else books_wo_isbns[id].get('My Rating', '')) + $elif key == 'Shelves': + $ shelves_raw = books_wo_isbns[id].get('Bookshelves', books_wo_isbns[id].get('Bookshelves with positions', '')) + $(", ".join([s.split(" (#")[0].strip() for s in str(shelves_raw).split(",") if s.strip()])) + $else: + $books_wo_isbns[id].get(key, '-') @@ -253,4 +264,101 @@

$_("Books Missing ISBNs")

-
\ No newline at end of file + +
+ + \ No newline at end of file diff --git a/vendor/infogami b/vendor/infogami index dee3ca9cc42..f70607aa7f7 160000 --- a/vendor/infogami +++ b/vendor/infogami @@ -1 +1 @@ -Subproject commit dee3ca9cc4213459c77877c8b83c958159e8ba76 +Subproject commit f70607aa7f7bb0f531bd10c3d7b0d6e622be5d89 From a8d416937629c761c871b6be2e83d527c13ce7cc Mon Sep 17 00:00:00 2001 From: Tanishq Sangwan Date: Thu, 2 Apr 2026 21:34:43 +0530 Subject: [PATCH 04/22] Enhance Goodreads import feature: support ISBN13 and improve error handling for book imports --- openlibrary/plugins/upstream/account.py | 73 ++++++++++++++++------- openlibrary/templates/account/import.html | 24 +++++--- 2 files changed, 69 insertions(+), 28 deletions(-) diff --git a/openlibrary/plugins/upstream/account.py b/openlibrary/plugins/upstream/account.py index 0cc2b3acd14..95c6f333400 100644 --- a/openlibrary/plugins/upstream/account.py +++ b/openlibrary/plugins/upstream/account.py @@ -927,33 +927,64 @@ def POST(self): list_keys_cache = {} for book in books: - raw_isbn = book.get('isbn') - if not raw_isbn: + row_id = book.get('row_id') + + raw_isbn = str(book.get('isbn', '')).replace('="', '').replace('"', '').strip() + raw_isbn13 = str(book.get('isbn13', '')).replace('="', '').replace('"', '').strip() + + if not raw_isbn and not raw_isbn13: + results.append({"row_id": row_id, "status": "error", "reason": "No valid ISBN provided"}) continue try: - isbn = canonical(raw_isbn) - isbn_val, asin = Edition.get_isbn_or_asin(isbn) - - if not Edition.is_valid_identifier(isbn_val, asin): - results.append({"isbn": raw_isbn, "status": "error", "reason": "Invalid ISBN"}) - continue - - forms = Edition.get_identifier_forms(isbn_val, asin) - edition = next((isbn_cache[f] for f in forms if f in isbn_cache), None) - - if not edition: - edition = Edition.from_isbn(isbn) - for f in forms: - isbn_cache[f] = edition + edition = None + if raw_isbn13: + try: + isbn13_canon = canonical(raw_isbn13) + isbn_val, asin = Edition.get_isbn_or_asin(isbn13_canon) + + if Edition.is_valid_identifier(isbn_val, asin): + forms = Edition.get_identifier_forms(isbn_val, asin) + edition = next((isbn_cache[f] for f in forms if f in isbn_cache), None) + + if not edition: + edition = Edition.from_isbn(isbn13_canon) + if edition: + for f in forms: + isbn_cache[f] = edition + except Exception as e: + logger.error(f"Error resolving ISBN13 {raw_isbn13}: {e}") + + if not edition and raw_isbn: + try: + isbn_canon = canonical(raw_isbn) + isbn_val, asin = Edition.get_isbn_or_asin(isbn_canon) + + if Edition.is_valid_identifier(isbn_val, asin): + forms = Edition.get_identifier_forms(isbn_val, asin) + edition = next((isbn_cache[f] for f in forms if f in isbn_cache), None) + + if not edition: + edition = Edition.from_isbn(isbn_canon) + if edition: + for f in forms: + isbn_cache[f] = edition + except Exception as e: + logger.error(f"Error resolving ISBN {raw_isbn}: {e}") + + # 3. Validate successful resolution if not edition or not getattr(edition, 'works', None): - results.append({"isbn": raw_isbn, "status": "error", "reason": "Book not found in Open Library"}) + results.append({ + "row_id": row_id, + "status": "error", + "reason": "Book not found in Open Library" + }) continue work_key = edition.works[0]['key'] if isinstance(edition.works[0], dict) else getattr(edition.works[0], 'key', None) if not work_key: - results.append({"isbn": raw_isbn, "status": "error", "reason": "Missing Work mapping"}) + results.append({"row_id": row_id, "status": "error", "reason": "Missing Work mapping"}) continue work_id = str(work_key.split('/')[-1][2:-1]) @@ -1004,11 +1035,11 @@ def POST(self): list_keys_cache[norm_shelf].add(work_key) - results.append({"isbn": raw_isbn, "status": "success"}) + results.append({"row_id": row_id, "status": "success"}) except Exception as e: - logger.error(f"Error processing book with ISBN {raw_isbn}: {e}", exc_info=True) - results.append({"isbn": raw_isbn, "status": "error", "reason": "Internal server error"}) + logger.error(f"Error processing book with Row ID {row_id}: {e}", exc_info=True) + results.append({"row_id": row_id, "status": "error", "reason": "Internal server error"}) if db_inserts: oldb.multiple_insert(Bookshelves.TABLENAME, db_inserts) diff --git a/openlibrary/templates/account/import.html b/openlibrary/templates/account/import.html index 3adb72f37da..3e508589c05 100644 --- a/openlibrary/templates/account/import.html +++ b/openlibrary/templates/account/import.html @@ -166,7 +166,7 @@

$_("Export your star ratings")

- $ keys = ['ISBN', 'Title', 'My Rating', 'Shelves', 'Exclusive Shelf', 'Date Read'] + $ keys = ['ISBN', 'ISBN13', 'Title', 'My Rating', 'Shelves', 'Exclusive Shelf', 'Date Read']

$_("Books Ready to Import")

@@ -189,11 +189,13 @@

$_("Books Ready to Import")

$for isbn in books: - $ shelves_raw = books[isbn].get('Bookshelves', books[isbn].get('Bookshelves with positions', '')) - $ parsed_shelves = [s.split(" (#")[0].strip() for s in str(shelves_raw).split(",") if s.strip()] + $ shelves_raw = books[isbn].get('Bookshelves', '') + $ parsed_shelves = [s.strip() for s in str(shelves_raw).split(",") if s.strip()] $ joined_shelves = ", ".join(parsed_shelves) + $ clean_isbn = str(books[isbn].get('ISBN', '')).replace('="', '').replace('"', '') + $ clean_isbn13 = str(books[isbn].get('ISBN13', '')).replace('="', '').replace('"', '') - + $_("Books Ready to Import") $('-' if str(books[isbn].get('My Rating', '')) == '0' else books[isbn].get('My Rating', '')) $elif key == 'Shelves': $joined_shelves + $elif key in ('ISBN', 'ISBN13'): + $(str(books[isbn].get(key, '')).replace('="', '').replace('"', '')) $else: $books[isbn].get(key, '') @@ -252,8 +256,10 @@

$_("Books Missing ISBNs")

$if key == 'My Rating': $('-' if str(books_wo_isbns[id].get('My Rating', '')) == '0' else books_wo_isbns[id].get('My Rating', '')) $elif key == 'Shelves': - $ shelves_raw = books_wo_isbns[id].get('Bookshelves', books_wo_isbns[id].get('Bookshelves with positions', '')) - $(", ".join([s.split(" (#")[0].strip() for s in str(shelves_raw).split(",") if s.strip()])) + $ shelves_raw = books_wo_isbns[id].get('Bookshelves', '') + $(", ".join([s.strip() for s in str(shelves_raw).split(",") if s.strip()])) + $elif key in ('ISBN', 'ISBN13'): + $(str(books_wo_isbns[id].get(key, '')).replace('="', '').replace('"', '')) $else: $books_wo_isbns[id].get(key, '-') @@ -309,13 +315,17 @@

$_("Books Missing ISBNs")

const booksToImport = []; document.querySelectorAll('.add-book:checked').forEach(cb => { const row = cb.closest('tr'); + const rowid = row.dataset.rowid; const isbn = row.dataset.isbn; + const isbn13 = row.dataset.isbn13; const shelvesStr = row.dataset.shelves || ""; const shelvesArray = shelvesStr.split(',').map(s => s.trim()).filter(Boolean); booksToImport.push({ + row_id: rowid, isbn: isbn, + isbn13: isbn13, shelves: shelvesArray }); }); @@ -335,7 +345,7 @@

$_("Books Missing ISBNs")

let errors = 0; data.results.forEach(result => { - const row = document.querySelector(`tr[data-isbn="$${result.isbn}"]`); + const row = document.querySelector(`tr[data-rowid="$${result.row_id}"]`); if (!row) return; const statusCell = row.querySelector('.import-status'); From 7b0ad74e8b6c1dba2ce35c2c1194d9b2f935675e Mon Sep 17 00:00:00 2001 From: Tanishq Sangwan Date: Fri, 3 Apr 2026 11:58:42 +0530 Subject: [PATCH 05/22] Revert unintended vendor/infogami submodule update --- vendor/infogami | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/infogami b/vendor/infogami index f70607aa7f7..dee3ca9cc42 160000 --- a/vendor/infogami +++ b/vendor/infogami @@ -1 +1 @@ -Subproject commit f70607aa7f7bb0f531bd10c3d7b0d6e622be5d89 +Subproject commit dee3ca9cc4213459c77877c8b83c958159e8ba76 From 2a8431168e28d8c2ddc3f6390e9082bea3fa92f9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 06:29:44 +0000 Subject: [PATCH 06/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- openlibrary/i18n/messages.pot | 43 +++-- openlibrary/plugins/upstream/account.py | 156 +++++++++++++----- .../plugins/upstream/tests/test_account.py | 3 - openlibrary/templates/account/import.html | 14 +- static/css/page-user.css | 7 +- 5 files changed, 151 insertions(+), 72 deletions(-) diff --git a/openlibrary/i18n/messages.pot b/openlibrary/i18n/messages.pot index 8d1ee33f7b0..2f2f77149c8 100644 --- a/openlibrary/i18n/messages.pot +++ b/openlibrary/i18n/messages.pot @@ -146,11 +146,12 @@ msgstr "" msgid "Create new list" msgstr "" -#: CreateListModal.html EditButtons.html account/notifications.html -#: account/password/reset.html account/privacy.html admin/imports-add.html -#: admin/permissions.html books/add.html databarEdit.html interstitial.html -#: merge/authors.html my_books/dropdown_content.html type/list/edit.html -#: type/series/edit.html type/tag/form.html +#: CreateListModal.html EditButtons.html account/import.html +#: account/notifications.html account/password/reset.html account/privacy.html +#: admin/imports-add.html admin/permissions.html books/add.html +#: databarEdit.html interstitial.html merge/authors.html +#: my_books/dropdown_content.html type/list/edit.html type/series/edit.html +#: type/tag/form.html msgid "Cancel" msgstr "" @@ -1731,10 +1732,10 @@ msgstr "" msgid "Added Time" msgstr "" -#: account/import.html account/loans.html admin/imports.html -#: admin/imports_by_date.html admin/loans_table.html admin/menu.html -#: batch_import_pending_view.html batch_import_view.html -#: librarian_dashboard/data_quality_table.html status.html +#: account/loans.html admin/imports.html admin/imports_by_date.html +#: admin/loans_table.html admin/menu.html batch_import_pending_view.html +#: batch_import_view.html librarian_dashboard/data_quality_table.html +#: status.html msgid "Status" msgstr "" @@ -1779,8 +1780,8 @@ msgstr "" msgid "Import Time" msgstr "" -#: account/import.html admin/imports.html admin/imports_by_date.html -#: batch_import_view.html librarian_dashboard/data_quality_table.html +#: admin/imports.html admin/imports_by_date.html batch_import_view.html +#: librarian_dashboard/data_quality_table.html msgid "Error" msgstr "" @@ -2870,15 +2871,29 @@ msgid "What are star ratings?" msgstr "" #: account/import.html -msgid "Importing..." +msgid "Import Books" msgstr "" #: account/import.html -msgid "Reason" +msgid "Books Ready to Import" msgstr "" #: account/import.html -msgid "No ISBN" +msgid "No valid books with ISBNs found to import." +msgstr "" + +#: account/import.html +msgid "Books Missing ISBNs" +msgstr "" + +#: account/import.html +msgid "Note:" +msgstr "" + +#: account/import.html +msgid "" +"Unfortunately, these books do not have ISBN codes and currently we can't " +"import these." msgstr "" #: account/loan_history.html diff --git a/openlibrary/plugins/upstream/account.py b/openlibrary/plugins/upstream/account.py index 95c6f333400..ee494ce9633 100644 --- a/openlibrary/plugins/upstream/account.py +++ b/openlibrary/plugins/upstream/account.py @@ -3,12 +3,12 @@ import json import logging from abc import ABC, abstractmethod +from collections import defaultdict from datetime import datetime from enum import Enum from math import ceil from typing import TYPE_CHECKING, Any, Final from urllib.parse import urlparse -from collections import defaultdict import requests import web @@ -32,8 +32,8 @@ clear_cookies, valid_email, ) -from openlibrary.core import helpers as h from openlibrary.core import db as ol_db +from openlibrary.core import helpers as h from openlibrary.core import lending, stats from openlibrary.core.auth import ExpiredTokenError, HMACToken, MissingKeyError from openlibrary.core.auth import TimedOneTimePassword as OTP @@ -44,7 +44,7 @@ get_items_and_add_availability, s3_loan_api, ) -from openlibrary.core.models import SubjectType, Edition +from openlibrary.core.models import Edition, SubjectType from openlibrary.core.observations import Observations from openlibrary.core.ratings import Ratings from openlibrary.i18n import gettext as _ @@ -865,20 +865,24 @@ def POST(self): books, books_wo_isbns = process_goodreads_csv(input_csv) return render['account/import'](books, books_wo_isbns) + # TODO: Need to deal with did_not_finish shelf - discussion pending _DEFAULT_SHELVES = { 'to_read': 1, 'currently_reading': 2, 'read': 3, - 'did_not_finish': 4 + 'did_not_finish': 4, } + def _normalize_shelf(name: str) -> str: return name.strip().lower().translate(str.maketrans(' -', '__')) + def _capitalize_shelf(name: str) -> str: return name.replace('_', ' ').title() + class process_imports(delegate.page): path = "/account/import/process/goodreads" @@ -886,16 +890,22 @@ class process_imports(delegate.page): def POST(self): raw = web.data() if not raw: - return delegate.RawText(json.dumps({"error": "missing_body"}), status="400 Bad Request") + return delegate.RawText( + json.dumps({"error": "missing_body"}), status="400 Bad Request" + ) try: data = json.loads(raw) except json.JSONDecodeError: - return delegate.RawText(json.dumps({"error": "invalid_json"}), status="400 Bad Request") + return delegate.RawText( + json.dumps({"error": "invalid_json"}), status="400 Bad Request" + ) books = data.get("books") if not isinstance(books, list): - return delegate.RawText(json.dumps({"error": "books_must_be_list"}), status="400 Bad Request") + return delegate.RawText( + json.dumps({"error": "books_must_be_list"}), status="400 Bad Request" + ) try: user = accounts.get_current_user() @@ -904,9 +914,9 @@ def POST(self): oldb = ol_db.get_db() existing_entries = oldb.query( "SELECT work_id, bookshelf_id FROM bookshelves_books WHERE username=$username", - vars={'username': username} + vars={'username': username}, ) - + existing_shelf_set = { (str(e.work_id), int(e.bookshelf_id)) for e in existing_entries } @@ -922,18 +932,31 @@ def POST(self): lists_to_save = set() pending_seeds = defaultdict(list) results = [] - - db_inserts = [] + + db_inserts = [] list_keys_cache = {} for book in books: row_id = book.get('row_id') - - raw_isbn = str(book.get('isbn', '')).replace('="', '').replace('"', '').strip() - raw_isbn13 = str(book.get('isbn13', '')).replace('="', '').replace('"', '').strip() + + raw_isbn = ( + str(book.get('isbn', '')).replace('="', '').replace('"', '').strip() + ) + raw_isbn13 = ( + str(book.get('isbn13', '')) + .replace('="', '') + .replace('"', '') + .strip() + ) if not raw_isbn and not raw_isbn13: - results.append({"row_id": row_id, "status": "error", "reason": "No valid ISBN provided"}) + results.append( + { + "row_id": row_id, + "status": "error", + "reason": "No valid ISBN provided", + } + ) continue try: @@ -943,11 +966,14 @@ def POST(self): try: isbn13_canon = canonical(raw_isbn13) isbn_val, asin = Edition.get_isbn_or_asin(isbn13_canon) - + if Edition.is_valid_identifier(isbn_val, asin): forms = Edition.get_identifier_forms(isbn_val, asin) - edition = next((isbn_cache[f] for f in forms if f in isbn_cache), None) - + edition = next( + (isbn_cache[f] for f in forms if f in isbn_cache), + None, + ) + if not edition: edition = Edition.from_isbn(isbn13_canon) if edition: @@ -960,11 +986,14 @@ def POST(self): try: isbn_canon = canonical(raw_isbn) isbn_val, asin = Edition.get_isbn_or_asin(isbn_canon) - + if Edition.is_valid_identifier(isbn_val, asin): forms = Edition.get_identifier_forms(isbn_val, asin) - edition = next((isbn_cache[f] for f in forms if f in isbn_cache), None) - + edition = next( + (isbn_cache[f] for f in forms if f in isbn_cache), + None, + ) + if not edition: edition = Edition.from_isbn(isbn_canon) if edition: @@ -975,16 +1004,28 @@ def POST(self): # 3. Validate successful resolution if not edition or not getattr(edition, 'works', None): - results.append({ - "row_id": row_id, - "status": "error", - "reason": "Book not found in Open Library" - }) + results.append( + { + "row_id": row_id, + "status": "error", + "reason": "Book not found in Open Library", + } + ) continue - work_key = edition.works[0]['key'] if isinstance(edition.works[0], dict) else getattr(edition.works[0], 'key', None) + work_key = ( + edition.works[0]['key'] + if isinstance(edition.works[0], dict) + else getattr(edition.works[0], 'key', None) + ) if not work_key: - results.append({"row_id": row_id, "status": "error", "reason": "Missing Work mapping"}) + results.append( + { + "row_id": row_id, + "status": "error", + "reason": "Missing Work mapping", + } + ) continue work_id = str(work_key.split('/')[-1][2:-1]) @@ -993,11 +1034,14 @@ def POST(self): shelves = set(_normalize_shelf(s) for s in book.get('shelves', [])) for norm_shelf in shelves: - if norm_shelf not in _DEFAULT_SHELVES and norm_shelf not in custom_list_map: + if ( + norm_shelf not in _DEFAULT_SHELVES + and norm_shelf not in custom_list_map + ): new_list = user.new_list( _capitalize_shelf(norm_shelf), "Imported from Goodreads", - seeds=[] + seeds=[], ) custom_list_map[norm_shelf] = new_list lists_to_save.add(norm_shelf) @@ -1008,18 +1052,20 @@ def POST(self): if (work_id, shelf_id) in existing_shelf_set: continue - db_inserts.append({ - 'username': username, - 'bookshelf_id': shelf_id, - 'work_id': work_id, - 'edition_id': edition_id - }) - + db_inserts.append( + { + 'username': username, + 'bookshelf_id': shelf_id, + 'work_id': work_id, + 'edition_id': edition_id, + } + ) + existing_shelf_set.add((work_id, shelf_id)) elif norm_shelf in custom_list_map: target_list = custom_list_map[norm_shelf] - + if norm_shelf not in list_keys_cache: current_seeds = getattr(target_list, 'seeds', []) or [] list_keys_cache[norm_shelf] = { @@ -1032,14 +1078,23 @@ def POST(self): pending_seeds[norm_shelf].append({"key": work_key}) lists_to_save.add(norm_shelf) - - list_keys_cache[norm_shelf].add(work_key) + + list_keys_cache[norm_shelf].add(work_key) results.append({"row_id": row_id, "status": "success"}) except Exception as e: - logger.error(f"Error processing book with Row ID {row_id}: {e}", exc_info=True) - results.append({"row_id": row_id, "status": "error", "reason": "Internal server error"}) + logger.error( + f"Error processing book with Row ID {row_id}: {e}", + exc_info=True, + ) + results.append( + { + "row_id": row_id, + "status": "error", + "reason": "Internal server error", + } + ) if db_inserts: oldb.multiple_insert(Bookshelves.TABLENAME, db_inserts) @@ -1047,14 +1102,21 @@ def POST(self): for list_name in lists_to_save: target_list = custom_list_map[list_name] if list_name in pending_seeds: - target_list.seeds = list(getattr(target_list, 'seeds', []) or []) + pending_seeds[list_name] + target_list.seeds = ( + list(getattr(target_list, 'seeds', []) or []) + + pending_seeds[list_name] + ) target_list._save(comment="Added books via Goodreads import") return delegate.RawText(json.dumps({"results": results})) except Exception as e: logger.error(f"Error in process_imports: {e}", exc_info=True) - raise web.HTTPError("500 Internal Server Error", headers={"Content-Type": "application/json"}) + raise web.HTTPError( + "500 Internal Server Error", + headers={"Content-Type": "application/json"}, + ) + class PatronExportException(Exception): pass @@ -1579,7 +1641,11 @@ def process_goodreads_csv(i): else: raw_bytes = i.csv - csv_payload = raw_bytes if isinstance(raw_bytes, str) else raw_bytes.decode('utf-8', errors='replace') + csv_payload = ( + raw_bytes + if isinstance(raw_bytes, str) + else raw_bytes.decode('utf-8', errors='replace') + ) csv_file = csv.reader(csv_payload.splitlines(), delimiter=',', quotechar='"') header = next(csv_file) books = {} diff --git a/openlibrary/plugins/upstream/tests/test_account.py b/openlibrary/plugins/upstream/tests/test_account.py index 5eb66d2f8fa..8bb2e89e9c0 100644 --- a/openlibrary/plugins/upstream/tests/test_account.py +++ b/openlibrary/plugins/upstream/tests/test_account.py @@ -1,7 +1,5 @@ import os -import sys -import pytest import web from .. import account @@ -147,4 +145,3 @@ def test_process_goodreads_csv_with_utf8(self): books, books_wo_isbns = account.process_goodreads_csv(web.storage({"csv": self.csv_data.decode("utf-8")})) assert books == self.expected_books assert books_wo_isbns == self.expected_books_wo_isbns - diff --git a/openlibrary/templates/account/import.html b/openlibrary/templates/account/import.html index 3e508589c05..10a513bc652 100644 --- a/openlibrary/templates/account/import.html +++ b/openlibrary/templates/account/import.html @@ -10,7 +10,7 @@

$_("Import from Goodreads")

- +

@@ -41,7 +41,7 @@

$_("Export your Reading Log")

- +
$_("Export your book notes") - +
$_("Export your reviews") - +
$_("Export your list overview") - +
$_("Export your star ratings") - +
$_("Books Missing ISBNs") const isbn = row.dataset.isbn; const isbn13 = row.dataset.isbn13; const shelvesStr = row.dataset.shelves || ""; - + const shelvesArray = shelvesStr.split(',').map(s => s.trim()).filter(Boolean); booksToImport.push({ diff --git a/static/css/page-user.css b/static/css/page-user.css index 39ba1bb219e..8f0af53602a 100644 --- a/static/css/page-user.css +++ b/static/css/page-user.css @@ -109,7 +109,6 @@ th.toggle-all { width: 100%; } - .my-imports { padding: var(--spacing-page-gutter); } @@ -143,8 +142,10 @@ th.toggle-all { background-color: var(--white); } -.import-export {} -.import-export__section {} +.import-export { +} +.import-export__section { +} #myProgress { margin-top: 5px; From 1b93bbb350de0826e17fec040948da01436ceb96 Mon Sep 17 00:00:00 2001 From: Tanishq Sangwan Date: Fri, 3 Apr 2026 12:35:57 +0530 Subject: [PATCH 07/22] Improve Goodreads import feature: enhance error handling, update UI elements, and add internationalization support --- openlibrary/plugins/upstream/account.py | 18 +++++++---- openlibrary/templates/account/import.html | 39 +++++++++++++++++------ static/css/page-user.css | 10 ++---- 3 files changed, 44 insertions(+), 23 deletions(-) diff --git a/openlibrary/plugins/upstream/account.py b/openlibrary/plugins/upstream/account.py index 95c6f333400..bc7a2d1094c 100644 --- a/openlibrary/plugins/upstream/account.py +++ b/openlibrary/plugins/upstream/account.py @@ -865,12 +865,10 @@ def POST(self): books, books_wo_isbns = process_goodreads_csv(input_csv) return render['account/import'](books, books_wo_isbns) -# TODO: Need to deal with did_not_finish shelf - discussion pending _DEFAULT_SHELVES = { 'to_read': 1, 'currently_reading': 2, 'read': 3, - 'did_not_finish': 4 } def _normalize_shelf(name: str) -> str: @@ -886,16 +884,16 @@ class process_imports(delegate.page): def POST(self): raw = web.data() if not raw: - return delegate.RawText(json.dumps({"error": "missing_body"}), status="400 Bad Request") + return delegate.RawText(json.dumps({"error": "missing_body"}), status="400 Bad Request", content_type="application/json") try: data = json.loads(raw) except json.JSONDecodeError: - return delegate.RawText(json.dumps({"error": "invalid_json"}), status="400 Bad Request") + return delegate.RawText(json.dumps({"error": "invalid_json"}), status="400 Bad Request", content_type="application/json") books = data.get("books") if not isinstance(books, list): - return delegate.RawText(json.dumps({"error": "books_must_be_list"}), status="400 Bad Request") + return delegate.RawText(json.dumps({"error": "books_must_be_list"}), status="400 Bad Request", content_type="application/json") try: user = accounts.get_current_user() @@ -927,6 +925,14 @@ def POST(self): list_keys_cache = {} for book in books: + if not isinstance(book, dict): + results.append({ + "row_id": None, + "status": "error", + "reason": "Invalid book format payload. Expected a dictionary object." + }) + continue + row_id = book.get('row_id') raw_isbn = str(book.get('isbn', '')).replace('="', '').replace('"', '').strip() @@ -1050,7 +1056,7 @@ def POST(self): target_list.seeds = list(getattr(target_list, 'seeds', []) or []) + pending_seeds[list_name] target_list._save(comment="Added books via Goodreads import") - return delegate.RawText(json.dumps({"results": results})) + return delegate.RawText(json.dumps({"results": results}), content_type="application/json") except Exception as e: logger.error(f"Error in process_imports: {e}", exc_info=True) diff --git a/openlibrary/templates/account/import.html b/openlibrary/templates/account/import.html index 3e508589c05..1ea234925be 100644 --- a/openlibrary/templates/account/import.html +++ b/openlibrary/templates/account/import.html @@ -144,6 +144,7 @@

$_("Export your star ratings")

$else:
+ $ keys = ['ISBN', 'ISBN13', 'Title', 'My Rating', 'Shelves', 'Exclusive Shelf', 'Date Read'] $if books: $_("Export your star ratings")
- $ keys = ['ISBN', 'ISBN13', 'Title', 'My Rating', 'Shelves', 'Exclusive Shelf', 'Date Read']

$_("Books Ready to Import")

@@ -182,8 +182,8 @@

$_("Books Ready to Import")

$for key in keys: $key - Status - Reason + $_('Status') + $_('Reason') @@ -273,12 +273,29 @@

$_("Books Missing ISBNs")

+ + \ No newline at end of file From 6f7d8efe40dbb904c73bafc5ebd29fd485625d96 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:42:52 +0000 Subject: [PATCH 20/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- openlibrary/i18n/messages.pot | 12 +++++++ openlibrary/plugins/upstream/data_import.py | 17 +++++---- openlibrary/templates/account/import.html | 38 ++++++++++----------- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/openlibrary/i18n/messages.pot b/openlibrary/i18n/messages.pot index e972da55d63..49e6408f089 100644 --- a/openlibrary/i18n/messages.pot +++ b/openlibrary/i18n/messages.pot @@ -2874,6 +2874,10 @@ msgstr "" msgid "Import Books" msgstr "" +#: account/import.html +msgid "Go to My Books" +msgstr "" + #: account/import.html msgid "Books Ready to Import" msgstr "" @@ -2904,6 +2908,14 @@ msgid "" "import these." msgstr "" +#: account/import.html +msgid "Action" +msgstr "" + +#: account/import.html +msgid "Search OL" +msgstr "" + #: account/import.html msgid "Importing, please wait..." msgstr "" diff --git a/openlibrary/plugins/upstream/data_import.py b/openlibrary/plugins/upstream/data_import.py index c6aef09b577..5df7655087d 100644 --- a/openlibrary/plugins/upstream/data_import.py +++ b/openlibrary/plugins/upstream/data_import.py @@ -60,7 +60,8 @@ def _prepare_context(user, username, oldb): return { "books_in_bookshelves": { # To check if book already in bookshelf - (str(e.work_id), int(e.bookshelf_id)) for e in books_already_in_default_bookshelves + (str(e.work_id), int(e.bookshelf_id)) + for e in books_already_in_default_bookshelves }, "lists_map": { # list-name: list object _normalize_shelf_name(lst.name): lst @@ -100,12 +101,14 @@ def _process_this_books_shelves( shelf_id = _DEFAULT_SHELVES[norm_shelf] if (work_id, shelf_id) not in ctx['books_in_bookshelves']: - ctx['pending_bookshelf_inserts'].append({ - 'username': username, - 'bookshelf_id': shelf_id, - 'work_id': work_id, - 'edition_id': edition_id, - }) + ctx['pending_bookshelf_inserts'].append( + { + 'username': username, + 'bookshelf_id': shelf_id, + 'work_id': work_id, + 'edition_id': edition_id, + } + ) ctx['books_in_bookshelves'].add((work_id, shelf_id)) # 3. Handle adding books to custom lists diff --git a/openlibrary/templates/account/import.html b/openlibrary/templates/account/import.html index 091e756931c..313a0a7d747 100644 --- a/openlibrary/templates/account/import.html +++ b/openlibrary/templates/account/import.html @@ -164,9 +164,9 @@

$_("Export your star ratings")

onclick="window.location.reload();"> $_("Cancel") - -