diff --git a/cps/editbooks.py b/cps/editbooks.py index 6eefc61fb..761c61c75 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -214,6 +214,76 @@ def table_get_custom_enum(c_id): @edit_required def edit_list_book(param): 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 = "" @@ -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 @@ -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 diff --git a/cps/helper.py b/cps/helper.py index 529ed561e..1955a2df4 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -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 diff --git a/cps/kobo_sync_status.py b/cps/kobo_sync_status.py index 357b84efe..941cd73f8 100644 --- a/cps/kobo_sync_status.py +++ b/cps/kobo_sync_status.py @@ -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 archived_book.last_modified = datetime.now(timezone.utc) # toDo. Check utc timestamp ub.session.merge(archived_book) diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 36361c3ca..58ed5999c 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -81,6 +81,43 @@ $(function() { $("#merge_books").addClass("disabled"); $("#merge_books").attr("aria-disabled", true); } + if (selections.length >= 1) { + $("#delete_selected_books").removeClass("disabled"); + $("#delete_selected_books").attr("aria-disabled", false); + + $("#archive_selected_books").removeClass("disabled"); + $("#archive_selected_books").attr("aria-disabled", false); + + $("#unarchive_selected_books").removeClass("disabled"); + $("#unarchive_selected_books").attr("aria-disabled", false); + + $("#read_selected_books").removeClass("disabled"); + $("#read_selected_books").attr("aria-disabled", false); + + $("#unread_selected_books").removeClass("disabled"); + $("#unread_selected_books").attr("aria-disabled", false); + + $("#edit_selected_books").removeClass("disabled"); + $("#edit_selected_books").attr("aria-disabled", false); + } else { + $("#delete_selected_books").addClass("disabled"); + $("#delete_selected_books").attr("aria-disabled", true); + + $("#archive_selected_books").addClass("disabled"); + $("#archive_selected_books").attr("aria-disabled", true); + + $("#unarchive_selected_books").addClass("disabled"); + $("#unarchive_selected_books").attr("aria-disabled", true); + + $("#read_selected_books").addClass("disabled"); + $("#read_selected_books").attr("aria-disabled", true); + + $("#unread_selected_books").addClass("disabled"); + $("#unread_selected_books").attr("aria-disabled", true); + + $("#edit_selected_books").addClass("disabled"); + $("#edit_selected_books").attr("aria-disabled", true); + } if (selections.length < 1) { $("#delete_selection").addClass("disabled"); $("#delete_selection").attr("aria-disabled", true); @@ -93,7 +130,29 @@ $(function() { $("#table_xchange").attr("aria-disabled", false); } + }); + + // Small block to initialize the state of the author/title sort inputs in metadata form + { + let checkA = $('#autoupdate_authorsort').prop('checked'); + $('#author_sort_input').prop('disabled', checkA); + let checkT = $('#autoupdate_titlesort').prop('checked'); + $('#title_sort_input').prop('disabled', checkT); + } + + // Disable/enable author and title sort input in respect to auto-update title/author sort being checked on or not + $("#autoupdate_authorsort").on('change', function(event) { + let checkA = $('#autoupdate_authorsort').prop('checked'); + $('#author_sort_input').prop('disabled', checkA); + }) + + $("#autoupdate_titlesort").on('change', function(event) { + let checkT = $('#autoupdate_titlesort').prop('checked'); + $('#title_sort_input').prop('disabled', checkT); + }) + ///// + $("#delete_selection").click(function() { $("#books-table").bootstrapTable("uncheckAll"); }); @@ -135,6 +194,232 @@ $(function() { }); }); + $("#edit_selected_books").click(function(event) { + if ($(this).hasClass("disabled")) { + event.stopPropagation() + } else { + $('#edit_selected_modal').modal("show"); + } + }); + + $("#edit_selected_confirm").click(function(event) { + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: window.location.pathname + "/../ajax/editselectedbooks", + data: JSON.stringify({ + "selections": selections, + "title": $("#title_input").val(), + "title_sort": $("#title_sort_input").val(), + "author_sort": $("#author_sort_input").val(), + "authors": $("#authors_input").val(), + "categories": $("#categories_input").val(), + "series": $("#series_input").val(), + "languages": $("#languages_input").val(), + "publishers": $("#publishers_input").val(), + "comments": $("#comments_input").val().toString(), + "checkA": $('#autoupdate_authorsort').prop('checked').toString() + }), + success: function success(booTitles) { + $("#books-table").bootstrapTable("refresh"); + $("#books-table").bootstrapTable("uncheckAll"); + + $("#title_input").val(""); + $("#title_sort_input").val(""); + $("#author_sort_input").val(""); + $("#authors_input").val(""); + $("#categories_input").val(""); + $("#series_input").val(""); + $("#languages_input").val(""); + $("#publishers_input").val(""); + $("#comments_input").val(""); + + handleListServerResponse; + } + }); + }); + + $(document).on('click', '#archive_selected_books', function(event) { + if ($(this).hasClass("disabled")) { + event.stopPropagation() + } else { + $('#archive_selected_modal').modal("show"); + } + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: window.location.pathname + "/../ajax/displayselectedbooks", + data: JSON.stringify({"selections":selections}), + success: function success(booTitles) { + $('#display-archive-selected-books').empty(); + $.each(booTitles.books, function(i, item) { + $("- " + item + "

