From d0d9985370be37506bdc4782d880885060a0528b Mon Sep 17 00:00:00 2001 From: James Armstong Date: Thu, 1 Aug 2024 19:19:14 -0700 Subject: [PATCH 01/34] Add bulk delete button --- cps/editbooks.py | 22 ++++++++++++++++++ cps/static/js/table.js | 44 +++++++++++++++++++++++++++++++++++ cps/templates/book_table.html | 20 ++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/cps/editbooks.py b/cps/editbooks.py index 5f58cf81..f976d7ea 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -488,6 +488,28 @@ def simulate_merge_list_book(): return json.dumps({'to': to_book, 'from': from_book}) return "" +@editbook.route("/ajax/simulatedeleteselectedbooks", methods=['POST']) +@user_login_required +@edit_required +def simulate_delete_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/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/mergebooks", methods=['POST']) @user_login_required diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 36361c3c..1686e534 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -81,6 +81,14 @@ $(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); + } else { + $("#delete_selected_books").addClass("disabled"); + $("#delete_selected_books").attr("aria-disabled", true); + + } if (selections.length < 1) { $("#delete_selection").addClass("disabled"); $("#delete_selection").attr("aria-disabled", true); @@ -135,6 +143,42 @@ $(function() { }); }); + $("#delete_selected_books").click(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/simulatedeleteselectedbooks", + data: JSON.stringify({"selections":selections}), + success: function success(booTitles) { + $('#selected-books').empty(); + $.each(booTitles.books, function(i, item) { + $("- " + item + "

").appendTo("#selected-books"); + }); + + } + }); + }); + + $("#delete_selected_confirm").click(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"); + } + }); + }); + $("#table_xchange").click(function() { $.ajax({ method:"post", diff --git a/cps/templates/book_table.html b/cps/templates/book_table.html index 16836b7b..12c52484 100644 --- a/cps/templates/book_table.html +++ b/cps/templates/book_table.html @@ -35,6 +35,7 @@
{{_('Merge selected books')}}
+
{{_('Delete selected books')}}
{{_('Remove Selections')}}
@@ -129,6 +130,25 @@
+ + + + + + + + {% endif %} - {% endblock %} + {% block js %} From ca9fc7454121f7572666995af97a6f6d7f8df132 Mon Sep 17 00:00:00 2001 From: James Armstrong <32995055+jmarmstrong1207@users.noreply.github.com> Date: Fri, 2 Aug 2024 00:40:47 -0700 Subject: [PATCH 03/34] typo fix --- cps/templates/book_table.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/templates/book_table.html b/cps/templates/book_table.html index 9800cb06..388323b1 100644 --- a/cps/templates/book_table.html +++ b/cps/templates/book_table.html @@ -186,7 +186,7 @@

From 34fec0ed97a36011076ceab3db3a16f78fb30406 Mon Sep 17 00:00:00 2001 From: James Armstrong Date: Fri, 2 Aug 2024 10:19:25 -0700 Subject: [PATCH 04/34] Move buttons into table --- cps/templates/book_table.html | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cps/templates/book_table.html b/cps/templates/book_table.html index 388323b1..61bdced1 100644 --- a/cps/templates/book_table.html +++ b/cps/templates/book_table.html @@ -19,6 +19,12 @@ {% if sort %}data-sortable="true" {% endif %} data-visible="{{visiblility.get(parameter)}}" data-formatter="bookCheckboxFormatter"> + {% if parameter == "is_archived"%} +
{{_('Archive selected books')}}
+
+
{{_('Unarchive selected books')}}
+
+ {% endif %} {{show_text}} {%- endmacro %} @@ -35,10 +41,7 @@
{{_('Merge selected books')}}
-
{{_('Delete selected books')}}
-
{{_('Archive selected books')}}
-
{{_('Unarchive selected books')}}
-
{{_('Remove Selections')}}
+
{{_('Clear selections')}}
{{_('Exchange author and title')}}
@@ -100,7 +103,7 @@ {% endif %} {% endfor %} {% if current_user.role_delete_books() and current_user.role_edit()%} - {{_('Delete')}} +
{{_('Delete selected books')}}

