diff --git a/cps/admin.py b/cps/admin.py index 26fbe42b2..086b614e8 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -40,7 +40,7 @@ from flask_babel import gettext as _ from flask_babel import get_locale, format_time, format_datetime, format_timedelta from sqlalchemy import and_ from sqlalchemy.orm.attributes import flag_modified -from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError +from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError, ArgumentError from sqlalchemy.sql.expression import func, or_, text from . import constants, logger, helper, services, cli_param @@ -386,13 +386,12 @@ def list_users(): @user_login_required @admin_required def delete_user(): - user_ids = request.form.to_dict(flat=False) - users = None + user_ids = request.get_json().get("userid") message = "" - if "userid[]" in user_ids: - users = ub.session.query(ub.User).filter(ub.User.id.in_(user_ids['userid[]'])).all() - elif "userid" in user_ids: - users = ub.session.query(ub.User).filter(ub.User.id == user_ids['userid'][0]).all() + try: + users = ub.session.query(ub.User).filter(ub.User.id.in_(user_ids)).all() + except (ArgumentError): + users = None count = 0 errors = list() success = list() @@ -408,10 +407,10 @@ def delete_user(): errors.append({'type': "danger", 'message': str(ex)}) if count == 1: - log.info("User {} deleted".format(user_ids)) + log.info("User {} deleted".format(user_ids[0])) success = [{'type': "success", 'message': message}] elif count > 1: - log.info("Users {} deleted".format(user_ids)) + log.info("Users {} deleted".format(", ".join([str(user_id) for user_id in user_ids]))) success = [{'type': "success", 'message': _("{} users deleted successfully").format(count)}] success.extend(errors) return make_response(jsonify(success)) @@ -618,6 +617,8 @@ def load_dialogtexts(element_id): texts["main"] = _('Do you really want to delete this domain?') elif element_id == "btndeluser": texts["main"] = _('Do you really want to delete this user?') + elif element_id == "btndelbook": + texts["main"] = _('Do you really want to delete this book?') elif element_id == "delete_shelf": texts["main"] = _('Are you sure you want to delete this shelf?') elif element_id == "select_locale": @@ -626,6 +627,10 @@ def load_dialogtexts(element_id): texts["main"] = _('Are you sure you want to change visible book languages for selected user(s)?') elif element_id == "role": texts["main"] = _('Are you sure you want to change the selected role for the selected user(s)?') + elif element_id == "archive_books": + texts["main"] = _('Are you sure you want to change the archive status for the selected book(s)?') + elif element_id == "read_books": + texts["main"] = _('Are you sure you want to change the read status for the selected book(s)?') elif element_id == "restrictions": texts["main"] = _('Are you sure you want to change the selected restrictions for the selected user(s)?') elif element_id == "sidebar_view": diff --git a/cps/editbooks.py b/cps/editbooks.py index 948ad1ab8..a2c39b9d9 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -73,17 +73,18 @@ def edit_required(f): return inner -@editbook.route("/ajax/delete/", methods=["POST"]) +@editbook.route("/ajax/deletebook", methods=["POST"]) @user_login_required -def delete_book_from_details(book_id): - return delete_book_from_table(book_id, "", True) +def delete_books_ajax(): + book_ids = request.get_json().get("bookid") + return check_delete_book(book_ids, "", True) @editbook.route("/delete/", defaults={'book_format': ""}, methods=["POST"]) @editbook.route("/delete//", methods=["POST"]) @user_login_required -def delete_book_ajax(book_id, book_format): - return delete_book_from_table(book_id, book_format, False, request.form.to_dict().get('location', "")) +def delete_book(book_id, book_format): + return check_delete_book(book_id, book_format, False, request.form.to_dict().get('location', "")) @editbook.route("/admin/book/", methods=['GET']) @@ -161,7 +162,7 @@ def upload(): return make_response(jsonify(resp)) else: resp = {"location": url_for('web.show_book', book_id=book_id)} - return make_response(jsonify(resp)) + return Response(json.dumps(resp), mimetype='application/json') except (OperationalError, IntegrityError, StaleDataError) as e: calibre_db.session.rollback() log.error_or_exception("Database error: {}".format(e)) @@ -213,96 +214,240 @@ def table_get_custom_enum(c_id): @login_required_if_no_ano @edit_required def edit_list_book(param): - vals = request.form.to_dict() - book = calibre_db.get_book(vals['pk']) - calibre_db.create_functions(config) - sort_param = "" - ret = "" - try: - if param == 'series_index': - edit_book_series_index(vals['value'], book) - ret = make_response(jsonify(success=True, newValue=book.series_index)) - elif param == 'tags': - edit_book_tags(vals['value'], book) - ret = make_response(jsonify(success=True, newValue=', '.join([tag.name for tag in book.tags]))) - elif param == 'series': - edit_book_series(vals['value'], book) - ret = make_response(jsonify(success=True, newValue=', '.join([serie.name for serie in book.series]))) - elif param == 'publishers': - edit_book_publisher(vals['value'], book) - ret = make_response(jsonify(success=True, - newValue=', '.join([publisher.name for publisher in book.publishers]))) - elif param == 'languages': - invalid = list() - edit_book_languages(vals['value'], book, invalid=invalid) - if invalid: - ret = make_response(jsonify(success=False, - msg='Invalid languages in request: {}'.format(','.join(invalid)))) - else: - lang_names = list() - for lang in book.languages: - lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code)) - ret = make_response(jsonify(success=True, newValue=', '.join(lang_names))) - elif param == 'author_sort': - book.author_sort = vals['value'] - ret = make_response(jsonify(success=True, newValue=book.author_sort)) - elif param == 'title': - sort_param = book.sort - if handle_title_on_edit(book, vals.get('value', "")): - rename_error = helper.update_dir_structure(book.id, config.get_book_path()) - if not rename_error: - ret = make_response(jsonify(success=True, newValue=book.title)) - else: - ret = make_response(jsonify(success=False, msg=rename_error)) - elif param == 'sort': - book.sort = vals['value'] - ret = make_response(jsonify(success=True,newValue=book.sort)) - elif param == 'comments': - edit_book_comments(vals['value'], book) - ret = make_response(jsonify(success=True, newValue=book.comments[0].text)) - elif param == 'authors': - input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true") - rename_error = helper.update_dir_structure(book.id, config.get_book_path(), input_authors[0]) - if not rename_error: - ret = make_response(jsonify( - success=True, - newValue=' & '.join([author.replace('|', ',') for author in input_authors]))) - else: - ret = make_response(jsonify(success=False, msg=rename_error)) - elif param == 'is_archived': - is_archived = change_archived_books(book.id, vals['value'] == "True", - message="Book {} archive bit set to: {}".format(book.id, vals['value'])) - if is_archived: - kobo_sync_status.remove_synced_book(book.id) - return "" - elif param == 'read_status': - ret = helper.edit_book_read_status(book.id, vals['value'] == "True") - if ret: - return ret, 400 - elif param.startswith("custom_column_"): - new_val = dict() - new_val[param] = vals['value'] - edit_single_cc_data(book.id, book, param[14:], new_val) - # ToDo: Very hacky find better solution - if vals['value'] in ["True", "False"]: - ret = "" - else: - ret = make_response(jsonify(success=True, newValue=vals['value'])) - else: - return _("Parameter not found"), 400 - book.last_modified = datetime.now(timezone.utc) + vals = request.get_json() + multi = vals.get('multi', False) == "True" + ret_value = edit_book_param(param, vals, multi) + if isinstance(ret_value, dict): + return jsonify(ret_value) + else: + return ret_value + +@editbook.route("/ajax/editselectedbooks", methods=['POST']) +@login_required_if_no_ano +@edit_required +def edit_selected_books(): + d = request.get_json() + selections = d.get('selections') + title = d.get('title') + title_sort = d.get('title_sort') + author_sort = d.get('author_sort') + authors = d.get('authors') + categories = d.get('categories') + series = d.get('series') + languages = d.get('languages') + publishers = d.get('publishers') + comments = d.get('comments') + + if not ( + title or title_sort or authors or categories or series or languages or publishers or comments) or not selections: + return _("Parameter not found"), 400 + vals = { + "pk": selections, + "value": None, + "checkA": d.get('checkA'), + "checkT": d.get('checkT'), + } + res = list() + if title: + vals['value'] = title + out = edit_book_param('title', vals, True) + if out[0].get('success') != True: + res.extend(out) + if title_sort: + vals['value'] = title_sort + out = edit_book_param('sort', vals, True) + if out[0].get('success') != True: + res.extend(out) + if author_sort: + vals['value'] = author_sort + out = edit_book_param('author_sort', vals, True) + if out[0].get('success') != True: + res.extend(out) + if authors: + vals['value'] = authors + out = edit_book_param('authors', vals, True) + if out[0].get('success') != True: + res.extend(out) + if categories: + vals['value'] = categories + out = edit_book_param('tags', vals, True) + if out[0].get('success') != True: + res.extend(out) + if series: + vals['value'] = series + out = edit_book_param('series', vals, True) + if out[0].get('success') != True: + res.extend(out) + if languages: + vals['value'] = languages + out = edit_book_param('languages', vals, True) + if out[0].get('success') != True: + res.extend(out) + if publishers: + vals['value'] = publishers + out = edit_book_param('publishers', vals, True) + if out[0].get('success') != True: + res.extend(out) + if comments: + vals['value'] = comments + out = edit_book_param('comments', vals, True) + if out[0].get('success') != True: + res.extend(out) + if len(res) == 0: + return jsonify([{'success': True, "msg": _("Changes successfully applied")}]) + else: + return jsonify(res) + +# Separated from /editbooks so that /editselectedbooks can also use this +# +# param: the property of the book to be changed +# vals - JSON Object: +# { +# 'pk': "the book id", +# 'value': "changes value of param to what's passed here" +# 'checkA': "Optional. Used to check if autosort author is enabled. Assumed as true if not passed" +# 'checkT': "Optional. Used to check if autotitle author is enabled. Assumed as true if not passed" +# } +# +@login_required_if_no_ano +@edit_required +def edit_book_param(param, vals, multi=False): + elements = vals.get('pk',[]) + if vals.get('value', None) is None: + return {'success':False, 'msg':_("Value is missing on request")} + if not elements or len(elements) > 1 and multi == False: + return {"success":False, "msg":_("Oops! Selected book is unavailable. File does not exist or is not accessible")} + ret = {} + out = list() + for elem in elements: + book = calibre_db.get_book(elem) + if not book: + ret = {"success": False, + "msg": _("Oops! Selected book is unavailable. File does not exist or is not accessible")} + if multi: + out.append(ret) + continue + else: + return ret + calibre_db.create_functions(config) + sort_param = "" + try: + if param == 'series_index': + edit_book_series_index(vals['value'], book) + ret = {"success":True, + "newValue":book.series_index} + elif param == 'tags': + edit_book_tags(vals['value'], book) + ret = {"success":True, + "newValue":', '.join([tag.name for tag in book.tags])} + elif param == 'series': + edit_book_series(vals['value'], book) + ret = {"success":True, + "newValue":', '.join([serie.name for serie in book.series])} + elif param == 'publishers': + edit_book_publisher(vals['value'], book) + ret = {"success":True, + "newValue":', '.join([publisher.name for publisher in book.publishers])} + elif param == 'languages': + invalid = list() + edit_book_languages(vals['value'], book, invalid=invalid) + if invalid: + ret = {"success": False, "msg": 'Invalid languages in request: {}'.format(','.join(invalid))} + if multi: + out.append(ret) + else: + lang_names = list() + for lang in book.languages: + lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code)) + ret = {"success":True, + "newValue":', '.join(lang_names)} + elif param == 'author_sort': + book.author_sort = vals['value'] + ret = {"success":True, + "newValue":book.author_sort} + elif param == 'title': + sort_param = book.sort + if handle_title_on_edit(book, vals.get('value', "")): + rename_error = helper.update_dir_structure(book.id, config.get_book_path()) + if not rename_error: + calibre_db.session.commit() + ret = {"success":True, + "newValue":book.title} + else: + calibre_db.session.rollback() + ret = {"success":False, "msg":rename_error} + if multi: + out.append(ret) + elif param == 'sort': + book.sort = vals['value'] + ret = {"success":True, + "newValue":book.sort} + elif param == 'comments': + edit_book_comments(vals['value'], book) + ret = {"success":True, + "newValue":book.comments[0].text} + elif param == 'authors': + input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == True) + rename_error = helper.update_dir_structure(book.id, config.get_book_path(), input_authors[0]) + if not rename_error: + calibre_db.session.commit() + ret = {"success":True, + "newValue":' & '.join([author.replace('|', ',') for author in input_authors])} + else: + calibre_db.session.rollback() + ret = {"success":False, "msg":rename_error} + if multi: + out.append(ret) + elif param == 'is_archived': + is_archived = change_archived_books(book.id, vals['value'] == "True", + message="Book {} archive bit set to: {}".format(book.id, + vals['value'])) + if is_archived: + kobo_sync_status.remove_synced_book(book.id) + continue + elif param == 'read_status': + error = helper.edit_book_read_status(book.id, vals['value'] == "True") + if error: + if multi: + out.append({"success":False, "msg":error}) + continue + else: + return error, 400 + continue + elif param.startswith("custom_column_"): + new_val = dict() + new_val[param] = vals['value'] + edit_single_cc_data(book.id, book, param[14:], new_val) + # ToDo: Very hacky find better solution + if vals['value'] in ["True", "False"]: + ret = {} + else: + ret = {"success":True, "newValue":vals['value']} + else: + if multi: + out.append({"success":False, "msg":_("Parameter not found")}) + continue + return _("Parameter not found"), 400 + book.last_modified = datetime.now(timezone.utc) - calibre_db.session.commit() - # revert change for sort if automatic fields link is deactivated - if param == 'title' and vals.get('checkT') == "false": - book.sort = sort_param calibre_db.session.commit() - except (OperationalError, IntegrityError, StaleDataError) as e: - calibre_db.session.rollback() - log.error_or_exception("Database error: {}".format(e)) - ret = make_response(jsonify(success=False, - msg='Database error: {}'.format(e.orig if hasattr(e, "orig") else e))) - return ret + # revert change for sort if automatic fields link is deactivated + if param == 'title' and vals.get('checkT') == False: + book.sort = sort_param + calibre_db.session.commit() + except (OperationalError, IntegrityError, StaleDataError, AttributeError) as e: + calibre_db.session.rollback() + log.error_or_exception("Database error: {}".format(e)) + ret = {"success":False, "msg":'Database error: {}'.format(e.orig if hasattr(e, "orig") else e)} + if multi: + out.append(ret) + if multi: + if len(out) > 0: + return out + else: + return [ret] + else: + return ret @editbook.route("/ajax/sort_value//") @@ -337,6 +482,55 @@ def simulate_merge_list_book(): return make_response(jsonify({'to': to_book, 'from': from_book})) return "" +@editbook.route("/ajax/displayselectedbooks", methods=['POST']) +@user_login_required +@edit_required +def display_selected_books(): + vals = request.get_json().get('selections') + books = [] + if vals: + for book_id in vals: + books.append(calibre_db.get_book(book_id).title) + return json.dumps({'books': books}) + return "" + +@editbook.route("/ajax/archiveselectedbooks", methods=['POST']) +@login_required_if_no_ano +@edit_required +def archive_selected_books(): + vals = request.get_json().get('selections') + state = request.get_json().get('archive') + if vals: + for book_id in vals: + is_archived = change_archived_books(book_id, state, + message="Book {} archive bit set to: {}".format(book_id, state)) + if is_archived: + kobo_sync_status.remove_synced_book(book_id) + return json.dumps({'success': True}) + return "" + + +@editbook.route("/ajax/readselectedbooks", methods=['POST']) +@user_login_required +@edit_required +def read_selected_books(): + vals = request.get_json().get('selections') + markAsRead = request.get_json().get('markAsRead') + if vals: + try: + for book_id in vals: + ret = helper.edit_book_read_status(book_id, markAsRead) + + except (OperationalError, IntegrityError, StaleDataError) as e: + calibre_db.session.rollback() + log.error_or_exception("Database error: {}".format(e)) + ret = Response(json.dumps({'success': False, + 'msg': 'Database error: {}'.format(e.orig if hasattr(e, "orig") else e)}), + mimetype='application/json') + + return json.dumps({'success': True}) + return "" + @editbook.route("/ajax/mergebooks", methods=['POST']) @user_login_required @@ -371,7 +565,7 @@ def merge_list_book(): element.format, element.uncompressed_size, to_name)) - delete_book_from_table(from_book.id, "", True) + check_delete_book([from_book.id], "", True) return make_response(jsonify(success=True)) return "" @@ -648,8 +842,9 @@ def prepare_authors(authr, calibre_path, gdrive=False): all_new_name = helper.get_valid_filename(one_book.title, chars=42) + ' - ' \ + helper.get_valid_filename(renamed_author.name, chars=42) # change location in database to new author/title path - helper.rename_all_files_on_change(one_book, new_path, new_path, all_new_name, gdrive) - + error = helper.rename_all_files_on_change(one_book, new_path, new_path, all_new_name, gdrive) + if error: + flash(error) return input_authors @@ -841,86 +1036,109 @@ def delete_whole_book(book_id, book): calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete() -def render_delete_book_result(book_format, json_response, warning, book_id, location=""): +def render_delete_book_result(book_format, book_id, location=""): if book_format: - if json_response: - return jsonify([warning, {"location": url_for("edit-book.show_edit_book", book_id=book_id), - "type": "success", - "format": book_format, - "message": _('Book Format Successfully Deleted')}]) - else: - flash(_('Book Format Successfully Deleted'), category="success") - return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) + flash(_('Book Format Successfully Deleted'), category="success") + return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) else: - if json_response: - return jsonify([warning, {"location": get_redirect_location(location, "web.index"), - "type": "success", - "format": book_format, - "message": _('Book Successfully Deleted')}]) - else: - flash(_('Book Successfully Deleted'), category="success") - return redirect(get_redirect_location(location, "web.index")) + flash(_('Book Successfully Deleted'), category="success") + return redirect(get_redirect_location(location, "web.index")) -def delete_book_from_table(book_id, book_format, json_response, location=""): - warning = {} +def check_delete_book(book_id, book_format, json_response, location=""): if current_user.role_delete_books(): - book = calibre_db.get_book(book_id) - if book: - try: - result, error = helper.delete_book(book, config.get_book_path(), book_format=book_format.upper()) - if not result: - if json_response: - return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id), - "type": "danger", - "format": "", - "message": error}]) - else: - flash(error, category="error") - return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) - if error: - if json_response: - warning = {"location": url_for("edit-book.show_edit_book", book_id=book_id), - "type": "warning", - "format": "", - "message": error} - else: - flash(error, category="warning") - if not book_format: - delete_whole_book(book_id, book) - else: - calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\ - filter(db.Data.format == book_format).delete() - if book_format.upper() in ['KEPUB', 'EPUB', 'EPUB3']: - kobo_sync_status.remove_synced_book(book.id, True) - calibre_db.session.commit() - except Exception as ex: - log.error_or_exception(ex) - calibre_db.session.rollback() - if json_response: - return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id), - "type": "danger", - "format": "", - "message": ex}]) - else: - flash(str(ex), category="error") - return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) - + if json_response: + # if json response is set, it's possible to delete more than one book, but never a format is deleted + res = list() + for b in book_id: + ret = delete_book_from_table(b) + if ret: + res.extend([ret]) + if len(res) == 0: + return [{"location": get_redirect_location(location, "web.index"), + "type": "success", + "format": "", + "message": _('Book Successfully Deleted')}] + return jsonify(res) else: - # book not found - log.error('Book with id "%s" could not be deleted: not found', book_id) - return render_delete_book_result(book_format, json_response, warning, book_id, location) + return delete_book_from_UI(book_id, book_format, location) message = _("You are missing permissions to delete books") if json_response: - return jsonify({"location": url_for("edit-book.show_edit_book", book_id=book_id), - "type": "danger", - "format": "", - "message": message}) + try: + return jsonify({"location": url_for("edit-book.show_edit_book", book_id=int(book_id)), + "type": "danger", + "format": "", + "message": message}) + except TypeError as e: + return jsonify({"location": url_for("web.index"), "type": "danger", "format": "", + "message": str(e)}) else: flash(message, category="error") return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) +def delete_book_from_UI(book_id, book_format, location=""): + book = calibre_db.get_book(book_id) + if book: + try: + result, error = helper.delete_book(book, config.get_book_path(), book_format=book_format.upper()) + if not result: + flash(error, category="error") + return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) + if error: + flash(error, category="warning") + if not book_format: + delete_whole_book(book_id, book) + else: + calibre_db.session.query(db.Data).filter(db.Data.book == book.id). \ + filter(db.Data.format == book_format).delete() + if book_format.upper() in ['KEPUB', 'EPUB', 'EPUB3']: + kobo_sync_status.remove_synced_book(book.id, True) + calibre_db.session.commit() + except Exception as ex: + log.error_or_exception(ex) + calibre_db.session.rollback() + flash(str(ex), category="error") + return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) + else: + # book not found + log.error('Book with id "%s" could not be deleted: not found', book_id) + return render_delete_book_result(book_format, book_id, location) + + +def delete_book_from_table(book_id): + book = calibre_db.get_book(book_id) + if book: + try: + result, error = helper.delete_book(book, config.get_book_path(), book_format="") + if not result: + return {"location": url_for("edit-book.show_edit_book", book_id=book_id), + "type": "danger", + "format": "", + "message": error} + delete_whole_book(book_id, book) + calibre_db.session.commit() + if error: + return {"location": url_for("edit-book.show_edit_book", book_id=book_id), + "type": "warning", + "format": "", + "message": error} + except Exception as ex: + log.error_or_exception(ex) + calibre_db.session.rollback() + return {"location": url_for("edit-book.show_edit_book", book_id=book_id), + "type": "danger", + "format": "", + "message": ex} + else: + # book not found + log.error('Book with id "%s" could not be deleted: not found', book_id) + return {"location": url_for("edit-book.show_edit_book", book_id=book_id), + "type": "danger", + "format": "", + "message": _('Book with id "{}" could not be deleted: not found'.format(book_id))} + + def render_edit_book(book_id): cc = calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).all() book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) diff --git a/cps/helper.py b/cps/helper.py index 8fa9bbac9..dd6298826 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -307,18 +307,16 @@ def edit_book_read_status(book_id, read_status=None): if not config.config_read_column: book = ub.session.query(ub.ReadBook).filter(and_(ub.ReadBook.user_id == int(current_user.id), ub.ReadBook.book_id == book_id)).first() - if book: - if read_status is None: - if book.read_status == ub.ReadBook.STATUS_FINISHED: - book.read_status = ub.ReadBook.STATUS_UNREAD - else: - book.read_status = ub.ReadBook.STATUS_FINISHED - else: - book.read_status = ub.ReadBook.STATUS_FINISHED if read_status else ub.ReadBook.STATUS_UNREAD - else: + if not book: read_book = ub.ReadBook(user_id=current_user.id, book_id=book_id) - read_book.read_status = ub.ReadBook.STATUS_FINISHED book = read_book + if read_status is None: + if book.read_status == ub.ReadBook.STATUS_FINISHED: + book.read_status = ub.ReadBook.STATUS_UNREAD + else: + book.read_status = ub.ReadBook.STATUS_FINISHED + else: + book.read_status = ub.ReadBook.STATUS_FINISHED if read_status == True else ub.ReadBook.STATUS_UNREAD if not book.kobo_reading_state: kobo_reading_state = ub.KoboReadingState(user_id=current_user.id, book_id=book_id) kobo_reading_state.current_bookmark = ub.KoboBookmark() @@ -396,10 +394,16 @@ def delete_book_file(book, calibrepath, book_format=None): def rename_all_files_on_change(one_book, new_path, old_path, all_new_name, gdrive=False): for file_format in one_book.data: if not gdrive: - if not os.path.exists(new_path): - os.makedirs(new_path) - shutil.move(os.path.join(old_path, file_format.name + '.' + file_format.format.lower()), - os.path.join(new_path, all_new_name + '.' + file_format.format.lower())) + try: + if not os.path.exists(new_path): + os.makedirs(new_path) + shutil.move(os.path.join(old_path, file_format.name + '.' + file_format.format.lower()), + os.path.join(new_path, all_new_name + '.' + file_format.format.lower())) + except (PermissionError, FileNotFoundError) as ex: + log.error("Moving book-id %s folder %s failed: %s", one_book.id, new_path, ex) + return _("Moving book path of Book %(book_id)s to: '%(src)s' failed with error: %(error)s", + book_id=one_book.id, src=new_path, error=str(ex)) + else: g_file = gd.getFileFromEbooksFolder(old_path, file_format.name + '.' + file_format.format.lower()) @@ -412,6 +416,7 @@ def rename_all_files_on_change(one_book, new_path, old_path, all_new_name, gdriv # change name in Database file_format.name = all_new_name + return False def rename_author_path(first_author, old_author_dir, renamed_author, calibre_path="", gdrive=False): @@ -466,14 +471,15 @@ def update_dir_structure_file(book_id, calibre_path, original_filepath, new_auth db_filename, original_filepath, path) - new_path = os.path.join(calibre_path, new_author_dir, new_title_dir).replace('\\', '/') - all_new_name = get_valid_filename(local_book.title, chars=42) + ' - ' \ - + get_valid_filename(new_author, chars=42) - # Book folder already moved, only files need to be renamed - rename_all_files_on_change(local_book, new_path, new_path, all_new_name) + if not error: + new_path = os.path.join(calibre_path, new_author_dir, new_title_dir).replace('\\', '/') + all_new_name = get_valid_filename(local_book.title, chars=42) + ' - ' \ + + get_valid_filename(new_author, chars=42) + # Book folder already moved, only files need to be renamed + renameerror = rename_all_files_on_change(local_book, new_path, new_path, all_new_name) - if error: - return error + if error or renameerror: + return error or renameerror return False @@ -517,7 +523,7 @@ def update_dir_structure_gdrive(book_id, first_author): if titledir != new_titledir or authordir != new_authordir : all_new_name = get_valid_filename(book.title, chars=42) + ' - ' \ + get_valid_filename(new_authordir, chars=42) - rename_all_files_on_change(book, book.path, book.path, all_new_name, gdrive=True) # todo: Move filenames on gdrive + return rename_all_files_on_change(book, book.path, book.path, all_new_name, gdrive=True) # todo: Move filenames on gdrive return False @@ -553,33 +559,13 @@ def move_files_on_change(calibre_path, new_author_dir, new_titledir, localbook, log.error("Deleting authorpath for book %s failed: %s", localbook.id, ex) # change location in database to new author/title path localbook.path = os.path.join(new_author_dir, new_titledir).replace('\\', '/') - except OSError as ex: + except (OSError, FileNotFoundError) as ex: log.error_or_exception("Rename title from {} to {} failed with error: {}".format(path, new_path, ex)) return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_path, error=str(ex)) return False -def rename_files_on_change(first_author, - renamed_author, - local_book, - original_filepath="", - path="", - calibre_path="", - gdrive=False): - # Rename all files from old names to new names - #try: - #clean_author_database(renamed_author, calibre_path, gdrive=gdrive) - #if first_author and first_author not in renamed_author: - # clean_author_database([first_author], calibre_path, local_book, gdrive) - #if not gdrive and not renamed_author and not original_filepath and len(os.listdir(os.path.dirname(path))) == 0: - # shutil.rmtree(os.path.dirname(path)) - #except (OSError, FileNotFoundError) as ex: - # log.error_or_exception("Error in rename file in path {}".format(ex)) - # return _("Error in rename file in path: {}".format(str(ex))) - return False - - def delete_book_gdrive(book, book_format): error = None if book_format: diff --git a/cps/kobo_sync_status.py b/cps/kobo_sync_status.py index 357b84efe..5df2f7a86 100644 --- a/cps/kobo_sync_status.py +++ b/cps/kobo_sync_status.py @@ -51,13 +51,15 @@ def remove_synced_book(book_id, all=False, session=None): ub.session_commit(_session=session) +# If state == none, it will toggle the archive state of the passed book_id. +# state = true archives it, state = false unarchives it def change_archived_books(book_id, state=None, message=None): archived_book = ub.session.query(ub.ArchivedBook).filter(and_(ub.ArchivedBook.user_id == int(current_user.id), ub.ArchivedBook.book_id == book_id)).first() - if not archived_book: + if not archived_book: # and (state == True or state == None): archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id) - archived_book.is_archived = state if state else not archived_book.is_archived + archived_book.is_archived = state if state != None else not archived_book.is_archived archived_book.last_modified = datetime.now(timezone.utc) # toDo. Check utc timestamp ub.session.merge(archived_book) diff --git a/cps/static/css/caliBlur_override.css b/cps/static/css/caliBlur_override.css index 29fd7e298..5e8c4d53f 100644 --- a/cps/static/css/caliBlur_override.css +++ b/cps/static/css/caliBlur_override.css @@ -26,3 +26,14 @@ body.serieslist.grid-view div.container-fluid > div > div.col-sm-10::before { input.datepicker {color: transparent} input.datepicker:focus {color: transparent} input.datepicker:focus + input {color: #555} + +.col-sm-3.col-lg-2.col-xs-6.book.session { + margin-left: 0; + margin-right: 0; +} + +@media only screen and (max-width: 767px) { + .row-fluid > .col-sm-2 { + visibility: hidden; + } +} diff --git a/cps/static/js/main.js b/cps/static/js/main.js index 505d5d9ec..815246a8d 100644 --- a/cps/static/js/main.js +++ b/cps/static/js/main.js @@ -229,10 +229,12 @@ $("#delete_confirm").click(function(event) { postButton(event, getPath() + "/delete/" + deleteId + "/" + bookFormat); } else { if (ajaxResponse) { - path = getPath() + "/ajax/delete/" + deleteId; $.ajax({ - method:"post", - url: path, + url: getPath() + "/ajax/deletebook", + method: "post", + contentType: "application/json; charset=utf-8", + dataType: "json", + data: JSON.stringify({"bookid": [deleteId]}), timeout: 900, success:function(data) { data.forEach(function(item) { diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 2a1bbe8e5..a5ac53c2d 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -81,20 +81,78 @@ $(function() { $("#merge_books").addClass("disabled"); $("#merge_books").attr("aria-disabled", true); } + if (selections.length >= 1) { + $("#delete_selected_books").removeClass("disabled"); + $("#delete_selected_books").attr("aria-disabled", false); + + $("#archive_selected_books").removeClass("disabled"); + $("#archive_selected_books").attr("aria-disabled", false); + + $("#unarchive_selected_books").removeClass("disabled"); + $("#unarchive_selected_books").attr("aria-disabled", false); + + $("#read_selected_books").removeClass("disabled"); + $("#read_selected_books").attr("aria-disabled", false); + + $("#unread_selected_books").removeClass("disabled"); + $("#unread_selected_books").attr("aria-disabled", false); + + $("#edit_selected_books").removeClass("disabled"); + $("#edit_selected_books").attr("aria-disabled", false); + } else { + $("#delete_selected_books").addClass("disabled"); + $("#delete_selected_books").attr("aria-disabled", true); + + $("#archive_selected_books").addClass("disabled"); + $("#archive_selected_books").attr("aria-disabled", true); + + $("#unarchive_selected_books").addClass("disabled"); + $("#unarchive_selected_books").attr("aria-disabled", true); + + $("#read_selected_books").addClass("disabled"); + $("#read_selected_books").attr("aria-disabled", true); + + $("#unread_selected_books").addClass("disabled"); + $("#unread_selected_books").attr("aria-disabled", true); + + $("#edit_selected_books").addClass("disabled"); + $("#edit_selected_books").attr("aria-disabled", true); + } if (selections.length < 1) { - $("#delete_selection").addClass("disabled"); - $("#delete_selection").attr("aria-disabled", true); + // $("#book_delete_selection").addClass("disabled"); + // $("#book_delete_selection").attr("aria-disabled", true); $("#table_xchange").addClass("disabled"); $("#table_xchange").attr("aria-disabled", true); } else { - $("#delete_selection").removeClass("disabled"); - $("#delete_selection").attr("aria-disabled", false); + // $("#book_delete_selection").removeClass("disabled"); + // $("#book_delete_selection").attr("aria-disabled", false); $("#table_xchange").removeClass("disabled"); $("#table_xchange").attr("aria-disabled", false); - } + handle_header_buttons(); }); - $("#delete_selection").click(function() { + + // Small block to initialize the state of the author/title sort inputs in metadata form + { + let checkA = $('#autoupdate_authorsort').prop('checked'); + $('#author_sort_input').prop('disabled', checkA); + let checkT = $('#autoupdate_titlesort').prop('checked'); + $('#title_sort_input').prop('disabled', checkT); + } + + // Disable/enable author and title sort input in respect to auto-update title/author sort being checked on or not + $("#autoupdate_authorsort").on('change', function(event) { + let checkA = $('#autoupdate_authorsort').prop('checked'); + $('#author_sort_input').prop('disabled', checkA); + }) + + $("#autoupdate_titlesort").on('change', function(event) { + let checkT = $('#autoupdate_titlesort').prop('checked'); + $('#title_sort_input').prop('disabled', checkT); + }) + ///// + + $("#book_delete_selection").click(function () { $("#books-table").bootstrapTable("uncheckAll"); }); @@ -135,6 +193,250 @@ $(function() { }); }); + $("#edit_selected_books").click(function(event) { + if ($(this).hasClass("disabled")) { + event.stopPropagation() + } else { + $('#edit_selected_modal').modal("show"); + } + }); + + $("#edit_selected_confirm").click(function(event) { + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: getPath() + "/ajax/editselectedbooks", + data: JSON.stringify({ + "selections": selections, + "title": $("#title_input").val(), + "title_sort": $("#title_sort_input").val(), + "author_sort": $("#author_sort_input").val(), + "authors": $("#authors_input").val(), + "categories": $("#categories_input").val(), + "series": $("#series_input").val(), + "languages": $("#languages_input").val(), + "publishers": $("#publishers_input").val(), + "comments": $("#comments_input").val().toString(), + "checkA": $('#autoupdate_authorsort').prop('checked'), + "checkT": $('#autoupdate_titlesort').prop('checked') + }), + success: function success(data) { + let result = ""; + $("#books-table").bootstrapTable("refresh"); + $("#books-table").bootstrapTable("uncheckAll"); + + $("#title_input").val(""); + $("#title_sort_input").val(""); + $("#author_sort_input").val(""); + $("#authors_input").val(""); + $("#categories_input").val(""); + $("#series_input").val(""); + $("#languages_input").val(""); + $("#publishers_input").val(""); + $("#comments_input").val(""); + + $("#flash_success").remove(); + $("#flash_danger").remove(); + + if (!jQuery.isEmptyObject(data)) { + data.forEach(function(item) { + if (item.success === true) { + result = "success"; + } else { + result = "danger"; + } + $(".navbar").after('
' + + '
' + item.msg + '
' + + '
'); + }); + } + $(".table.table-striped").bootstrapTable("refresh"); + // handleListServerResponse(data); + } + }); + }); + + $(document).on('click', '#archive_selected_books', function(event) { + if ($(this).hasClass("disabled")) { + event.stopPropagation() + } else { + $('#archive_selected_modal').modal("show"); + } + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: getPath() + "/ajax/displayselectedbooks", + data: JSON.stringify({"selections":selections}), + success: function success(booTitles) { + $('#display-archive-selected-books').empty(); + $.each(booTitles.books, function(i, item) { + $("- " + item + "

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

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

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

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

