Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bulk delete, bulk archive/unarchive, and bulk metadata edit buttons in books table page #3113

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d0d9985
Add bulk delete button
jmarmstrong1207 Aug 2, 2024
d9a2a7a
Add bulk archive/unarchive buttons
jmarmstrong1207 Aug 2, 2024
ca9fc74
typo fix
jmarmstrong1207 Aug 2, 2024
34fec0e
Move buttons into table
jmarmstrong1207 Aug 2, 2024
4ed0633
Add bulk read/unread buttons; Fix buttons not working when moved into…
jmarmstrong1207 Aug 2, 2024
ecda717
Add bulk metadata edit button
jmarmstrong1207 Aug 2, 2024
ab3d4e4
Fix emptying metadata form after submit
jmarmstrong1207 Aug 3, 2024
9def910
switch title_sort to sort in api edit request
jmarmstrong1207 Aug 3, 2024
96fb2c1
Auto disable author/title sort input in metadata edit form
jmarmstrong1207 Aug 3, 2024
a335dd7
Make edit metadata pass all data in a single REST call. Modularize ed…
jmarmstrong1207 Aug 3, 2024
bee6a35
remove spacing
jmarmstrong1207 Aug 3, 2024
e31763d
Fix kobo sync status marking as archived even though state = false
jmarmstrong1207 Aug 3, 2024
2ae80d3
Fix book_read_status marking as read even though read_status is passe…
jmarmstrong1207 Aug 3, 2024
2afce66
Fix change_archived so state=none is a toggle. Fixes /togglearchived …
jmarmstrong1207 Aug 3, 2024
de3f883
Add change_archived_books() description
jmarmstrong1207 Aug 3, 2024
fe78222
Add shift-click to select multiple books at once
jmarmstrong1207 Aug 5, 2024
7e5d897
Merge branch 'master' into bulk-delete
jmarmstrong1207 Sep 11, 2024
31380f2
fix typo
jmarmstrong1207 Sep 17, 2024
338441f
fix author sort not updating when bulk editing
jmarmstrong1207 Sep 17, 2024
54d9d33
Revert "Add shift-click to select multiple books at once"
jmarmstrong1207 Sep 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 128 additions & 1 deletion cps/editbooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,76 @@ def table_get_custom_enum(c_id):
@edit_required
def edit_list_book(param):
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the function to edit_book_param() in order to modularize it. This is so it can be used with the '/editselectedbooks/' calls. This essentially makes the change atomic from the client's perspective. Otherwise when bulk editing metadata, each metadata for each book needs an individual rest API call.

vals = request.form.to_dict()
return edit_book_param(param, vals)

@editbook.route("/ajax/editselectedbooks", methods=['POST'])
@login_required_if_no_ano
@edit_required
def edit_selected_books():
d = request.get_json()
selections = d.get('selections')
title = d.get('title')
title_sort = d.get('title_sort')
author_sort = d.get('author_sort')
authors = d.get('authors')
categories = d.get('categories')
series = d.get('series')
languages = d.get('languages')
publishers = d.get('publishers')
comments = d.get('comments')
checkA = d.get('checkA')

if len(selections) != 0:
for book_id in selections:
vals = {
"pk": book_id,
"value": None,
"checkA": checkA,
}
if title:
vals['value'] = title
edit_book_param('title', vals)
if title_sort:
vals['value'] = title_sort
edit_book_param('sort', vals)
if author_sort:
vals['value'] = author_sort
edit_book_param('author_sort', vals)
if authors:
vals['value'] = authors
edit_book_param('authors', vals)
if categories:
vals['value'] = categories
edit_book_param('tags', vals)
if series:
vals['value'] = series
edit_book_param('series', vals)
if languages:
vals['value'] = languages
edit_book_param('languages', vals)
if publishers:
vals['value'] = publishers
edit_book_param('publishers', vals)
if comments:
vals['value'] = comments
edit_book_param('comments', vals)
return json.dumps({'success': True})
return ""