{{_('Delete')}} {% endif %} From 4ed0633edfd4a5cbbc355756e3e1da8084a7dd68 Mon Sep 17 00:00:00 2001 From: James Armstrong Date: Fri, 2 Aug 2024 14:57:20 -0700 Subject: [PATCH 05/34] Add bulk read/unread buttons; Fix buttons not working when moved into table --- cps/editbooks.py | 21 ++++++++ cps/static/js/table.js | 96 ++++++++++++++++++++++++++++++++--- cps/templates/book_table.html | 84 ++++++++++++++++++++++++++---- 3 files changed, 184 insertions(+), 17 deletions(-) diff --git a/cps/editbooks.py b/cps/editbooks.py index 230b6ec5..45cec33f 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -525,6 +525,27 @@ def delete_selected_books(): 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 @edit_required diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 73ebc2f1..24a1c685 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -90,6 +90,12 @@ $(function() { $("#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); } else { $("#delete_selected_books").addClass("disabled"); $("#delete_selected_books").attr("aria-disabled", true); @@ -99,6 +105,12 @@ $(function() { $("#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); } if (selections.length < 1) { $("#delete_selection").addClass("disabled"); @@ -154,7 +166,7 @@ $(function() { }); }); - $("#archive_selected_books").click(function(event) { + $(document).on('click', '#archive_selected_books', function(event) { if ($(this).hasClass("disabled")) { event.stopPropagation() } else { @@ -176,7 +188,7 @@ $(function() { }); }); - $("#archive_selected_confirm").click(function(event) { + $(document).on('click', '#archive_selected_confirm', function(event) { $.ajax({ method:"post", contentType: "application/json; charset=utf-8", @@ -190,7 +202,7 @@ $(function() { }); }); - $("#unarchive_selected_books").click(function(event) { + $(document).on('click', '#unarchive_selected_books', function(event) { if ($(this).hasClass("disabled")) { event.stopPropagation() } else { @@ -212,7 +224,7 @@ $(function() { }); }); - $("#unarchive_selected_confirm").click(function(event) { + $(document).on('click', '#unarchive_selected_confirm', function(event) { $.ajax({ method:"post", contentType: "application/json; charset=utf-8", @@ -226,7 +238,7 @@ $(function() { }); }); - $("#delete_selected_books").click(function(event) { + $(document).on('click', '#delete_selected_books', function(event) { if ($(this).hasClass("disabled")) { event.stopPropagation() } else { @@ -248,7 +260,7 @@ $(function() { }); }); - $("#delete_selected_confirm").click(function(event) { + $(document).on('click', '#delete_selected_confirm', function(event) { $.ajax({ method:"post", contentType: "application/json; charset=utf-8", @@ -262,6 +274,78 @@ $(function() { }); }); + $(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", diff --git a/cps/templates/book_table.html b/cps/templates/book_table.html index 61bdced1..396a812d 100644 --- a/cps/templates/book_table.html +++ b/cps/templates/book_table.html @@ -15,14 +15,26 @@ {%- endmacro %} {% macro book_checkbox_row(parameter, show_text, sort) -%} - - {% if parameter == "is_archived"%} -
{{_('Archive selected books')}}
+ {% if parameter == "is_archived" %} +
+ {{_('Archive selected books')}} +

-
{{_('Unarchive selected books')}}
+
+ {{_('Unarchive selected books')}} +
+
+ {% elif parameter == "read_status" %} +
+ {{_('Mark selected books as read')}} +
+
+
+ {{_('Mark selected books as unread')}}

{% endif %} {{show_text}} @@ -40,8 +52,12 @@
-
{{_('Merge selected books')}}
-
{{_('Clear selections')}}
+
+ {{_('Merge selected books')}} +
+
+ {{_('Clear selections')}} +
{{_('Exchange author and title')}}
@@ -103,7 +119,13 @@ {% endif %} {% endfor %} {% if current_user.role_delete_books() and current_user.role_edit()%} -
{{_('Delete selected books')}}

{{_('Delete')}} + +
+ {{_('Delete selected books')}} +
+
+ {{_('Delete')}} + {% endif %} @@ -130,7 +152,7 @@
@@ -149,7 +171,7 @@

@@ -169,7 +191,7 @@

@@ -189,13 +211,53 @@

+ + + + {% endif %} {% endblock %} From ecda717bed2406ef0d1c864f19a0eaf7313fee95 Mon Sep 17 00:00:00 2001 From: James Armstong Date: Fri, 2 Aug 2024 01:11:58 -0700 Subject: [PATCH 06/34] Add bulk metadata edit button --- cps/static/js/table.js | 68 +++++++++++++++++++++++++++++++++++ cps/templates/book_table.html | 58 ++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 24a1c685..2cdd16db 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -96,6 +96,9 @@ $(function() { $("#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); @@ -111,6 +114,9 @@ $(function() { $("#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"); @@ -166,6 +172,68 @@ $(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) { + let title = $("#title_input").val() + let title_sort = $("#title_sort_input").val() + let author_sort = $("#author_sort_input").val() + let authors = $("#authors_input").val() + let categories = $("#categories_input").val() + let series = $("#series_input").val() + let languages = $("#languages_input").val() + let publishers = $("#publishers_input").val() + let comments = $("#comments_input").val().toString() + + function loopThrough(param, value) + { + selections.forEach((book_id) => { + $.ajax({ + method: "post", + url: getPath() + "/ajax/editbooks/" + param, + data: { pk: book_id, value: value }, + error: function (data) { + handleListServerResponse([ + { type: "danger", message: data.responseText }, + ]); + }, + success: function success(booTitles) { + $("#books-table").bootstrapTable("refresh"); + $("#books-table").bootstrapTable("uncheckAll"); + + $("#title_input").value = ""; + $("#title_sort_input").value = ""; + $("#author_sort_input").value = ""; + $("#authors_input").value = ""; + $("#categories_input").value = ""; + $("#series_input").value = ""; + $("#languages_input").value = ""; + $("#publishers_input").value = ""; + $("#comments_input").value = ""; + + handleListServerResponse; + }, + }); + }) + } + + if (title) loopThrough('title', title); + if (title_sort) loopThrough('title_sort', title_sort); + if (author_sort) loopThrough('author_sort', author_sort); + if (authors) loopThrough('authors', authors); + if (categories) loopThrough('tags', categories); + if (series) loopThrough('series', series); + if (languages) loopThrough('languages', languages); + if (publishers) loopThrough('publishers', publishers); + if (comments) loopThrough('comments', comments); + }); + $(document).on('click', '#archive_selected_books', function(event) { if ($(this).hasClass("disabled")) { event.stopPropagation() diff --git a/cps/templates/book_table.html b/cps/templates/book_table.html index 396a812d..e93fbd57 100644 --- a/cps/templates/book_table.html +++ b/cps/templates/book_table.html @@ -58,6 +58,9 @@
{{_('Clear selections')}}
+
+ {{_('Edit selected books')}} +
{{_('Exchange author and title')}}
@@ -258,6 +261,61 @@
+ + + {% endif %} {% endblock %} From ab3d4e4f212f463016a9d7ec9ff57af256a049b0 Mon Sep 17 00:00:00 2001 From: James Armstong Date: Fri, 2 Aug 2024 20:10:43 -0700 Subject: [PATCH 07/34] Fix emptying metadata form after submit --- cps/static/js/table.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 2cdd16db..24e63767 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -207,15 +207,15 @@ $(function() { $("#books-table").bootstrapTable("refresh"); $("#books-table").bootstrapTable("uncheckAll"); - $("#title_input").value = ""; - $("#title_sort_input").value = ""; - $("#author_sort_input").value = ""; - $("#authors_input").value = ""; - $("#categories_input").value = ""; - $("#series_input").value = ""; - $("#languages_input").value = ""; - $("#publishers_input").value = ""; - $("#comments_input").value = ""; + $("#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; }, From 9def9109249bf59af9c6c5012a5c2367cf200811 Mon Sep 17 00:00:00 2001 From: James Armstong Date: Fri, 2 Aug 2024 21:16:34 -0700 Subject: [PATCH 08/34] switch title_sort to sort in api edit request --- cps/static/js/table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 24e63767..e556d85c 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -224,7 +224,7 @@ $(function() { } if (title) loopThrough('title', title); - if (title_sort) loopThrough('title_sort', title_sort); + if (title_sort) loopThrough('sort', title_sort); if (author_sort) loopThrough('author_sort', author_sort); if (authors) loopThrough('authors', authors); if (categories) loopThrough('tags', categories); From 96fb2c143960c6ec238868002082adc396ea9377 Mon Sep 17 00:00:00 2001 From: James Armstong Date: Fri, 2 Aug 2024 21:39:01 -0700 Subject: [PATCH 09/34] Auto disable author/title sort input in metadata edit form --- cps/static/js/table.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/cps/static/js/table.js b/cps/static/js/table.js index e556d85c..269549a4 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -130,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"); }); From a335dd75a182bdca58f2b529c00b8870d6a04e03 Mon Sep 17 00:00:00 2001 From: James Armstong Date: Fri, 2 Aug 2024 23:29:34 -0700 Subject: [PATCH 10/34] Make edit metadata pass all data in a single REST call. Modularize editbooks --- cps/editbooks.py | 71 +++++++++++++++++++++++++++++++++++++ cps/static/js/table.js | 80 +++++++++++++++++------------------------- 2 files changed, 103 insertions(+), 48 deletions(-) diff --git a/cps/editbooks.py b/cps/editbooks.py index 45cec33f..3bb74b25 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -350,6 +350,77 @@ 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_sortj') + authors = d.get('authors') + categories = d.get('categories') + series = d.get('series') + languages = d.get('languages') + publishers = d.get('publishers') + comments = d.get('comments') + + + + if len(selections) != 0: + for book_id in selections: + vals = { + "pk": book_id, + "value": None, + } + 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 = "" diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 269549a4..acc8ccfb 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -203,57 +203,41 @@ $(function() { }); $("#edit_selected_confirm").click(function(event) { - let title = $("#title_input").val() - let title_sort = $("#title_sort_input").val() - let author_sort = $("#author_sort_input").val() - let authors = $("#authors_input").val() - let categories = $("#categories_input").val() - let series = $("#series_input").val() - let languages = $("#languages_input").val() - let publishers = $("#publishers_input").val() - let comments = $("#comments_input").val().toString() - function loopThrough(param, value) - { - selections.forEach((book_id) => { - $.ajax({ - method: "post", - url: getPath() + "/ajax/editbooks/" + param, - data: { pk: book_id, value: value }, - error: function (data) { - handleListServerResponse([ - { type: "danger", message: data.responseText }, - ]); - }, - success: function success(booTitles) { - $("#books-table").bootstrapTable("refresh"); - $("#books-table").bootstrapTable("uncheckAll"); + $.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(), + }), + 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(""); + $("#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; - }, - }); - }) - } - - if (title) loopThrough('title', title); - if (title_sort) loopThrough('sort', title_sort); - if (author_sort) loopThrough('author_sort', author_sort); - if (authors) loopThrough('authors', authors); - if (categories) loopThrough('tags', categories); - if (series) loopThrough('series', series); - if (languages) loopThrough('languages', languages); - if (publishers) loopThrough('publishers', publishers); - if (comments) loopThrough('comments', comments); + handleListServerResponse; + } + }); }); $(document).on('click', '#archive_selected_books', function(event) { From bee6a3593dcf603eb614c6c73e00a7dd3078b58f Mon Sep 17 00:00:00 2001 From: James Armstong Date: Fri, 2 Aug 2024 23:31:04 -0700 Subject: [PATCH 11/34] remove spacing --- cps/editbooks.py | 3 --- cps/static/js/table.js | 1 - 2 files changed, 4 deletions(-) diff --git a/cps/editbooks.py b/cps/editbooks.py index 3bb74b25..938b19f3 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -368,8 +368,6 @@ def edit_selected_books(): publishers = d.get('publishers') comments = d.get('comments') - - if len(selections) != 0: for book_id in selections: vals = { @@ -403,7 +401,6 @@ def edit_selected_books(): if comments: vals['value'] = comments edit_book_param('comments', vals) - return json.dumps({'success': True}) return "" diff --git a/cps/static/js/table.js b/cps/static/js/table.js index acc8ccfb..b80fccc7 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -203,7 +203,6 @@ $(function() { }); $("#edit_selected_confirm").click(function(event) { - $.ajax({ method:"post", contentType: "application/json; charset=utf-8", From e31763d38879e5c8c0a0a67b247472a7c64e0981 Mon Sep 17 00:00:00 2001 From: James Armstong Date: Sat, 3 Aug 2024 11:27:58 -0700 Subject: [PATCH 12/34] Fix kobo sync status marking as archived even though state = false --- cps/kobo_sync_status.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cps/kobo_sync_status.py b/cps/kobo_sync_status.py index ef732aaa..8c1ca7c5 100644 --- a/cps/kobo_sync_status.py +++ b/cps/kobo_sync_status.py @@ -54,10 +54,10 @@ def remove_synced_book(book_id, all=False, session=None): 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: 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.datetime.utcnow() # toDo. Check utc timestamp ub.session.merge(archived_book) From 2ae80d3007d4d42147ce02b23bafe26aa3ef7d1d Mon Sep 17 00:00:00 2001 From: James Armstong Date: Sat, 3 Aug 2024 11:40:11 -0700 Subject: [PATCH 13/34] Fix book_read_status marking as read even though read_status is passed as false --- cps/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/helper.py b/cps/helper.py index 004e1b0e..bc982cfe 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -313,7 +313,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 From 2afce66486c91f818adea79a7cadda305ddc5e28 Mon Sep 17 00:00:00 2001 From: James Armstong Date: Sat, 3 Aug 2024 11:50:28 -0700 Subject: [PATCH 14/34] Fix change_archived so state=none is a toggle. Fixes /togglearchived endpoint --- cps/kobo_sync_status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/kobo_sync_status.py b/cps/kobo_sync_status.py index 8c1ca7c5..ee9f0779 100644 --- a/cps/kobo_sync_status.py +++ b/cps/kobo_sync_status.py @@ -54,7 +54,7 @@ def remove_synced_book(book_id, all=False, session=None): 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 and state == True: + 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 != None else not archived_book.is_archived From de3f883992dc949a67c3053b2e555699b1da2476 Mon Sep 17 00:00:00 2001 From: James Armstong Date: Sat, 3 Aug 2024 11:52:28 -0700 Subject: [PATCH 15/34] Add change_archived_books() description --- cps/kobo_sync_status.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cps/kobo_sync_status.py b/cps/kobo_sync_status.py index ee9f0779..94f1d073 100644 --- a/cps/kobo_sync_status.py +++ b/cps/kobo_sync_status.py @@ -51,6 +51,8 @@ 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() From fe78222142448d58d55737329d164e3190650c50 Mon Sep 17 00:00:00 2001 From: James Armstrong Date: Mon, 5 Aug 2024 10:43:51 -0700 Subject: [PATCH 16/34] Add shift-click to select multiple books at once --- cps/static/js/table.js | 1 + 1 file changed, 1 insertion(+) diff --git a/cps/static/js/table.js b/cps/static/js/table.js index b80fccc7..6acf6308 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -484,6 +484,7 @@ $(function() { searchOnEnterKey: true, checkboxHeader: false, maintainMetaData: true, + multipleSelectRow: true, responseHandler: responseHandler, columns: column, formatNoMatches: function () { From 31380f2af9e85ed04904f490aacbc621c2135f0c Mon Sep 17 00:00:00 2001 From: James Armstrong <32995055+jmarmstrong1207@users.noreply.github.com> Date: Tue, 17 Sep 2024 03:19:03 -0700 Subject: [PATCH 17/34] fix typo --- cps/editbooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/editbooks.py b/cps/editbooks.py index 84fa754e..bc76ec74 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -224,7 +224,7 @@ def edit_selected_books(): selections = d.get('selections') title = d.get('title') title_sort = d.get('title_sort') - author_sort = d.get('author_sortj') + author_sort = d.get('author_sort') authors = d.get('authors') categories = d.get('categories') series = d.get('series') From 338441f1165fe71ef9f5983d040b90775bc62f10 Mon Sep 17 00:00:00 2001 From: James Armstrong Date: Tue, 17 Sep 2024 12:26:35 -0700 Subject: [PATCH 18/34] fix author sort not updating when bulk editing --- cps/editbooks.py | 2 ++ cps/static/js/table.js | 1 + 2 files changed, 3 insertions(+) diff --git a/cps/editbooks.py b/cps/editbooks.py index bc76ec74..761c61c7 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -231,12 +231,14 @@ def edit_selected_books(): 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 diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 6acf6308..15c4f958 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -219,6 +219,7 @@ $(function() { "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"); From 54d9d33e538e6f625b7e191046ef1197897092a2 Mon Sep 17 00:00:00 2001 From: James Armstrong Date: Mon, 23 Sep 2024 09:42:04 -0700 Subject: [PATCH 19/34] Revert "Add shift-click to select multiple books at once" This reverts commit fe78222142448d58d55737329d164e3190650c50. --- cps/static/js/table.js | 1 - 1 file changed, 1 deletion(-) diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 15c4f958..58ed5999 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -485,7 +485,6 @@ $(function() { searchOnEnterKey: true, checkboxHeader: false, maintainMetaData: true, - multipleSelectRow: true, responseHandler: responseHandler, columns: column, formatNoMatches: function () { From 08527ae3ce62e96acabb85d64691724ddb8de841 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Mon, 9 Dec 2024 17:08:58 +0100 Subject: [PATCH 20/34] Update mass edit Refactored delete User function Updated testresults --- cps/admin.py | 23 +- cps/editbooks.py | 421 ++++++++++++------------ cps/helper.py | 18 +- cps/kobo_sync_status.py | 2 +- cps/static/js/main.js | 8 +- cps/static/js/table.js | 210 ++++++++---- cps/templates/book_table.html | 43 +-- cps/templates/user_table.html | 4 +- pyproject.toml | 4 +- test/Calibre-Web TestSummary_Linux.html | 112 +++---- 10 files changed, 473 insertions(+), 372 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 71403e6c..3ac5eff9 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -40,7 +40,7 @@ from flask_babel import gettext as _ from flask_babel import get_locale, format_time, format_datetime, format_timedelta from sqlalchemy import and_ from sqlalchemy.orm.attributes import flag_modified -from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError +from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError, ArgumentError from sqlalchemy.sql.expression import func, or_, text from . import constants, logger, helper, services, cli_param @@ -386,13 +386,12 @@ def list_users(): @user_login_required @admin_required def delete_user(): - user_ids = request.form.to_dict(flat=False) - users = None + user_ids = request.get_json().get("userid") message = "" - if "userid[]" in user_ids: - users = ub.session.query(ub.User).filter(ub.User.id.in_(user_ids['userid[]'])).all() - elif "userid" in user_ids: - users = ub.session.query(ub.User).filter(ub.User.id == user_ids['userid'][0]).all() + try: + users = ub.session.query(ub.User).filter(ub.User.id.in_(user_ids)).all() + except (ArgumentError): + users = None count = 0 errors = list() success = list() @@ -408,10 +407,10 @@ def delete_user(): errors.append({'type': "danger", 'message': str(ex)}) if count == 1: - log.info("User {} deleted".format(user_ids)) + log.info("User {} deleted".format(user_ids[0])) success = [{'type': "success", 'message': message}] elif count > 1: - log.info("Users {} deleted".format(user_ids)) + log.info("Users {} deleted".format(", ".join([str(user_id) for user_id in user_ids]))) success = [{'type': "success", 'message': _("{} users deleted successfully").format(count)}] success.extend(errors) return make_response(jsonify(success)) @@ -618,6 +617,8 @@ def load_dialogtexts(element_id): texts["main"] = _('Do you really want to delete this domain?') elif element_id == "btndeluser": texts["main"] = _('Do you really want to delete this user?') + elif element_id == "btndelbook": + texts["main"] = _('Do you really want to delete this book?') elif element_id == "delete_shelf": texts["main"] = _('Are you sure you want to delete this shelf?') elif element_id == "select_locale": @@ -626,6 +627,10 @@ def load_dialogtexts(element_id): texts["main"] = _('Are you sure you want to change visible book languages for selected user(s)?') elif element_id == "role": texts["main"] = _('Are you sure you want to change the selected role for the selected user(s)?') + elif element_id == "archive_books": + texts["main"] = _('Are you sure you want to change the archive status for the selected book(s)?') + elif element_id == "read_books": + texts["main"] = _('Are you sure you want to change the read status for the selected book(s)?') elif element_id == "restrictions": texts["main"] = _('Are you sure you want to change the selected restrictions for the selected user(s)?') elif element_id == "sidebar_view": diff --git a/cps/editbooks.py b/cps/editbooks.py index 6499cb3c..f82899f4 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -73,17 +73,18 @@ def edit_required(f): return inner -@editbook.route("/ajax/delete/", methods=["POST"]) +@editbook.route("/ajax/deletebook", methods=["POST"]) @user_login_required -def delete_book_from_details(book_id): - return delete_book_from_table(book_id, "", True) # , mimetype='application/json') +def delete_books_ajax(): + book_ids = request.get_json().get("bookid") + return check_delete_book(book_ids, "", True) @editbook.route("/delete/", defaults={'book_format': ""}, methods=["POST"]) @editbook.route("/delete//", methods=["POST"]) @user_login_required -def delete_book_ajax(book_id, book_format): - return delete_book_from_table(book_id, book_format, False, request.form.to_dict().get('location', "")) +def delete_book(book_id, book_format): + return check_delete_book(book_id, book_format, False, request.form.to_dict().get('location', "")) @editbook.route("/admin/book/", methods=['GET']) @@ -213,7 +214,7 @@ def table_get_custom_enum(c_id): @login_required_if_no_ano @edit_required def edit_list_book(param): - vals = request.form.to_dict() + vals = request.get_json() # form.to_dict(flat=False) return edit_book_param(param, vals) @editbook.route("/ajax/editselectedbooks", methods=['POST']) @@ -233,42 +234,39 @@ def edit_selected_books(): 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 "" + vals = { + "pk": selections, + "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}) # Separated from /editbooks so that /editselectedbooks can also use this # @@ -284,94 +282,96 @@ def edit_selected_books(): @login_required_if_no_ano @edit_required def edit_book_param(param, vals): - book = calibre_db.get_book(vals['pk']) - calibre_db.create_functions(config) - sort_param = "" - ret = "" - try: - if param == 'series_index': - edit_book_series_index(vals['value'], book) - ret = make_response(jsonify(success=True, newValue=book.series_index)) - elif param == 'tags': - edit_book_tags(vals['value'], book) - ret = make_response(jsonify(success=True, newValue=', '.join([tag.name for tag in book.tags]))) - elif param == 'series': - edit_book_series(vals['value'], book) - ret = make_response(jsonify(success=True, newValue=', '.join([serie.name for serie in book.series]))) - elif param == 'publishers': - edit_book_publisher(vals['value'], book) - ret = make_response(jsonify(success=True, - newValue=', '.join([publisher.name for publisher in book.publishers]))) - elif param == 'languages': - invalid = list() - edit_book_languages(vals['value'], book, invalid=invalid) - if invalid: - ret = make_response(jsonify(success=False, - msg='Invalid languages in request: {}'.format(','.join(invalid)))) - else: - lang_names = list() - for lang in book.languages: - lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code)) - ret = make_response(jsonify(success=True, newValue=', '.join(lang_names))) - elif param == 'author_sort': - book.author_sort = vals['value'] - ret = make_response(jsonify(success=True, newValue=book.author_sort)) - elif param == 'title': - sort_param = book.sort - if handle_title_on_edit(book, vals.get('value', "")): - rename_error = helper.update_dir_structure(book.id, config.get_book_path()) - if not rename_error: - ret = make_response(jsonify(success=True, newValue=book.title)) + elements = vals.get('pk',[]) + ret = {} + for elem in elements: + book = calibre_db.get_book(elem) + calibre_db.create_functions(config) + sort_param = "" + try: + if param == 'series_index': + edit_book_series_index(vals['value'], book) + ret = jsonify(success=True, newValue=book.series_index) + elif param == 'tags': + edit_book_tags(vals['value'], book) + ret = jsonify(success=True, newValue=', '.join([tag.name for tag in book.tags])) + elif param == 'series': + edit_book_series(vals['value'], book) + ret = jsonify(success=True, newValue=', '.join([serie.name for serie in book.series])) + elif param == 'publishers': + edit_book_publisher(vals['value'], book) + ret = jsonify(success=True, + newValue=', '.join([publisher.name for publisher in book.publishers])) + elif param == 'languages': + invalid = list() + edit_book_languages(vals['value'], book, invalid=invalid) + if invalid: + ret = jsonify(success=False, msg='Invalid languages in request: {}'.format(','.join(invalid))) else: - ret = make_response(jsonify(success=False, msg=rename_error)) - elif param == 'sort': - book.sort = vals['value'] - ret = make_response(jsonify(success=True,newValue=book.sort)) - elif param == 'comments': - edit_book_comments(vals['value'], book) - ret = make_response(jsonify(success=True, newValue=book.comments[0].text)) - elif param == 'authors': - input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true") - rename_error = helper.update_dir_structure(book.id, config.get_book_path(), input_authors[0]) - if not rename_error: - ret = make_response(jsonify( - success=True, - newValue=' & '.join([author.replace('|', ',') for author in input_authors]))) + lang_names = list() + for lang in book.languages: + lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code)) + ret = jsonify(success=True, newValue=', '.join(lang_names)) + elif param == 'author_sort': + book.author_sort = vals['value'] + ret = jsonify(success=True, newValue=book.author_sort) + elif param == 'title': + sort_param = book.sort + if handle_title_on_edit(book, vals.get('value', "")): + rename_error = helper.update_dir_structure(book.id, config.get_book_path()) + if not rename_error: + ret = jsonify(success=True, newValue=book.title) + else: + ret = jsonify(success=False, msg=rename_error) + elif param == 'sort': + book.sort = vals['value'] + ret = jsonify(success=True,newValue=book.sort) + elif param == 'comments': + edit_book_comments(vals['value'], book) + ret = jsonify(success=True, newValue=book.comments[0].text) + elif param == 'authors': + input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == True) + rename_error = helper.update_dir_structure(book.id, config.get_book_path(), input_authors[0]) + if not rename_error: + ret = jsonify( + success=True, + newValue=' & '.join([author.replace('|', ',') for author in input_authors])) + else: + ret = jsonify(success=False, msg=rename_error) + elif param == 'is_archived': + is_archived = change_archived_books(book.id, vals['value'] == "True", + message="Book {} archive bit set to: {}".format(book.id, + vals['value'])) + if is_archived: + kobo_sync_status.remove_synced_book(book.id) + continue + elif param == 'read_status': + error = helper.edit_book_read_status(book.id, vals['value'] == "True") + if error: + return error, 400 + continue + elif param.startswith("custom_column_"): + new_val = dict() + new_val[param] = vals['value'] + edit_single_cc_data(book.id, book, param[14:], new_val) + # ToDo: Very hacky find better solution + if vals['value'] in ["True", "False"]: + ret = {} + else: + ret = jsonify(success=True, newValue=vals['value']) else: - ret = make_response(jsonify(success=False, msg=rename_error)) - elif param == 'is_archived': - is_archived = change_archived_books(book.id, vals['value'] == "True", - message="Book {} archive bit set to: {}".format(book.id, vals['value'])) - if is_archived: - kobo_sync_status.remove_synced_book(book.id) - return "" - elif param == 'read_status': - ret = helper.edit_book_read_status(book.id, vals['value'] == "True") - if ret: - return ret, 400 - elif param.startswith("custom_column_"): - new_val = dict() - new_val[param] = vals['value'] - edit_single_cc_data(book.id, book, param[14:], new_val) - # ToDo: Very hacky find better solution - if vals['value'] in ["True", "False"]: - ret = "" - else: - ret = make_response(jsonify(success=True, newValue=vals['value'])) - else: - return _("Parameter not found"), 400 - book.last_modified = datetime.now(timezone.utc) + return _("Parameter not found"), 400 + book.last_modified = datetime.now(timezone.utc) - calibre_db.session.commit() - # revert change for sort if automatic fields link is deactivated - if param == 'title' and vals.get('checkT') == "false": - book.sort = sort_param calibre_db.session.commit() - except (OperationalError, IntegrityError, StaleDataError) as e: - calibre_db.session.rollback() - log.error_or_exception("Database error: {}".format(e)) - ret = make_response(jsonify(success=False, - msg='Database error: {}'.format(e.orig if hasattr(e, "orig") else e))) + # revert change for sort if automatic fields link is deactivated + if param == 'title' and vals.get('checkT') == False: + book.sort = sort_param + calibre_db.session.commit() + except (OperationalError, IntegrityError, StaleDataError, AttributeError) as e: + calibre_db.session.rollback() + log.error_or_exception("Database error: {}".format(e)) + ret = jsonify(success=False, msg='Database error: {}'.format(e.orig if hasattr(e, "orig") else e)) return ret @@ -433,16 +433,6 @@ def archive_selected_books(): 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 @@ -498,7 +488,7 @@ def merge_list_book(): element.format, element.uncompressed_size, to_name)) - delete_book_from_table(from_book.id, "", True) + check_delete_book([from_book.id], "", True) return make_response(jsonify(success=True)) return "" @@ -968,86 +958,109 @@ def delete_whole_book(book_id, book): calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete() -def render_delete_book_result(book_format, json_response, warning, book_id, location=""): +def render_delete_book_result(book_format, book_id, location=""): if book_format: - if json_response: - return jsonify([warning, {"location": url_for("edit-book.show_edit_book", book_id=book_id), - "type": "success", - "format": book_format, - "message": _('Book Format Successfully Deleted')}]) - else: - flash(_('Book Format Successfully Deleted'), category="success") - return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) + flash(_('Book Format Successfully Deleted'), category="success") + return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) else: - if json_response: - return jsonify([warning, {"location": get_redirect_location(location, "web.index"), - "type": "success", - "format": book_format, - "message": _('Book Successfully Deleted')}]) - else: - flash(_('Book Successfully Deleted'), category="success") - return redirect(get_redirect_location(location, "web.index")) + flash(_('Book Successfully Deleted'), category="success") + return redirect(get_redirect_location(location, "web.index")) -def delete_book_from_table(book_id, book_format, json_response, location=""): - warning = {} +def check_delete_book(book_id, book_format, json_response, location=""): if current_user.role_delete_books(): - book = calibre_db.get_book(book_id) - if book: - try: - result, error = helper.delete_book(book, config.get_book_path(), book_format=book_format.upper()) - if not result: - if json_response: - return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id), - "type": "danger", - "format": "", - "message": error}]) - else: - flash(error, category="error") - return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) - if error: - if json_response: - warning = {"location": url_for("edit-book.show_edit_book", book_id=book_id), - "type": "warning", - "format": "", - "message": error} - else: - flash(error, category="warning") - if not book_format: - delete_whole_book(book_id, book) - else: - calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\ - filter(db.Data.format == book_format).delete() - if book_format.upper() in ['KEPUB', 'EPUB', 'EPUB3']: - kobo_sync_status.remove_synced_book(book.id, True) - calibre_db.session.commit() - except Exception as ex: - log.error_or_exception(ex) - calibre_db.session.rollback() - if json_response: - return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id), - "type": "danger", - "format": "", - "message": ex}]) - else: - flash(str(ex), category="error") - return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) - + if json_response: + # if json response is set, it's possible to delete more than one book, but never a format is deleted + res = list() + for b in book_id: + ret = delete_book_from_table(b) + if ret: + res.extend([ret]) + if len(res) == 0: + return [{"location": get_redirect_location(location, "web.index"), + "type": "success", + "format": "", + "message": _('Book Successfully Deleted')}] + return jsonify(res) else: - # book not found - log.error('Book with id "%s" could not be deleted: not found', book_id) - return render_delete_book_result(book_format, json_response, warning, book_id, location) + return delete_book_from_UI(book_id, book_format, location) message = _("You are missing permissions to delete books") if json_response: - return jsonify({"location": url_for("edit-book.show_edit_book", book_id=book_id), - "type": "danger", - "format": "", - "message": message}) + try: + return jsonify({"location": url_for("edit-book.show_edit_book", book_id=int(book_id)), + "type": "danger", + "format": "", + "message": message}) + except TypeError as e: + return jsonify({"location": url_for("web.index"), "type": "danger", "format": "", + "message": str(e)}) else: flash(message, category="error") return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) +def delete_book_from_UI(book_id, book_format, location=""): + book = calibre_db.get_book(book_id) + if book: + try: + result, error = helper.delete_book(book, config.get_book_path(), book_format=book_format.upper()) + if not result: + flash(error, category="error") + return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) + if error: + flash(error, category="warning") + if not book_format: + delete_whole_book(book_id, book) + else: + calibre_db.session.query(db.Data).filter(db.Data.book == book.id). \ + filter(db.Data.format == book_format).delete() + if book_format.upper() in ['KEPUB', 'EPUB', 'EPUB3']: + kobo_sync_status.remove_synced_book(book.id, True) + calibre_db.session.commit() + except Exception as ex: + log.error_or_exception(ex) + calibre_db.session.rollback() + flash(str(ex), category="error") + return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) + else: + # book not found + log.error('Book with id "%s" could not be deleted: not found', book_id) + return render_delete_book_result(book_format, book_id, location) + + +def delete_book_from_table(book_id): + book = calibre_db.get_book(book_id) + if book: + try: + result, error = helper.delete_book(book, config.get_book_path(), book_format="") + if not result: + return {"location": url_for("edit-book.show_edit_book", book_id=book_id), + "type": "danger", + "format": "", + "message": error} + delete_whole_book(book_id, book) + calibre_db.session.commit() + if error: + return {"location": url_for("edit-book.show_edit_book", book_id=book_id), + "type": "warning", + "format": "", + "message": error} + except Exception as ex: + log.error_or_exception(ex) + calibre_db.session.rollback() + return {"location": url_for("edit-book.show_edit_book", book_id=book_id), + "type": "danger", + "format": "", + "message": ex} + else: + # book not found + log.error('Book with id "%s" could not be deleted: not found', book_id) + return {"location": url_for("edit-book.show_edit_book", book_id=book_id), + "type": "danger", + "format": "", + "message": _('Book with id "{}" could not be deleted: not found'.format(book_id))} + + def render_edit_book(book_id): cc = calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).all() book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) diff --git a/cps/helper.py b/cps/helper.py index 44770143..e1949c83 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -307,18 +307,16 @@ def edit_book_read_status(book_id, read_status=None): if not config.config_read_column: book = ub.session.query(ub.ReadBook).filter(and_(ub.ReadBook.user_id == int(current_user.id), ub.ReadBook.book_id == book_id)).first() - if book: - if read_status is None: - if book.read_status == ub.ReadBook.STATUS_FINISHED: - book.read_status = ub.ReadBook.STATUS_UNREAD - else: - book.read_status = ub.ReadBook.STATUS_FINISHED - else: - book.read_status = ub.ReadBook.STATUS_FINISHED if read_status == True else ub.ReadBook.STATUS_UNREAD - else: + if not book: read_book = ub.ReadBook(user_id=current_user.id, book_id=book_id) - read_book.read_status = ub.ReadBook.STATUS_FINISHED book = read_book + if read_status is None: + if book.read_status == ub.ReadBook.STATUS_FINISHED: + book.read_status = ub.ReadBook.STATUS_UNREAD + else: + book.read_status = ub.ReadBook.STATUS_FINISHED + else: + book.read_status = ub.ReadBook.STATUS_FINISHED if read_status == True else ub.ReadBook.STATUS_UNREAD if not book.kobo_reading_state: kobo_reading_state = ub.KoboReadingState(user_id=current_user.id, book_id=book_id) kobo_reading_state.current_bookmark = ub.KoboBookmark() diff --git a/cps/kobo_sync_status.py b/cps/kobo_sync_status.py index 941cd73f..5df2f7a8 100644 --- a/cps/kobo_sync_status.py +++ b/cps/kobo_sync_status.py @@ -56,7 +56,7 @@ def remove_synced_book(book_id, all=False, session=None): 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 and (state == True or state == None): + 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 != None else not archived_book.is_archived diff --git a/cps/static/js/main.js b/cps/static/js/main.js index 9994b450..ea72fdfd 100644 --- a/cps/static/js/main.js +++ b/cps/static/js/main.js @@ -229,10 +229,12 @@ $("#delete_confirm").click(function(event) { postButton(event, getPath() + "/delete/" + deleteId + "/" + bookFormat); } else { if (ajaxResponse) { - path = getPath() + "/ajax/delete/" + deleteId; $.ajax({ - method:"post", - url: path, + url: getPath() + "/ajax/deletebook", + method: "post", + contentType: "application/json; charset=utf-8", + dataType: "json", + data: JSON.stringify({"bookid": [deleteId]}), timeout: 900, success:function(data) { data.forEach(function(item) { diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 67934489..421c2e29 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -119,18 +119,17 @@ $(function() { $("#edit_selected_books").attr("aria-disabled", true); } if (selections.length < 1) { - $("#delete_selection").addClass("disabled"); - $("#delete_selection").attr("aria-disabled", true); + // $("#book_delete_selection").addClass("disabled"); + // $("#book_delete_selection").attr("aria-disabled", true); $("#table_xchange").addClass("disabled"); $("#table_xchange").attr("aria-disabled", true); } else { - $("#delete_selection").removeClass("disabled"); - $("#delete_selection").attr("aria-disabled", false); + // $("#book_delete_selection").removeClass("disabled"); + // $("#book_delete_selection").attr("aria-disabled", false); $("#table_xchange").removeClass("disabled"); $("#table_xchange").attr("aria-disabled", false); - } - + handle_header_buttons(); }); // Small block to initialize the state of the author/title sort inputs in metadata form @@ -153,7 +152,7 @@ $(function() { }) ///// - $("#delete_selection").click(function() { + $("#book_delete_selection").click(function () { $("#books-table").bootstrapTable("uncheckAll"); }); @@ -162,7 +161,7 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/mergebooks", + url: getPath() + "/ajax/mergebooks", data: JSON.stringify({"Merge_books":selections}), success: function success() { $("#books-table").bootstrapTable("refresh"); @@ -181,7 +180,7 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/simulatemerge", + url: getPath() + "/ajax/simulatemerge", data: JSON.stringify({"Merge_books":selections}), success: function success(booTitles) { $('#merge_from').empty(); @@ -207,7 +206,7 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/editselectedbooks", + url: getPath() + "/ajax/editselectedbooks", data: JSON.stringify({ "selections": selections, "title": $("#title_input").val(), @@ -250,7 +249,7 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/displayselectedbooks", + url: getPath() + "/ajax/displayselectedbooks", data: JSON.stringify({"selections":selections}), success: function success(booTitles) { $('#display-archive-selected-books').empty(); @@ -262,12 +261,12 @@ $(function() { }); }); - $(document).on('click', '#archive_selected_confirm', function(event) { + /*$(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", + url: getPath() + "/ajax/archiveselectedbooks", data: JSON.stringify({"selections":selections, "archive": true}), success: function success(booTitles) { $("#books-table").bootstrapTable("refresh"); @@ -286,7 +285,7 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/displayselectedbooks", + url: getPath() + "/ajax/displayselectedbooks", data: JSON.stringify({"selections":selections}), success: function success(booTitles) { $('#display-unarchive-selected-books').empty(); @@ -303,7 +302,7 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/archiveselectedbooks", + url: getPath() + "/ajax/archiveselectedbooks", data: JSON.stringify({"selections":selections, "archive": false}), success: function success(booTitles) { $("#books-table").bootstrapTable("refresh"); @@ -322,7 +321,7 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/displayselectedbooks", + url: getPath() + "/ajax/displayselectedbooks", data: JSON.stringify({"selections":selections}), success: function success(booTitles) { $('#display-delete-selected-books').empty(); @@ -339,7 +338,7 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/deleteselectedbooks", + url: getPath() + "/ajax/deleteselectedbooks", data: JSON.stringify({"selections":selections}), success: function success(booTitles) { $("#books-table").bootstrapTable("refresh"); @@ -358,7 +357,7 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/displayselectedbooks", + url: getPath() + "/ajax/displayselectedbooks", data: JSON.stringify({"selections":selections}), success: function success(booTitles) { $('#display-read-selected-books').empty(); @@ -375,7 +374,7 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/readselectedbooks", + url: getPath() + "/ajax/readselectedbooks", data: JSON.stringify({"selections":selections, "markAsRead": true}), success: function success(booTitles) { $("#books-table").bootstrapTable("refresh"); @@ -394,7 +393,7 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/displayselectedbooks", + url: getPath() + "/ajax/displayselectedbooks", data: JSON.stringify({"selections":selections}), success: function success(booTitles) { $('#display-unread-selected-books').empty(); @@ -411,21 +410,21 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/readselectedbooks", + url: getPath() + "/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", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/xchange", + url: getPath() + "/ajax/xchange", data: JSON.stringify({"xchange":selections}), success: function success() { $("#books-table").bootstrapTable("refresh"); @@ -442,6 +441,10 @@ $(function() { editable: { mode: "inline", emptytext: "", + ajaxOptions: { + contentType: "application/json; charset=utf-8", + dataType: "json", + }, success: function (response, __) { if (!response.success) return response.msg; return {newValue: response.newValue}; @@ -449,7 +452,8 @@ $(function() { params: function (params) { params.checkA = $('#autoupdate_authorsort').prop('checked'); params.checkT = $('#autoupdate_titlesort').prop('checked'); - return params + params.pk = [params.pk]; + return JSON.stringify(params); } } }; @@ -483,7 +487,7 @@ $(function() { searchAlign: "left", showSearchButton : true, searchOnEnterKey: true, - checkboxHeader: false, + checkboxHeader: true, maintainMetaData: true, responseHandler: responseHandler, columns: column, @@ -497,7 +501,7 @@ $(function() { $.ajax({ method:"get", dataType: "json", - url: window.location.pathname + "/../ajax/sort_value/" + field + "/" + row.id, + url: getPath() + "/ajax/sort_value/" + field + "/" + row.id, success: function success(data) { var key = Object.keys(data)[0]; $("#books-table").bootstrapTable("updateCellByUniqueId", { @@ -509,6 +513,66 @@ $(function() { }); } }, + onPostBody () { + // Remove all checkboxes from Headers for showing the texts in the column selector + $('.columns [data-field]').each(function(){ + var elText = $(this).next().text(); + $(this).next().empty(); + var index = elText.lastIndexOf('\n', elText.length - 2); + if ( index > -1) { + elText = elText.substr(index); + } + $(this).next().text(elText); + }); + }, + onPostHeader() { + $(".form-check").each(function () { + var item = $(this).parent(); + var parent = item.parent().parent(); + if (parent.prop('nodeName') === "TH") { + item.prependTo(parent); + } + }); + + if ($(".button_head").length) { + if (!$._data($(".button_head").get(0), "events")) { + $(".button_head").on("click", function () { + var result = $('#books-table').bootstrapTable('getSelections').map(a => a.id); + confirmDialog( + "btndelbook", + "GeneralDeleteModal", + 0, + function () { + $.ajax({ + method: "post", + url: getPath() + "/ajax/deletebook", + contentType: "application/json; charset=utf-8", + dataType: "json", + data: JSON.stringify({"bookid": result}), + success: function (data) { + selections = selections.filter((el) => !result.includes(el)); + handleListServerResponse(data); + }, + error: function (data) { + handleListServerResponse([{type: "danger", message: data.responseText}]) + }, + }); + } + ); + }); + } + } + if ($(".check_head").length) { + if (!$._data($(".check_head").get(0), "events")) { + $(".check_head").on("change", function () { + var val = $(this).data("set"); + var name = $(this).data("name"); + var data = $(this).data("val"); + bookCheckboxHeader(val, name, data); + }); + } + } + }, // eslint-disable-next-line no-unused-vars onColumnSwitch: function (field, checked) { var visible = $("#books-table").bootstrapTable("getVisibleColumns"); @@ -525,10 +589,16 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/table_settings", + url: getPath() + "/ajax/table_settings", data: "{" + st + "}", }); + handle_header_buttons(); }, + onLoadSuccess: function() { + $("input:radio.check_head:checked").each(function () { + $(this).prop('checked', false); + }); + } }); $("#domain_allow_submit").click(function(event) { @@ -537,7 +607,7 @@ $(function() { $(this).closest("form").submit(); $.ajax ({ method:"get", - url: window.location.pathname + "/../../ajax/domainlist/1", + url: getPath() + "/ajax/domainlist/1", async: true, timeout: 900, success:function(data) { @@ -558,7 +628,7 @@ $(function() { $(this).closest("form").submit(); $.ajax ({ method:"get", - url: window.location.pathname + "/../../ajax/domainlist/0", + url: getPath() + "/ajax/domainlist/0", async: true, timeout: 900, success:function(data) { @@ -576,12 +646,12 @@ $(function() { function domainHandle(domainId) { $.ajax({ method:"post", - url: window.location.pathname + "/../../ajax/deletedomain", + url: getPath() + "/ajax/deletedomain", data: {"domainid":domainId} }); $.ajax({ method:"get", - url: window.location.pathname + "/../../ajax/domainlist/1", + url: getPath() + "/ajax/domainlist/1", async: true, timeout: 900, success:function(data) { @@ -590,7 +660,7 @@ $(function() { }); $.ajax({ method:"get", - url: window.location.pathname + "/../../ajax/domainlist/0", + url: getPath() + "/ajax/domainlist/0", async: true, timeout: 900, success:function(data) { @@ -826,7 +896,7 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../../ajax/user_table_settings", + url: getPath() + "/ajax/user_table_settings", data: "{" + st + "}", }); handle_header_buttons(); @@ -852,8 +922,8 @@ $(function() { function handle_header_buttons () { if (selections.length < 1) { - $("#user_delete_selection").addClass("disabled"); - $("#user_delete_selection").attr("aria-disabled", true); + $(".mass_selection").addClass("disabled"); + $(".mass_selection").attr("aria-disabled", true); $(".check_head").attr("aria-disabled", true); $(".check_head").attr("disabled", true); $(".check_head").prop('checked', false); @@ -865,8 +935,8 @@ function handle_header_buttons () { $(".multi_selector").attr("disabled", true); $(".header_select").attr("disabled", true); } else { - $("#user_delete_selection").removeClass("disabled"); - $("#user_delete_selection").attr("aria-disabled", false); + $(".mass_selection").removeClass("disabled"); + $(".mass_selection").attr("aria-disabled", false); $(".check_head").attr("aria-disabled", false); $(".check_head").removeAttr("disabled"); $(".button_head").attr("aria-disabled", false); @@ -875,8 +945,10 @@ function handle_header_buttons () { $(".multi_head").removeClass("hidden"); $(".multi_selector").attr("aria-disabled", false); $(".multi_selector").removeAttr("disabled"); - $('.multi_selector').selectpicker('refresh'); $(".header_select").removeAttr("disabled"); + if (typeof $.fn.selectpicker === "function") { + $('.multi_selector').selectpicker('refresh'); + } } } @@ -1003,7 +1075,7 @@ function loadSuccess() { $("input[data-name='passwd_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true); $("input[data-name='edit_shelf_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true); $("input[data-name='sidebar_read_and_unread'][data-pk='"+guest.data("pk")+"']").prop("disabled", true); - $(".user-remove[data-pk='"+guest.data("pk")+"']").hide(); + $(".user-remove[data-pk='" + guest.data("pk") + "']").hide(); } function move_header_elements() { @@ -1045,7 +1117,7 @@ function move_header_elements() { function () { $.ajax({ method: "post", - url: window.location.pathname + "/../../ajax/editlistusers/" + field, + url: getPath() + "/ajax/editlistusers/" + field, data: {"pk": result, "value": values, "action": val}, success: function (data) { handleListServerResponse(data); @@ -1059,7 +1131,6 @@ function move_header_elements() { }); } } - $("#user_delete_selection").click(function () { $("#user-table").bootstrapTable("uncheckAll"); }); @@ -1090,8 +1161,10 @@ function move_header_elements() { function () { $.ajax({ method: "post", - url: window.location.pathname + "/../../ajax/deleteuser", - data: {"userid": result}, + url: getPath() + "/ajax/deleteuser", + contentType: "application/json; charset=utf-8", + dataType: "json", + data: JSON.stringify({"userid": result}), success: function (data) { selections = selections.filter((el) => !result.includes(el)); handleListServerResponse(data); @@ -1117,7 +1190,7 @@ function handleListServerResponse (data) { ''); }); } - $("#user-table").bootstrapTable("refresh"); + $(".table.table-striped").bootstrapTable("refresh"); } function checkboxChange(checkbox, userId, field, field_index) { @@ -1132,20 +1205,21 @@ function checkboxChange(checkbox, userId, field, field_index) { }); } -function BookCheckboxChange(checkbox, userId, field) { +function BookCheckboxChange(checkbox, bookId, field) { var value = checkbox.checked ? "True" : "False"; var element = checkbox; $.ajax({ method: "post", url: getPath() + "/ajax/editbooks/" + field, - data: {"pk": userId, "value": value}, + data: JSON.stringify({"pk": [bookId], "value": value}), + contentType: "application/json; charset=utf-8", + dataType: "json", error: function(data) { element.checked = !element.checked; handleListServerResponse([{type:"danger", message:data.responseText}]) }, success: handleListServerResponse }); - console.log("test"); } function selectHeader(element, field) { @@ -1154,7 +1228,7 @@ function selectHeader(element, field) { var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id); $.ajax({ method: "post", - url: window.location.pathname + "/../../ajax/editlistusers/" + field, + url: getPath() + "/ajax/editlistusers/" + field, data: {"pk": result, "value": element.value}, error: function (data) { handleListServerResponse([{type:"danger", message:data.responseText}]) @@ -1167,12 +1241,35 @@ function selectHeader(element, field) { } } +function bookCheckboxHeader(CheckboxState, text, field_index) { + confirmDialog(text, "GeneralChangeModal", 0, function() { + var result = $('#books-table').bootstrapTable('getSelections').map(a => a.id); + $.ajax({ + method: "post", + url: getPath() + "/ajax/editbooks/" + field_index, + data: JSON.stringify({"pk": result, "field_index": field_index, "value": CheckboxState}), + contentType: "application/json; charset=utf-8", + dataType: "json", + error: function (data) { + handleListServerResponse([{type:"danger", message:data.responseText}]) + }, + success: function (data) { + handleListServerResponse (data, true) + }, + }); + },function() { + $("input:radio.check_head:checked").each(function() { + $(this).prop('checked', false); + }); + }); +} + function checkboxHeader(CheckboxState, field, field_index) { confirmDialog(field, "GeneralChangeModal", 0, function() { var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id); $.ajax({ method: "post", - url: window.location.pathname + "/../../ajax/editlistusers/" + field, + url: getPath() + "/ajax/editlistusers/" + field, data: {"pk": result, "field_index": field_index, "value": CheckboxState}, error: function (data) { handleListServerResponse([{type:"danger", message:data.responseText}]) @@ -1188,7 +1285,7 @@ function checkboxHeader(CheckboxState, field, field_index) { }); } -function deleteUser(a,id){ +function deleteUser(a, id){ confirmDialog( "btndeluser", "GeneralDeleteModal", @@ -1196,8 +1293,10 @@ function deleteUser(a,id){ function() { $.ajax({ method:"post", - url: window.location.pathname + "/../../ajax/deleteuser", - data: {"userid":id}, + url: getPath() + "/ajax/deleteuser", + contentType: "application/json; charset=utf-8", + dataType: "json", + data: JSON.stringify({"userid": [id]}), success: function (data) { userId = parseInt(id, 10); selections = selections.filter(item => item !== userId); @@ -1224,8 +1323,10 @@ function storeLocation() { function user_handle (userId) { $.ajax({ method:"post", - url: window.location.pathname + "/../../ajax/deleteuser", - data: {"userid":userId} + contentType: "application/json; charset=utf-8", + dataType: "json", + data: JSON.stringify({"userid": [userId]}), + url: getPath() + "/ajax/deleteuser", }); $("#user-table").bootstrapTable("refresh"); } @@ -1233,6 +1334,5 @@ function user_handle (userId) { function shorten_html(value, response) { if(value) { $(this).html("[...]"); - // value.split('\n').slice(0, 2).join("") + } } diff --git a/cps/templates/book_table.html b/cps/templates/book_table.html index efc1c780..4b22e584 100644 --- a/cps/templates/book_table.html +++ b/cps/templates/book_table.html @@ -15,27 +15,28 @@ {%- endmacro %} {% macro book_checkbox_row(parameter, show_text, sort) -%} - {% if parameter == "is_archived" %} -
- {{_('Archive selected books')}} +
+
+ {{_('Archive selected books')}}
-
-
- {{_('Unarchive selected books')}} +
+ {{_('Unarchive selected books')}}
-
+
{% elif parameter == "read_status" %} -
- {{_('Mark selected books as read')}} +
+
+ {{_('Mark selected books as read')}}
-
-
- {{_('Mark selected books as unread')}}
-
+
+ {{_('Mark selected books as unread')}}
+
+
{% endif %} {{show_text}} @@ -55,7 +56,7 @@
{{_('Merge selected books')}}
-
+
{{_('Clear selections')}}
@@ -82,7 +83,7 @@ {% if current_user.role_edit() %} - + {% endif %} {{ text_table_row('title', _('Enter Title'),_('Title'), true, true) }} @@ -123,9 +124,9 @@ {% endfor %} {% if current_user.role_delete_books() and current_user.role_edit()%} -
+
{{_('Delete selected books')}} -
+

{{_('Delete')}} @@ -137,6 +138,8 @@ {% endblock %} {% block modal %} {{ delete_book(current_user.role_delete_books()) }} +{{ delete_confirm_modal() }} +{{ change_confirm_modal() }} {% if current_user.role_edit() %} -