").appendTo("#display-archive-selected-books"); + }); + + } + }); + }); + + $(document).on('click', '#archive_selected_confirm', function(event) { + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: window.location.pathname + "/../ajax/archiveselectedbooks", + data: JSON.stringify({"selections":selections, "archive": true}), + success: function success(booTitles) { + $("#books-table").bootstrapTable("refresh"); + $("#books-table").bootstrapTable("uncheckAll"); + } + }); + }); + + $(document).on('click', '#unarchive_selected_books', function(event) { + if ($(this).hasClass("disabled")) { + event.stopPropagation() + } else { + $('#unarchive_selected_modal').modal("show"); + } + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: window.location.pathname + "/../ajax/displayselectedbooks", + data: JSON.stringify({"selections":selections}), + success: function success(booTitles) { + $('#display-unarchive-selected-books').empty(); + $.each(booTitles.books, function(i, item) { + $("- " + item + "

").appendTo("#display-unarchive-selected-books"); + }); + + } + }); + }); + + $(document).on('click', '#unarchive_selected_confirm', function(event) { + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: window.location.pathname + "/../ajax/archiveselectedbooks", + data: JSON.stringify({"selections":selections, "archive": false}), + success: function success(booTitles) { + $("#books-table").bootstrapTable("refresh"); + $("#books-table").bootstrapTable("uncheckAll"); + } + }); + }); + + $(document).on('click', '#delete_selected_books', function(event) { + if ($(this).hasClass("disabled")) { + event.stopPropagation() + } else { + $('#delete_selected_modal').modal("show"); + } + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: window.location.pathname + "/../ajax/displayselectedbooks", + data: JSON.stringify({"selections":selections}), + success: function success(booTitles) { + $('#display-delete-selected-books').empty(); + $.each(booTitles.books, function(i, item) { + $("- " + item + "

").appendTo("#display-delete-selected-books"); + }); + + } + }); + }); + + $(document).on('click', '#delete_selected_confirm', function(event) { + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: window.location.pathname + "/../ajax/deleteselectedbooks", + data: JSON.stringify({"selections":selections}), + success: function success(booTitles) { + $("#books-table").bootstrapTable("refresh"); + $("#books-table").bootstrapTable("uncheckAll"); + } + }); + }); + + $(document).on('click', '#read_selected_books', function(event) { + if ($(this).hasClass("disabled")) { + event.stopPropagation() + } else { + $('#read_selected_modal').modal("show"); + } + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: window.location.pathname + "/../ajax/displayselectedbooks", + data: JSON.stringify({"selections":selections}), + success: function success(booTitles) { + $('#display-read-selected-books').empty(); + $.each(booTitles.books, function(i, item) { + $("- " + item + "

").appendTo("#display-read-selected-books"); + }); + + } + }); + }); + + $(document).on('click', '#read_selected_confirm', function(event) { + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: window.location.pathname + "/../ajax/readselectedbooks", + data: JSON.stringify({"selections":selections, "markAsRead": true}), + success: function success(booTitles) { + $("#books-table").bootstrapTable("refresh"); + $("#books-table").bootstrapTable("uncheckAll"); + } + }); + }); + + $(document).on('click', '#unread_selected_books', function(event) { + if ($(this).hasClass("disabled")) { + event.stopPropagation() + } else { + $('#unread_selected_modal').modal("show"); + } + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: window.location.pathname + "/../ajax/displayselectedbooks", + data: JSON.stringify({"selections":selections}), + success: function success(booTitles) { + $('#display-unread-selected-books').empty(); + $.each(booTitles.books, function(i, item) { + $("- " + item + "

").appendTo("#display-unread-selected-books"); + }); + + } + }); + }); + + $(document).on('click', '#unread_selected_confirm', function(event) { + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: window.location.pathname + "/../ajax/readselectedbooks", + data: JSON.stringify({"selections":selections, "markAsRead": false}), + success: function success(booTitles) { + $("#books-table").bootstrapTable("refresh"); + $("#books-table").bootstrapTable("uncheckAll"); + } + }); + }); + $("#table_xchange").click(function() { $.ajax({ method:"post", @@ -851,7 +1136,6 @@ function BookCheckboxChange(checkbox, userId, field) { }); } - function selectHeader(element, field) { if (element.value !== "None") { confirmDialog(element.id, "GeneralChangeModal", 0, function () { diff --git a/cps/templates/book_table.html b/cps/templates/book_table.html index 16836b7b2..e93fbd573 100644 --- a/cps/templates/book_table.html +++ b/cps/templates/book_table.html @@ -15,10 +15,28 @@ {%- endmacro %} {% macro book_checkbox_row(parameter, show_text, sort) -%} - + {% if parameter == "is_archived" %} +
+ {{_('Archive selected books')}} +
+
+
+ {{_('Unarchive selected books')}} +
+
+ {% elif parameter == "read_status" %} +
+ {{_('Mark selected books as read')}} +
+
+
+ {{_('Mark selected books as unread')}}
+
+ {% endif %} {{show_text}} {%- endmacro %} @@ -34,8 +52,15 @@

{{_(title)}}

-
{{_('Merge selected books')}}
-
{{_('Remove Selections')}}
+
+ {{_('Merge selected books')}} +
+
+ {{_('Clear selections')}} +
+
+ {{_('Edit selected books')}} +
{{_('Exchange author and title')}}
@@ -97,11 +122,18 @@

{{_(title)}}

{% endif %} {% endfor %} {% if current_user.role_delete_books() and current_user.role_edit()%} - {{_('Delete')}} + +
+ {{_('Delete selected books')}} +
+
+ {{_('Delete')}} + {% endif %} + {% endblock %} {% block modal %} {{ delete_book(current_user.role_delete_books()) }} @@ -123,15 +155,170 @@

{{_(title)}}

-{% endif %} + + + + + + + + + + + + +{% endif %} {% endblock %} + {% block js %}