# Separated from /editbooks so that /editselectedbooks can also use this
#
# param: the property of the book to be changed
# vals - JSON Object:
# {
# 'pk': "the book id",
# 'value': "changes value of param to what's passed here"
# 'checkA': "Optional. Used to check if autosort author is enabled. Assumed as true if not passed"
# 'checkT': "Optional. Used to check if autotitle author is enabled. Assumed as true if not passed"
# }
#
@login_required_if_no_ano
@edit_required
def edit_book_param(param, vals):
book = calibre_db.get_book(vals['pk'])
sort_param = ""
ret = ""
Expand Down Expand Up @@ -336,7 +406,6 @@ def get_sorted_entry(field, bookid):
return json.dumps({'authors': " & ".join([a.name for a in calibre_db.order_authors([book])])})
return ""


@editbook.route("/ajax/simulatemerge", methods=['POST'])
@user_login_required
@edit_required
Expand All @@ -352,6 +421,64 @@ def simulate_merge_list_book():
return json.dumps({'to': to_book, 'from': from_book})
return ""

@editbook.route("/ajax/displayselectedbooks", methods=['POST'])
@user_login_required
@edit_required
def display_selected_books():
vals = request.get_json().get('selections')
books = []
if vals:
for book_id in vals:
books.append(calibre_db.get_book(book_id).title)
return json.dumps({'books': books})
return ""

@editbook.route("/ajax/archiveselectedbooks", methods=['POST'])
@login_required_if_no_ano
@edit_required
def archive_selected_books():
vals = request.get_json().get('selections')
state = request.get_json().get('archive')
if vals:
for book_id in vals:
is_archived = change_archived_books(book_id, state,
message="Book {} archive bit set to: {}".format(book_id, state))
if is_archived:
kobo_sync_status.remove_synced_book(book_id)
return json.dumps({'success': True})
return ""

@editbook.route("/ajax/deleteselectedbooks", methods=['POST'])
@user_login_required
@edit_required
def delete_selected_books():
vals = request.get_json().get('selections')
if vals:
for book_id in vals:
delete_book_from_table(book_id, "", True)
return json.dumps({'success': True})
return ""

@editbook.route("/ajax/readselectedbooks", methods=['POST'])
@user_login_required
@edit_required
def read_selected_books():
vals = request.get_json().get('selections')
markAsRead = request.get_json().get('markAsRead')
if vals:
try:
for book_id in vals:
ret = helper.edit_book_read_status(book_id, markAsRead)

except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e))
ret = Response(json.dumps({'success': False,
'msg': 'Database error: {}'.format(e.orig if hasattr(e, "orig") else e)}),
mimetype='application/json')

return json.dumps({'success': True})
return ""

@editbook.route("/ajax/mergebooks", methods=['POST'])
@user_login_required
Expand Down
2 changes: 1 addition & 1 deletion cps/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ def edit_book_read_status(book_id, read_status=None):
else:
book.read_status = ub.ReadBook.STATUS_FINISHED
else:
book.read_status = ub.ReadBook.STATUS_FINISHED if read_status else ub.ReadBook.STATUS_UNREAD
book.read_status = ub.ReadBook.STATUS_FINISHED if read_status == True else ub.ReadBook.STATUS_UNREAD
else:
read_book = ub.ReadBook(user_id=current_user.id, book_id=book_id)
read_book.read_status = ub.ReadBook.STATUS_FINISHED
Expand Down
6 changes: 4 additions & 2 deletions cps/kobo_sync_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@ def remove_synced_book(book_id, all=False, session=None):
ub.session_commit(_session=session)


# If state == none, it will toggle the archive state of the passed book_id.
# state = true archives it, state = false unarchives it
def change_archived_books(book_id, state=None, message=None):
archived_book = ub.session.query(ub.ArchivedBook).filter(and_(ub.ArchivedBook.user_id == int(current_user.id),
ub.ArchivedBook.book_id == book_id)).first()
if not archived_book:
if not archived_book and (state == True or state == None):
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)

archived_book.is_archived = state if state else not archived_book.is_archived
archived_book.is_archived = state if state != None else not archived_book.is_archived
Copy link
Author

@jmarmstrong1207 jmarmstrong1207 Aug 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this, bulk unarchiving on already-unarchived books will actually archive them instead. Without this, this function is just a toggler if state is false.

Same exact thing with edit_book_read_status is fixed in the commit below

archived_book.last_modified = datetime.now(timezone.utc) # toDo. Check utc timestamp

ub.session.merge(archived_book)
Expand Down
Loading