").appendTo("#display-unread-selected-books"); + }); + + } + }); + }); + + $(document).on('click', '#unread_selected_confirm', function(event) { + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: getPath() + "/ajax/readselectedbooks", + data: JSON.stringify({"selections":selections, "markAsRead": false}), + success: function success(booTitles) { + $("#books-table").bootstrapTable("refresh"); + $("#books-table").bootstrapTable("uncheckAll"); + } + }); + });*/ + $("#table_xchange").click(function() { $.ajax({ method:"post", @@ -157,6 +459,10 @@ $(function() { editable: { mode: "inline", emptytext: "", + ajaxOptions: { + contentType: "application/json; charset=utf-8", + dataType: "json", + }, success: function (response, __) { if (!response.success) return response.msg; return {newValue: response.newValue}; @@ -164,7 +470,8 @@ $(function() { params: function (params) { params.checkA = $('#autoupdate_authorsort').prop('checked'); params.checkT = $('#autoupdate_titlesort').prop('checked'); - return params + params.pk = [params.pk]; + return JSON.stringify(params); } } }; @@ -198,7 +505,7 @@ $(function() { searchAlign: "left", showSearchButton : true, searchOnEnterKey: true, - checkboxHeader: false, + checkboxHeader: true, maintainMetaData: true, responseHandler: responseHandler, columns: column, @@ -212,7 +519,7 @@ $(function() { $.ajax({ method:"get", dataType: "json", - url: window.location.pathname + "/../ajax/sort_value/" + field + "/" + row.id, + url: getPath() + "/ajax/sort_value/" + field + "/" + row.id, success: function success(data) { var key = Object.keys(data)[0]; $("#books-table").bootstrapTable("updateCellByUniqueId", { @@ -224,6 +531,66 @@ $(function() { }); } }, + onPostBody () { + // Remove all checkboxes from Headers for showing the texts in the column selector + $('.columns [data-field]').each(function(){ + var elText = $(this).next().text(); + $(this).next().empty(); + var index = elText.lastIndexOf('\n', elText.length - 2); + if ( index > -1) { + elText = elText.substr(index); + } + $(this).next().text(elText); + }); + }, + onPostHeader() { + $(".form-check").each(function () { + var item = $(this).parent(); + var parent = item.parent().parent(); + if (parent.prop('nodeName') === "TH") { + item.prependTo(parent); + } + }); + + if ($(".button_head").length) { + if (!$._data($(".button_head").get(0), "events")) { + $(".button_head").on("click", function () { + var result = $('#books-table').bootstrapTable('getSelections').map(a => a.id); + confirmDialog( + "btndelbook", + "GeneralDeleteModal", + 0, + function () { + $.ajax({ + method: "post", + url: getPath() + "/ajax/deletebook", + contentType: "application/json; charset=utf-8", + dataType: "json", + data: JSON.stringify({"bookid": result}), + success: function (data) { + selections = selections.filter((el) => !result.includes(el)); + handleListServerResponse(data); + }, + error: function (data) { + handleListServerResponse([{type: "danger", message: data.responseText}]) + }, + }); + } + ); + }); + } + } + if ($(".check_head").length) { + if (!$._data($(".check_head").get(0), "events")) { + $(".check_head").on("change", function () { + var val = $(this).data("set"); + var name = $(this).data("name"); + var data = $(this).data("val"); + bookCheckboxHeader(val, name, data); + }); + } + } + }, // eslint-disable-next-line no-unused-vars onColumnSwitch: function (field, checked) { var visible = $("#books-table").bootstrapTable("getVisibleColumns"); @@ -240,10 +607,16 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/table_settings", + url: getPath() + "/ajax/table_settings", data: "{" + st + "}", }); + handle_header_buttons(); }, + onLoadSuccess: function() { + $("input:radio.check_head:checked").each(function () { + $(this).prop('checked', false); + }); + } }); $("#domain_allow_submit").click(function(event) { @@ -252,7 +625,7 @@ $(function() { $(this).closest("form").submit(); $.ajax ({ method:"get", - url: window.location.pathname + "/../../ajax/domainlist/1", + url: getPath() + "/ajax/domainlist/1", async: true, timeout: 900, success:function(data) { @@ -273,7 +646,7 @@ $(function() { $(this).closest("form").submit(); $.ajax ({ method:"get", - url: window.location.pathname + "/../../ajax/domainlist/0", + url: getPath() + "/ajax/domainlist/0", async: true, timeout: 900, success:function(data) { @@ -291,12 +664,12 @@ $(function() { function domainHandle(domainId) { $.ajax({ method:"post", - url: window.location.pathname + "/../../ajax/deletedomain", + url: getPath() + "/ajax/deletedomain", data: {"domainid":domainId} }); $.ajax({ method:"get", - url: window.location.pathname + "/../../ajax/domainlist/1", + url: getPath() + "/ajax/domainlist/1", async: true, timeout: 900, success:function(data) { @@ -305,7 +678,7 @@ $(function() { }); $.ajax({ method:"get", - url: window.location.pathname + "/../../ajax/domainlist/0", + url: getPath() + "/ajax/domainlist/0", async: true, timeout: 900, success:function(data) { @@ -541,7 +914,7 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../../ajax/user_table_settings", + url: getPath() + "/ajax/user_table_settings", data: "{" + st + "}", }); handle_header_buttons(); @@ -567,8 +940,8 @@ $(function() { function handle_header_buttons () { if (selections.length < 1) { - $("#user_delete_selection").addClass("disabled"); - $("#user_delete_selection").attr("aria-disabled", true); + $(".mass_selection").addClass("disabled"); + $(".mass_selection").attr("aria-disabled", true); $(".check_head").attr("aria-disabled", true); $(".check_head").attr("disabled", true); $(".check_head").prop('checked', false); @@ -580,8 +953,8 @@ function handle_header_buttons () { $(".multi_selector").attr("disabled", true); $(".header_select").attr("disabled", true); } else { - $("#user_delete_selection").removeClass("disabled"); - $("#user_delete_selection").attr("aria-disabled", false); + $(".mass_selection").removeClass("disabled"); + $(".mass_selection").attr("aria-disabled", false); $(".check_head").attr("aria-disabled", false); $(".check_head").removeAttr("disabled"); $(".button_head").attr("aria-disabled", false); @@ -590,8 +963,10 @@ function handle_header_buttons () { $(".multi_head").removeClass("hidden"); $(".multi_selector").attr("aria-disabled", false); $(".multi_selector").removeAttr("disabled"); - $('.multi_selector').selectpicker('refresh'); $(".header_select").removeAttr("disabled"); + if (typeof $.fn.selectpicker === "function") { + $('.multi_selector').selectpicker('refresh'); + } } } @@ -718,7 +1093,7 @@ function loadSuccess() { $("input[data-name='passwd_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true); $("input[data-name='edit_shelf_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true); $("input[data-name='sidebar_read_and_unread'][data-pk='"+guest.data("pk")+"']").prop("disabled", true); - $(".user-remove[data-pk='"+guest.data("pk")+"']").hide(); + $(".user-remove[data-pk='" + guest.data("pk") + "']").hide(); } function move_header_elements() { @@ -760,7 +1135,7 @@ function move_header_elements() { function () { $.ajax({ method: "post", - url: window.location.pathname + "/../../ajax/editlistusers/" + field, + url: getPath() + "/ajax/editlistusers/" + field, data: {"pk": result, "value": values, "action": val}, success: function (data) { handleListServerResponse(data); @@ -774,7 +1149,6 @@ function move_header_elements() { }); } } - $("#user_delete_selection").click(function () { $("#user-table").bootstrapTable("uncheckAll"); }); @@ -805,8 +1179,10 @@ function move_header_elements() { function () { $.ajax({ method: "post", - url: window.location.pathname + "/../../ajax/deleteuser", - data: {"userid": result}, + url: getPath() + "/ajax/deleteuser", + contentType: "application/json; charset=utf-8", + dataType: "json", + data: JSON.stringify({"userid": result}), success: function (data) { selections = selections.filter((el) => !result.includes(el)); handleListServerResponse(data); @@ -832,7 +1208,7 @@ function handleListServerResponse (data) { ''); }); } - $("#user-table").bootstrapTable("refresh"); + $(".table.table-striped").bootstrapTable("refresh"); } function checkboxChange(checkbox, userId, field, field_index) { @@ -847,30 +1223,30 @@ function checkboxChange(checkbox, userId, field, field_index) { }); } -function BookCheckboxChange(checkbox, userId, field) { +function BookCheckboxChange(checkbox, bookId, field) { var value = checkbox.checked ? "True" : "False"; var element = checkbox; $.ajax({ method: "post", url: getPath() + "/ajax/editbooks/" + field, - data: {"pk": userId, "value": value}, + data: JSON.stringify({"pk": [bookId], "value": value}), + contentType: "application/json; charset=utf-8", + dataType: "json", error: function(data) { element.checked = !element.checked; handleListServerResponse([{type:"danger", message:data.responseText}]) }, success: handleListServerResponse }); - console.log("test"); } - function selectHeader(element, field) { if (element.value !== "None") { confirmDialog(element.id, "GeneralChangeModal", 0, function () { var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id); $.ajax({ method: "post", - url: window.location.pathname + "/../../ajax/editlistusers/" + field, + url: getPath() + "/ajax/editlistusers/" + field, data: {"pk": result, "value": element.value}, error: function (data) { handleListServerResponse([{type:"danger", message:data.responseText}]) @@ -883,12 +1259,35 @@ function selectHeader(element, field) { } } +function bookCheckboxHeader(CheckboxState, text, field_index) { + confirmDialog(text, "GeneralChangeModal", 0, function() { + var result = $('#books-table').bootstrapTable('getSelections').map(a => a.id); + $.ajax({ + method: "post", + url: getPath() + "/ajax/editbooks/" + field_index, + data: JSON.stringify({"pk": result, "field_index": field_index, "value": CheckboxState, multi: "True"}), + contentType: "application/json; charset=utf-8", + dataType: "json", + error: function (data) { + handleListServerResponse([{type:"danger", message:data.responseText}]) + }, + success: function (data) { + handleListServerResponse (data, true) + }, + }); + },function() { + $("input:radio.check_head:checked").each(function() { + $(this).prop('checked', false); + }); + }); +} + function checkboxHeader(CheckboxState, field, field_index) { confirmDialog(field, "GeneralChangeModal", 0, function() { var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id); $.ajax({ method: "post", - url: window.location.pathname + "/../../ajax/editlistusers/" + field, + url: getPath() + "/ajax/editlistusers/" + field, data: {"pk": result, "field_index": field_index, "value": CheckboxState}, error: function (data) { handleListServerResponse([{type:"danger", message:data.responseText}]) @@ -904,7 +1303,7 @@ function checkboxHeader(CheckboxState, field, field_index) { }); } -function deleteUser(a,id){ +function deleteUser(a, id){ confirmDialog( "btndeluser", "GeneralDeleteModal", @@ -912,8 +1311,10 @@ function deleteUser(a,id){ function() { $.ajax({ method:"post", - url: window.location.pathname + "/../../ajax/deleteuser", - data: {"userid":id}, + url: getPath() + "/ajax/deleteuser", + contentType: "application/json; charset=utf-8", + dataType: "json", + data: JSON.stringify({"userid": [id]}), success: function (data) { userId = parseInt(id, 10); selections = selections.filter(item => item !== userId); @@ -940,8 +1341,10 @@ function storeLocation() { function user_handle (userId) { $.ajax({ method:"post", - url: window.location.pathname + "/../../ajax/deleteuser", - data: {"userid":userId} + contentType: "application/json; charset=utf-8", + dataType: "json", + data: JSON.stringify({"userid": [userId]}), + url: getPath() + "/ajax/deleteuser", }); $("#user-table").bootstrapTable("refresh"); } @@ -949,6 +1352,5 @@ function user_handle (userId) { function shorten_html(value, response) { if(value) { $(this).html("[...]"); - // value.split('\n').slice(0, 2).join("") + } } diff --git a/cps/templates/book_table.html b/cps/templates/book_table.html index 1a0806b08..807e9e497 100644 --- a/cps/templates/book_table.html +++ b/cps/templates/book_table.html @@ -19,6 +19,25 @@ {% if sort %}data-sortable="true" {% endif %} data-visible="{{visiblility.get(parameter)}}" data-formatter="bookCheckboxFormatter"> + {% if parameter == "is_archived" %} +
+
+ {{_('Archive selected books')}} +
+
+ {{_('Unarchive selected books')}} +
+
+ {% elif parameter == "read_status" %} +
+
+ {{_('Mark selected books as read')}} +
+
+ {{_('Mark selected books as unread')}}
+
+ + {% endif %} {{show_text}} {%- endmacro %} @@ -34,8 +53,15 @@
-
{{_('Merge selected books')}}
-
{{_('Remove Selections')}}
+
+ {{_('Merge selected books')}} +
+
+ {{_('Clear selections')}} +
+
+ {{_('Edit selected books')}} +
{{_('Exchange author and title')}}
@@ -57,7 +83,7 @@ {% if current_user.role_edit() %} - + {% endif %} {{ text_table_row('title', _('Enter Title'),_('Title'), true, true) }} @@ -97,14 +123,23 @@ {% endif %} {% endfor %} {% if current_user.role_delete_books() and current_user.role_edit()%} - {{_('Delete')}} + +
+ {{_('Delete selected books')}} +
+
+ {{_('Delete')}} + {% endif %} + {% endblock %} {% block modal %} {{ delete_book(current_user.role_delete_books()) }} +{{ delete_confirm_modal() }} +{{ change_confirm_modal() }} {% if current_user.role_edit() %}
-{% endif %} + + + + + + + + + + + + +{% endif %} {% endblock %} + {% block js %} diff --git a/cps/templates/user_table.html b/cps/templates/user_table.html index 3f998f951..dc2be0cfe 100644 --- a/cps/templates/user_table.html +++ b/cps/templates/user_table.html @@ -53,7 +53,7 @@ data-visible="{{element.get(array_field)}}" data-column="{{value.get(array_field)}}" data-formatter="checkboxFormatter"> -
+
{{_('Deny')}}
@@ -121,7 +121,7 @@
-
{{_('Remove Selections')}}
+
{{_('Clear selections')}}
=2.73.00,<2.200.0 gevent>20.6.0,<24.12.0 -greenlet>=0.4.17,<3.2.0 +greenlet>=0.4.17,<3.3.0 httplib2>=0.9.2,<0.23.0 oauth2client>=4.0.0,<4.1.4 -uritemplate>=3.0.0,<4.2.0 +uritemplate>=3.0.0,<4.3.0 pyasn1-modules>=0.0.8,<0.7.0 pyasn1>=0.1.9,<0.7.0 PyDrive2>=1.15.0,<1.22.0 diff --git a/pyproject.toml b/pyproject.toml index 146ae4fdb..61358d62e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,10 +71,10 @@ content-type = "text/markdown" gdrive = [ "google-api-python-client>=1.7.11,<2.200.0", "gevent>20.6.0,<24.12.0", - "greenlet>=0.4.17,<3.2.0", + "greenlet>=0.4.17,<3.3.0", "httplib2>=0.9.2,<0.23.0", "oauth2client>=4.0.0,<4.1.4", - "uritemplate>=3.0.0,<4.2.0", + "uritemplate>=3.0.0,<4.3.0", "pyasn1-modules>=0.0.8,<0.7.0", "pyasn1>=0.1.9,<0.7.0", "PyDrive2>=1.3.1,<1.22.0", diff --git a/test/Calibre-Web TestSummary_Linux.html b/test/Calibre-Web TestSummary_Linux.html index fee912ee9..8de336e2a 100644 --- a/test/Calibre-Web TestSummary_Linux.html +++ b/test/Calibre-Web TestSummary_Linux.html @@ -37,20 +37,20 @@
-

Start Time: 2025-08-05 20:14:09

+

Start Time: 2025-08-03 18:23:44

-

Stop Time: 2025-08-06 03:42:45

+

Stop Time: 2025-08-04 01:42:17

-

Duration: 6h 15 min

+

Duration: 6h 6 min

@@ -462,11 +462,11 @@ - + - - + + + - + @@ -1023,12 +1043,12 @@ - + - - - + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -2208,11 +1864,11 @@ AttributeError: 'bool' object has no attribute 'click' - + - - + + + - + @@ -3933,51 +3569,90 @@ IndexError: list index out of range - - - - + + + + + - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3995,13 +3670,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4010,7 +3685,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4028,13 +3703,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4043,7 +3718,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4052,7 +3727,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4061,7 +3736,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4070,7 +3745,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4079,7 +3754,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4088,7 +3763,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4097,7 +3772,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4106,7 +3781,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4115,7 +3790,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4124,7 +3799,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4133,7 +3808,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4142,7 +3817,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4151,7 +3826,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4160,7 +3835,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4169,7 +3844,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4178,7 +3853,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4187,7 +3862,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4196,7 +3871,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4205,7 +3880,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4214,7 +3889,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4223,7 +3898,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4232,7 +3907,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4241,7 +3916,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4250,7 +3925,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4259,7 +3934,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4277,13 +3952,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4301,13 +3976,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4316,7 +3991,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4325,7 +4000,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4343,13 +4018,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4358,7 +4033,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4367,7 +4042,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4376,7 +4051,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4385,7 +4060,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4394,7 +4069,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4403,7 +4078,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4412,7 +4087,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4421,7 +4096,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4439,13 +4114,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4463,13 +4138,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4478,7 +4153,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4487,7 +4162,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4496,7 +4171,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4505,7 +4180,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4514,7 +4189,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4523,7 +4198,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4532,7 +4207,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4550,13 +4225,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4565,7 +4240,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4574,7 +4249,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4583,7 +4258,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4601,13 +4276,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4616,7 +4291,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4625,7 +4300,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4634,7 +4309,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4643,7 +4318,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4652,7 +4327,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4661,7 +4336,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4670,7 +4345,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4679,7 +4354,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4688,7 +4363,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4697,7 +4372,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4706,7 +4381,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4715,7 +4390,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4724,19 +4399,19 @@ AttributeError: 'bool' object has no attribute 'click' - + + @@ -4759,7 +4434,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4768,7 +4443,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4786,13 +4461,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4810,13 +4485,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4825,7 +4500,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4834,7 +4509,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4843,7 +4518,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4852,7 +4527,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4861,7 +4536,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4870,7 +4545,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4879,7 +4554,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4897,13 +4572,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4921,13 +4596,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4945,13 +4620,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4960,7 +4635,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4969,7 +4644,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4978,7 +4653,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4987,7 +4662,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -4996,7 +4671,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5005,7 +4680,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5014,7 +4689,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5032,13 +4707,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5047,7 +4722,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5056,7 +4731,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5065,7 +4740,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5074,7 +4749,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5083,7 +4758,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5092,19 +4767,19 @@ AttributeError: 'bool' object has no attribute 'click' - + + @@ -5127,7 +4802,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5145,13 +4820,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5160,7 +4835,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5169,7 +4844,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5178,7 +4853,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5187,7 +4862,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5196,7 +4871,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5205,7 +4880,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5214,7 +4889,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5223,7 +4898,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5232,7 +4907,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5241,7 +4916,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5250,7 +4925,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5268,13 +4943,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5283,7 +4958,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5292,7 +4967,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5301,7 +4976,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5310,7 +4985,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5319,7 +4994,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5337,13 +5012,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5352,7 +5027,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5361,7 +5036,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5370,7 +5045,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5379,7 +5054,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5388,7 +5063,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5397,7 +5072,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5406,7 +5081,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5415,7 +5090,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5424,7 +5099,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5433,7 +5108,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5442,7 +5117,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5451,7 +5126,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5460,7 +5135,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5469,7 +5144,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5478,7 +5153,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5487,7 +5162,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5496,7 +5171,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5514,13 +5189,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5538,13 +5213,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5553,7 +5228,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5562,7 +5237,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5571,7 +5246,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5580,7 +5255,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5589,7 +5264,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5598,7 +5273,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5607,7 +5282,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5616,7 +5291,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5625,7 +5300,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5634,7 +5309,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5643,7 +5318,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5652,7 +5327,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5661,7 +5336,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5670,7 +5345,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5679,7 +5354,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5688,7 +5363,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5697,7 +5372,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5706,7 +5381,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5715,7 +5390,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5724,7 +5399,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5742,13 +5417,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5757,7 +5432,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5766,7 +5441,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5775,7 +5450,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5784,7 +5459,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5793,7 +5468,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5802,7 +5477,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5811,7 +5486,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5820,7 +5495,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5829,7 +5504,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5838,7 +5513,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5847,7 +5522,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5856,7 +5531,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5865,7 +5540,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5874,7 +5549,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5883,7 +5558,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5892,7 +5567,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5901,7 +5576,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5910,7 +5585,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5919,7 +5594,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5928,7 +5603,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5937,7 +5612,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5946,7 +5621,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5955,7 +5630,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5964,7 +5639,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5973,7 +5648,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5982,7 +5657,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -5991,7 +5666,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6000,7 +5675,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6009,7 +5684,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6018,7 +5693,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6027,7 +5702,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6036,7 +5711,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6045,7 +5720,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6054,7 +5729,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6072,13 +5747,13 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6087,7 +5762,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6096,7 +5771,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6105,7 +5780,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6114,7 +5789,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6123,7 +5798,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6132,7 +5807,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6141,7 +5816,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6150,7 +5825,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6159,7 +5834,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6168,7 +5843,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6177,7 +5852,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6186,7 +5861,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6195,7 +5870,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6204,7 +5879,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6213,7 +5888,7 @@ AttributeError: 'bool' object has no attribute 'click' - + @@ -6224,10 +5899,10 @@ AttributeError: 'bool' object has no attribute 'click' - - - - + + + + @@ -6772,7 +6447,7 @@ AttributeError: 'bool' object has no attribute 'click'
TestCli 13130121 0 0 @@ -530,11 +530,31 @@ -
TestCli - test_dryrun_update
PASS +
+ FAIL +
+ + + +
TestEditAdditionalBooks 1831141800 0 Detail @@ -1064,501 +1084,137 @@ -
TestEditAdditionalBooks - test_delete_role
-
- FAIL -
- - - -
PASS
TestEditAdditionalBooks - test_details_popup
-
- ERROR -
- - - -
PASS
TestEditAdditionalBooks - test_edit_book_identifier
-
- ERROR -
- - - -
PASS
TestEditAdditionalBooks - test_edit_book_identifier_capital
-
- ERROR -
- - - -
PASS
TestEditAdditionalBooks - test_edit_book_identifier_standard
-
- ERROR -
- - - -
PASS
TestEditAdditionalBooks - test_edit_special_book_identifier
-
- ERROR -
- - - -
PASS
TestEditAdditionalBooks - test_title_sort
-
- ERROR -
- - - -
PASS
TestEditAdditionalBooks - test_upload_cbz_coverformats
-
- ERROR -
- - - -
PASS
TestEditAdditionalBooks - test_upload_edit_role
-
- ERROR -
- - - -
PASS
TestEditAdditionalBooks - test_upload_metadata_cb7
-
- ERROR -
- - - -
PASS
TestEditAdditionalBooks - test_upload_metadata_cbr
-
- ERROR -
- - - -
PASS
TestEditAdditionalBooks - test_upload_metadata_cbt
-
- ERROR -
- - - -
PASS
TestEditAdditionalBooks - test_xss_author_edit
-
- ERROR -
- - - -
PASS
TestEditAdditionalBooks - test_xss_comment_edit
-
- ERROR -
- - - -
PASS
TestEditAdditionalBooks - test_xss_custom_comment_edit
-
- ERROR -
- - - -
PASS
TestEditBooksList 19181190 0 0 @@ -2222,31 +1878,11 @@ AttributeError: 'bool' object has no attribute 'click' -
TestEditBooksList - test_booklist_xss
-
- FAIL -
- - - -
PASS
TestMergeBooksList21
TestMassEditBooksList550 01 0 - Detail + Detail
-
TestMergeBooksList - test_book_merge
-
-
- ERROR -
- - - +
TestMassEditBooksList - test_author_title_combi
PASS
+
TestMassEditBooksList - test_invalid_author_title
+
PASS
+
TestMassEditBooksList - test_protected_author_title
+
PASS
+
TestMassEditBooksList - test_wrong_parameter_multi
+
PASS
+
TestMassEditBooksList - test_wrong_parameter_single
+
PASS
TestMergeBooksList22000 + Detail +
+
TestMergeBooksList - test_book_merge
+
PASS
TestMergeBooksList - test_delete_book
0 0 - Detail + Detail
TestOAuthLogin - test_oauth_about
TestOAuthLogin - test_visible_oauth
0 0 - Detail + Detail
TestOPDSFeed - test_access_right_guest
TestOPDSFeed - test_access_right_user
TestOPDSFeed - test_opds
TestOPDSFeed - test_opds_author
TestOPDSFeed - test_opds_books
TestOPDSFeed - test_opds_calibre_companion
TestOPDSFeed - test_opds_colon_password
TestOPDSFeed - test_opds_cover
TestOPDSFeed - test_opds_download_book
TestOPDSFeed - test_opds_formats
TestOPDSFeed - test_opds_guest_user
TestOPDSFeed - test_opds_hot
TestOPDSFeed - test_opds_language
TestOPDSFeed - test_opds_non_admin
TestOPDSFeed - test_opds_publisher
TestOPDSFeed - test_opds_random
TestOPDSFeed - test_opds_ratings
TestOPDSFeed - test_opds_read_unread
TestOPDSFeed - test_opds_search
TestOPDSFeed - test_opds_series
TestOPDSFeed - test_opds_shelf_access
TestOPDSFeed - test_opds_stats
TestOPDSFeed - test_opds_tags
TestOPDSFeed - test_opds_top_rated
TestOPDSFeed - test_opds_unicode_user
TestOPDSFeed - test_recently_added
0 0 - Detail + Detail
TestUploadPDF - test_upload_invalid_pdf
0 0 - Detail + Detail
TestPipInstall - test_command_start
TestPipInstall - test_foldername_database_location
TestPipInstall - test_module_start
0 1 - Detail + Detail
TestReader - test_cb7_reader
TestReader - test_comic_MACOS_files
TestReader - test_comic_reader
TestReader - test_epub_reader
TestReader - test_kepub_reader
TestReader - test_pdf_reader
TestReader - test_single_file_comic
TestReader - test_sound_listener
TestReader - test_txt_reader
0 0 - Detail + Detail
TestReadOnlyDatabase - test_readonly_path
0 0 - Detail + Detail
TestRegister - test_forgot_password
TestRegister - test_illegal_email
TestRegister - test_limit_domain
TestRegister - test_register_no_server
TestRegister - test_registering_only_email
TestRegister - test_registering_user
TestRegister - test_registering_user_fail
TestRegister - test_user_change_password
0 0 - Detail + Detail
TestReverseProxy - test_logout
TestReverseProxy - test_move_page
TestReverseProxy - test_next
TestReverseProxy - test_reverse_about
0 1 - Detail + Detail
TestShelf - test_access_shelf
TestShelf - test_add_shelf_from_search
TestShelf - test_adv_search_shelf
TestShelf - test_arrange_shelf
TestShelf - test_create_public_shelf
TestShelf - test_create_public_shelf_no_permission
TestShelf - test_delete_book_of_shelf
TestShelf - test_private_shelf
TestShelf - test_public_private_shelf
TestShelf - test_public_shelf
TestShelf - test_rename_shelf
TestShelf - test_shelf_action_non_shelf_edit_role
TestShelf - test_shelf_anonymous
TestShelf - test_shelf_database_change
- SKIP + SKIP
-
TestShelf - test_shelf_long_name
TestShelf - test_shelf_order
TestShelf - test_xss_shelf
0 0 - Detail + Detail
TestSocket - test_socket_communication
0 0 - Detail + Detail
TestSplitLibrary - test_change_ebook
TestSplitLibrary - test_convert_ebook
TestSplitLibrary - test_download_book
TestSplitLibrary - test_email_ebook
TestSplitLibrary - test_kobo
TestSplitLibrary - test_thumbnails
TestSplitLibrary - test_upload_ebook
TestSplitLibrary - test_wrong_config_lib
0 0 - Detail + Detail
TestSystemdActivation - test_systemd_activation
0 0 - Detail + Detail
TestThumbnailsEnv - test_cover_cache_env_on_database_change
0 1 - Detail + Detail
TestThumbnails - test_cache_non_writable
TestThumbnails - test_cache_of_deleted_book
TestThumbnails - test_cover_cache_on_database_change
TestThumbnails - test_cover_change_on_upload_new_cover
TestThumbnails - test_cover_for_series
TestThumbnails - test_cover_on_upload_book
TestThumbnails - test_remove_cover_from_cache
TestThumbnails - test_sideloaded_book
0 1 - Detail + Detail
TestUpdater - test_check_update_nightly_errors
TestUpdater - test_check_update_nightly_request_errors
TestUpdater - test_check_update_stable_errors
TestUpdater - test_check_update_stable_versions
TestUpdater - test_perform_update
TestUpdater - test_perform_update_stable_errors
TestUpdater - test_perform_update_timeout
- SKIP + SKIP
-
TestUpdater - test_reconnect_database
TestUpdater - test_update_write_protect
0 0 - Detail + Detail
TestUploadAudio - test_upload_aac
TestUploadAudio - test_upload_aiff
TestUploadAudio - test_upload_asf
TestUploadAudio - test_upload_flac
TestUploadAudio - test_upload_m4a
TestUploadAudio - test_upload_m4b
TestUploadAudio - test_upload_mp3
TestUploadAudio - test_upload_mp4
TestUploadAudio - test_upload_oggvorbis
TestUploadAudio - test_upload_ogv
TestUploadAudio - test_upload_opus
TestUploadAudio - test_upload_wav
0 0 - Detail + Detail
TestUploadEPubs - test_upload_epub_comments
TestUploadEPubs - test_upload_epub_cover
TestUploadEPubs - test_upload_epub_cover_formats
TestUploadEPubs - test_upload_epub_duplicate
TestUploadEPubs - test_upload_epub_identifier
TestUploadEPubs - test_upload_epub_lang
0 0 - Detail + Detail
TestUserList - test_edit_user_email
TestUserList - test_list_visibility
TestUserList - test_user_list_admin_role
TestUserList - test_user_list_check_sort
TestUserList - test_user_list_denied_tags
TestUserList - test_user_list_download_role
TestUserList - test_user_list_edit_button
TestUserList - test_user_list_edit_email
TestUserList - test_user_list_edit_kindle
TestUserList - test_user_list_edit_language
TestUserList - test_user_list_edit_locale
TestUserList - test_user_list_edit_name
TestUserList - test_user_list_edit_visiblility
TestUserList - test_user_list_guest_edit
TestUserList - test_user_list_remove_admin
TestUserList - test_user_list_requests
TestUserList - test_user_list_search
TestUserList - test_user_list_sort
0 0 - Detail + Detail
TestUserLoad - test_user_change_vis
0 0 - Detail + Detail
TestUserTemplate - test_allow_column_restriction
TestUserTemplate - test_allow_tag_restriction
TestUserTemplate - test_archived_format_template
TestUserTemplate - test_author_user_template
TestUserTemplate - test_best_user_template
TestUserTemplate - test_category_user_template
TestUserTemplate - test_deny_column_restriction
TestUserTemplate - test_deny_tag_restriction
TestUserTemplate - test_detail_random_user_template
TestUserTemplate - test_download_user_template
TestUserTemplate - test_format_user_template
TestUserTemplate - test_hot_user_template
TestUserTemplate - test_language_user_template
TestUserTemplate - test_limit_book_languages
TestUserTemplate - test_list_user_template
TestUserTemplate - test_publisher_user_template
TestUserTemplate - test_random_user_template
TestUserTemplate - test_read_user_template
TestUserTemplate - test_recent_user_template
TestUserTemplate - test_series_user_template
TestUserTemplate - test_ui_language_settings
0 0 - Detail + Detail
TestCalibreWebVisibilitys - test_about
TestCalibreWebVisibilitys - test_admin_SMTP_Settings
TestCalibreWebVisibilitys - test_admin_add_user
TestCalibreWebVisibilitys - test_admin_change_password
TestCalibreWebVisibilitys - test_admin_change_visibility_archived
TestCalibreWebVisibilitys - test_admin_change_visibility_authors
TestCalibreWebVisibilitys - test_admin_change_visibility_category
TestCalibreWebVisibilitys - test_admin_change_visibility_file_formats
TestCalibreWebVisibilitys - test_admin_change_visibility_hot
TestCalibreWebVisibilitys - test_admin_change_visibility_language
TestCalibreWebVisibilitys - test_admin_change_visibility_publisher
TestCalibreWebVisibilitys - test_admin_change_visibility_random
TestCalibreWebVisibilitys - test_admin_change_visibility_rated
TestCalibreWebVisibilitys - test_admin_change_visibility_rating
TestCalibreWebVisibilitys - test_admin_change_visibility_read
TestCalibreWebVisibilitys - test_admin_change_visibility_series
TestCalibreWebVisibilitys - test_allow_columns
TestCalibreWebVisibilitys - test_allow_tags
TestCalibreWebVisibilitys - test_archive_books
TestCalibreWebVisibilitys - test_authors_max_settings
TestCalibreWebVisibilitys - test_change_title
TestCalibreWebVisibilitys - test_checked_logged_in
TestCalibreWebVisibilitys - test_hide_custom_column
TestCalibreWebVisibilitys - test_link_column_to_read_status
TestCalibreWebVisibilitys - test_random_books_available
TestCalibreWebVisibilitys - test_read_status_visible
TestCalibreWebVisibilitys - test_request_link_column_to_read_status
TestCalibreWebVisibilitys - test_restrict_columns
TestCalibreWebVisibilitys - test_restrict_tags
TestCalibreWebVisibilitys - test_save_views_recent
TestCalibreWebVisibilitys - test_search_functions
TestCalibreWebVisibilitys - test_search_order
TestCalibreWebVisibilitys - test_search_string
TestCalibreWebVisibilitys - test_user_email_available
TestCalibreWebVisibilitys - test_user_visibility_sidebar
0 0 - Detail + Detail
TestCalibreHelper - test_author_sort
TestCalibreHelper - test_author_sort_comma
TestCalibreHelper - test_author_sort_junior
TestCalibreHelper - test_author_sort_oneword
TestCalibreHelper - test_author_sort_roman
TestCalibreHelper - test_check_Limit_Length
TestCalibreHelper - test_check_char_replacement
TestCalibreHelper - test_check_chinese_Characters
TestCalibreHelper - test_check_deg_eur_replacement
TestCalibreHelper - test_check_doubleS
TestCalibreHelper - test_check_finish_Dot
TestCalibreHelper - test_check_high23
TestCalibreHelper - test_check_umlauts
TestCalibreHelper - test_random_password
TestCalibreHelper - test_split_authors
TestCalibreHelper - test_whitespaces
Total52550021653052111 7