diff --git a/bundlesize.config.json b/bundlesize.config.json
index 2224e73ff88..96f4cb84530 100644
--- a/bundlesize.config.json
+++ b/bundlesize.config.json
@@ -40,10 +40,6 @@
"path": "static/build/js/readinglog-stats.*.js",
"maxSize": "137KB"
},
- {
- "path": "static/build/js/goodreads-import.*.js",
- "maxSize": "2KB"
- },
{
"path": "static/build/js/admin.*.js",
"maxSize": "1.3KB"
diff --git a/openlibrary/i18n/messages.pot b/openlibrary/i18n/messages.pot
index 8d1ee33f7b0..f68d365de66 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 ""
@@ -2870,7 +2871,30 @@ msgid "What are star ratings?"
msgstr ""
#: account/import.html
-msgid "Importing..."
+msgid "Import Books"
+msgstr ""
+
+#: account/import.html
+msgid "Go to My Books"
+msgstr ""
+
+#: account/import.html
+msgid "Books Ready to Import"
+msgstr ""
+
+#: account/import.html
+msgid "Note:"
+msgstr ""
+
+#: account/import.html
+msgid ""
+"Books from your 'Did Not Finish' shelf will be skipped during import as "
+"we do not currently support it. We plan to add support for this in the "
+"future."
+msgstr ""
+
+#: account/import.html
+msgid "Select all books ready to import"
msgstr ""
#: account/import.html
@@ -2878,7 +2902,53 @@ msgid "Reason"
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 ""
+"Unfortunately, these books do not have ISBN codes and currently we can't "
+"import these."
+msgstr ""
+
+#: account/import.html
+msgid "Action"
+msgstr ""
+
+#: account/import.html
+msgid "Search OL"
+msgstr ""
+
+#: account/import.html
+msgid "Importing, please wait..."
+msgstr ""
+
+#: account/import.html
+msgid "Imported"
+msgstr ""
+
+#: account/import.html
+msgid "Unknown error"
+msgstr ""
+
+#: account/import.html
+msgid "Something went wrong. Try again."
+msgstr ""
+
+#: account/import.html
+msgid "Import 1 Book"
+msgstr ""
+
+#: account/import.html
+msgid "Import {count} Books"
+msgstr ""
+
+#: account/import.html
+msgid "Done - {succeeded} imported, {errors} failed"
msgstr ""
#: account/loan_history.html
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 6a52e5de47b..f74bf7cf28e 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,17 @@ def g(*a, **kw):
def process_goodreads_csv(i):
-
- csv_payload = i.csv if isinstance(i.csv, str) else i.csv.decode()
+ if hasattr(i.csv, 'file'):
+ i.csv.file.seek(0)
+ raw_bytes = i.csv.file.read()
+ else:
+ raw_bytes = i.csv
+
+ 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/code.py b/openlibrary/plugins/upstream/code.py
index fd77cd8257a..1bdba95b290 100644
--- a/openlibrary/plugins/upstream/code.py
+++ b/openlibrary/plugins/upstream/code.py
@@ -29,6 +29,7 @@
borrow, # noqa: F401 side effects may be needed
checkins,
covers,
+ data_import,
edits,
merge_authors,
models,
@@ -386,6 +387,7 @@ def setup():
edits.setup()
checkins.setup()
yearly_reading_goals.setup()
+ data_import.setup()
from openlibrary.plugins.upstream import data, jsdef
diff --git a/openlibrary/plugins/upstream/data_import.py b/openlibrary/plugins/upstream/data_import.py
new file mode 100644
index 00000000000..4f0a202905c
--- /dev/null
+++ b/openlibrary/plugins/upstream/data_import.py
@@ -0,0 +1,340 @@
+import json
+import logging
+from collections import defaultdict
+
+import web
+
+from infogami.utils import delegate
+from infogami.utils.view import require_login
+from openlibrary import accounts
+from openlibrary.core import db
+from openlibrary.core.bookshelves import Bookshelves
+from openlibrary.core.bookshelves_events import BookshelfEvent, BookshelvesEvents
+from openlibrary.core.models import Edition, Ratings
+from openlibrary.plugins.upstream.checkins import make_date_string
+from openlibrary.utils import extract_numeric_id_from_olid
+
+logger = logging.getLogger("openlibrary.dataimporter")
+
+_DEFAULT_SHELVES = {
+ 'to_read': 1,
+ 'currently_reading': 2,
+ 'read': 3,
+}
+
+_IGNORED_SHELVES = {'did_not_finish'}
+
+
+def _normalize_shelf_name(name: str) -> str:
+ """Standardizes shelf names for consistent lookup and creation."""
+ return name.strip().lower().translate(str.maketrans(' -', '__'))
+
+
+def _get_edition_from_isbn(isbn, isbn_cache):
+ """Retrieves an Edition via ISBN/ASIN, using a memory cache to avoid redundant lookups."""
+ if not isbn:
+ return None
+
+ isbn_val, asin = Edition.get_isbn_or_asin(isbn)
+ if not Edition.is_valid_identifier(isbn_val, asin):
+ return None
+
+ forms = Edition.get_identifier_forms(isbn_val, asin)
+
+ for f in forms:
+ if f in isbn_cache:
+ return isbn_cache[f]
+
+ edition = Edition.from_isbn(isbn)
+ if edition:
+ for f in forms:
+ isbn_cache[f] = edition
+
+ return edition
+
+
+def _prepare_context(user, username, oldb):
+ """Initializes the shared memory state and queues for batch database inserts."""
+ books_already_in_default_bookshelves = oldb.query(
+ "SELECT work_id, bookshelf_id FROM bookshelves_books WHERE username=$username",
+ vars={'username': username},
+ )
+
+ 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
+ },
+ "lists_map": { # list-name: list object
+ _normalize_shelf_name(lst.name): lst
+ for lst in user.get_lists(limit=None)
+ if getattr(lst, 'name', None)
+ },
+ "isbn_cache": {},
+ "works_in_list_cache": {},
+ "lists_to_save": set(), # Tracks dirty lists
+ "pending_seeds": defaultdict(
+ list
+ ), # Queues new books to be added to custom lists
+ "pending_bookshelf_inserts": [], # Queues raw rows for the bookshelves_books table
+ "pending_dateread_events": [], # Queues raw rows for the bookshelves_events (dates read) table
+ }
+
+
+def _process_this_books_shelves(
+ book, user, username, work_id, work_key, edition_id, ctx
+):
+ """Maps Goodreads shelves to Open Library shelves/lists, and handles list/book insertions."""
+ shelves = {_normalize_shelf_name(s) for s in book.get('shelves', [])}
+
+ for norm_shelf in shelves:
+ # Ignore the did_not_finish shelf
+ if norm_shelf in _IGNORED_SHELVES:
+ continue
+
+ # 1. Handle creation of completely new custom lists
+ if norm_shelf not in _DEFAULT_SHELVES and norm_shelf not in ctx['lists_map']:
+ new_list = user.new_list(
+ norm_shelf.replace('_', ' ').title(),
+ "Imported from Goodreads",
+ seeds=[],
+ )
+ ctx['lists_map'][norm_shelf] = new_list
+ ctx['lists_to_save'].add(norm_shelf)
+
+ # 2. Handle default Open Library shelves (Read, Currently Reading, Want to Read)
+ if norm_shelf in _DEFAULT_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['books_in_bookshelves'].add((work_id, shelf_id))
+
+ # 3. Handle adding books to custom lists
+ elif norm_shelf in ctx['lists_map']:
+ target_list = ctx['lists_map'][norm_shelf]
+
+ # Lazy-load existing seeds for this list into memory on first access
+ if norm_shelf not in ctx['works_in_list_cache']:
+ ctx['works_in_list_cache'][norm_shelf] = {
+ s.key if hasattr(s, 'key') else s.get('key')
+ for s in (getattr(target_list, 'seeds', []) or [])
+ }
+
+ if work_key in ctx['works_in_list_cache'][norm_shelf]:
+ continue
+
+ ctx['pending_seeds'][norm_shelf].append({"key": work_key})
+ ctx['lists_to_save'].add(norm_shelf)
+ ctx['works_in_list_cache'][norm_shelf].add(work_key)
+
+
+def _process_this_books_rating(book, username, work_id, edition_id, ctx):
+ """
+ Processes the 1-5 star user rating for a book.
+ Updates the context to prevent `_process_this_books_shelves` from writing
+ duplicate "Already Read" entries, as Ratings.add() enforces this inherently.
+ """
+ raw_rating = book.get('rating')
+
+ if not raw_rating or raw_rating == "0":
+ return
+
+ try:
+ rating_val = int(raw_rating)
+ if 1 <= rating_val <= 5:
+ # Prevent double-insert conflict: Ratings.add() forces the book onto the 'Read' shelf.
+ # We add it to the context here so the shelf processor skips queueing it a second time.
+ ctx["books_in_bookshelves"].add((int(work_id), _DEFAULT_SHELVES['read']))
+
+ Ratings.add(
+ username=username,
+ work_id=work_id,
+ rating=rating_val,
+ edition_id=edition_id,
+ )
+ except (ValueError, TypeError):
+ logger.warning(
+ f"Invalid rating value '{raw_rating}' for row {book.get('row_id', '')}"
+ )
+
+
+def _process_this_book_date_read(book, username, work_id, edition_id, ctx):
+ """Parses completion dates and queues the event insertion."""
+ raw_date_read = book.get('date_read')
+
+ if not raw_date_read:
+ return
+
+ try:
+ parts = [
+ int(p)
+ for p in str(raw_date_read).replace("/", "-").strip().split("-")
+ if p.strip()
+ ]
+ if not parts:
+ return
+
+ year = parts[0]
+ month = parts[1] if len(parts) > 1 else None
+ day = parts[2] if len(parts) > 2 else None
+
+ ctx["pending_dateread_events"].append(
+ {
+ 'username': username,
+ 'work_id': work_id,
+ 'edition_id': edition_id or BookshelvesEvents.NULL_EDITION_ID,
+ 'event_date': make_date_string(year, month, day),
+ 'event_type': BookshelfEvent.FINISH,
+ }
+ )
+ except (ValueError, TypeError) as e:
+ logger.warning(
+ f"Failed to parse date_read '{raw_date_read}' for row {book.get('row_id', '')}: {e}"
+ )
+
+
+def _process_book(book, user, username, ctx):
+ """Master processor for resolving a book's identity and delegating its data processing."""
+ if not isinstance(book, dict):
+ return {
+ "row_id": None,
+ "status": "error",
+ "reason": "Invalid book format payload. Expected a dictionary object.",
+ }
+
+ row_id = book.get('row_id', '')
+
+ isbn = str(book.get('isbn', '')).replace('="', '').replace('"', '').strip()
+ isbn13 = str(book.get('isbn13', '')).replace('="', '').replace('"', '').strip()
+
+ if not isbn and not isbn13:
+ return {"row_id": row_id, "status": "error", "reason": "No valid ISBN provided"}
+
+ try:
+ edition = _get_edition_from_isbn(
+ isbn13, ctx["isbn_cache"]
+ ) or _get_edition_from_isbn(isbn, ctx["isbn_cache"])
+ if not edition or not getattr(edition, 'works', None):
+ return {
+ "row_id": row_id,
+ "status": "error",
+ "reason": "Book not found in Open Library",
+ }
+
+ work_key = (
+ edition.works[0]['key']
+ if isinstance(edition.works[0], dict)
+ else getattr(edition.works[0], 'key', None)
+ )
+ if not work_key:
+ return {
+ "row_id": row_id,
+ "status": "error",
+ "reason": "Missing Work mapping",
+ }
+
+ work_id = extract_numeric_id_from_olid(work_key)
+ edition_id = extract_numeric_id_from_olid(edition.key)
+
+ # IMPORTANT:
+ # Ratings must be processed first so it can register the "Already Read" side-effect
+ # in the context dictionary before the shelves processor runs.
+ _process_this_books_rating(book, username, work_id, edition_id, ctx)
+ _process_this_books_shelves(
+ book, user, username, work_id, work_key, edition_id, ctx
+ )
+ _process_this_book_date_read(book, username, work_id, edition_id, ctx)
+
+ return {"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)
+ return {"row_id": row_id, "status": "error", "reason": "Internal server error"}
+
+
+def _commit_changes(oldb, ctx):
+ """Executes all bulk inserts and saves custom list modifications."""
+ if ctx["pending_bookshelf_inserts"]:
+ oldb.multiple_insert(Bookshelves.TABLENAME, ctx["pending_bookshelf_inserts"])
+
+ if ctx["pending_dateread_events"]:
+ oldb.multiple_insert(
+ BookshelvesEvents.TABLENAME, ctx["pending_dateread_events"]
+ )
+
+ for list_name in ctx["lists_to_save"]:
+ target_list = ctx["lists_map"][list_name]
+
+ if list_name in ctx["pending_seeds"]:
+ target_list.seeds = (
+ list(getattr(target_list, 'seeds', []) or [])
+ + ctx["pending_seeds"][list_name]
+ )
+
+ target_list._save(comment="Added books via Goodreads import")
+
+
+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",
+ content_type="application/json",
+ )
+
+ try:
+ data = json.loads(raw)
+ except json.JSONDecodeError:
+ 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",
+ content_type="application/json",
+ )
+
+ try:
+ user = accounts.get_current_user()
+ username = user.get_username()
+ oldb = db.get_db()
+
+ ctx = _prepare_context(user, username, oldb)
+
+ results = [_process_book(book, user, username, ctx) for book in books]
+
+ _commit_changes(oldb, ctx)
+
+ 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)
+ raise web.HTTPError(
+ "500 Internal Server Error",
+ headers={"Content-Type": "application/json"},
+ )
+
+
+def setup():
+ pass
diff --git a/openlibrary/plugins/upstream/tests/test_account.py b/openlibrary/plugins/upstream/tests/test_account.py
index a8a2174e9b2..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
@@ -143,15 +141,7 @@ 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
diff --git a/openlibrary/templates/account/import.html b/openlibrary/templates/account/import.html
index 34607ff1160..2e6a864c620 100644
--- a/openlibrary/templates/account/import.html
+++ b/openlibrary/templates/account/import.html
@@ -1,137 +1,520 @@
$def with (books=None, books_wo_isbns=None)
-
-
- $if not books:
+
+ $if not books and not books_wo_isbns:
+
$else:
-
+
+ $ keys = ['ISBN', 'ISBN13', 'Title', 'My Rating', 'Shelves', 'Date Read']
+
+ $if books:
+
+
+
+
+
+
+
+
+
$_("Books Ready to Import")
+
+
+ $_("Note:")
+ $_("Books from your 'Did Not Finish' shelf will be skipped during import as we do not currently support it. We plan to add support for this in the future.")
+
+
+
+
+
+
+
+
+
+
+ $for key in keys:
+ $key
+
+ $_('Status')
+ $_('Reason')
+
+
+
+
+ $for isbn in books:
+ $ shelves_raw = books[isbn].get('Bookshelves', '')
+ $ parsed_shelves = [s.strip() for s in str(shelves_raw).split(",") if s.strip()]
+ $ exclusive_shelf = str(books[isbn].get('Exclusive Shelf', '')).strip()
+ $ exclusive_shelf and exclusive_shelf not in parsed_shelves and parsed_shelves.append(exclusive_shelf)
+ $ joined_shelves = ", ".join(parsed_shelves)
+ $ clean_isbn = str(books[isbn].get("ISBN"))
+ $ clean_isbn13 = str(books[isbn].get("ISBN13"))
+ $ rating = str(books[isbn].get('My Rating', ''))
+ $ date_read = str(books[isbn].get('Date Read', ''))
+
+
+
+
+
+
+ $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
+ $elif key in ('ISBN', 'ISBN13'):
+ $(str(books[isbn].get(key, '')).replace('="', '').replace('"', ''))
+ $else:
+ $books[isbn].get(key, '')
+
+
+
+
+
+
+
+
+
+
+
+
+ $else:
+
+ $_("No valid books with ISBNs found to import.")
+
+
+ $if books_wo_isbns:
+
+
$_("Books Missing ISBNs")
+
+
+ $_("Note:")
+ $_("Unfortunately, these books do not have ISBN codes and currently we can't import these.")
+
+
+
+
+
+
+ $for key in keys:
+ $key
+ $_("Action")
+
+
+
+
+ $for id in books_wo_isbns:
+
+ $for key in keys:
+
+ $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', '')
+ $(", ".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, '-')
+
+
+
+ $_("Search OL")
+
+
+
+
+
+
+
-
- $ keys = ['ISBN', 'Title', 'My Rating', 'Exclusive Shelf', 'Date Read']
-
+
+
+
+
+
+
diff --git a/static/css/page-user.css b/static/css/page-user.css
index 49c343d91d2..2834ad0366f 100644
--- a/static/css/page-user.css
+++ b/static/css/page-user.css
@@ -109,36 +109,38 @@ th.toggle-all {
width: 100%;
}
-.import-table {
- border: 2px;
+/* Goodreads Imports Page */
+.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);
}
+
.import-table thead th {
- position: sticky;
- top: 0;
background-color: var(--white);
}
-.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;