From 87884c1af2bc7275916e60c086a5b0565017198e Mon Sep 17 00:00:00 2001 From: robochud Date: Wed, 31 Mar 2021 00:56:12 +0200 Subject: [PATCH 01/44] Update messages.po very little typo --- cps/translations/de/LC_MESSAGES/messages.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/translations/de/LC_MESSAGES/messages.po b/cps/translations/de/LC_MESSAGES/messages.po index 719381a2..2b9c830b 100644 --- a/cps/translations/de/LC_MESSAGES/messages.po +++ b/cps/translations/de/LC_MESSAGES/messages.po @@ -812,7 +812,7 @@ msgstr "Bücherliste" #: cps/render_template.py:93 msgid "Show Books List" -msgstr "Zeiche Bücherliste" +msgstr "Zeige Bücherliste" #: cps/shelf.py:69 cps/shelf.py:122 msgid "Invalid shelf specified" From a43021e87c6a2f33a49bec763df6059a9eb3f2a8 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Wed, 7 Apr 2021 17:46:17 +0200 Subject: [PATCH 02/44] Fix for #1938 --- cps/static/js/table.js | 2 +- cps/templates/user_table.html | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 96901d0b..332b7f54 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -618,7 +618,7 @@ function responseHandler(res) { } function singleUserFormatter(value, row) { - return '' + this.buttontext + '' + return '' + this.buttontext + '' } function checkboxFormatter(value, row, index){ diff --git a/cps/templates/user_table.html b/cps/templates/user_table.html index e7ddf156..a7bee4f6 100644 --- a/cps/templates/user_table.html +++ b/cps/templates/user_table.html @@ -111,13 +111,13 @@ {{ user_table_row('allowed_column_value', _("Edit Allowed Column Values"), _("Allowed Column Values"), false, true, 2) }} {{ user_table_row('denied_column_value', _("Edit Denied Column Values"), _("Denied Columns Values"), false, true, 3) }} {{ user_checkbox_row("role", "admin_role", _('Admin'), visiblility, all_roles)}} - {{ user_checkbox_row("role", "download_role",_('Upload'), visiblility, all_roles)}} - {{ user_checkbox_row("role", "upload_role", _('Download'), visiblility, all_roles)}} - {{ user_checkbox_row("role", "edit_role", _('Edit'), visiblility, all_roles)}} {{ user_checkbox_row("role", "passwd_role", _('Change Password'), visiblility, all_roles)}} - {{ user_checkbox_row("role", "edit_shelf_role", _('Edit Public Shelfs'), visiblility, all_roles)}} - {{ user_checkbox_row("role", "delete_role", _('Delete'), visiblility, all_roles)}} + {{ user_checkbox_row("role", "upload_role",_('Upload'), visiblility, all_roles)}} + {{ user_checkbox_row("role", "download_role", _('Download'), visiblility, all_roles)}} {{ user_checkbox_row("role", "viewer_role", _('View'), visiblility, all_roles)}} + {{ user_checkbox_row("role", "edit_role", _('Edit'), visiblility, all_roles)}} + {{ user_checkbox_row("role", "delete_role", _('Delete'), visiblility, all_roles)}} + {{ user_checkbox_row("role", "edit_shelf_role", _('Edit Public Shelfs'), visiblility, all_roles)}} {{ user_checkbox_row("sidebar_view", "detail_random", _('Show Random Books in Detail View'), visiblility, sidebar_settings)}} {{ user_checkbox_row("sidebar_view", "sidebar_language", _('Show language selection'), visiblility, sidebar_settings)}} {{ user_checkbox_row("sidebar_view", "sidebar_series", _('Show series selection'), visiblility, sidebar_settings)}} From fac232229ecbd8a3fc92faeaabb8c49729809293 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Wed, 7 Apr 2021 17:49:19 +0200 Subject: [PATCH 03/44] Added missing ead/unread category in user list #1938 --- cps/templates/user_table.html | 1 + 1 file changed, 1 insertion(+) diff --git a/cps/templates/user_table.html b/cps/templates/user_table.html index a7bee4f6..b8a9d2c2 100644 --- a/cps/templates/user_table.html +++ b/cps/templates/user_table.html @@ -120,6 +120,7 @@ {{ user_checkbox_row("role", "edit_shelf_role", _('Edit Public Shelfs'), visiblility, all_roles)}} {{ user_checkbox_row("sidebar_view", "detail_random", _('Show Random Books in Detail View'), visiblility, sidebar_settings)}} {{ user_checkbox_row("sidebar_view", "sidebar_language", _('Show language selection'), visiblility, sidebar_settings)}} + {{ user_checkbox_row("sidebar_view", "sidebar_read_and_unread", _('Show read/unread selection'), visiblility, sidebar_settings)}} {{ user_checkbox_row("sidebar_view", "sidebar_series", _('Show series selection'), visiblility, sidebar_settings)}} {{ user_checkbox_row("sidebar_view", "sidebar_category", _('Show category selection'), visiblility, sidebar_settings)}} {{ user_checkbox_row("sidebar_view", "sidebar_random", _('Show random books'), visiblility, sidebar_settings)}} From 78071841cca9ef8eed7b60c23739b8a0a46e63c1 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Wed, 7 Apr 2021 18:19:48 +0200 Subject: [PATCH 04/44] After Deleting a book the list page is still displayed #1938 --- cps/editbooks.py | 4 ++-- cps/static/js/main.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cps/editbooks.py b/cps/editbooks.py index cb8388ef..931ad13e 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -229,14 +229,14 @@ def modify_identifiers(input_identifiers, db_identifiers, db_session): @editbook.route("/ajax/delete/") @login_required def delete_book_from_details(book_id): - return Response(delete_book(book_id,"", True), mimetype='application/json') + return Response(delete_book(book_id, "", True), mimetype='application/json') @editbook.route("/delete/", defaults={'book_format': ""}) @editbook.route("/delete//") @login_required def delete_book_ajax(book_id, book_format): - return delete_book(book_id,book_format, False) + return delete_book(book_id, book_format, False) def delete_whole_book(book_id, book): diff --git a/cps/static/js/main.js b/cps/static/js/main.js index 11ce6ed1..81308c64 100644 --- a/cps/static/js/main.js +++ b/cps/static/js/main.js @@ -142,10 +142,11 @@ $("#delete_confirm").click(function() { //get data-id attribute of the clicked element var deleteId = $(this).data("delete-id"); var bookFormat = $(this).data("delete-format"); + var ajaxResponse = $(this).data("ajax"); if (bookFormat) { window.location.href = getPath() + "/delete/" + deleteId + "/" + bookFormat; } else { - if ($(this).data("delete-format")) { + if (ajaxResponse) { path = getPath() + "/ajax/delete/" + deleteId; $.ajax({ method:"get", @@ -187,6 +188,7 @@ $("#deleteModal").on("show.bs.modal", function(e) { } $(e.currentTarget).find("#delete_confirm").data("delete-id", bookId); $(e.currentTarget).find("#delete_confirm").data("delete-format", bookfomat); + $(e.currentTarget).find("#delete_confirm").data("ajax", $(e.relatedTarget).data("ajax")); }); From 067fb1b0b727fdfe22dc6d0cc3bfd253847f5900 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Wed, 7 Apr 2021 18:47:48 +0200 Subject: [PATCH 05/44] Prevent delete Guest user and redirect to admin page after user delete --- cps/admin.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 1d4b5a84..966e01ff 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -1185,10 +1185,14 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support): if to_save.get("delete"): if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, ub.User.id != content.id).count(): - ub.session.query(ub.User).filter(ub.User.id == content.id).delete() - ub.session_commit() - flash(_(u"User '%(nick)s' deleted", nick=content.name), category="success") - return redirect(url_for('admin.admin')) + if content.name != "Guest": + ub.session.query(ub.User).filter(ub.User.id == content.id).delete() + ub.session_commit() + flash(_(u"User '%(nick)s' deleted", nick=content.name), category="success") + return redirect(url_for('admin.admin')) + else: + flash(_(u"Can't delete Guest User"), category="error") + return redirect(url_for('admin.admin')) else: flash(_(u"No admin user remaining, can't delete user", nick=content.name), category="error") return redirect(url_for('admin.admin')) @@ -1255,6 +1259,7 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support): except OperationalError: ub.session.rollback() flash(_(u"Settings DB is not Writeable"), category="error") + return "" @admi.route("/admin/user/new", methods=["GET", "POST"]) @@ -1350,7 +1355,9 @@ def edit_user(user_id): kobo_support = feature_support['kobo'] and config.config_kobo_sync if request.method == "POST": to_save = request.form.to_dict() - _handle_edit_user(to_save, content, languages, translations, kobo_support) + resp = _handle_edit_user(to_save, content, languages, translations, kobo_support) + if resp: + return resp return render_title_template("user_edit.html", translations=translations, languages=languages, From 7561eabe522903a5f5ea5e6b8aef0f417697dadd Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Wed, 7 Apr 2021 18:56:17 +0200 Subject: [PATCH 06/44] Implement Backend to deny editing Guest rights restriction #1938 --- cps/admin.py | 7 +++++++ cps/static/js/table.js | 1 + 2 files changed, 8 insertions(+) diff --git a/cps/admin.py b/cps/admin.py index 966e01ff..04ddbed1 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -335,6 +335,9 @@ def edit_list_user(param): elif param == 'kindle_mail': user.kindle_mail = valid_email(vals['value']) if vals['value'] else "" elif param == 'role': + if user.name == "Guest" and int(vals['field_index']) in \ + [constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]: + raise Exception(_("Guest can't have this role")) if vals['value'] == 'true': user.role |= int(vals['field_index']) else: @@ -345,6 +348,8 @@ def edit_list_user(param): return _(u"No admin user remaining, can't remove admin role", nick=user.name), 400 user.role &= ~int(vals['field_index']) elif param == 'sidebar_view': + if user.name == "Guest" and int(vals['field_index']) == constants.SIDEBAR_READ_AND_UNREAD: + raise Exception(_("Guest can't have this view")) if vals['value'] == 'true': user.sidebar_view |= int(vals['field_index']) else: @@ -358,6 +363,8 @@ def edit_list_user(param): elif param == 'denied_column_value': user.denied_column_value = vals['value'] elif param == 'locale': + if user.name == "Guest": + raise Exception(_("Guest's Locale is determined automatically and can't be set")) user.locale = vals['value'] elif param == 'default_language': user.default_language = vals['value'] diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 332b7f54..b9e6a202 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -459,6 +459,7 @@ $(function() { $("input[data-name='admin_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true); $("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); // ToDo: Disable delete }, From 2d73f541c0a4bd5c7d5e95459e1a07a25d6b21ed Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Thu, 8 Apr 2021 19:37:08 +0200 Subject: [PATCH 07/44] Bugfix sort books list and user list Prevent transferring password hash to client --- cps/admin.py | 13 ++++++++++--- cps/config_sql.py | 2 +- cps/db.py | 4 ++-- cps/gdriveutils.py | 2 +- cps/static/js/table.js | 14 +++++++++----- cps/ub.py | 2 +- cps/web.py | 9 +++++---- 7 files changed, 29 insertions(+), 17 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 04ddbed1..4038977e 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -37,7 +37,7 @@ from flask_babel import gettext as _ from sqlalchemy import and_ from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError -from sqlalchemy.sql.expression import func, or_ +from sqlalchemy.sql.expression import func, or_, text from . import constants, logger, helper, services from .cli import filepicker @@ -244,6 +244,13 @@ def list_users(): off = request.args.get("offset") or 0 limit = request.args.get("limit") or 10 search = request.args.get("search") + sort = request.args.get("sort") + order = request.args.get("order") + if sort and order: + order = text(sort + " " + order) + else: + order = ub.User.name.desc() + all_user = ub.session.query(ub.User) if not config.config_anonbrowse: all_user = all_user.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS) @@ -252,10 +259,10 @@ def list_users(): users = all_user.filter(or_(func.lower(ub.User.name).ilike("%" + search + "%"), func.lower(ub.User.kindle_mail).ilike("%" + search + "%"), func.lower(ub.User.email).ilike("%" + search + "%")))\ - .offset(off).limit(limit).all() + .order_by(order).offset(off).limit(limit).all() filtered_count = len(users) else: - users = all_user.offset(off).limit(limit).all() + users = all_user.order_by(order).offset(off).limit(limit).all() filtered_count = total_count for user in users: diff --git a/cps/config_sql.py b/cps/config_sql.py index 2ab0e3d6..3e5e4c59 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -24,7 +24,7 @@ import sys from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON from sqlalchemy.exc import OperationalError try: - # Compability with sqlalchemy 2.0 + # Compatibility with sqlalchemy 2.0 from sqlalchemy.orm import declarative_base except ImportError: from sqlalchemy.ext.declarative import declarative_base diff --git a/cps/db.py b/cps/db.py index 5cb04ed3..b875ded7 100644 --- a/cps/db.py +++ b/cps/db.py @@ -33,7 +33,7 @@ from sqlalchemy.orm.collections import InstrumentedList from sqlalchemy.ext.declarative import DeclarativeMeta from sqlalchemy.exc import OperationalError try: - # Compability with sqlalchemy 2.0 + # Compatibility with sqlalchemy 2.0 from sqlalchemy.orm import declarative_base except ImportError: from sqlalchemy.ext.declarative import declarative_base @@ -393,7 +393,7 @@ class AlchemyEncoder(json.JSONEncoder): if isinstance(o.__class__, DeclarativeMeta): # an SQLAlchemy class fields = {} - for field in [x for x in dir(o) if not x.startswith('_') and x != 'metadata']: + for field in [x for x in dir(o) if not x.startswith('_') and x != 'metadata' and x!="password"]: if field == 'books': continue data = o.__getattribute__(field) diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py index a98d0b66..4c262661 100644 --- a/cps/gdriveutils.py +++ b/cps/gdriveutils.py @@ -29,7 +29,7 @@ from sqlalchemy import Column, UniqueConstraint from sqlalchemy import String, Integer from sqlalchemy.orm import sessionmaker, scoped_session try: - # Compability with sqlalchemy 2.0 + # Compatibility with sqlalchemy 2.0 from sqlalchemy.orm import declarative_base except ImportError: from sqlalchemy.ext.declarative import declarative_base diff --git a/cps/static/js/table.js b/cps/static/js/table.js index b9e6a202..a0503976 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -460,8 +460,7 @@ $(function() { $("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); - // ToDo: Disable delete - + $(".user-remove[data-pk='"+guest.data("pk")+"']").prop("disabled", true); }, // eslint-disable-next-line no-unused-vars @@ -604,7 +603,7 @@ function EbookActions (value, row) { /* Function for deleting books */ function UserActions (value, row) { return [ - "
", + "
", "", "
" ].join(""); @@ -624,9 +623,9 @@ function singleUserFormatter(value, row) { function checkboxFormatter(value, row, index){ if(value & this.column) - return ''; + return ''; else - return ''; + return ''; } function checkboxChange(checkbox, userId, field, field_index) { @@ -733,6 +732,11 @@ function user_handle (userId) { }); } +function checkboxSorter(a, b, c, d) +{ + return a - b +} + function test(){ console.log("hello"); } diff --git a/cps/ub.py b/cps/ub.py index 6cbc0383..a85f7404 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -44,7 +44,7 @@ from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float, from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.sql.expression import func try: - # Compability with sqlalchemy 2.0 + # Compatibility with sqlalchemy 2.0 from sqlalchemy.orm import declarative_base except ImportError: from sqlalchemy.ext.declarative import declarative_base diff --git a/cps/web.py b/cps/web.py index e1acdcef..658ff735 100644 --- a/cps/web.py +++ b/cps/web.py @@ -755,11 +755,12 @@ def books_table(): def list_books(): off = request.args.get("offset") or 0 limit = request.args.get("limit") or config.config_books_per_page - # sort = request.args.get("sort") - if request.args.get("order") == 'desc': - order = [db.Books.timestamp.desc()] + sort = request.args.get("sort") + order = request.args.get("order") + if sort and order: + order = [text(sort + " " + order)] else: - order = [db.Books.timestamp.asc()] + order = [db.Books.timestamp.desc()] search = request.args.get("search") total_count = calibre_db.session.query(db.Books).count() if search: From ae97e87506421716e5a2d42174198911b9af05d7 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sat, 10 Apr 2021 11:32:11 +0200 Subject: [PATCH 08/44] Delete user working from user table (#1938) Comment in helper --- cps/admin.py | 48 ++++++++++++++++++++++++-------------- cps/helper.py | 2 +- cps/static/js/table.js | 52 ++++++++++++++++++++++++++---------------- 3 files changed, 64 insertions(+), 38 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 4038977e..76092993 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -31,7 +31,7 @@ from datetime import datetime, timedelta from babel import Locale as LC from babel.dates import format_datetime -from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g +from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response from flask_login import login_required, current_user, logout_user, confirm_login from flask_babel import gettext as _ from sqlalchemy import and_ @@ -277,12 +277,19 @@ def list_users(): response.headers["Content-Type"] = "application/json; charset=utf-8" return response -@admi.route("/ajax/deleteuser") +@admi.route("/ajax/deleteuser", methods=['POST']) @login_required @admin_required def delete_user(): - # ToDo User delete check also not last one - return "" + user_id = request.values.get('userid', -1) + content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).one_or_none() + try: + message = _delete_user(content) + return Response(json.dumps({'type': "success", 'message': message}), mimetype='application/json') + except Exception as ex: + return Response(json.dumps({'type': "danger", 'message':str(ex)}), mimetype='application/json') + log.error("User not found") + return Response(json.dumps({'type': "danger", 'message':_("User not found")}), mimetype='application/json') @admi.route("/ajax/getlocale") @login_required @@ -1194,22 +1201,29 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support): ub.session.rollback() flash(_(u"Settings DB is not Writeable"), category="error") +def _delete_user(content): + if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, + ub.User.id != content.id).count(): + if content.name != "Guest": + ub.session.query(ub.User).filter(ub.User.id == content.id).delete() + ub.session_commit() + log.info(u"User {} deleted".format(content.name)) + return(_(u"User '%(nick)s' deleted", nick=content.name)) + else: + log.warning(_(u"Can't delete Guest User")) + raise Exception(_(u"Can't delete Guest User")) + else: + log.warning(u"No admin user remaining, can't delete user") + raise Exception(_(u"No admin user remaining, can't delete user")) + def _handle_edit_user(to_save, content, languages, translations, kobo_support): if to_save.get("delete"): - if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, - ub.User.id != content.id).count(): - if content.name != "Guest": - ub.session.query(ub.User).filter(ub.User.id == content.id).delete() - ub.session_commit() - flash(_(u"User '%(nick)s' deleted", nick=content.name), category="success") - return redirect(url_for('admin.admin')) - else: - flash(_(u"Can't delete Guest User"), category="error") - return redirect(url_for('admin.admin')) - else: - flash(_(u"No admin user remaining, can't delete user", nick=content.name), category="error") - return redirect(url_for('admin.admin')) + try: + flash(_delete_user(content), category="success") + except Exception as ex: + flash(str(ex), category="error") + return redirect(url_for('admin.admin')) else: if not ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, ub.User.id != content.id).count() and 'admin_role' not in to_save: diff --git a/cps/helper.py b/cps/helper.py index f1c32ea0..29163685 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -795,8 +795,8 @@ def tags_filters(): # checks if domain is in database (including wildcards) # example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name; # from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/ +# in all calls the email address is checked for validity def check_valid_domain(domain_text): - # domain_text = domain_text.split('@', 1)[-1].lower() sql = "SELECT * FROM registration WHERE (:domain LIKE domain and allow = 1);" result = ub.session.query(ub.Registration).from_statement(text(sql)).params(domain=domain_text).all() if not len(result): diff --git a/cps/static/js/table.js b/cps/static/js/table.js index a0503976..96d28c85 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -525,7 +525,6 @@ $(function() { }); } - $("#user-table").on("click-cell.bs.table", function (field, value, row, $element) { if (value === "denied_column_value") { ConfirmDialog("btndeluser", "GeneralDeleteModal", $element.id, user_handle); @@ -563,7 +562,6 @@ $(function() { $(".button_head").removeClass("disabled"); $(".header_select").removeAttr("disabled"); } - }); }); @@ -603,7 +601,7 @@ function EbookActions (value, row) { /* Function for deleting books */ function UserActions (value, row) { return [ - "
", + "
", "", "
" ].join(""); @@ -715,26 +713,40 @@ function checkboxHeader(CheckboxState, field, field_index) { }); } -function user_handle (userId) { - $.ajax({ - method:"post", - url: window.location.pathname + "/../../ajax/deleteuser", - data: {"userid":userId} - }); - $.ajax({ - method:"get", - url: window.location.pathname + "/../../ajax/listusers", - async: true, - timeout: 900, - success:function(data) { - $("#user-table").bootstrapTable("load", data); +function deleteUser(a,b){ + confirmDialog( + "btndeluser", + "GeneralDeleteModal", + 0, + function() { + $.ajax({ + method:"post", + url: window.location.pathname + "/../../ajax/deleteuser", + data: {"userid":b}, + success:function(data) { + $("#flash_success").remove(); + $("#flash_danger").remove(); + if (!jQuery.isEmptyObject(data)) { + $( ".navbar" ).after( '
' + + '
'+data.message+'
' + + '
'); + } + $.ajax({ + method: "get", + url: window.location.pathname + "/../../ajax/listusers", + async: true, + timeout: 900, + success: function (data) { + $("#user-table").bootstrapTable("load", data); + } + }); + } + }); } - }); + ); } -function checkboxSorter(a, b, c, d) -{ - return a - b +function user_handle (userId) { } function test(){ From b6177b27f4d49a7a7e2c7e860f06c2ee3ad0d368 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sun, 11 Apr 2021 19:59:20 +0200 Subject: [PATCH 09/44] Sorting of users in table according to selection possible --- cps/admin.py | 36 ++++++++++++++++++++++++---------- cps/db.py | 15 ++++++++------ cps/static/js/table.js | 32 ++++++++++-------------------- cps/templates/user_table.html | 2 +- cps/web.py | 37 +++++++++++++++++++++++++++-------- 5 files changed, 75 insertions(+), 47 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 76092993..935aee45 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -38,6 +38,7 @@ from sqlalchemy import and_ from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError from sqlalchemy.sql.expression import func, or_, text +# from sqlalchemy.func import field from . import constants, logger, helper, services from .cli import filepicker @@ -241,29 +242,44 @@ def edit_user_table(): @login_required @admin_required def list_users(): - off = request.args.get("offset") or 0 - limit = request.args.get("limit") or 10 + off = int(request.args.get("offset") or 0) + limit = int(request.args.get("limit") or 10) search = request.args.get("search") - sort = request.args.get("sort") + sort = request.args.get("sort", "state") order = request.args.get("order") - if sort and order: + state = None + if sort != "state" and order: order = text(sort + " " + order) else: order = ub.User.name.desc() + if sort == "state": + state = json.loads(request.args.get("state")) all_user = ub.session.query(ub.User) if not config.config_anonbrowse: all_user = all_user.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS) - total_count = all_user.count() + + total_count = filtered_count = all_user.count() + if search: - users = all_user.filter(or_(func.lower(ub.User.name).ilike("%" + search + "%"), + all_user = all_user.filter(or_(func.lower(ub.User.name).ilike("%" + search + "%"), func.lower(ub.User.kindle_mail).ilike("%" + search + "%"), - func.lower(ub.User.email).ilike("%" + search + "%")))\ - .order_by(order).offset(off).limit(limit).all() - filtered_count = len(users) + func.lower(ub.User.email).ilike("%" + search + "%"))) + if state: + outcome = list() + userlist = {user.id:user for user in all_user.all()} + for entry in state: + outcome.append(userlist[entry]) + del userlist[entry] + for entry in userlist: + outcome.append(userlist[entry]) + if request.args.get("order", "").lower() == "asc": + outcome.reverse() + users = outcome[off:off + limit] else: users = all_user.order_by(order).offset(off).limit(limit).all() - filtered_count = total_count + if search: + filtered_count = len(users) for user in users: if user.default_language == "all": diff --git a/cps/db.py b/cps/db.py index b875ded7..51aeeca1 100644 --- a/cps/db.py +++ b/cps/db.py @@ -689,23 +689,26 @@ class CalibreDB(): return self.session.query(Books) \ .filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first() - # read search results from calibre-database and return it (function is used for feed and simple search - def get_search_results(self, term, offset=None, order=None, limit=None): - order = order or [Books.sort] - pagination = None + def search_query(self, term): term.strip().lower() self.session.connection().connection.connection.create_function("lower", 1, lcase) q = list() authorterms = re.split("[, ]+", term) for authorterm in authorterms: q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%"))) - result = self.session.query(Books).filter(self.common_filters(True)).filter( + return self.session.query(Books).filter(self.common_filters(True)).filter( or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")), Books.series.any(func.lower(Series.name).ilike("%" + term + "%")), Books.authors.any(and_(*q)), Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")), func.lower(Books.title).ilike("%" + term + "%") - )).order_by(*order).all() + )) + + # read search results from calibre-database and return it (function is used for feed and simple search + def get_search_results(self, term, offset=None, order=None, limit=None): + order = order or [Books.sort] + pagination = None + result = self.search_query(term).order_by(*order).all() result_count = len(result) if offset != None and limit != None: offset = int(offset) diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 96d28c85..59b70626 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -422,6 +422,7 @@ $(function() { $("#user-table").bootstrapTable({ sidePagination: "server", + queryParams: queryParams, pagination: true, paginationLoop: false, paginationDetailHAlign: " hidden", @@ -462,28 +463,10 @@ $(function() { $("input[data-name='sidebar_read_and_unread'][data-pk='"+guest.data("pk")+"']").prop("disabled", true); $(".user-remove[data-pk='"+guest.data("pk")+"']").prop("disabled", true); }, - - // eslint-disable-next-line no-unused-vars - /*onEditableSave: function (field, row, oldvalue, $el) { - if (field === "title" || field === "authors") { - $.ajax({ - method:"get", - dataType: "json", - url: window.location.pathname + "/../../ajax/sort_value/" + field + "/" + row.id, - success: function success(data) { - var key = Object.keys(data)[0]; - $("#books-table").bootstrapTable("updateCellByUniqueId", { - id: row.id, - field: key, - value: data[key] - }); - // console.log(data); - } - }); - } - },*/ - // eslint-disable-next-line no-unused-vars - onColumnSwitch: function (field, checked) { + onSort: function(a, b) { + console.log("huh"); + }, + onColumnSwitch: function () { var visible = $("#user-table").bootstrapTable("getVisibleColumns"); var hidden = $("#user-table").bootstrapTable("getHiddenColumns"); var st = ""; @@ -746,6 +729,11 @@ function deleteUser(a,b){ ); } +function queryParams(params) +{ + params.state = JSON.stringify(selections); + return params; +} function user_handle (userId) { } diff --git a/cps/templates/user_table.html b/cps/templates/user_table.html index b8a9d2c2..5f33cb99 100644 --- a/cps/templates/user_table.html +++ b/cps/templates/user_table.html @@ -144,7 +144,7 @@ {{ restrict_modal() }} {% endblock %} {% block js %} - + diff --git a/cps/web.py b/cps/web.py index 658ff735..a8c7b71a 100644 --- a/cps/web.py +++ b/cps/web.py @@ -753,21 +753,42 @@ def books_table(): @web.route("/ajax/listbooks") @login_required def list_books(): - off = request.args.get("offset") or 0 - limit = request.args.get("limit") or config.config_books_per_page - sort = request.args.get("sort") + off = int(request.args.get("offset") or 0) + limit = int(request.args.get("limit") or config.config_books_per_page) + search = request.args.get("search") + sort = request.args.get("sort", "state") order = request.args.get("order") - if sort and order: + state = None + if sort != "state" and order: order = [text(sort + " " + order)] else: order = [db.Books.timestamp.desc()] - search = request.args.get("search") - total_count = calibre_db.session.query(db.Books).count() - if search: + if sort == "state": + state = json.loads(request.args.get("state")) + + total_count = filtered_count = calibre_db.session.query(db.Books).count() + + if state: + outcome = list() + if search: + books = calibre_db.search_query(search) + filtered_count = len(books) + else: + books = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).all() + booklist = {book.id: book for book in books} + for entry in state: + outcome.append(booklist[entry]) + del booklist[entry] + for entry in booklist: + outcome.append(booklist[entry]) + if request.args.get("order", "").lower() == "asc": + outcome.reverse() + entries = outcome[off:off + limit] + elif search: entries, filtered_count, __ = calibre_db.get_search_results(search, off, order, limit) else: entries, __, __ = calibre_db.fill_indexpage((int(off) / (int(limit)) + 1), limit, db.Books, True, order) - filtered_count = total_count + for entry in entries: for index in range(0, len(entry.languages)): try: From 04971f86724da18e3e2a8ed126058bdc9f68048b Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sun, 11 Apr 2021 20:01:40 +0200 Subject: [PATCH 10/44] Bugfix wrong js file in user list --- cps/templates/user_table.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/templates/user_table.html b/cps/templates/user_table.html index 5f33cb99..b8a9d2c2 100644 --- a/cps/templates/user_table.html +++ b/cps/templates/user_table.html @@ -144,7 +144,7 @@ {{ restrict_modal() }} {% endblock %} {% block js %} - + From 0f95800dde1ed015e878ce1b5c2afe153e7f04e5 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Mon, 12 Apr 2021 18:23:25 +0200 Subject: [PATCH 11/44] Update sqlalchemy-utils dependency for oauth Sort for state checkbox in user list and books list working --- cps/static/js/table.js | 3 +-- optional-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 59b70626..68f00aee 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -117,6 +117,7 @@ $(function() { $("#books-table").bootstrapTable({ sidePagination: "server", + queryParams: queryParams, pagination: true, paginationLoop: false, paginationDetailHAlign: " hidden", @@ -734,8 +735,6 @@ function queryParams(params) params.state = JSON.stringify(selections); return params; } -function user_handle (userId) { -} function test(){ console.log("hello"); diff --git a/optional-requirements.txt b/optional-requirements.txt index b6fee806..ca54fe4d 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -27,7 +27,7 @@ Flask-SimpleLDAP>=1.4.0,<1.5.0 #oauth Flask-Dance>=1.4.0,<3.1.0 -SQLAlchemy-Utils>=0.33.5,<0.37.0 +SQLAlchemy-Utils>=0.33.5,<0.38.0 # extracting metadata lxml>=3.8.0,<4.7.0 From 90f2b3fb212352a3aa9e6e47060bf5e01f7cb8af Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Mon, 12 Apr 2021 18:39:09 +0200 Subject: [PATCH 12/44] Refactored list checkbox sort --- cps/admin.py | 22 +++++++--------------- cps/db.py | 13 +++++++++++++ cps/web.py | 23 ++++++++--------------- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 935aee45..10f9a5b4 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -246,15 +246,16 @@ def list_users(): limit = int(request.args.get("limit") or 10) search = request.args.get("search") sort = request.args.get("sort", "state") - order = request.args.get("order") + order = request.args.get("order", "").lower() state = None - if sort != "state" and order: - order = text(sort + " " + order) - else: - order = ub.User.name.desc() if sort == "state": state = json.loads(request.args.get("state")) + if sort != "state" and order: + order = text(sort + " " + order) + elif not state: + order = ub.User.name.desc() + all_user = ub.session.query(ub.User) if not config.config_anonbrowse: all_user = all_user.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS) @@ -266,16 +267,7 @@ def list_users(): func.lower(ub.User.kindle_mail).ilike("%" + search + "%"), func.lower(ub.User.email).ilike("%" + search + "%"))) if state: - outcome = list() - userlist = {user.id:user for user in all_user.all()} - for entry in state: - outcome.append(userlist[entry]) - del userlist[entry] - for entry in userlist: - outcome.append(userlist[entry]) - if request.args.get("order", "").lower() == "asc": - outcome.reverse() - users = outcome[off:off + limit] + users = calibre_db.get_checkbox_sorted(all_user.all(), state, off, limit, request.args.get("order", "").lower()) else: users = all_user.order_by(order).offset(off).limit(limit).all() if search: diff --git a/cps/db.py b/cps/db.py index 51aeeca1..0d7055a4 100644 --- a/cps/db.py +++ b/cps/db.py @@ -616,6 +616,19 @@ class CalibreDB(): return and_(lang_filter, pos_content_tags_filter, ~neg_content_tags_filter, pos_content_cc_filter, ~neg_content_cc_filter, archived_filter) + @staticmethod + def get_checkbox_sorted(inputlist, state, offset, limit, order): + outcome = list() + elementlist = {ele.id: ele for ele in inputlist} + for entry in state: + outcome.append(elementlist[entry]) + del elementlist[entry] + for entry in elementlist: + outcome.append(elementlist[entry]) + if order == "asc": + outcome.reverse() + return outcome[offset:offset + limit] + # Fill indexpage with all requested data from database def fill_indexpage(self, page, pagesize, database, db_filter, order, *join): return self.fill_indexpage_with_archived_books(page, pagesize, database, db_filter, order, False, *join) diff --git a/cps/web.py b/cps/web.py index a8c7b71a..528e285a 100644 --- a/cps/web.py +++ b/cps/web.py @@ -757,33 +757,26 @@ def list_books(): limit = int(request.args.get("limit") or config.config_books_per_page) search = request.args.get("search") sort = request.args.get("sort", "state") - order = request.args.get("order") + order = request.args.get("order", "").lower() state = None - if sort != "state" and order: - order = [text(sort + " " + order)] - else: - order = [db.Books.timestamp.desc()] + if sort == "state": state = json.loads(request.args.get("state")) + if sort != "state" and order: + order = [text(sort + " " + order)] + elif not state: + order = [db.Books.timestamp.desc()] + total_count = filtered_count = calibre_db.session.query(db.Books).count() if state: - outcome = list() if search: books = calibre_db.search_query(search) filtered_count = len(books) else: books = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).all() - booklist = {book.id: book for book in books} - for entry in state: - outcome.append(booklist[entry]) - del booklist[entry] - for entry in booklist: - outcome.append(booklist[entry]) - if request.args.get("order", "").lower() == "asc": - outcome.reverse() - entries = outcome[off:off + limit] + entries = calibre_db.get_checkbox_sorted(books, state, off, limit,order) elif search: entries, filtered_count, __ = calibre_db.get_search_results(search, off, order, limit) else: From d0a895628ea1137a7bede955d84f42eda849617d Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Mon, 12 Apr 2021 18:45:44 +0200 Subject: [PATCH 13/44] Prevent delete of Guest user #1938 --- 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 68f00aee..7a07d82d 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -462,7 +462,7 @@ $(function() { $("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")+"']").prop("disabled", true); + $(".user-remove[data-pk='"+guest.data("pk")+"']").hide(); }, onSort: function(a, b) { console.log("huh"); From d32b2ca524371e377ca19deabbab6f3553ddc8b6 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Mon, 12 Apr 2021 19:04:27 +0200 Subject: [PATCH 14/44] Prevent traceback after delete user flash message in case last admin role is removed #1938 --- cps/admin.py | 8 +++++--- cps/static/js/table.js | 35 ++++++++++++++++++++--------------- cps/web.py | 2 +- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 10f9a5b4..5cd31f18 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -249,7 +249,7 @@ def list_users(): order = request.args.get("order", "").lower() state = None if sort == "state": - state = json.loads(request.args.get("state")) + state = json.loads(request.args.get("state", "[]")) if sort != "state" and order: order = text(sort + " " + order) @@ -356,7 +356,7 @@ def edit_list_user(param): user.email = check_email(vals['value']) elif param == 'kindle_mail': user.kindle_mail = valid_email(vals['value']) if vals['value'] else "" - elif param == 'role': + elif param.endswith('role'): if user.name == "Guest" and int(vals['field_index']) in \ [constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]: raise Exception(_("Guest can't have this role")) @@ -367,7 +367,9 @@ def edit_list_user(param): if not ub.session.query(ub.User).\ filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, ub.User.id != user.id).count(): - return _(u"No admin user remaining, can't remove admin role", nick=user.name), 400 + return Response(json.dumps({'type': "danger", + 'message':_(u"No admin user remaining, can't remove admin role", + nick=user.name)}), mimetype='application/json') user.role &= ~int(vals['field_index']) elif param == 'sidebar_view': if user.name == "Guest" and int(vals['field_index']) == constants.SIDEBAR_READ_AND_UNREAD: diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 7a07d82d..7c4b04a7 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -612,25 +612,30 @@ function checkboxFormatter(value, row, index){ function checkboxChange(checkbox, userId, field, field_index) { $.ajax({ - method:"post", + method: "post", url: window.location.pathname + "/../../ajax/editlistusers/" + field, - data: {"pk":userId, "field_index":field_index, "value": checkbox.checked} - /*
- -
*/ - /*
Text to show
*/ - }); - $.ajax({ - method:"get", - url: window.location.pathname + "/../../ajax/listusers", - async: true, - timeout: 900, - success:function(data) { - $("#user-table").bootstrapTable("load", data); + data: {"pk": userId, "field_index": field_index, "value": checkbox.checked}, + success: function (data) { + if (!jQuery.isEmptyObject(data)) { + $("#flash_success").remove(); + $("#flash_danger").remove(); + $( ".navbar" ).after( '
' + + '
'+data.message+'
' + + '
'); + } + $.ajax({ + method: "get", + url: window.location.pathname + "/../../ajax/listusers", + async: true, + timeout: 900, + success: function (data) { + $("#user-table").bootstrapTable("load", data); + } + }); } }); } + function deactivateHeaderButtons(e) { $("#user_delete_selection").addClass("disabled"); $("#user_delete_selection").attr("aria-disabled", true); diff --git a/cps/web.py b/cps/web.py index 528e285a..cf488986 100644 --- a/cps/web.py +++ b/cps/web.py @@ -761,7 +761,7 @@ def list_books(): state = None if sort == "state": - state = json.loads(request.args.get("state")) + state = json.loads(request.args.get("state", "[]")) if sort != "state" and order: order = [text(sort + " " + order)] From 05933a5f0c2e19ed6330b87aa2959ad2eb08c674 Mon Sep 17 00:00:00 2001 From: ElQuimm <50202052+ElQuimm@users.noreply.github.com> Date: Tue, 13 Apr 2021 18:51:50 +0200 Subject: [PATCH 15/44] Updated italian message.po I have not removed the #, fuzzy lines, but modified the corresponding values. --- cps/translations/it/LC_MESSAGES/messages.po | 86 ++++++++++----------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/cps/translations/it/LC_MESSAGES/messages.po b/cps/translations/it/LC_MESSAGES/messages.po index 5c62edf4..590064e1 100644 --- a/cps/translations/it/LC_MESSAGES/messages.po +++ b/cps/translations/it/LC_MESSAGES/messages.po @@ -63,12 +63,12 @@ msgstr "Configurazione dell'interfaccia utente" #: cps/admin.py:237 cps/templates/admin.html:46 #, fuzzy msgid "Edit Users" -msgstr "Utente amministratore" +msgstr "Modifica gli utenti" #: cps/admin.py:263 #, fuzzy msgid "all" -msgstr "Tutti" +msgstr "tutti" #: cps/admin.py:298 cps/templates/user_edit.html:44 #: cps/templates/user_table.html:52 @@ -77,7 +77,7 @@ msgstr "tutte le lingue presenti" #: cps/admin.py:331 cps/admin.py:1233 msgid "Guest Name can't be changed" -msgstr "" +msgstr "Il nome dell'utente Guest non può essere modificato" #: cps/admin.py:345 cps/admin.py:1198 msgid "No admin user remaining, can't remove admin role" @@ -106,19 +106,19 @@ msgstr "Vuoi veramente eliminare questo scaffale?" #: cps/admin.py:435 #, fuzzy msgid "Are you sure you want to change locales of selected user(s)?" -msgstr "Vuoi veramente eliminare questo scaffale?" +msgstr "Sei sicuro di voler modificare le impostazioni locali dell'/degli utente/i selezionato/i?" #: cps/admin.py:437 msgid "Are you sure you want to change visible book languages for selected user(s)?" -msgstr "" +msgstr "Sei sicuro di voler modificare le impostazioni delle lingue visualizzabili dell'/degli utente/i selezionato/i?" #: cps/admin.py:439 msgid "Are you sure you want to change the selected role for the selected user(s)?" -msgstr "" +msgstr "Sei sicuro di voler modificare il ruolo evidenziato dell'/degli utente/i selezionato/i?" #: cps/admin.py:441 msgid "Are you sure you want to change the selected visibility restrictions for the selected user(s)?" -msgstr "" +msgstr "Sei sicuro di voler modificare le impostazioni delle restrizioni di visualizzazione dell'/degli utente/i selezionato/i?" #: cps/admin.py:687 cps/admin.py:693 cps/admin.py:703 cps/admin.py:713 #: cps/templates/modal_dialogs.html:29 cps/templates/user_table.html:28 @@ -254,12 +254,12 @@ msgstr "Modifica le impostazioni del server e-mail" #: cps/admin.py:1303 msgid "G-Mail Account Verification Successful" -msgstr "" +msgstr "L'account g-mail è stato verificato con successo" #: cps/admin.py:1328 #, python-format msgid "Test e-mail queued for sending to %(email)s, please check Tasks for result" -msgstr "" +msgstr "L'e-mail di test è stato accodato con successo per essere spedito a %(email)s, per favore verifica tramite il pulsante 'Compito' il risultato" #: cps/admin.py:1331 #, python-format @@ -714,7 +714,7 @@ msgstr "Tutti" #: cps/opds.py:385 msgid "{} Stars" -msgstr "" +msgstr "{} Stelle" #: cps/remotelogin.py:65 cps/web.py:1488 msgid "login" @@ -1109,7 +1109,7 @@ msgstr "Libro accodato con successo per essere spedito a %(kindlemail)s" #: cps/web.py:1357 #, python-format msgid "Oops! There was an error sending this book: %(res)s" -msgstr "Si è verificato un errore durante l'invio di questo libro: %(res)s" +msgstr "Oops! Si è verificato un errore durante l'invio di questo libro: %(res)s" #: cps/web.py:1359 msgid "Please update your profile with a valid Send to Kindle E-mail Address." @@ -1183,7 +1183,7 @@ msgstr "Leggi un libro" #: cps/services/gmail.py:41 msgid "Found no valid gmail.json file with OAuth information" -msgstr "" +msgstr "Ho trovato un gmail.json file senza informazione OAuth" #: cps/tasks/convert.py:114 #, python-format @@ -1314,11 +1314,11 @@ msgstr "E-mail da" #: cps/templates/admin.html:84 msgid "E-Mail Service" -msgstr "" +msgstr "Servizio e-mail" #: cps/templates/admin.html:85 msgid "Gmail via Oauth2" -msgstr "" +msgstr "Gmail via Oauth2" #: cps/templates/admin.html:100 msgid "Configuration" @@ -1370,11 +1370,11 @@ msgstr "Nome intestazione reverse proxy" #: cps/templates/admin.html:153 msgid "Edit Basic Configuration" -msgstr "Edita la configurazione di base" +msgstr "Modifica la configurazione di base" #: cps/templates/admin.html:154 msgid "Edit UI Configuration" -msgstr "Edita la configurazione dell'interfaccia utente" +msgstr "Modifica la configurazione dell'interfaccia utente" #: cps/templates/admin.html:159 msgid "Administration" @@ -2018,7 +2018,7 @@ msgstr "LDAP Member User Filter Detection" #: cps/templates/config_edit.html:344 msgid "Autodetect" -msgstr "Autodetect - determina automaticamente" +msgstr "Autodetect - identifica automaticamente" #: cps/templates/config_edit.html:345 msgid "Custom Filter" @@ -2225,23 +2225,23 @@ msgstr "Modifica metadati" #: cps/templates/email_edit.html:12 msgid "Choose Server Type" -msgstr "" +msgstr "Scegli il tipo di server" #: cps/templates/email_edit.html:14 msgid "Use Standard E-Mail Account" -msgstr "" +msgstr "Utilizza un account e-mail standard" #: cps/templates/email_edit.html:15 msgid "G-Mail Account with OAuth2 Verfification" -msgstr "" +msgstr "Utilizza un account g-mail con la verifica OAuth2" #: cps/templates/email_edit.html:21 msgid "Setup Gmail Account as E-Mail Server" -msgstr "" +msgstr "Configura l'account gmail quale server e-mail" #: cps/templates/email_edit.html:23 msgid "Revoke G-Mail Access" -msgstr "" +msgstr "Revoca l'accesso g-mail" #: cps/templates/email_edit.html:41 msgid "STARTTLS" @@ -2298,7 +2298,7 @@ msgstr "Apri il file .kobo/Kobo eReader.conf in un editore di testi e aggiungi ( #: cps/templates/http_error.html:38 msgid "Create Issue" -msgstr "Crea un rapporto di segnalazione di un problema" +msgstr "Crea un rapporto di segnalazione di problema" #: cps/templates/http_error.html:45 msgid "Return to Home" @@ -2328,11 +2328,11 @@ msgstr "Avvio" #: cps/templates/index.xml:18 msgid "Alphabetical Books" -msgstr "" +msgstr "Libri in ordine alfabetico" #: cps/templates/index.xml:22 msgid "Books sorted alphabetically" -msgstr "" +msgstr "Libri ordinati alfabeticamente" #: cps/templates/index.xml:29 msgid "Popular publications from this catalog based on Downloads." @@ -2910,86 +2910,86 @@ msgstr "Seleziona" #: cps/templates/user_table.html:101 #, fuzzy msgid "Edit User" -msgstr "Utente amministratore" +msgstr "Modifica utente" #: cps/templates/user_table.html:104 #, fuzzy msgid "Enter Username" -msgstr "Scegli un nome utente" +msgstr "Digita il nome utente" #: cps/templates/user_table.html:105 #, fuzzy msgid "Enter E-mail Address" -msgstr "Il tuo indirizzo e-mail" +msgstr "Digita l'indirizzo e-mail" #: cps/templates/user_table.html:106 #, fuzzy msgid "Enter Kindle E-mail Address" -msgstr "Invia all'email di Kindle" +msgstr "Digita l'email di Kindle" #: cps/templates/user_table.html:106 #, fuzzy msgid "Kindle E-mail" -msgstr "E-mail di test" +msgstr "E-mail di Kindle" #: cps/templates/user_table.html:107 #, fuzzy msgid "Locale" -msgstr "Scala" +msgstr "Locale" #: cps/templates/user_table.html:108 msgid "Visible Book Languages" -msgstr "" +msgstr "Lingue dei libri visualizzabili" #: cps/templates/user_table.html:109 #, fuzzy msgid "Edit Denied Tags" -msgstr "Seleziona le categorie consentite/negate" +msgstr "Modifica le categorie negate" #: cps/templates/user_table.html:109 msgid "Denied Tags" -msgstr "" +msgstr "Categorie negate" #: cps/templates/user_table.html:110 #, fuzzy msgid "Edit Allowed Tags" -msgstr "Seleziona le categorie consentite/negate" +msgstr "Modifica le categorie permesse" #: cps/templates/user_table.html:110 msgid "Allowed Tags" -msgstr "" +msgstr "Categorie permesse" #: cps/templates/user_table.html:111 #, fuzzy msgid "Edit Allowed Column Values" -msgstr "Aggiungi valori personali permetti/nega nelle colonne" +msgstr "Modifica i valori delle colonne permesse" #: cps/templates/user_table.html:111 #, fuzzy msgid "Allowed Column Values" -msgstr "Aggiungi valori personali permetti/nega nelle colonne" +msgstr "Valori delle colonne permesse" #: cps/templates/user_table.html:112 #, fuzzy msgid "Edit Denied Column Values" -msgstr "Aggiungi valori personali permetti/nega nelle colonne" +msgstr "Modifica i valori delle colonne negate" #: cps/templates/user_table.html:112 #, fuzzy msgid "Denied Columns Values" -msgstr "Aggiungi valori personali permetti/nega nelle colonne" +msgstr "Valori delle colonne negate" #: cps/templates/user_table.html:117 #, fuzzy msgid "Change Password" -msgstr "Permetti la modifica della password" +msgstr "Modifica la password" #: cps/templates/user_table.html:118 #, fuzzy msgid "Edit Public Shelfs" -msgstr "Scaffale pubblico" +msgstr "Modifica scaffali pubblici" #: cps/templates/user_table.html:120 msgid "View" -msgstr "" +msgstr "Visualizza" From 67775bc797e8cd04fbf38a5c94cb43513feb74a8 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Tue, 13 Apr 2021 19:08:02 +0200 Subject: [PATCH 16/44] Update requirements Catch error for invalid oauth tokens Fixes for displaying error messages on deleting books from list Fixes for displaying error messages on deleting bookformats --- cps/admin.py | 6 +- cps/editbooks.py | 19 +- cps/oauth_bb.py | 37 +- cps/templates/layout.html | 2 +- cps/web.py | 2 +- optional-requirements.txt | 2 +- test/Calibre-Web TestSummary_Linux.html | 567 +++++++++++++++++++----- 7 files changed, 514 insertions(+), 121 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 5cd31f18..d32d6d7c 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -245,7 +245,7 @@ def list_users(): off = int(request.args.get("offset") or 0) limit = int(request.args.get("limit") or 10) search = request.args.get("search") - sort = request.args.get("sort", "state") + sort = request.args.get("sort", "id") order = request.args.get("order", "").lower() state = None if sort == "state": @@ -254,7 +254,7 @@ def list_users(): if sort != "state" and order: order = text(sort + " " + order) elif not state: - order = ub.User.name.desc() + order = ub.User.id.asc() all_user = ub.session.query(ub.User) if not config.config_anonbrowse: @@ -371,7 +371,7 @@ def edit_list_user(param): 'message':_(u"No admin user remaining, can't remove admin role", nick=user.name)}), mimetype='application/json') user.role &= ~int(vals['field_index']) - elif param == 'sidebar_view': + elif param.startswith('sidebar'): if user.name == "Guest" and int(vals['field_index']) == constants.SIDEBAR_READ_AND_UNREAD: raise Exception(_("Guest can't have this view")) if vals['value'] == 'true': diff --git a/cps/editbooks.py b/cps/editbooks.py index 931ad13e..67ab06d9 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -315,19 +315,19 @@ def delete_book(book_id, book_format, jsonResponse): result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper()) if not result: if jsonResponse: - return json.dumps({"location": url_for("editbook.edit_book"), - "type": "alert", + return json.dumps([{"location": url_for("editbook.edit_book", book_id=book_id), + "type": "danger", "format": "", - "error": error}), + "message": error}]) else: flash(error, category="error") return redirect(url_for('editbook.edit_book', book_id=book_id)) if error: if jsonResponse: - warning = {"location": url_for("editbook.edit_book"), + warning = {"location": url_for("editbook.edit_book", book_id=book_id), "type": "warning", "format": "", - "error": error} + "message": error} else: flash(error, category="warning") if not book_format: @@ -339,6 +339,15 @@ def delete_book(book_id, book_format, jsonResponse): except Exception as ex: log.debug_or_exception(ex) calibre_db.session.rollback() + if jsonResponse: + return json.dumps([{"location": url_for("editbook.edit_book", book_id=book_id), + "type": "danger", + "format": "", + "message": ex}]) + else: + flash(str(ex), category="error") + return redirect(url_for('editbook.edit_book', book_id=book_id)) + else: # book not found log.error('Book with id "%s" could not be deleted: not found', book_id) diff --git a/cps/oauth_bb.py b/cps/oauth_bb.py index 5d909d91..c8cc2e3e 100644 --- a/cps/oauth_bb.py +++ b/cps/oauth_bb.py @@ -30,6 +30,7 @@ from flask_babel import gettext as _ from flask_dance.consumer import oauth_authorized, oauth_error from flask_dance.contrib.github import make_github_blueprint, github from flask_dance.contrib.google import make_google_blueprint, google +from oauthlib.oauth2 import TokenExpiredError, InvalidGrantError from flask_login import login_user, current_user, login_required from sqlalchemy.orm.exc import NoResultFound @@ -146,6 +147,7 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider ub.session.add(oauth_entry) ub.session.commit() flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success") + log.info("Link to {} Succeeded".format(provider_name)) return redirect(url_for('web.profile')) except Exception as ex: log.debug_or_exception(ex) @@ -194,6 +196,7 @@ def unlink_oauth(provider): ub.session.commit() logout_oauth_user() flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success") + log.info("Unlink to {} Succeeded".format(oauth_check[provider])) except Exception as ex: log.debug_or_exception(ex) ub.session.rollback() @@ -257,11 +260,13 @@ if ub.oauth_support: def github_logged_in(blueprint, token): if not token: flash(_(u"Failed to log in with GitHub."), category="error") + log.error("Failed to log in with GitHub") return False resp = blueprint.session.get("/user") if not resp.ok: flash(_(u"Failed to fetch user info from GitHub."), category="error") + log.error("Failed to fetch user info from GitHub") return False github_info = resp.json() @@ -273,11 +278,13 @@ if ub.oauth_support: def google_logged_in(blueprint, token): if not token: flash(_(u"Failed to log in with Google."), category="error") + log.error("Failed to log in with Google") return False resp = blueprint.session.get("/oauth2/v2/userinfo") if not resp.ok: flash(_(u"Failed to fetch user info from Google."), category="error") + log.error("Failed to fetch user info from Google") return False google_info = resp.json() @@ -318,11 +325,16 @@ if ub.oauth_support: def github_login(): if not github.authorized: return redirect(url_for('github.login')) - account_info = github.get('/user') - if account_info.ok: - account_info_json = account_info.json() - return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github') - flash(_(u"GitHub Oauth error, please retry later."), category="error") + try: + account_info = github.get('/user') + if account_info.ok: + account_info_json = account_info.json() + return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github') + flash(_(u"GitHub Oauth error, please retry later."), category="error") + log.error("GitHub Oauth error, please retry later") + except (InvalidGrantError, TokenExpiredError) as e: + flash(_(u"GitHub Oauth error: {}").format(e), category="error") + log.error(e) return redirect(url_for('web.login')) @@ -337,11 +349,16 @@ def github_login_unlink(): def google_login(): if not google.authorized: return redirect(url_for("google.login")) - resp = google.get("/oauth2/v2/userinfo") - if resp.ok: - account_info_json = resp.json() - return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google') - flash(_(u"Google Oauth error, please retry later."), category="error") + try: + resp = google.get("/oauth2/v2/userinfo") + if resp.ok: + account_info_json = resp.json() + return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google') + flash(_(u"Google Oauth error, please retry later."), category="error") + log.error("Google Oauth error, please retry later") + except (InvalidGrantError, TokenExpiredError) as e: + flash(_(u"Google Oauth error: {}").format(e), category="error") + log.error(e) return redirect(url_for('web.login')) diff --git a/cps/templates/layout.html b/cps/templates/layout.html index d4e0800e..6e68204c 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -92,7 +92,7 @@ {% for message in get_flashed_messages(with_categories=True) %} {%if message[0] == "error" %}
-
{{ message[1] }}
+
{{ message[1] }}
{%endif%} {%if message[0] == "info" %} diff --git a/cps/web.py b/cps/web.py index cf488986..65398e6c 100644 --- a/cps/web.py +++ b/cps/web.py @@ -756,7 +756,7 @@ def list_books(): off = int(request.args.get("offset") or 0) limit = int(request.args.get("limit") or config.config_books_per_page) search = request.args.get("search") - sort = request.args.get("sort", "state") + sort = request.args.get("sort", "id") order = request.args.get("order", "").lower() state = None diff --git a/optional-requirements.txt b/optional-requirements.txt index ca54fe4d..eb67f59b 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -26,7 +26,7 @@ python-ldap>=3.0.0,<3.4.0 Flask-SimpleLDAP>=1.4.0,<1.5.0 #oauth -Flask-Dance>=1.4.0,<3.1.0 +Flask-Dance>=1.4.0,<4.1.0 SQLAlchemy-Utils>=0.33.5,<0.38.0 # extracting metadata diff --git a/test/Calibre-Web TestSummary_Linux.html b/test/Calibre-Web TestSummary_Linux.html index 2d41ea3a..17df679b 100644 --- a/test/Calibre-Web TestSummary_Linux.html +++ b/test/Calibre-Web TestSummary_Linux.html @@ -37,20 +37,20 @@
-

Start Time: 2021-04-05 18:59:35

+

Start Time: 2021-04-12 21:44:07

-

Stop Time: 2021-04-05 21:34:25

+

Stop Time: 2021-04-13 00:22:44

-

Duration: 2h 5 min

+

Duration: 2h 7 min

@@ -1148,12 +1148,12 @@ - + TestEditBooksList 10 - 10 - 0 - 0 + 6 + 3 + 1 0 Detail @@ -1171,20 +1171,74 @@ - +
TestEditBooksList - test_bookslist_edit_categories
- PASS + +
+ ERROR +
+ + + + - +
TestEditBooksList - test_bookslist_edit_languages
- PASS + +
+ FAIL +
+ + + + @@ -1207,11 +1261,33 @@ - +
TestEditBooksList - test_bookslist_edit_seriesindex
- PASS + +
+ FAIL +
+ + + + @@ -1243,22 +1319,46 @@ - +
TestEditBooksList - test_search_books_list
- PASS + +
+ FAIL +
+ + + + - + TestEditBooksOnGdrive 20 18 - 2 - 0 + 1 + 1 0 Detail @@ -1293,12 +1393,13 @@
Traceback (most recent call last):
-  File "/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py", line 340, in test_edit_author
-    self.assertEqual(u'Sigurd Lindgren & Leo Baskerville', author.get_attribute('value'))
-AssertionError: 'Sigurd Lindgren & Leo Baskerville' != 'Sigurd Lindgren&Leo Baskerville'
-- Sigurd Lindgren & Leo Baskerville
-?                - -
-+ Sigurd Lindgren&Leo Baskerville
+ File "/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py", line 369, in test_edit_author + self.assertEqual(u'Pipo, Pipe', author.get_attribute('value')) +AssertionError: 'Pipo, Pipe' != 'Pipo| Pipe' +- Pipo, Pipe +? ^ ++ Pipo| Pipe +? ^
@@ -1425,11 +1526,31 @@ AssertionError: 'Sigurd Lindgren & Leo Baskerville' != 'Sigurd Lindgren&Leo Bask - +
TestEditBooksOnGdrive - test_edit_title
- PASS + +
+ ERROR +
+ + + + @@ -1461,31 +1582,11 @@ AssertionError: 'Sigurd Lindgren & Leo Baskerville' != 'Sigurd Lindgren&Leo Bask - +
TestEditBooksOnGdrive - test_watch_metadata
- -
- FAIL -
- - - - + PASS @@ -1701,13 +1802,13 @@ AssertionError: 'series' unexpectedly found in {'id': 5, 'reader': [], 'title': - + TestKoboSync 9 8 - 0 1 0 + 0 Detail @@ -1715,26 +1816,35 @@ AssertionError: 'series' unexpectedly found in {'id': 5, 'reader': [], 'title': - +
TestKoboSync - test_book_download
- ERROR + FAIL
- From f07cc8b10359ca83b426dc7ff316d3ea90c0e038 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Tue, 13 Apr 2021 19:26:10 +0200 Subject: [PATCH 17/44] Update optional-requirements: flask-dance Catch error for invalid oauth tokens Fixes for displaying error messages on deleting books from list Fixes for displaying error messages on deleting bookformats Removed non working sorting in books list --- cps/templates/book_table.html | 19 ++++++++++--------- cps/web.py | 1 - 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cps/templates/book_table.html b/cps/templates/book_table.html index 6a31c235..e700eb53 100644 --- a/cps/templates/book_table.html +++ b/cps/templates/book_table.html @@ -1,6 +1,7 @@ {% extends "layout.html" %} -{% macro text_table_row(parameter, edit_text, show_text, validate) -%} - {% endif %} - {{ text_table_row('title', _('Enter Title'),_('Title'), true) }} - {{ text_table_row('sort', _('Enter Title Sort'),_('Title Sort'), false) }} - {{ text_table_row('author_sort', _('Enter Author Sort'),_('Author Sort'), false) }} + {{ text_table_row('title', _('Enter Title'),_('Title'), true, true) }} + {{ text_table_row('sort', _('Enter Title Sort'),_('Title Sort'), false, true) }} + {{ text_table_row('author_sort', _('Enter Author Sort'),_('Author Sort'), false, true) }} {{ text_table_row('authors', _('Enter Authors'),_('Authors'), true) }} - {{ text_table_row('tags', _('Enter Categories'),_('Categories'), false) }} - {{ text_table_row('series', _('Enter Series'),_('Series'), false) }} + {{ text_table_row('tags', _('Enter Categories'),_('Categories'), false, false) }} + {{ text_table_row('series', _('Enter Series'),_('Series'), false, false) }} {{_('Series Index')}} - {{ text_table_row('languages', _('Enter Languages'),_('Languages'), false) }} + {{ text_table_row('languages', _('Enter Languages'),_('Languages'), false, false) }} - {{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false) }} + {{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false, false) }} {% if g.user.role_delete_books() and g.user.role_edit()%} {{_('Delete')}} {% endif %} diff --git a/cps/web.py b/cps/web.py index 65398e6c..52f2b82c 100644 --- a/cps/web.py +++ b/cps/web.py @@ -762,7 +762,6 @@ def list_books(): if sort == "state": state = json.loads(request.args.get("state", "[]")) - if sort != "state" and order: order = [text(sort + " " + order)] elif not state: From 0e1dbb5377aef12b4912ff43f30d554b0f770588 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Tue, 13 Apr 2021 19:41:44 +0200 Subject: [PATCH 18/44] Copy author names for displaying (#1935) --- cps/web.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cps/web.py b/cps/web.py index 52f2b82c..01c04350 100644 --- a/cps/web.py +++ b/cps/web.py @@ -26,6 +26,7 @@ from datetime import datetime import json import mimetypes import chardet # dependency of requests +import copy from babel.dates import format_date from babel import Locale as LC @@ -830,9 +831,10 @@ def author_list(): charlist = calibre_db.session.query(func.upper(func.substr(db.Authors.sort, 1, 1)).label('char')) \ .join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters()) \ .group_by(func.upper(func.substr(db.Authors.sort, 1, 1))).all() - for entry in entries: + autor_copy = copy.deepcopy(entries) + for entry in autor_copy: entry.Authors.name = entry.Authors.name.replace('|', ',') - return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist, + return render_title_template('list.html', entries=autor_copy, folder='web.books_list', charlist=charlist, title=u"Authors", page="authorlist", data='author', order=order_no) else: abort(404) From b38877e19361eb41f328642667d1f44170a7ed5d Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Tue, 13 Apr 2021 19:47:55 +0200 Subject: [PATCH 19/44] Document change --- cps/web.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cps/web.py b/cps/web.py index 01c04350..0eaacdb1 100644 --- a/cps/web.py +++ b/cps/web.py @@ -831,6 +831,8 @@ def author_list(): charlist = calibre_db.session.query(func.upper(func.substr(db.Authors.sort, 1, 1)).label('char')) \ .join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters()) \ .group_by(func.upper(func.substr(db.Authors.sort, 1, 1))).all() + # If not creating a copy, readonly databases can not display authornames with "|" in it as changing the name + # starts a change session autor_copy = copy.deepcopy(entries) for entry in autor_copy: entry.Authors.name = entry.Authors.name.replace('|', ',') From 3973362457f65bec6c98b6ee42377cb3b4fe4687 Mon Sep 17 00:00:00 2001 From: malletfils Date: Wed, 14 Apr 2021 19:57:02 +0200 Subject: [PATCH 20/44] Update db.py Just adding support for Babelio (french website about books) in the link section --- cps/db.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cps/db.py b/cps/db.py index 0d7055a4..34804255 100644 --- a/cps/db.py +++ b/cps/db.py @@ -121,6 +121,8 @@ class Identifiers(Base): return u"Douban" elif format_type == "goodreads": return u"Goodreads" + elif format_type == "babelio": + return u"Babelio" elif format_type == "google": return u"Google Books" elif format_type == "kobo": @@ -148,6 +150,8 @@ class Identifiers(Base): return u"https://dx.doi.org/{0}".format(self.val) elif format_type == "goodreads": return u"https://www.goodreads.com/book/show/{0}".format(self.val) + elif format_type == "babelio": + return u"https://www.babelio.com/livres/titre/{0}".format(self.val) elif format_type == "douban": return u"https://book.douban.com/subject/{0}".format(self.val) elif format_type == "google": From e13820bbf004eb7b15788a53fe94a534713899a2 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Thu, 15 Apr 2021 12:05:03 +0200 Subject: [PATCH 21/44] Update exception upon migration of database #1935 --- cps/config_sql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/config_sql.py b/cps/config_sql.py index 3e5e4c59..6e1ff54e 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -358,7 +358,7 @@ def _migrate_table(session, orm_class): if column_name[0] != '_': try: session.query(column).first() - except exc.OperationalError as err: + except OperationalError as err: log.debug("%s: %s", column_name, err.args[0]) if column.default is not None: if sys.version_info < (3, 0): From 1cb8dbe795660c713ebf7f683707435ca0720836 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Thu, 15 Apr 2021 17:42:39 +0200 Subject: [PATCH 22/44] Update migration routine --- cps/config_sql.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cps/config_sql.py b/cps/config_sql.py index 6e1ff54e..6e13489d 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -21,15 +21,16 @@ from __future__ import division, print_function, unicode_literals import os import sys -from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON +from sqlalchemy import Column, String, Integer, SmallInteger, Boolean, BLOB, JSON from sqlalchemy.exc import OperationalError +from sqlalchemy.sql.expression import text try: # Compatibility with sqlalchemy 2.0 from sqlalchemy.orm import declarative_base except ImportError: from sqlalchemy.ext.declarative import declarative_base -from . import constants, cli, logger, ub +from . import constants, cli, logger log = logger.create() @@ -368,17 +369,17 @@ def _migrate_table(session, orm_class): column_default = "" else: if isinstance(column.default.arg, bool): - column_default = ("DEFAULT %r" % int(column.default.arg)) + column_default = "DEFAULT {}".format(int(column.default.arg)) else: - column_default = ("DEFAULT '%r'" % column.default.arg) + column_default = "DEFAULT `{}`".format(column.default.arg) if isinstance(column.type, JSON): column_type = "JSON" else: column_type = column.type - alter_table = "ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__, + alter_table = text("ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__, column_name, column_type, - column_default) + column_default)) log.debug(alter_table) session.execute(alter_table) changed = True From 39dda3f5344ba56e5ecc78c16c42091fbb1f6049 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Thu, 15 Apr 2021 18:02:52 +0200 Subject: [PATCH 23/44] Fix opds login with colon in password #1952 --- cps/usermanagement.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cps/usermanagement.py b/cps/usermanagement.py index ef7174c4..78e80afe 100644 --- a/cps/usermanagement.py +++ b/cps/usermanagement.py @@ -75,8 +75,9 @@ def load_user_from_auth_header(header_val): basic_username = basic_password = '' # nosec try: header_val = base64.b64decode(header_val).decode('utf-8') - basic_username = header_val.split(':')[0] - basic_password = header_val.split(':')[1] + # Users with colon are invalid: rfc7617 page 4 + basic_username = header_val.split(':', 1)[0] + basic_password = header_val.split(':', 1)[1] except (TypeError, UnicodeDecodeError, binascii.Error): pass user = _fetch_user_by_name(basic_username) From 39bbee0eebf0b894ced33cffb300a581ed95cf04 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sat, 17 Apr 2021 10:27:30 +0200 Subject: [PATCH 24/44] Books sort with non Books table column working #1938 --- cps/db.py | 15 +++++++++++---- cps/services/__init__.py | 2 +- cps/templates/book_table.html | 10 +++++----- cps/templates/email_edit.html | 4 ++-- cps/web.py | 22 +++++++++++++++++++--- 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/cps/db.py b/cps/db.py index 0d7055a4..7f05e24c 100644 --- a/cps/db.py +++ b/cps/db.py @@ -702,14 +702,21 @@ class CalibreDB(): return self.session.query(Books) \ .filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first() - def search_query(self, term): + def search_query(self, term, *join): term.strip().lower() self.session.connection().connection.connection.create_function("lower", 1, lcase) q = list() authorterms = re.split("[, ]+", term) for authorterm in authorterms: q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%"))) - return self.session.query(Books).filter(self.common_filters(True)).filter( + query = self.session.query(Books) + if len(join) == 3: + query = query.outerjoin(join[0], join[1]).outerjoin(join[2]) + elif len(join) == 2: + query = query.outerjoin(join[0], join[1]) + elif len(join) == 1: + query = query.outerjoin(join[0]) + return query.filter(self.common_filters(True)).filter( or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")), Books.series.any(func.lower(Series.name).ilike("%" + term + "%")), Books.authors.any(and_(*q)), @@ -718,10 +725,10 @@ class CalibreDB(): )) # read search results from calibre-database and return it (function is used for feed and simple search - def get_search_results(self, term, offset=None, order=None, limit=None): + def get_search_results(self, term, offset=None, order=None, limit=None, *join): order = order or [Books.sort] pagination = None - result = self.search_query(term).order_by(*order).all() + result = self.search_query(term, *join).order_by(*order).all() result_count = len(result) if offset != None and limit != None: offset = int(offset) diff --git a/cps/services/__init__.py b/cps/services/__init__.py index efd55621..e6e5954c 100644 --- a/cps/services/__init__.py +++ b/cps/services/__init__.py @@ -49,5 +49,5 @@ except ImportError as err: try: from . import gmail except ImportError as err: - log.debug("Cannot import Gmail, sending books via G-Mail Accounts will not work: %s", err) + log.debug("Cannot import gmail, sending books via Gmail Oauth2 Verification will not work: %s", err) gmail = None diff --git a/cps/templates/book_table.html b/cps/templates/book_table.html index e700eb53..bb167ad3 100644 --- a/cps/templates/book_table.html +++ b/cps/templates/book_table.html @@ -47,13 +47,13 @@ {{ text_table_row('title', _('Enter Title'),_('Title'), true, true) }} {{ text_table_row('sort', _('Enter Title Sort'),_('Title Sort'), false, true) }} {{ text_table_row('author_sort', _('Enter Author Sort'),_('Author Sort'), false, true) }} - {{ text_table_row('authors', _('Enter Authors'),_('Authors'), true) }} - {{ text_table_row('tags', _('Enter Categories'),_('Categories'), false, false) }} - {{ text_table_row('series', _('Enter Series'),_('Series'), false, false) }} + {{ text_table_row('authors', _('Enter Authors'),_('Authors'), true, true) }} + {{ text_table_row('tags', _('Enter Categories'),_('Categories'), false, true) }} + {{ text_table_row('series', _('Enter Series'),_('Series'), false, true) }} {{_('Series Index')}} - {{ text_table_row('languages', _('Enter Languages'),_('Languages'), false, false) }} + {{ text_table_row('languages', _('Enter Languages'),_('Languages'), false, true) }} - {{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false, false) }} + {{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false, true) }} {% if g.user.role_delete_books() and g.user.role_edit()%} {{_('Delete')}} {% endif %} diff --git a/cps/templates/email_edit.html b/cps/templates/email_edit.html index 8b92a248..ac1c36d2 100644 --- a/cps/templates/email_edit.html +++ b/cps/templates/email_edit.html @@ -12,7 +12,7 @@
@@ -20,7 +20,7 @@ {% if content.mail_gmail_token == {} %} {% else %} - + {% endif %}
diff --git a/cps/web.py b/cps/web.py index 0eaacdb1..e01b4b22 100644 --- a/cps/web.py +++ b/cps/web.py @@ -760,10 +760,26 @@ def list_books(): sort = request.args.get("sort", "id") order = request.args.get("order", "").lower() state = None + join = tuple() if sort == "state": state = json.loads(request.args.get("state", "[]")) - if sort != "state" and order: + elif sort == "tags": + order = [db.Tags.name.asc()] if order == "asc" else [db.Tags.name.desc()] + join = db.books_tags_link,db.Books.id == db.books_tags_link.c.book, db.Tags + elif sort == "series": + order = [db.Series.name.asc()] if order == "asc" else [db.Series.name.desc()] + join = db.books_series_link,db.Books.id == db.books_series_link.c.book, db.Series + elif sort == "publishers": + order = [db.Publishers.name.asc()] if order == "asc" else [db.Publishers.name.desc()] + join = db.books_publishers_link,db.Books.id == db.books_publishers_link.c.book, db.Publishers + elif sort == "authors": + order = [db.Authors.name.asc()] if order == "asc" else [db.Authors.name.desc()] + join = db.books_authors_link,db.Books.id == db.books_authors_link.c.book, db.Authors + elif sort == "languages": + order = [db.Languages.lang_code.asc()] if order == "asc" else [db.Languages.lang_code.desc()] + join = db.books_languages_link,db.Books.id == db.books_languages_link.c.book, db.Languages + elif order and sort in ["sort", "title", "authors_sort", "series_index"]: order = [text(sort + " " + order)] elif not state: order = [db.Books.timestamp.desc()] @@ -778,9 +794,9 @@ def list_books(): books = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).all() entries = calibre_db.get_checkbox_sorted(books, state, off, limit,order) elif search: - entries, filtered_count, __ = calibre_db.get_search_results(search, off, order, limit) + entries, filtered_count, __ = calibre_db.get_search_results(search, off, order, limit, *join) else: - entries, __, __ = calibre_db.fill_indexpage((int(off) / (int(limit)) + 1), limit, db.Books, True, order) + entries, __, __ = calibre_db.fill_indexpage((int(off) / (int(limit)) + 1), limit, db.Books, True, order, *join) for entry in entries: for index in range(0, len(entry.languages)): From c0a4addf30f265eef78235f17899a1254f5e4f6a Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sat, 17 Apr 2021 18:30:55 +0200 Subject: [PATCH 25/44] Cancel button in User edit now leads to the right page back #1938 --- cps/static/js/main.js | 14 ++++++++++++++ cps/static/js/table.js | 6 +++--- cps/templates/admin.html | 2 +- cps/templates/email_edit.html | 2 +- cps/templates/user_edit.html | 2 +- cps/templates/user_table.html | 3 +++ 6 files changed, 23 insertions(+), 6 deletions(-) diff --git a/cps/static/js/main.js b/cps/static/js/main.js index 81308c64..7231c51c 100644 --- a/cps/static/js/main.js +++ b/cps/static/js/main.js @@ -114,6 +114,20 @@ $(document).ready(function() { } }); +$(".session").click(function() { + window.sessionStorage.setItem("back", window.location.pathname); +}); + +$("#back").click(function() { + var loc = sessionStorage.getItem("back"); + if (!loc) { + loc = $(this).data("back"); + } + sessionStorage.removeItem("back"); + window.location.href = loc; + +}); + function confirmDialog(id, dialogid, dataValue, yesFn, noFn) { var $confirm = $("#" + dialogid); $confirm.modal('show'); diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 7c4b04a7..d5a070c9 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -600,7 +600,7 @@ function responseHandler(res) { } function singleUserFormatter(value, row) { - return '' + this.buttontext + '' + return '' + this.buttontext + '' } function checkboxFormatter(value, row, index){ @@ -741,6 +741,6 @@ function queryParams(params) return params; } -function test(){ - console.log("hello"); +function storeLocation() { + window.sessionStorage.setItem("back", window.location.pathname); } diff --git a/cps/templates/admin.html b/cps/templates/admin.html index 5226151e..c06b7e72 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -26,7 +26,7 @@ {% for user in allUser %} {% if not user.role_anonymous() or config.config_anonbrowse %} - {{user.name}} + {{user.name}} {{user.email}} {{user.kindle_mail}} {{user.downloads.count()}} diff --git a/cps/templates/email_edit.html b/cps/templates/email_edit.html index ac1c36d2..59978ea9 100644 --- a/cps/templates/email_edit.html +++ b/cps/templates/email_edit.html @@ -66,7 +66,7 @@ {% if feature_support['gmail'] %} {% endif %} - {{_('Back')}} + {{_('Back')}} {% if g.allow_registration %}
diff --git a/cps/templates/user_edit.html b/cps/templates/user_edit.html index e2b184fc..846ad978 100644 --- a/cps/templates/user_edit.html +++ b/cps/templates/user_edit.html @@ -129,7 +129,7 @@
{{_('Save')}}
{% if not profile %} - {{_('Cancel')}} +
{{_('Cancel')}}
{% endif %} {% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
{{_('Delete User')}}
diff --git a/cps/templates/user_table.html b/cps/templates/user_table.html index b8a9d2c2..c538dfa4 100644 --- a/cps/templates/user_table.html +++ b/cps/templates/user_table.html @@ -137,6 +137,9 @@ + {% endblock %} {% block modal %} {{ delete_confirm_modal() }} From 0138294b36923c89292be9ba86f96843dd0b16c1 Mon Sep 17 00:00:00 2001 From: subdiox Date: Sun, 18 Apr 2021 03:20:41 +0900 Subject: [PATCH 26/44] Fix an issue that space key doesn't work intentionally when inverting reading direction --- cps/static/js/kthoom.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cps/static/js/kthoom.js b/cps/static/js/kthoom.js index 56038fc6..613eed2b 100644 --- a/cps/static/js/kthoom.js +++ b/cps/static/js/kthoom.js @@ -532,11 +532,11 @@ function keyHandler(evt) { if (evt.shiftKey && atTop) { evt.preventDefault(); // If it's Shift + Space and the container is at the top of the page - showLeftPage(); + showPrevPage(); } else if (!evt.shiftKey && atBottom) { evt.preventDefault(); // If you're at the bottom of the page and you only pressed space - showRightPage(); + showNextPage(); container.scrollTop(0); } break; From 755eb1405bba48646ccdd48d06a05e50027a8ce2 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 18 Apr 2021 04:25:45 +0900 Subject: [PATCH 27/44] Fix progress bar direction --- cps/static/css/kthoom.css | 15 ++++++++++++--- cps/static/js/kthoom.js | 30 ++++++++++++++++++++++++------ cps/templates/readcbr.html | 4 ++-- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/cps/static/css/kthoom.css b/cps/static/css/kthoom.css index 233cfe94..267a2a84 100644 --- a/cps/static/css/kthoom.css +++ b/cps/static/css/kthoom.css @@ -84,15 +84,24 @@ body { #progress .bar-load, #progress .bar-read { display: flex; - align-items: flex-end; - justify-content: flex-end; position: absolute; top: 0; - left: 0; bottom: 0; transition: width 150ms ease-in-out; } +#progress .from-left { + left: 0; + align-items: flex-end; + justify-content: flex-end; +} + +#progress .from-right { + right: 0; + align-items: flex-start; + justify-content: flex-start; +} + #progress .bar-load { color: #000; background-color: #ccc; diff --git a/cps/static/js/kthoom.js b/cps/static/js/kthoom.js index 613eed2b..f6c1e4d7 100644 --- a/cps/static/js/kthoom.js +++ b/cps/static/js/kthoom.js @@ -171,7 +171,10 @@ kthoom.ImageFile = function(file) { function initProgressClick() { $("#progress").click(function(e) { - var page = Math.max(1, Math.ceil((e.offsetX / $(this).width()) * totalImages)) - 1; + var offset = $(this).offset(); + var x = e.pageX - offset.left; + var rate = settings.direction === 0 ? x / $(this).width() : 1 - x / $(this).width(); + var page = Math.max(1, Math.ceil(rate * totalImages)) - 1; currentImage = page; updatePage(); }); @@ -285,6 +288,22 @@ function updatePage() { } function updateProgress(loadPercentage) { + if (settings.direction === 0) { + $("#progress .bar-read") + .removeClass("from-right") + .addClass("from-left"); + $("#progress .bar-load") + .removeClass("from-right") + .addClass("from-left"); + } else { + $("#progress .bar-read") + .removeClass("from-left") + .addClass("from-right"); + $("#progress .bar-load") + .removeClass("from-left") + .addClass("from-right"); + } + // Set the load/unzip progress if it's passed in if (loadPercentage) { $("#progress .bar-load").css({ width: loadPercentage + "%" }); @@ -526,18 +545,17 @@ function keyHandler(evt) { break; case kthoom.Key.SPACE: var container = $("#mainContent"); - var atTop = container.scrollTop() === 0; - var atBottom = container.scrollTop() >= container[0].scrollHeight - container.height(); + // var atTop = container.scrollTop() === 0; + // var atBottom = container.scrollTop() >= container[0].scrollHeight - container.height(); - if (evt.shiftKey && atTop) { + if (evt.shiftKey) { evt.preventDefault(); // If it's Shift + Space and the container is at the top of the page showPrevPage(); - } else if (!evt.shiftKey && atBottom) { + } else { evt.preventDefault(); // If you're at the bottom of the page and you only pressed space showNextPage(); - container.scrollTop(0); } break; default: diff --git a/cps/templates/readcbr.html b/cps/templates/readcbr.html index 35943b34..5723947e 100644 --- a/cps/templates/readcbr.html +++ b/cps/templates/readcbr.html @@ -60,12 +60,12 @@ Fullscreen
-
+
Loading...
-
+
From 9cc14ac5c7029a3a64bbd5fee59a598cc5fcabb8 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sun, 18 Apr 2021 11:33:14 +0200 Subject: [PATCH 28/44] Fix for #1905 prevent invalid json data in gmail_token --- cps/config_sql.py | 6 ++++-- cps/services/gmail.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cps/config_sql.py b/cps/config_sql.py index 6e13489d..f7419ec9 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -20,6 +20,7 @@ from __future__ import division, print_function, unicode_literals import os import sys +import json from sqlalchemy import Column, String, Integer, SmallInteger, Boolean, BLOB, JSON from sqlalchemy.exc import OperationalError @@ -261,7 +262,6 @@ class _ConfigSQL(object): """ new_value = dictionary.get(field, default) if new_value is None: - # log.debug("_ConfigSQL set_from_dictionary field '%s' not found", field) return False if field not in self.__dict__: @@ -278,7 +278,6 @@ class _ConfigSQL(object): if current_value == new_value: return False - # log.debug("_ConfigSQL set_from_dictionary '%s' = %r (was %r)", field, new_value, current_value) setattr(self, field, new_value) return True @@ -383,6 +382,9 @@ def _migrate_table(session, orm_class): log.debug(alter_table) session.execute(alter_table) changed = True + except json.decoder.JSONDecodeError as e: + log.error("Database corrupt column: {}".format(column_name)) + log.debug(e) if changed: try: diff --git a/cps/services/gmail.py b/cps/services/gmail.py index 9524dd75..f85d56b0 100644 --- a/cps/services/gmail.py +++ b/cps/services/gmail.py @@ -53,6 +53,7 @@ def setup_gmail(token): 'expiry': creds.expiry.isoformat(), 'email': user_info } + return {} def get_user_info(credentials): user_info_service = build(serviceName='oauth2', version='v2',credentials=credentials) From 04ac5b69acc3b851f399a5b1af917593032a5ed2 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sun, 18 Apr 2021 12:01:11 +0200 Subject: [PATCH 29/44] Further improvements on user list #1938 --- cps/static/js/table.js | 177 +++++++++++++++++----------------- cps/templates/user_table.html | 19 ++-- 2 files changed, 100 insertions(+), 96 deletions(-) diff --git a/cps/static/js/table.js b/cps/static/js/table.js index d5a070c9..3f3b431f 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -177,7 +177,6 @@ $(function() { }, }); - $("#domain_allow_submit").click(function(event) { event.preventDefault(); $("#domain_add_allow").ajaxForm(); @@ -388,7 +387,6 @@ $(function() { var target = $(e.relatedTarget).attr('id'); var dataId; $(e.relatedTarget).one('focus', function(e){$(this).blur();}); - //$(e.relatedTarget).blur(); if ($(e.relatedTarget).hasClass("button_head")) { dataId = $('#user-table').bootstrapTable('getSelections').map(a => a.id); } else { @@ -454,18 +452,28 @@ $(function() { $(this).next().text(elText); }); }, - onLoadSuccess: function () { - var guest = $(".editable[data-name='name'][data-value='Guest']"); - guest.editable("disable"); - $(".editable[data-name='locale'][data-pk='"+guest.data("pk")+"']").editable("disable"); - $("input[data-name='admin_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true); - $("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(); + onPostHeader () { + deactivateHeaderButtons(); }, - onSort: function(a, b) { - console.log("huh"); + onLoadSuccess: function () { + loadSuccess(); + var element = $(".header_select"); + element.each(function() { + var item = $(this).parent(); + var parent = item.parent().parent(); + if (parent.prop('nodeName') === "TH") { + item.prependTo(parent); + } + }); + var element = $(".form-check"); + element.each(function() { + var item = $(this).parent(); + var parent = item.parent().parent(); + if (parent.prop('nodeName') === "TH") { + item.prependTo(parent); + } + }); + }, onColumnSwitch: function () { var visible = $("#user-table").bootstrapTable("getVisibleColumns"); @@ -491,7 +499,18 @@ $(function() { $("#user_delete_selection").click(function() { $("#user-table").bootstrapTable("uncheckAll"); }); - + $("#select_locale").on("change",function() { + selectHeader(this, "locale"); + }); + $("#select_default_language").on("change",function() { + selectHeader(this, "default_language"); + }); + $(".check_head").on("change",function() { + var val = $(this).val() === "on"; + var name = $(this).data("name"); + var data = $(this).data("val"); + checkboxHeader(val, name, data); + }); function user_handle (userId) { $.ajax({ method:"post", @@ -505,6 +524,7 @@ $(function() { timeout: 900, success:function(data) { $("#user-table").bootstrapTable("load", data); + loadSuccess(); } }); } @@ -560,10 +580,6 @@ function TableActions (value, row) { ].join(""); } -function editEntry(param) -{ - console.log(param); -} /* Function for deleting domain restrictions */ function RestrictionActions (value, row) { return [ @@ -582,7 +598,7 @@ function EbookActions (value, row) { ].join(""); } -/* Function for deleting books */ +/* Function for deleting Users */ function UserActions (value, row) { return [ "
", @@ -610,33 +626,55 @@ function checkboxFormatter(value, row, index){ return ''; } +function loadSuccess() { + var guest = $(".editable[data-name='name'][data-value='Guest']"); + guest.editable("disable"); + $(".editable[data-name='locale'][data-pk='"+guest.data("pk")+"']").editable("disable"); + $(".editable[data-name='locale'][data-pk='"+guest.data("pk")+"']").hide(); + $("input[data-name='admin_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true); + $("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(); +} + +function handleListServerResponse (data, disableButtons) { + $("#flash_success").remove(); + $("#flash_danger").remove(); + if (!jQuery.isEmptyObject(data)) { + $( ".navbar" ).after( '
' + + '
'+data.message+'
' + + '
'); + } + $.ajax({ + method: "get", + url: window.location.pathname + "/../../ajax/listusers", + async: true, + timeout: 900, + success: function (data) { + $("#user-table").bootstrapTable("load", data); + if (disableButtons) { + deactivateHeaderButtons(); + } + loadSuccess(); + } + }); +} + + function checkboxChange(checkbox, userId, field, field_index) { $.ajax({ method: "post", url: window.location.pathname + "/../../ajax/editlistusers/" + field, data: {"pk": userId, "field_index": field_index, "value": checkbox.checked}, - success: function (data) { - if (!jQuery.isEmptyObject(data)) { - $("#flash_success").remove(); - $("#flash_danger").remove(); - $( ".navbar" ).after( '
' + - '
'+data.message+'
' + - '
'); - } - $.ajax({ - method: "get", - url: window.location.pathname + "/../../ajax/listusers", - async: true, - timeout: 900, - success: function (data) { - $("#user-table").bootstrapTable("load", data); - } - }); - } + error: function(data) { + handleListServerResponse({type:"danger", message:data.responseText}) + }, + success: handleListServerResponse }); } -function deactivateHeaderButtons(e) { +function deactivateHeaderButtons() { $("#user_delete_selection").addClass("disabled"); $("#user_delete_selection").attr("aria-disabled", true); $(".check_head").attr("aria-disabled", true); @@ -655,18 +693,10 @@ function selectHeader(element, field) { method: "post", url: window.location.pathname + "/../../ajax/editlistusers/" + field, data: {"pk": result, "value": element.value}, - success: function () { - $.ajax({ - method: "get", - url: window.location.pathname + "/../../ajax/listusers", - async: true, - timeout: 900, - success: function (data) { - $("#user-table").bootstrapTable("load", data); - deactivateHeaderButtons(); - } - }); - } + error: function (data) { + handleListServerResponse({type:"danger", message:data.responseText}) + }, + success: handleListServerResponse, }); }); } @@ -679,25 +709,12 @@ function checkboxHeader(CheckboxState, field, field_index) { method: "post", url: window.location.pathname + "/../../ajax/editlistusers/" + field, data: {"pk": result, "field_index": field_index, "value": CheckboxState}, - success: function () { - $.ajax({ - method: "get", - url: window.location.pathname + "/../../ajax/listusers", - async: true, - timeout: 900, - success: function (data) { - $("#user-table").bootstrapTable("load", data); - $("#user_delete_selection").addClass("disabled"); - $("#user_delete_selection").attr("aria-disabled", true); - $(".check_head").attr("aria-disabled", true); - $(".check_head").attr("disabled", true); - $(".check_head").prop('checked', false); - $(".button_head").attr("aria-disabled", true); - $(".button_head").addClass("disabled"); - $(".header_select").attr("disabled", true); - } - }); - } + error: function (data) { + handleListServerResponse({type:"danger", message:data.responseText}, true) + }, + success: function (data) { + handleListServerResponse (data, true) + }, }); }); } @@ -712,24 +729,10 @@ function deleteUser(a,b){ method:"post", url: window.location.pathname + "/../../ajax/deleteuser", data: {"userid":b}, - success:function(data) { - $("#flash_success").remove(); - $("#flash_danger").remove(); - if (!jQuery.isEmptyObject(data)) { - $( ".navbar" ).after( '
' + - '
'+data.message+'
' + - '
'); - } - $.ajax({ - method: "get", - url: window.location.pathname + "/../../ajax/listusers", - async: true, - timeout: 900, - success: function (data) { - $("#user-table").bootstrapTable("load", data); - } - }); - } + success: handleListServerResponse, + error: function (data) { + handleListServerResponse({type:"danger", message:data.responseText}) + }, }); } ); diff --git a/cps/templates/user_table.html b/cps/templates/user_table.html index c538dfa4..6cfa09cf 100644 --- a/cps/templates/user_table.html +++ b/cps/templates/user_table.html @@ -23,16 +23,18 @@ data-visible="{{element.get(array_field)}}" data-column="{{value.get(array_field)}}" data-formatter="checkboxFormatter"> -
+
+
-
+
+
{{show_text}} {%- endmacro %} @@ -48,14 +50,13 @@ data-editable-source={{url}} {% if validate %}data-edit-validate="{{ _('This Field is Required') }}"{% endif %}>
- {% for language in languages %} {% endfor %} -

- +
{{ show_text }} {%- endmacro %} @@ -71,13 +72,13 @@ data-editable-source={{url}} {% if validate %}data-edit-validate="{{ _('This Field is Required') }}"{% endif %}>
- {% for translation in translations %} {% endfor %} -

+
{{ show_text }} {%- endmacro %} From 5aa37c68a2635cb9d0d0e32535860855621c4621 Mon Sep 17 00:00:00 2001 From: ElQuimm <50202052+ElQuimm@users.noreply.github.com> Date: Mon, 19 Apr 2021 10:11:26 +0200 Subject: [PATCH 30/44] Updated version of italian.po Thank you. --- cps/translations/it/LC_MESSAGES/messages.po | 57 ++++++--------------- 1 file changed, 17 insertions(+), 40 deletions(-) diff --git a/cps/translations/it/LC_MESSAGES/messages.po b/cps/translations/it/LC_MESSAGES/messages.po index ba186301..85af2150 100644 --- a/cps/translations/it/LC_MESSAGES/messages.po +++ b/cps/translations/it/LC_MESSAGES/messages.po @@ -61,12 +61,10 @@ msgid "UI Configuration" msgstr "Configurazione dell'interfaccia utente" #: cps/admin.py:238 cps/templates/admin.html:46 -#, fuzzy msgid "Edit Users" msgstr "Modifica gli utenti" #: cps/admin.py:278 -#, fuzzy msgid "all" msgstr "tutti" @@ -81,11 +79,11 @@ msgstr "tutte le lingue presenti" #: cps/admin.py:353 cps/admin.py:1275 msgid "Guest Name can't be changed" -msgstr "Il nome dell'utente Guest non può essere modificato" +msgstr "Il nome dell'utente Guest (ospite) non può essere modificato" #: cps/admin.py:362 msgid "Guest can't have this role" -msgstr "" +msgstr "L'utente Guest (ospite) non può avere questo ruolo" #: cps/admin.py:371 cps/admin.py:1240 msgid "No admin user remaining, can't remove admin role" @@ -93,11 +91,11 @@ msgstr "Non rimarrebbe nessun utente amministratore, non posso rimuovere il ruol #: cps/admin.py:376 msgid "Guest can't have this view" -msgstr "" +msgstr " L'utente Guest (ospite) non può avere questa schermata" #: cps/admin.py:391 msgid "Guest's Locale is determined automatically and can't be set" -msgstr "" +msgstr "Le impostazioni locali dell'utente Guest (ospite) sono determinate automaticamente e non possono essere configurate" #: cps/admin.py:447 cps/admin.py:1132 msgid "Calibre-Web configuration updated" @@ -120,7 +118,6 @@ msgid "Are you sure you want to delete this shelf?" msgstr "Vuoi veramente eliminare questo scaffale?" #: cps/admin.py:466 -#, fuzzy msgid "Are you sure you want to change locales of selected user(s)?" msgstr "Sei sicuro di voler modificare le impostazioni locali dell'/degli utente/i selezionato/i?" @@ -237,7 +234,6 @@ msgid "User '%(user)s' created" msgstr "L'utente '%(user)s' è stato creato" #: cps/admin.py:1209 -#, fuzzy msgid "Found an existing account for this e-mail address or name." msgstr "Trovato un account esistente con questo e-mail o nome di utente" @@ -248,7 +244,7 @@ msgstr "L'utente '%(nick)s' è stato eliminato" #: cps/admin.py:1223 cps/admin.py:1224 msgid "Can't delete Guest User" -msgstr "" +msgstr "Non posso eliminare l'utente Guest (ospite)" #: cps/admin.py:1227 msgid "No admin user remaining, can't delete user" @@ -593,9 +589,8 @@ msgid "Book path %(path)s not found on Google Drive" msgstr "Non ho trovato la cartella %(path)s del libro su Google Drive" #: cps/helper.py:511 -#, fuzzy msgid "Found an existing account for this e-mail address" -msgstr "Ho trovato un account creato in precedenza con questa e-mail." +msgstr "Ho trovato un account creato in precedenza con questo indirizzo e-mail." #: cps/helper.py:519 msgid "This username is already taken" @@ -721,7 +716,7 @@ msgstr "GitHub, errore Oauth: per favore riprova più tardi." #: cps/oauth_bb.py:336 msgid "GitHub Oauth error: {}" -msgstr "" +msgstr "GitHub, errore Oauth: {}" #: cps/oauth_bb.py:357 msgid "Google Oauth error, please retry later." @@ -729,7 +724,7 @@ msgstr "Google, errore Oauth: per favore riprova più tardi." #: cps/oauth_bb.py:360 msgid "Google Oauth error: {}" -msgstr "" +msgstr "Google, errore Oauth: {}" #: cps/opds.py:110 cps/opds.py:199 cps/opds.py:276 cps/opds.py:328 #: cps/templates/grid.html:14 cps/templates/list.html:14 @@ -1257,11 +1252,11 @@ msgstr "Utente" #: cps/templates/admin.html:14 cps/templates/register.html:13 #: cps/templates/user_edit.html:14 cps/templates/user_table.html:106 msgid "E-mail Address" -msgstr "E-mail" +msgstr "Indirizzo e-mail" #: cps/templates/admin.html:15 cps/templates/user_edit.html:27 msgid "Send to Kindle E-mail Address" -msgstr "Invia all'email di Kindle" +msgstr "Invia all'indirizzo e-mail di Kindle" #: cps/templates/admin.html:17 cps/templates/layout.html:77 #: cps/templates/user_table.html:114 @@ -1806,7 +1801,7 @@ msgstr "Autenticazione Google Drive" #: cps/templates/config_edit.html:51 msgid "Please hit save to continue with setup" -msgstr "Per favore premi invio per proseguire con la configurazione" +msgstr "Per favore premi invio per proseguire la configurazione" #: cps/templates/config_edit.html:54 msgid "Please finish Google Drive setup after login" @@ -2588,9 +2583,8 @@ msgid "Select" msgstr "Seleziona" #: cps/templates/modal_dialogs.html:134 -#, fuzzy msgid "Ok" -msgstr "Libro" +msgstr "Ok" #: cps/templates/osd.xml:5 msgid "Calibre-Web eBook Catalog" @@ -2765,9 +2759,8 @@ msgid "Exclude Series" msgstr "Escludi serie" #: cps/templates/search_form.html:88 -#, fuzzy msgid "Exclude Shelves" -msgstr "Escludi serie" +msgstr "Escludi scaffali" #: cps/templates/search_form.html:108 msgid "Exclude Languages" @@ -2923,44 +2916,37 @@ msgstr "Aggiungi valori personali permessi/negati nelle colonne" #: cps/templates/user_edit.html:135 cps/templates/user_table.html:137 msgid "Delete User" -msgstr "Elimina questo utente" +msgstr "Elimina utente" #: cps/templates/user_edit.html:146 msgid "Generate Kobo Auth URL" msgstr "Genera un URL di autenticazione per Kobo" #: cps/templates/user_table.html:76 -#, fuzzy msgid "Select..." -msgstr "Seleziona" +msgstr "Seleziona..." #: cps/templates/user_table.html:102 -#, fuzzy msgid "Edit User" msgstr "Modifica utente" #: cps/templates/user_table.html:105 -#, fuzzy msgid "Enter Username" msgstr "Digita il nome utente" #: cps/templates/user_table.html:106 -#, fuzzy msgid "Enter E-mail Address" msgstr "Digita l'indirizzo e-mail" #: cps/templates/user_table.html:107 -#, fuzzy msgid "Enter Kindle E-mail Address" msgstr "Digita l'email di Kindle" #: cps/templates/user_table.html:107 -#, fuzzy msgid "Kindle E-mail" msgstr "E-mail di Kindle" #: cps/templates/user_table.html:108 -#, fuzzy msgid "Locale" msgstr "Locale" @@ -2969,7 +2955,6 @@ msgid "Visible Book Languages" msgstr "Lingue dei libri visualizzabili" #: cps/templates/user_table.html:110 -#, fuzzy msgid "Edit Denied Tags" msgstr "Modifica le categorie negate" @@ -2978,7 +2963,6 @@ msgid "Denied Tags" msgstr "Categorie negate" #: cps/templates/user_table.html:111 -#, fuzzy msgid "Edit Allowed Tags" msgstr "Modifica le categorie permesse" @@ -2987,27 +2971,22 @@ msgid "Allowed Tags" msgstr "Categorie permesse" #: cps/templates/user_table.html:112 -#, fuzzy msgid "Edit Allowed Column Values" msgstr "Modifica i valori delle colonne permesse" #: cps/templates/user_table.html:112 -#, fuzzy msgid "Allowed Column Values" msgstr "Valori delle colonne permesse" #: cps/templates/user_table.html:113 -#, fuzzy msgid "Edit Denied Column Values" msgstr "Modifica i valori delle colonne negate" #: cps/templates/user_table.html:113 -#, fuzzy msgid "Denied Columns Values" msgstr "Valori delle colonne negate" #: cps/templates/user_table.html:115 -#, fuzzy msgid "Change Password" msgstr "Modifica la password" @@ -3016,12 +2995,10 @@ msgid "View" msgstr "Visualizza" #: cps/templates/user_table.html:121 -#, fuzzy msgid "Edit Public Shelfs" -msgstr "Modifica scaffali pubblici" +msgstr "Modifica gli scaffali pubblici" #: cps/templates/user_table.html:124 -#, fuzzy msgid "Show read/unread selection" -msgstr "Mostra l'opzione per la selezione delle serie" +msgstr "Mostra l'opzione per la selezione dello stato letto/non letto" From bb4749c65b0db73bb2168e76e32d9bf5685d361f Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Wed, 21 Apr 2021 19:23:11 +0200 Subject: [PATCH 31/44] Result for testrun Added mass delete of users refactores user table refresh Bugfix for sorting with selected users Bugfix delete books #1938 --- cps/admin.py | 37 +- cps/db.py | 5 +- cps/editbooks.py | 2 +- cps/static/js/main.js | 13 + cps/static/js/table.js | 84 ++-- test/Calibre-Web TestSummary_Linux.html | 572 ++++++------------------ test/js/runner.js | 5 +- 7 files changed, 219 insertions(+), 499 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index d32d6d7c..5b18bb64 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -289,15 +289,30 @@ def list_users(): @login_required @admin_required def delete_user(): - user_id = request.values.get('userid', -1) - content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).one_or_none() - try: - message = _delete_user(content) - return Response(json.dumps({'type': "success", 'message': message}), mimetype='application/json') - except Exception as ex: - return Response(json.dumps({'type': "danger", 'message':str(ex)}), mimetype='application/json') - log.error("User not found") - return Response(json.dumps({'type': "danger", 'message':_("User not found")}), mimetype='application/json') + user_ids = request.form.to_dict(flat=False) + 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() + count = 0 + errors = list() + success = list() + if not users: + log.error("User not found") + return Response(json.dumps({'type': "danger", 'message': _("User not found")}), mimetype='application/json') + for user in users: + try: + message = _delete_user(user) + count += 1 + except Exception as ex: + errors.append({'type': "danger", 'message': str(ex)}) + + if count == 1: + success = [{'type': "success", 'message': message}] + elif count > 1: + success = [{'type': "success", 'message': _("{} users deleted successfully").format(count)}] + success.extend(errors) + return Response(json.dumps(success), mimetype='application/json') @admi.route("/ajax/getlocale") @login_required @@ -367,9 +382,9 @@ def edit_list_user(param): if not ub.session.query(ub.User).\ filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, ub.User.id != user.id).count(): - return Response(json.dumps({'type': "danger", + return Response(json.dumps([{'type': "danger", 'message':_(u"No admin user remaining, can't remove admin role", - nick=user.name)}), mimetype='application/json') + nick=user.name)}]), mimetype='application/json') user.role &= ~int(vals['field_index']) elif param.startswith('sidebar'): if user.name == "Guest" and int(vals['field_index']) == constants.SIDEBAR_READ_AND_UNREAD: diff --git a/cps/db.py b/cps/db.py index 1a305d33..39adcd4b 100644 --- a/cps/db.py +++ b/cps/db.py @@ -625,7 +625,10 @@ class CalibreDB(): outcome = list() elementlist = {ele.id: ele for ele in inputlist} for entry in state: - outcome.append(elementlist[entry]) + try: + outcome.append(elementlist[entry]) + except KeyError: + pass del elementlist[entry] for entry in elementlist: outcome.append(elementlist[entry]) diff --git a/cps/editbooks.py b/cps/editbooks.py index 67ab06d9..45e0f9fe 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -1185,6 +1185,6 @@ def merge_list_book(): element.format, element.uncompressed_size, to_name)) - delete_book(from_book.id,"", True) # json_resp = + delete_book(from_book.id,"", True) return json.dumps({'success': True}) return "" diff --git a/cps/static/js/main.js b/cps/static/js/main.js index 7231c51c..92c83f60 100644 --- a/cps/static/js/main.js +++ b/cps/static/js/main.js @@ -178,6 +178,19 @@ $("#delete_confirm").click(function() { } }); + $("#books-table").bootstrapTable("refresh"); + /*$.ajax({ + method:"get", + url: window.location.pathname + "/../../ajax/listbooks", + async: true, + timeout: 900, + success:function(data) { + + + $("#book-table").bootstrapTable("load", data); + loadSuccess(); + } + });*/ } }); } else { diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 3f3b431f..97dee842 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -511,22 +511,36 @@ $(function() { var data = $(this).data("val"); checkboxHeader(val, name, data); }); + $(".button_head").on("click",function() { + var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id); + confirmDialog( + "btndeluser", + "GeneralDeleteModal", + 0, + function() { + $.ajax({ + method:"post", + url: window.location.pathname + "/../../ajax/deleteuser", + data: {"userid": result}, + success: function (data) { + selections = selections.filter( ( el ) => !result.includes( el ) ); + // selections = selections.filter(item => item !== userId); + handleListServerResponse(data); + }, + error: function (data) { + handleListServerResponse({type:"danger", message:data.responseText}) + }, + }); + } + ); + }); function user_handle (userId) { $.ajax({ method:"post", url: window.location.pathname + "/../../ajax/deleteuser", data: {"userid":userId} }); - $.ajax({ - method:"get", - url: window.location.pathname + "/../../ajax/listusers", - async: true, - timeout: 900, - success:function(data) { - $("#user-table").bootstrapTable("load", data); - loadSuccess(); - } - }); + $("#user-table").bootstrapTable("refresh"); } $("#user-table").on("click-cell.bs.table", function (field, value, row, $element) { @@ -642,23 +656,13 @@ function handleListServerResponse (data, disableButtons) { $("#flash_success").remove(); $("#flash_danger").remove(); if (!jQuery.isEmptyObject(data)) { - $( ".navbar" ).after( '
' + - '
'+data.message+'
' + - '
'); + data.forEach(function(item) { + $(".navbar").after('
' + + '
' + item.message + '
' + + '
'); + }); } - $.ajax({ - method: "get", - url: window.location.pathname + "/../../ajax/listusers", - async: true, - timeout: 900, - success: function (data) { - $("#user-table").bootstrapTable("load", data); - if (disableButtons) { - deactivateHeaderButtons(); - } - loadSuccess(); - } - }); + $("#user-table").bootstrapTable("refresh"); } @@ -675,14 +679,16 @@ function checkboxChange(checkbox, userId, field, field_index) { } function deactivateHeaderButtons() { - $("#user_delete_selection").addClass("disabled"); - $("#user_delete_selection").attr("aria-disabled", true); - $(".check_head").attr("aria-disabled", true); - $(".check_head").attr("disabled", true); - $(".check_head").prop('checked', false); - $(".button_head").attr("aria-disabled", true); - $(".button_head").addClass("disabled"); - $(".header_select").attr("disabled", true); + if (selections.length < 1) { + $("#user_delete_selection").addClass("disabled"); + $("#user_delete_selection").attr("aria-disabled", true); + $(".check_head").attr("aria-disabled", true); + $(".check_head").attr("disabled", true); + $(".check_head").prop('checked', false); + $(".button_head").attr("aria-disabled", true); + $(".button_head").addClass("disabled"); + $(".header_select").attr("disabled", true); + } } function selectHeader(element, field) { @@ -719,7 +725,7 @@ function checkboxHeader(CheckboxState, field, field_index) { }); } -function deleteUser(a,b){ +function deleteUser(a,id){ confirmDialog( "btndeluser", "GeneralDeleteModal", @@ -728,8 +734,12 @@ function deleteUser(a,b){ $.ajax({ method:"post", url: window.location.pathname + "/../../ajax/deleteuser", - data: {"userid":b}, - success: handleListServerResponse, + data: {"userid":id}, + success: function (data) { + userId = parseInt(id, 10); + selections = selections.filter(item => item !== userId); + handleListServerResponse(data); + }, error: function (data) { handleListServerResponse({type:"danger", message:data.responseText}) }, diff --git a/test/Calibre-Web TestSummary_Linux.html b/test/Calibre-Web TestSummary_Linux.html index 17df679b..0376c51f 100644 --- a/test/Calibre-Web TestSummary_Linux.html +++ b/test/Calibre-Web TestSummary_Linux.html @@ -37,20 +37,20 @@
-

Start Time: 2021-04-12 21:44:07

+

Start Time: 2021-04-21 07:09:48

-

Stop Time: 2021-04-13 00:22:44

+

Stop Time: 2021-04-21 09:55:49

-

Duration: 2h 7 min

+

Duration: 2h 16 min

@@ -1148,12 +1148,12 @@ - + TestEditBooksList 10 - 6 - 3 - 1 + 10 + 0 + 0 0 Detail @@ -1171,74 +1171,20 @@ - +
TestEditBooksList - test_bookslist_edit_categories
- -
- ERROR -
- - - - + PASS - +
TestEditBooksList - test_bookslist_edit_languages
- -
- FAIL -
- - - - + PASS @@ -1261,33 +1207,11 @@ AssertionError: '+' != 'English' - +
TestEditBooksList - test_bookslist_edit_seriesindex
- -
- FAIL -
- - - - + PASS @@ -1319,46 +1243,22 @@ AssertionError: '+' != '3' - +
TestEditBooksList - test_search_books_list
- -
- FAIL -
- - - - + PASS - + TestEditBooksOnGdrive 20 - 18 - 1 - 1 + 20 + 0 + 0 0 Detail @@ -1376,35 +1276,11 @@ AssertionError: 'Beutlin, Frodo & Halagal, Norbert & Yang, Liu & Gonçalves, Hec - +
TestEditBooksOnGdrive - test_edit_author
- -
- FAIL -
- - - - + PASS @@ -1526,31 +1402,11 @@ AssertionError: 'Pipo, Pipe' != 'Pipo| Pipe' - +
TestEditBooksOnGdrive - test_edit_title
- -
- ERROR -
- - - - + PASS @@ -1802,11 +1658,11 @@ AttributeError: 'bool' object has no attribute 'text' - + TestKoboSync 9 - 8 - 1 + 9 + 0 0 0 @@ -1816,40 +1672,11 @@ AttributeError: 'bool' object has no attribute 'text' - +
TestKoboSync - test_book_download
- -
- FAIL -
- - - - + PASS @@ -2295,11 +2122,11 @@ AssertionError: IndexError('list index out of range') is not false : [{'NewEntit - + TestMergeBooksList 2 - 1 - 1 + 2 + 0 0 0 @@ -2309,31 +2136,11 @@ AssertionError: IndexError('list index out of range') is not false : [{'NewEntit - +
TestMergeBooksList - test_delete_book
- -
- FAIL -
- - - - + PASS @@ -2383,13 +2190,13 @@ AssertionError: False is not true TestOPDSFeed - 22 - 22 + 23 + 23 0 0 0 - Detail + Detail @@ -2433,7 +2240,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_cover
+
TestOPDSFeed - test_opds_colon_password
PASS @@ -2442,7 +2249,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_download_book
+
TestOPDSFeed - test_opds_cover
PASS @@ -2451,7 +2258,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_formats
+
TestOPDSFeed - test_opds_download_book
PASS @@ -2460,7 +2267,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_guest_user
+
TestOPDSFeed - test_opds_formats
PASS @@ -2469,7 +2276,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_hot
+
TestOPDSFeed - test_opds_guest_user
PASS @@ -2478,7 +2285,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_language
+
TestOPDSFeed - test_opds_hot
PASS @@ -2487,7 +2294,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_non_admin
+
TestOPDSFeed - test_opds_language
PASS @@ -2496,7 +2303,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_publisher
+
TestOPDSFeed - test_opds_non_admin
PASS @@ -2505,7 +2312,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_random
+
TestOPDSFeed - test_opds_publisher
PASS @@ -2514,7 +2321,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_ratings
+
TestOPDSFeed - test_opds_random
PASS @@ -2523,7 +2330,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_read_unread
+
TestOPDSFeed - test_opds_ratings
PASS @@ -2532,7 +2339,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_search
+
TestOPDSFeed - test_opds_read_unread
PASS @@ -2541,7 +2348,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_series
+
TestOPDSFeed - test_opds_search
PASS @@ -2550,7 +2357,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_shelf_access
+
TestOPDSFeed - test_opds_series
PASS @@ -2559,7 +2366,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_tags
+
TestOPDSFeed - test_opds_shelf_access
PASS @@ -2568,7 +2375,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_top_rated
+
TestOPDSFeed - test_opds_tags
PASS @@ -2577,7 +2384,7 @@ AssertionError: False is not true -
TestOPDSFeed - test_opds_unicode_user
+
TestOPDSFeed - test_opds_top_rated
PASS @@ -2585,6 +2392,15 @@ AssertionError: False is not true + +
TestOPDSFeed - test_opds_unicode_user
+ + PASS + + + + +
TestOPDSFeed - test_recently_added
@@ -3009,74 +2825,34 @@ AssertionError: False is not true - + TestUserList - 11 - 2 - 6 - 2 + 14 + 13 + 0 + 0 1 - Detail + Detail - +
TestUserList - test_list_visibility
- -
- FAIL -
- - - - + PASS - +
TestUserList - test_user_list_admin_role
- -
- FAIL -
- - - - + PASS @@ -3107,93 +2883,29 @@ AssertionError: False is not true - +
TestUserList - test_user_list_download_role
- -
- FAIL -
- - - - + PASS - +
TestUserList - test_user_list_edit_button
- -
- FAIL -
- - - - + PASS - +
TestUserList - test_user_list_edit_email
- -
- ERROR -
- - - - + PASS @@ -3207,33 +2919,11 @@ AttributeError: 'bool' object has no attribute 'text' - +
TestUserList - test_user_list_edit_language
- -
- FAIL -
- - - - + PASS @@ -3247,60 +2937,47 @@ AssertionError: 'English' != 'German' - +
TestUserList - test_user_list_edit_name
- -
- ERROR -
- - - - + PASS - +
TestUserList - test_user_list_edit_visiblility
- -
- FAIL -
- - - + PASS + + + + + + +
TestUserList - test_user_list_guest_edit
+ PASS + + + + + + +
TestUserList - test_user_list_search
+ + PASS + + + + + + +
TestUserList - test_user_list_sort
+ + PASS @@ -4006,10 +3683,10 @@ AssertionError: True is not false Total - 322 - 298 - 12 - 4 + 326 + 318 + 0 + 0 8   @@ -4038,7 +3715,7 @@ AssertionError: True is not false Platform - Linux 5.8.0-48-generic #54~20.04.1-Ubuntu SMP Sat Mar 20 13:40:25 UTC 2021 x86_64 x86_64 + Linux 5.8.0-50-generic #56~20.04.1-Ubuntu SMP Mon Apr 12 21:46:35 UTC 2021 x86_64 x86_64 Basic @@ -4158,7 +3835,7 @@ AssertionError: True is not false google-api-python-client - 2.1.0 + 2.2.0 TestEbookConvertCalibreGDrive @@ -4182,7 +3859,7 @@ AssertionError: True is not false PyDrive2 - 1.8.1 + 1.8.2 TestEbookConvertCalibreGDrive @@ -4194,7 +3871,7 @@ AssertionError: True is not false google-api-python-client - 2.1.0 + 2.2.0 TestEbookConvertGDriveKepubify @@ -4218,7 +3895,7 @@ AssertionError: True is not false PyDrive2 - 1.8.1 + 1.8.2 TestEbookConvertGDriveKepubify @@ -4254,7 +3931,7 @@ AssertionError: True is not false google-api-python-client - 2.1.0 + 2.2.0 TestEditBooksOnGdrive @@ -4278,7 +3955,7 @@ AssertionError: True is not false PyDrive2 - 1.8.1 + 1.8.2 TestEditBooksOnGdrive @@ -4290,7 +3967,7 @@ AssertionError: True is not false google-api-python-client - 2.1.0 + 2.2.0 TestSetupGdrive @@ -4308,7 +3985,7 @@ AssertionError: True is not false PyDrive2 - 1.8.1 + 1.8.2 TestSetupGdrive @@ -4380,7 +4057,8 @@ AssertionError: True is not false
diff --git a/test/js/runner.js b/test/js/runner.js index 67b3fa39..800338d1 100644 --- a/test/js/runner.js +++ b/test/js/runner.js @@ -1,3 +1,4 @@ + output_list = Array(); /* Level - 0: Summary; 1: Failed; 2: All; 3: Skipped 4: Error*/ @@ -24,9 +25,9 @@ function showCase(level) { row.classList.add('hiddenRow'); } } - // Show skipped if all or skipped or summary problems selected + // Show skipped if all or skipped selected if (id.substr(0,2) == 'st') { - if (level ==2 || level ==3 || level == 5) { + if (level ==2 || level ==3) { row.classList.remove('hiddenRow'); } else { From c0b2e886d2343a2d69cd6d5b6912b3be92ca9e99 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Wed, 21 Apr 2021 19:28:22 +0200 Subject: [PATCH 32/44] bugfix books list with selected books and stored on state --- cps/web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/web.py b/cps/web.py index e01b4b22..9b56babf 100644 --- a/cps/web.py +++ b/cps/web.py @@ -788,7 +788,7 @@ def list_books(): if state: if search: - books = calibre_db.search_query(search) + books = calibre_db.search_query(search).all() filtered_count = len(books) else: books = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).all() From 450ee436778a8fc67022b4bad4d5e9873f91bd9d Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Thu, 22 Apr 2021 19:14:56 +0200 Subject: [PATCH 33/44] Additional logging for emails --- cps/logger.py | 6 +++--- cps/services/worker.py | 3 ++- cps/static/js/table.js | 1 + cps/tasks/mail.py | 14 +++++++++++--- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/cps/logger.py b/cps/logger.py index b204de31..cd38e3d3 100644 --- a/cps/logger.py +++ b/cps/logger.py @@ -62,11 +62,11 @@ class _Logger(logging.Logger): def debug_no_auth(self, message, *args, **kwargs): + message = message.strip("\r\n") if message.startswith("send: AUTH"): - self.debug(message[:16], stacklevel=2, *args, **kwargs) + self.debug(message[:16], *args, **kwargs) else: - self.debug(message, stacklevel=2, *args, **kwargs) - + self.debug(message, *args, **kwargs) def get(name=None): diff --git a/cps/services/worker.py b/cps/services/worker.py index 8433e408..1baf25fe 100644 --- a/cps/services/worker.py +++ b/cps/services/worker.py @@ -45,7 +45,7 @@ class ImprovedQueue(queue.Queue): with self.mutex: return list(self.queue) -#Class for all worker tasks in the background +# Class for all worker tasks in the background class WorkerThread(threading.Thread): _instance = None @@ -69,6 +69,7 @@ class WorkerThread(threading.Thread): def add(cls, user, task): ins = cls.getInstance() ins.num += 1 + log.debug("Add Task for user: {}: {}".format(user, task)) ins.queue.put(QueuedTask( num=ins.num, user=user, diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 97dee842..a15b2e45 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -640,6 +640,7 @@ function checkboxFormatter(value, row, index){ return ''; } +/* Do some hiding disabling after user list is loaded */ function loadSuccess() { var guest = $(".editable[data-name='name'][data-value='Guest']"); guest.editable("disable"); diff --git a/cps/tasks/mail.py b/cps/tasks/mail.py index 8d2d4188..e8b3ebf3 100644 --- a/cps/tasks/mail.py +++ b/cps/tasks/mail.py @@ -115,6 +115,7 @@ class TaskEmail(CalibreTask): self.results = dict() def prepare_message(self): + log.debug("prepare email message for sending") message = MIMEMultipart() message['to'] = self.recipent message['from'] = self.settings["mail_from"] @@ -134,9 +135,9 @@ class TaskEmail(CalibreTask): return message def run(self, worker_thread): - # create MIME message - msg = self.prepare_message() try: + # create MIME message + msg = self.prepare_message() if self.settings['mail_server_type'] == 0: self.send_standard_email(msg) else: @@ -170,9 +171,11 @@ class TaskEmail(CalibreTask): # redirect output to logfile on python2 on python3 debugoutput is caught with overwritten # _print_debug function if sys.version_info < (3, 0): + log.debug("Redirect output on python2 for email") org_smtpstderr = smtplib.stderr smtplib.stderr = logger.StderrLogger('worker.smtp') + log.debug("Start send email") if use_ssl == 2: self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"], timeout=timeout) @@ -185,6 +188,7 @@ class TaskEmail(CalibreTask): if use_ssl == 1: self.asyncSMTP.starttls() if self.settings["mail_password"]: + log.debug("Login to email server") self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password"])) # Convert message to something to send @@ -192,14 +196,15 @@ class TaskEmail(CalibreTask): gen = Generator(fp, mangle_from_=False) gen.flatten(msg) + log.debug("Sending email") self.asyncSMTP.sendmail(self.settings["mail_from"], self.recipent, fp.getvalue()) self.asyncSMTP.quit() self._handleSuccess() + log.debug("Email send successfully") if sys.version_info < (3, 0): smtplib.stderr = org_smtpstderr - def send_gmail_email(self, message): return gmail.send_messsage(self.settings.get('mail_gmail_token', None), message) @@ -258,3 +263,6 @@ class TaskEmail(CalibreTask): @property def name(self): return "Email" + + def __str__(self): + return "{}, {}".format(self.name, self.subject) From 97e4707f72d853da05f79249fbc6c6eee73773ec Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sun, 25 Apr 2021 10:42:41 +0200 Subject: [PATCH 34/44] user list column value and tags editor (#1938) Bugfixes for massedit in user list (#1938) Changed no. of debug messages --- cps/admin.py | 122 ++++++++++------- cps/services/gmail.py | 2 + cps/static/css/main.css | 4 + cps/static/js/table.js | 245 ++++++++++++++++++++++------------ cps/tasks/mail.py | 6 +- cps/templates/user_table.html | 46 ++++--- 6 files changed, 276 insertions(+), 149 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 5b18bb64..ba82d2c2 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -225,11 +225,23 @@ def edit_user_table(): languages = calibre_db.speaking_language() translations = babel.list_translations() + [LC('en')] allUser = ub.session.query(ub.User) + tags = calibre_db.session.query(db.Tags)\ + .join(db.books_tags_link)\ + .join(db.Books)\ + .filter(calibre_db.common_filters()) \ + .group_by(text('books_tags_link.tag'))\ + .order_by(db.Tags.name).all() + if config.config_restricted_column: + custom_values = calibre_db.session.query(db.cc_classes[config.config_restricted_column]).all() + else: + custom_values = [] if not config.config_anonbrowse: allUser = allUser.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS) return render_title_template("user_table.html", users=allUser.all(), + tags=tags, + custom_values=custom_values, translations=translations, languages=languages, visiblility=visibility, @@ -238,6 +250,7 @@ def edit_user_table(): title=_(u"Edit Users"), page="usertable") + @admi.route("/ajax/listusers") @login_required @admin_required @@ -332,7 +345,7 @@ def table_get_locale(): def table_get_default_lang(): languages = calibre_db.speaking_language() ret = list() - ret.append({'value':'all','text':_('Show All')}) + ret.append({'value': 'all', 'text': _('Show All')}) for lang in languages: ret.append({'value': lang.lang_code, 'text': lang.name}) return json.dumps(ret) @@ -358,56 +371,55 @@ def edit_list_user(param): vals['field_index'] = vals['field_index'][0] if 'value' in vals: vals['value'] = vals['value'][0] - else: + elif not ('value[]' in vals): return "" for user in users: try: - vals['value'] = vals['value'].strip() - if param == 'name': - if user.name == "Guest": - raise Exception(_("Guest Name can't be changed")) - user.name = check_username(vals['value']) - elif param =='email': - user.email = check_email(vals['value']) - elif param == 'kindle_mail': - user.kindle_mail = valid_email(vals['value']) if vals['value'] else "" - elif param.endswith('role'): - if user.name == "Guest" and int(vals['field_index']) in \ - [constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]: - raise Exception(_("Guest can't have this role")) - if vals['value'] == 'true': - user.role |= int(vals['field_index']) + if param in ['denied_tags', 'allowed_tags', 'allowed_column_value', 'denied_column_value']: + if 'value[]' in vals: + setattr(user, param, prepare_tags(user, vals['action'][0], param, vals['value[]'])) else: - if int(vals['field_index']) == constants.ROLE_ADMIN: - if not ub.session.query(ub.User).\ - filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, - ub.User.id != user.id).count(): - return Response(json.dumps([{'type': "danger", - 'message':_(u"No admin user remaining, can't remove admin role", - nick=user.name)}]), mimetype='application/json') - user.role &= ~int(vals['field_index']) - elif param.startswith('sidebar'): - if user.name == "Guest" and int(vals['field_index']) == constants.SIDEBAR_READ_AND_UNREAD: - raise Exception(_("Guest can't have this view")) - if vals['value'] == 'true': - user.sidebar_view |= int(vals['field_index']) - else: - user.sidebar_view &= ~int(vals['field_index']) - elif param == 'denied_tags': - user.denied_tags = vals['value'] - elif param == 'allowed_tags': - user.allowed_tags = vals['value'] - elif param == 'allowed_column_value': - user.allowed_column_value = vals['value'] - elif param == 'denied_column_value': - user.denied_column_value = vals['value'] - elif param == 'locale': - if user.name == "Guest": - raise Exception(_("Guest's Locale is determined automatically and can't be set")) - user.locale = vals['value'] - elif param == 'default_language': - user.default_language = vals['value'] + setattr(user, param, vals['value'].strip()) + else: + vals['value'] = vals['value'].strip() + if param == 'name': + if user.name == "Guest": + raise Exception(_("Guest Name can't be changed")) + user.name = check_username(vals['value']) + elif param =='email': + user.email = check_email(vals['value']) + elif param == 'kindle_mail': + user.kindle_mail = valid_email(vals['value']) if vals['value'] else "" + elif param.endswith('role'): + if user.name == "Guest" and int(vals['field_index']) in \ + [constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]: + raise Exception(_("Guest can't have this role")) + if vals['value'] == 'true': + user.role |= int(vals['field_index']) + else: + if int(vals['field_index']) == constants.ROLE_ADMIN: + if not ub.session.query(ub.User).\ + filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, + ub.User.id != user.id).count(): + return Response(json.dumps([{'type': "danger", + 'message':_(u"No admin user remaining, can't remove admin role", + nick=user.name)}]), mimetype='application/json') + user.role &= ~int(vals['field_index']) + elif param.startswith('sidebar'): + if user.name == "Guest" and int(vals['field_index']) == constants.SIDEBAR_READ_AND_UNREAD: + raise Exception(_("Guest can't have this view")) + if vals['value'] == 'true': + user.sidebar_view |= int(vals['field_index']) + else: + user.sidebar_view &= ~int(vals['field_index']) + elif param == 'locale': + if user.name == "Guest": + raise Exception(_("Guest's Locale is determined automatically and can't be set")) + user.locale = vals['value'] + elif param == 'default_language': + user.default_language = vals['value'] except Exception as ex: + log.debug_or_exception(ex) return str(ex), 400 ub.session_commit() return "" @@ -483,6 +495,8 @@ 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 == "restrictions": + texts["main"] = _('Are you sure you want to change the selected restrictions for the selected user(s)?') elif element_id == "sidebar_view": texts["main"] = _('Are you sure you want to change the selected visibility restrictions for the selected user(s)?') return json.dumps(texts) @@ -629,6 +643,22 @@ def restriction_deletion(element, list_func): return ','.join(elementlist) +def prepare_tags(user, action, tags_name, id_list): + if "tags" in tags_name: + tags = calibre_db.session.query(db.Tags).filter(db.Tags.id.in_(id_list)).all() + new_tags_list = [x.name for x in tags] + else: + tags = calibre_db.session.query(db.cc_classes[config.config_restricted_column])\ + .filter(db.cc_classes[config.config_restricted_column].id.in_(id_list)).all() + new_tags_list = [x.value for x in tags] + saved_tags_list = user.__dict__[tags_name].split(",") if len(user.__dict__[tags_name]) else [] + if action == "remove": + saved_tags_list = [x for x in saved_tags_list if x not in new_tags_list] + else: + saved_tags_list.extend(x for x in new_tags_list if x not in saved_tags_list) + return ",".join(saved_tags_list) + + @admi.route("/ajax/addrestriction/", defaults={"user_id": 0}, methods=['POST']) @admi.route("/ajax/addrestriction//", methods=['POST']) @login_required diff --git a/cps/services/gmail.py b/cps/services/gmail.py index f85d56b0..9380121a 100644 --- a/cps/services/gmail.py +++ b/cps/services/gmail.py @@ -61,6 +61,7 @@ def get_user_info(credentials): return user_info.get('email', "") def send_messsage(token, msg): + log.debug("Start sending email via Gmail") creds = Credentials( token=token['token'], refresh_token=token['refresh_token'], @@ -79,3 +80,4 @@ def send_messsage(token, msg): body = {'raw': raw} (service.users().messages().send(userId='me', body=body).execute()) + log.debug("Email send successfully via Gmail") diff --git a/cps/static/css/main.css b/cps/static/css/main.css index e97497de..adbfbfdf 100644 --- a/cps/static/css/main.css +++ b/cps/static/css/main.css @@ -15,6 +15,10 @@ body { overflow: hidden; } +.myselect { + overflow: visible !important; +} + #main { position: absolute; width: 100%; diff --git a/cps/static/js/table.js b/cps/static/js/table.js index a15b2e45..2ccee233 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -21,7 +21,6 @@ var selections = []; $(function() { - $("#books-table").on("check.bs.table check-all.bs.table uncheck.bs.table uncheck-all.bs.table", function (e, rowsAfter, rowsBefore) { var rows = rowsAfter; @@ -453,27 +452,11 @@ $(function() { }); }, onPostHeader () { - deactivateHeaderButtons(); + move_header_elements(); }, onLoadSuccess: function () { loadSuccess(); - var element = $(".header_select"); - element.each(function() { - var item = $(this).parent(); - var parent = item.parent().parent(); - if (parent.prop('nodeName') === "TH") { - item.prependTo(parent); - } - }); - var element = $(".form-check"); - element.each(function() { - var item = $(this).parent(); - var parent = item.parent().parent(); - if (parent.prop('nodeName') === "TH") { - item.prependTo(parent); - } - }); - + move_header_elements(); }, onColumnSwitch: function () { var visible = $("#user-table").bootstrapTable("getVisibleColumns"); @@ -493,56 +476,10 @@ $(function() { url: window.location.pathname + "/../../ajax/user_table_settings", data: "{" + st + "}", }); + handle_header_buttons(); }, }); - $("#user_delete_selection").click(function() { - $("#user-table").bootstrapTable("uncheckAll"); - }); - $("#select_locale").on("change",function() { - selectHeader(this, "locale"); - }); - $("#select_default_language").on("change",function() { - selectHeader(this, "default_language"); - }); - $(".check_head").on("change",function() { - var val = $(this).val() === "on"; - var name = $(this).data("name"); - var data = $(this).data("val"); - checkboxHeader(val, name, data); - }); - $(".button_head").on("click",function() { - var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id); - confirmDialog( - "btndeluser", - "GeneralDeleteModal", - 0, - function() { - $.ajax({ - method:"post", - url: window.location.pathname + "/../../ajax/deleteuser", - data: {"userid": result}, - success: function (data) { - selections = selections.filter( ( el ) => !result.includes( el ) ); - // selections = selections.filter(item => item !== userId); - handleListServerResponse(data); - }, - error: function (data) { - handleListServerResponse({type:"danger", message:data.responseText}) - }, - }); - } - ); - }); - function user_handle (userId) { - $.ajax({ - method:"post", - url: window.location.pathname + "/../../ajax/deleteuser", - data: {"userid":userId} - }); - $("#user-table").bootstrapTable("refresh"); - } - $("#user-table").on("click-cell.bs.table", function (field, value, row, $element) { if (value === "denied_column_value") { ConfirmDialog("btndeluser", "GeneralDeleteModal", $element.id, user_handle); @@ -562,7 +499,8 @@ $(function() { }); var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference"; selections = window._[func](selections, ids); - if (selections.length < 1) { + handle_header_buttons(); + /*if (selections.length < 1) { $("#user_delete_selection").addClass("disabled"); $("#user_delete_selection").attr("aria-disabled", true); $(".check_head").attr("aria-disabled", true); @@ -570,6 +508,12 @@ $(function() { $(".check_head").prop('checked', false); $(".button_head").attr("aria-disabled", true); $(".button_head").addClass("disabled"); + $(".multi_head").attr("aria-disabled", true); + $(".multi_head").addClass("hidden"); + $(".multi_selector").attr("aria-disabled", true); + $(".multi_selector").attr("disabled", true); + $('.multi_selector').selectpicker('deselectAll'); + $('.multi_selector').selectpicker('refresh'); $(".header_select").attr("disabled", true); } else { $("#user_delete_selection").removeClass("disabled"); @@ -578,12 +522,47 @@ $(function() { $(".check_head").removeAttr("disabled"); $(".button_head").attr("aria-disabled", false); $(".button_head").removeClass("disabled"); + $(".multi_head").attr("aria-disabled", false); + $(".multi_head").removeClass("hidden"); + $(".multi_selector").attr("aria-disabled", false); + $(".multi_selector").removeAttr("disabled"); + $('.multi_selector').selectpicker('refresh'); $(".header_select").removeAttr("disabled"); - } + }*/ }); }); - +function handle_header_buttons () { + if (selections.length < 1) { + $("#user_delete_selection").addClass("disabled"); + $("#user_delete_selection").attr("aria-disabled", true); + $(".check_head").attr("aria-disabled", true); + $(".check_head").attr("disabled", true); + $(".check_head").prop('checked', false); + $(".button_head").attr("aria-disabled", true); + $(".button_head").addClass("disabled"); + $(".multi_head").attr("aria-disabled", true); + $(".multi_head").addClass("hidden"); + $(".multi_selector").attr("aria-disabled", true); + $(".multi_selector").attr("disabled", true); + $('.multi_selector').selectpicker('deselectAll'); + $('.multi_selector').selectpicker('refresh'); + $(".header_select").attr("disabled", true); + } else { + $("#user_delete_selection").removeClass("disabled"); + $("#user_delete_selection").attr("aria-disabled", false); + $(".check_head").attr("aria-disabled", false); + $(".check_head").removeAttr("disabled"); + $(".button_head").attr("aria-disabled", false); + $(".button_head").removeClass("disabled"); + $(".multi_head").attr("aria-disabled", false); + $(".multi_head").removeClass("hidden"); + $(".multi_selector").attr("aria-disabled", false); + $(".multi_selector").removeAttr("disabled"); + $('.multi_selector').selectpicker('refresh'); + $(".header_select").removeAttr("disabled"); + } +} /* Function for deleting domain restrictions */ function TableActions (value, row) { return [ @@ -644,6 +623,12 @@ function checkboxFormatter(value, row, index){ function loadSuccess() { var guest = $(".editable[data-name='name'][data-value='Guest']"); guest.editable("disable"); + $("input:radio.check_head:checked").each(function() { + $(this).prop('checked', false); + }); + $(".header_select").each(function() { + $(this).prop("selectedIndex", 0); + }); $(".editable[data-name='locale'][data-pk='"+guest.data("pk")+"']").editable("disable"); $(".editable[data-name='locale'][data-pk='"+guest.data("pk")+"']").hide(); $("input[data-name='admin_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true); @@ -653,7 +638,99 @@ function loadSuccess() { $(".user-remove[data-pk='"+guest.data("pk")+"']").hide(); } -function handleListServerResponse (data, disableButtons) { +function move_header_elements() { + $(".header_select").each(function() { + var item = $(this).parent(); + var parent = item.parent().parent(); + if (parent.prop('nodeName') === "TH") { + item.prependTo(parent); + } + }); + $(".form-check").each(function() { + var item = $(this).parent(); + var parent = item.parent().parent(); + if (parent.prop('nodeName') === "TH") { + item.prependTo(parent); + } + }); + $(".multi_select").each(function() { + var item = $(this); + var parent = item.parent().parent(); + if (parent.prop('nodeName') === "TH") { + item.prependTo(parent); + item.addClass("myselect"); + } + }); + $(".multi_selector").selectpicker(); + + // Functions have to be here, otherwise the callbacks are not fired if visible columns are changed + $(".multi_head").on("click",function() { + var val = $(this).data("set"); + var field = $(this).data("name"); + var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id); + var values = $("#" + field).val(); + confirmDialog( + "restrictions", + "GeneralChangeModal", + 0, + function() { + $.ajax({ + method:"post", + url: window.location.pathname + "/../../ajax/editlistusers/" + field, + data: {"pk": result, "value": values, "action": val}, + success: function (data) { + handleListServerResponse(data); + }, + error: function (data) { + handleListServerResponse({type:"danger", message:data.responseText}) + }, + }); + } + ); + }); + + $("#user_delete_selection").click(function() { + $("#user-table").bootstrapTable("uncheckAll"); + }); + $("#select_locale").on("change",function() { + selectHeader(this, "locale"); + }); + $("#select_default_language").on("change",function() { + selectHeader(this, "default_language"); + }); + $(".check_head").on("change",function() { + var val = $(this).data("set"); + var name = $(this).data("name"); + var data = $(this).data("val"); + checkboxHeader(val, name, data); + }); + + $(".button_head").on("click",function() { + var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id); + confirmDialog( + "btndeluser", + "GeneralDeleteModal", + 0, + function() { + $.ajax({ + method:"post", + url: window.location.pathname + "/../../ajax/deleteuser", + data: {"userid": result}, + success: function (data) { + selections = selections.filter( ( el ) => !result.includes( el ) ); + handleListServerResponse(data); + }, + error: function (data) { + handleListServerResponse({type:"danger", message:data.responseText}) + }, + }); + } + ); + }); + +} + +function handleListServerResponse (data) { $("#flash_success").remove(); $("#flash_danger").remove(); if (!jQuery.isEmptyObject(data)) { @@ -679,19 +756,6 @@ function checkboxChange(checkbox, userId, field, field_index) { }); } -function deactivateHeaderButtons() { - if (selections.length < 1) { - $("#user_delete_selection").addClass("disabled"); - $("#user_delete_selection").attr("aria-disabled", true); - $(".check_head").attr("aria-disabled", true); - $(".check_head").attr("disabled", true); - $(".check_head").prop('checked', false); - $(".button_head").attr("aria-disabled", true); - $(".button_head").addClass("disabled"); - $(".header_select").attr("disabled", true); - } -} - function selectHeader(element, field) { if (element.value !== "None") { confirmDialog(element.id, "GeneralChangeModal", 0, function () { @@ -705,6 +769,8 @@ function selectHeader(element, field) { }, success: handleListServerResponse, }); + },function() { + $(element).prop("selectedIndex", 0); }); } } @@ -723,6 +789,10 @@ function checkboxHeader(CheckboxState, field, field_index) { handleListServerResponse (data, true) }, }); + },function() { + $("input:radio.check_head:checked").each(function() { + $(this).prop('checked', false); + }); }); } @@ -758,3 +828,12 @@ function queryParams(params) function storeLocation() { window.sessionStorage.setItem("back", window.location.pathname); } + +function user_handle (userId) { + $.ajax({ + method:"post", + url: window.location.pathname + "/../../ajax/deleteuser", + data: {"userid":userId} + }); + $("#user-table").bootstrapTable("refresh"); +} diff --git a/cps/tasks/mail.py b/cps/tasks/mail.py index e8b3ebf3..f19231ec 100644 --- a/cps/tasks/mail.py +++ b/cps/tasks/mail.py @@ -115,7 +115,6 @@ class TaskEmail(CalibreTask): self.results = dict() def prepare_message(self): - log.debug("prepare email message for sending") message = MIMEMultipart() message['to'] = self.recipent message['from'] = self.settings["mail_from"] @@ -171,11 +170,10 @@ class TaskEmail(CalibreTask): # redirect output to logfile on python2 on python3 debugoutput is caught with overwritten # _print_debug function if sys.version_info < (3, 0): - log.debug("Redirect output on python2 for email") org_smtpstderr = smtplib.stderr smtplib.stderr = logger.StderrLogger('worker.smtp') - log.debug("Start send email") + log.debug("Start sending email") if use_ssl == 2: self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"], timeout=timeout) @@ -188,7 +186,6 @@ class TaskEmail(CalibreTask): if use_ssl == 1: self.asyncSMTP.starttls() if self.settings["mail_password"]: - log.debug("Login to email server") self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password"])) # Convert message to something to send @@ -196,7 +193,6 @@ class TaskEmail(CalibreTask): gen = Generator(fp, mangle_from_=False) gen.flatten(msg) - log.debug("Sending email") self.asyncSMTP.sendmail(self.settings["mail_from"], self.recipent, fp.getvalue()) self.asyncSMTP.quit() self._handleSuccess() diff --git a/cps/templates/user_table.html b/cps/templates/user_table.html index 6cfa09cf..b174e481 100644 --- a/cps/templates/user_table.html +++ b/cps/templates/user_table.html @@ -1,5 +1,5 @@ {% extends "layout.html" %} -{% macro user_table_row(parameter, edit_text, show_text, validate, button=False, id=0) -%} +{% macro user_table_row(parameter, edit_text, show_text, validate, elements=False) -%} - {% if button %} -
+ {% if elements %} +
+ +
+
+ +
+
+ +
+
+
{% endif %} {{ show_text }} @@ -25,14 +39,14 @@ data-formatter="checkboxFormatter">
- + + {{_('Deny')}} +
- + + {{_('Allow')}} +
{{show_text}} @@ -51,6 +65,7 @@ {% if validate %}data-edit-validate="{{ _('This Field is Required') }}"{% endif %}>
{{_('Deny')}} + {{_('Deny')}}
From 5470acd3af3b868a532f4de960572b903c4aac4f Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sat, 1 May 2021 12:45:28 +0200 Subject: [PATCH 41/44] Fix #1964 (RefreshError with unconfigured gdrive) --- cps/admin.py | 7 ++++--- cps/gdriveutils.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index f1320d8a..c859eef5 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -38,7 +38,6 @@ from sqlalchemy import and_ from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError from sqlalchemy.sql.expression import func, or_, text -# from sqlalchemy.func import field from . import constants, logger, helper, services from .cli import filepicker @@ -977,7 +976,10 @@ def _configuration_gdrive_helper(to_save): ) # always show google drive settings, but in case of error deny support - config.config_use_google_drive = (not gdrive_error) and ("config_use_google_drive" in to_save) + new_gdrive_value = (not gdrive_error) and ("config_use_google_drive" in to_save) + if config.config_use_google_drive and not new_gdrive_value: + config.config_google_drive_watch_changes_response = {} + config.config_use_google_drive = new_gdrive_value if _config_string(to_save, "config_google_drive_folder"): gdriveutils.deleteDatabaseOnChange() return gdrive_error @@ -1230,7 +1232,6 @@ def _configuration_result(error_flash=None, gdrive_error=None, configured=True): log.error(gdrive_error) gdrive_error = _(gdrive_error) else: - # if config.config_use_google_drive and\ if not gdrive_authenticate and gdrive_support: gdrivefolders = gdriveutils.listRootFolders() diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py index 13f83bd5..7c8c23b0 100644 --- a/cps/gdriveutils.py +++ b/cps/gdriveutils.py @@ -221,7 +221,7 @@ def listRootFolders(): drive = getDrive(Gdrive.Instance().drive) folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false" fileList = drive.ListFile({'q': folder}).GetList() - except (ServerNotFoundError, ssl.SSLError) as e: + except (ServerNotFoundError, ssl.SSLError, RefreshError) as e: log.info("GDrive Error %s" % e) fileList = [] return fileList From c0b561cb5ad601167f471aa4a50fba0366cd9f77 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sat, 1 May 2021 17:10:29 +0200 Subject: [PATCH 42/44] Better input check for custom_columns --- cps/admin.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/cps/admin.py b/cps/admin.py index c859eef5..fb01e24e 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -473,6 +473,21 @@ def update_table_settings(): return "Invalid request", 400 return "" +def check_valid_read_column(column): + if column is not "0": + if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \ + .filter(and_(db.Custom_Columns.datatype == 'bool', db.Custom_Columns.mark_for_delete == 0)).all(): + return False + return True + +def check_valid_restricted_column(column): + if column is not "0": + if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \ + .filter(and_(db.Custom_Columns.datatype == 'text', db.Custom_Columns.mark_for_delete == 0)).all(): + return False + return True + + @admi.route("/admin/viewconfig", methods=["POST"]) @login_required @@ -488,12 +503,23 @@ def update_view_configuration(): if _config_string("config_title_regex"): calibre_db.update_title_sort(config) + if not check_valid_read_column(to_save.get("config_read_column", "0")): + flash(_(u"Invalid Read Column"), category="error") + log.debug("Invalid Read column") + return view_configuration() _config_int("config_read_column") + + if not check_valid_restricted_column(to_save.get("config_restricted_column", "0")): + flash(_(u"Invalid Restricted Column"), category="error") + log.debug("Invalid Restricted Column") + return view_configuration() + _config_int("config_restricted_column") + _config_int("config_theme") _config_int("config_random_books") _config_int("config_books_per_page") _config_int("config_authors_max") - _config_int("config_restricted_column") + config.config_default_role = constants.selected_roles(to_save) config.config_default_role &= ~constants.ROLE_ANONYMOUS From b97373bf376befc2dd5aab1e8510b460993797b6 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sat, 1 May 2021 18:42:57 +0200 Subject: [PATCH 43/44] Improved error handling for disapearing custom column linked to read status --- cps/admin.py | 4 ++-- cps/static/js/details.js | 16 +++++++++++++++- cps/web.py | 22 +++++++++++++++------- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index fb01e24e..36599d61 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -474,14 +474,14 @@ def update_table_settings(): return "" def check_valid_read_column(column): - if column is not "0": + if column != "0": if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \ .filter(and_(db.Custom_Columns.datatype == 'bool', db.Custom_Columns.mark_for_delete == 0)).all(): return False return True def check_valid_restricted_column(column): - if column is not "0": + if column != "0": if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \ .filter(and_(db.Custom_Columns.datatype == 'text', db.Custom_Columns.mark_for_delete == 0)).all(): return False diff --git a/cps/static/js/details.js b/cps/static/js/details.js index 395518cb..81c1a395 100644 --- a/cps/static/js/details.js +++ b/cps/static/js/details.js @@ -22,7 +22,21 @@ $(function() { }); $("#have_read_cb").on("change", function() { - $(this).closest("form").submit(); + $.post({ + url: this.closest("form").action, + error: function(response) { + var data = [{type:"danger", message:response.responseText}] + $("#flash_success").remove(); + $("#flash_danger").remove(); + if (!jQuery.isEmptyObject(data)) { + data.forEach(function (item) { + $(".navbar").after('
' + + '
' + item.message + '
' + + '
'); + }); + } + } + }); }); $(function() { diff --git a/cps/web.py b/cps/web.py index 4203a812..0f45d0fe 100644 --- a/cps/web.py +++ b/cps/web.py @@ -185,10 +185,11 @@ def toggle_read(book_id): calibre_db.session.commit() except (KeyError, AttributeError): log.error(u"Custom Column No.%d is not exisiting in calibre database", config.config_read_column) + return "Custom Column No.{} is not exisiting in calibre database".format(config.config_read_column), 400 except (OperationalError, InvalidRequestError) as e: calibre_db.session.rollback() log.error(u"Read status could not set: %e", e) - + return "Read status could not set: {}".format(e), 400 return "" @web.route("/ajax/togglearchived/", methods=['POST']) @@ -1117,12 +1118,19 @@ def adv_search_ratings(q, rating_high, rating_low): def adv_search_read_status(q, read_status): if read_status: if config.config_read_column: - if read_status == "True": - q = q.join(db.cc_classes[config.config_read_column], isouter=True) \ - .filter(db.cc_classes[config.config_read_column].value == True) - else: - q = q.join(db.cc_classes[config.config_read_column], isouter=True) \ - .filter(coalesce(db.cc_classes[config.config_read_column].value, False) != True) + try: + if read_status == "True": + q = q.join(db.cc_classes[config.config_read_column], isouter=True) \ + .filter(db.cc_classes[config.config_read_column].value == True) + else: + q = q.join(db.cc_classes[config.config_read_column], isouter=True) \ + .filter(coalesce(db.cc_classes[config.config_read_column].value, False) != True) + except (KeyError, AttributeError): + log.error(u"Custom Column No.%d is not exisiting in calibre database", config.config_read_column) + flash(_("Custom Column No.%(column)d is not existing in calibre database", + column=config.config_read_column), + category="error") + return q else: if read_status == "True": q = q.join(ub.ReadBook, db.Books.id == ub.ReadBook.book_id, isouter=True) \ From 541c8c4b93c1f19bbe60cb2d800db72e7657a6c3 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sat, 1 May 2021 20:52:48 +0200 Subject: [PATCH 44/44] Improved error handling for disapearing custom column linked to visiblility restrictions --- cps/admin.py | 8 ++++---- cps/db.py | 31 +++++++++++++++++++++---------- cps/web.py | 6 +++--- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 36599d61..633ee0f2 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -749,10 +749,10 @@ def add_restriction(res_type, user_id): usr = current_user if 'submit_allow' in element: usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags) - ub.session_commit("Changed allowed tags of user {} to {}".format(usr.name, usr.list_allowed_tags)) + ub.session_commit("Changed allowed tags of user {} to {}".format(usr.name, usr.list_allowed_tags())) elif 'submit_deny' in element: usr.denied_tags = restriction_addition(element, usr.list_denied_tags) - ub.session_commit("Changed denied tags of user {} to {}".format(usr.name, usr.list_denied_tags)) + ub.session_commit("Changed denied tags of user {} to {}".format(usr.name, usr.list_denied_tags())) if res_type == 3: # CustomC per user if isinstance(user_id, int): usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() @@ -761,11 +761,11 @@ def add_restriction(res_type, user_id): if 'submit_allow' in element: usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values) ub.session_commit("Changed allowed columns of user {} to {}".format(usr.name, - usr.list_allowed_column_values)) + usr.list_allowed_column_values())) elif 'submit_deny' in element: usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values) ub.session_commit("Changed denied columns of user {} to {}".format(usr.name, - usr.list_denied_column_values)) + usr.list_denied_column_values())) return "" diff --git a/cps/db.py b/cps/db.py index 39adcd4b..66c289dd 100644 --- a/cps/db.py +++ b/cps/db.py @@ -44,6 +44,7 @@ from flask_login import current_user from babel import Locale as LC from babel.core import UnknownLocaleError from flask_babel import gettext as _ +from flask import flash from . import logger, ub, isoLanguages from .pagination import Pagination @@ -122,7 +123,7 @@ class Identifiers(Base): elif format_type == "goodreads": return u"Goodreads" elif format_type == "babelio": - return u"Babelio" + return u"Babelio" elif format_type == "google": return u"Google Books" elif format_type == "kobo": @@ -151,7 +152,7 @@ class Identifiers(Base): elif format_type == "goodreads": return u"https://www.goodreads.com/book/show/{0}".format(self.val) elif format_type == "babelio": - return u"https://www.babelio.com/livres/titre/{0}".format(self.val) + return u"https://www.babelio.com/livres/titre/{0}".format(self.val) elif format_type == "douban": return u"https://book.douban.com/subject/{0}".format(self.val) elif format_type == "google": @@ -606,14 +607,24 @@ class CalibreDB(): neg_content_tags_filter = false() if negtags_list == [''] else Books.tags.any(Tags.name.in_(negtags_list)) pos_content_tags_filter = true() if postags_list == [''] else Books.tags.any(Tags.name.in_(postags_list)) if self.config.config_restricted_column: - pos_cc_list = current_user.allowed_column_value.split(',') - pos_content_cc_filter = true() if pos_cc_list == [''] else \ - getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \ - any(cc_classes[self.config.config_restricted_column].value.in_(pos_cc_list)) - neg_cc_list = current_user.denied_column_value.split(',') - neg_content_cc_filter = false() if neg_cc_list == [''] else \ - getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \ - any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list)) + try: + pos_cc_list = current_user.allowed_column_value.split(',') + pos_content_cc_filter = true() if pos_cc_list == [''] else \ + getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \ + any(cc_classes[self.config.config_restricted_column].value.in_(pos_cc_list)) + neg_cc_list = current_user.denied_column_value.split(',') + neg_content_cc_filter = false() if neg_cc_list == [''] else \ + getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \ + any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list)) + except (KeyError, AttributeError): + pos_content_cc_filter = false() + neg_content_cc_filter = true() + log.error(u"Custom Column No.%d is not existing in calibre database", + self.config.config_restricted_column) + flash(_("Custom Column No.%(column)d is not existing in calibre database", + column=self.config.config_restricted_column), + category="error") + else: pos_content_cc_filter = true() neg_content_cc_filter = false() diff --git a/cps/web.py b/cps/web.py index 0f45d0fe..adf0d51e 100644 --- a/cps/web.py +++ b/cps/web.py @@ -184,8 +184,8 @@ def toggle_read(book_id): calibre_db.session.add(new_cc) calibre_db.session.commit() except (KeyError, AttributeError): - log.error(u"Custom Column No.%d is not exisiting in calibre database", config.config_read_column) - return "Custom Column No.{} is not exisiting in calibre database".format(config.config_read_column), 400 + log.error(u"Custom Column No.%d is not existing in calibre database", config.config_read_column) + return "Custom Column No.{} is not existing in calibre database".format(config.config_read_column), 400 except (OperationalError, InvalidRequestError) as e: calibre_db.session.rollback() log.error(u"Read status could not set: %e", e) @@ -1126,7 +1126,7 @@ def adv_search_read_status(q, read_status): q = q.join(db.cc_classes[config.config_read_column], isouter=True) \ .filter(coalesce(db.cc_classes[config.config_read_column].value, False) != True) except (KeyError, AttributeError): - log.error(u"Custom Column No.%d is not exisiting in calibre database", config.config_read_column) + log.error(u"Custom Column No.%d is not existing in calibre database", config.config_read_column) flash(_("Custom Column No.%(column)d is not existing in calibre database", column=config.config_read_column), category="error")