1
0
mirror of https://github.com/janeczku/calibre-web synced 2025-01-17 20:52:57 +00:00

Added logging of ip address (#3237)

Refactored Response(json.dumps -> make_response(jsonify..)
Update mimetypes - Allow different mimetypes for download and file upload check (#3245, #3243)
Bugfixes from tests
Updated optional-requirements
This commit is contained in:
Ozzie Isaacs 2024-12-02 15:31:46 +01:00
parent 14e57e3714
commit 42924d9508
11 changed files with 154 additions and 127 deletions

View File

@ -55,13 +55,13 @@ mimetypes.init()
mimetypes.add_type('application/xhtml+xml', '.xhtml')
mimetypes.add_type('application/epub+zip', '.epub')
mimetypes.add_type('application/epub+zip', '.kepub')
mimetypes.add_type('text/xml', '.fb2')
mimetypes.add_type('application/fb2+zip', '.fb2')
mimetypes.add_type('application/octet-stream', '.mobi')
mimetypes.add_type('application/octet-stream', '.prc')
mimetypes.add_type('application/vnd.amazon.ebook', '.azw')
mimetypes.add_type('application/x-mobi8-ebook', '.azw3')
mimetypes.add_type('application/x-rar', '.cbr')
mimetypes.add_type('application/zip', '.cbz')
mimetypes.add_type('application/x-cbr', '.cbr')
mimetypes.add_type('application/x-cbz', '.cbz')
mimetypes.add_type('application/x-tar', '.cbt')
mimetypes.add_type('application/x-7z-compressed', '.cb7')
mimetypes.add_type('image/vnd.djv', '.djv')

View File

@ -32,7 +32,8 @@ from datetime import time as datetime_time
from functools import wraps
from urllib.parse import urlparse
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, \
send_from_directory, g, jsonify
from markupsafe import Markup
from .cw_login import current_user
from flask_babel import gettext as _
@ -378,10 +379,7 @@ def list_users():
user.default = get_user_locale_language(user.default_language)
table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": users}
js_list = json.dumps(table_entries, cls=db.AlchemyEncoder)
response = make_response(js_list)
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
return make_response(json.dumps(table_entries, cls=db.AlchemyEncoder))
@admi.route("/ajax/deleteuser", methods=['POST'])
@ -400,7 +398,7 @@ def delete_user():
success = list()
if not users:
log.error("User not found")
return Response(json.dumps({'type': "danger", 'message': _("User not found")}), mimetype='application/json')
return make_response(jsonify(type="danger", message=_("User not found")))
for user in users:
try:
message = _delete_user(user)
@ -416,7 +414,7 @@ def delete_user():
log.info("Users {} deleted".format(user_ids))
success = [{'type': "success", 'message': _("{} users deleted successfully").format(count)}]
success.extend(errors)
return Response(json.dumps(success), mimetype='application/json')
return make_response(jsonify(success))
@admi.route("/ajax/getlocale")
@ -498,10 +496,10 @@ 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 make_response(
jsonify([{'type': "danger",
'message': _("No admin user remaining, can't remove admin role",
nick=user.name)}]), mimetype='application/json')
nick=user.name)}]))
user.role &= ~value
else:
raise Exception(_("Value has to be true or false"))
@ -947,7 +945,7 @@ def do_full_kobo_sync(userid):
count = ub.session.query(ub.KoboSyncedBooks).filter(userid == ub.KoboSyncedBooks.user_id).delete()
message = _("{} sync entries deleted").format(count)
ub.session_commit(message)
return Response(json.dumps([{"type": "success", "message": message}]), mimetype='application/json')
return make_response(jsonify(type="success", message=message))
def check_valid_read_column(column):
@ -1264,7 +1262,7 @@ def _configuration_ldap_helper(to_save):
@admin_required
def simulatedbchange():
db_change, db_valid = _db_simulate_change()
return Response(json.dumps({"change": db_change, "valid": db_valid}), mimetype='application/json')
return make_response(jsonify(change=db_change, valid=db_valid))
@admi.route("/admin/user/new", methods=["GET", "POST"])
@ -1896,7 +1894,7 @@ def _configuration_result(error_flash=None, reboot=False):
resp['result'] = [{'type': "success", 'message': _("Calibre-Web configuration updated")}]
resp['reboot'] = reboot
resp['config_upload'] = config.config_upload_formats
return Response(json.dumps(resp), mimetype='application/json')
return make_response(jsonify(resp))
def _db_configuration_result(error_flash=None, gdrive_error=None):

View File

@ -28,11 +28,11 @@ from shutil import copyfile
from markupsafe import escape, Markup # dependency of flask
from functools import wraps
from flask import Blueprint, request, flash, redirect, url_for, abort, Response
from flask import Blueprint, request, flash, redirect, url_for, abort, jsonify, make_response, Response
from flask_babel import gettext as _
from flask_babel import lazy_gettext as N_
from flask_babel import get_locale
from .cw_login import current_user, login_required
from .cw_login import current_user
from sqlalchemy.exc import OperationalError, IntegrityError, InterfaceError
from sqlalchemy.orm.exc import StaleDataError
from sqlalchemy.sql.expression import func
@ -76,7 +76,7 @@ def edit_required(f):
@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
@user_login_required
def delete_book_from_details(book_id):
return Response(delete_book_from_table(book_id, "", True), mimetype='application/json')
return delete_book_from_table(book_id, "", True) # , mimetype='application/json')
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
@ -158,16 +158,16 @@ def upload():
if len(request.files.getlist("btn-upload")) < 2:
if current_user.role_edit() or current_user.role_admin():
resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)}
return Response(json.dumps(resp), mimetype='application/json')
return make_response(jsonify(resp))
else:
resp = {"location": url_for('web.show_book', book_id=book_id)}
return Response(json.dumps(resp), mimetype='application/json')
return make_response(jsonify(resp))
except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e))
flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
return make_response(jsonify(location=url_for("web.index")))
abort(404)
@ -206,7 +206,7 @@ def table_get_custom_enum(c_id):
ret.append({'value': "", 'text': ""})
for idx, en in enumerate(cc.get_display_dict()['enum_values']):
ret.append({'value': en, 'text': en})
return json.dumps(ret)
return make_response(jsonify(ret))
@editbook.route("/ajax/editbooks/<param>", methods=['POST'])
@ -221,68 +221,54 @@ def edit_list_book(param):
try:
if param == 'series_index':
edit_book_series_index(vals['value'], book)
ret = Response(json.dumps({'success': True, 'newValue': book.series_index}), mimetype='application/json')
ret = make_response(jsonify(success=True, newValue=book.series_index))
elif param == 'tags':
edit_book_tags(vals['value'], book)
ret = Response(json.dumps({'success': True, 'newValue': ', '.join([tag.name for tag in book.tags])}),
mimetype='application/json')
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 = Response(json.dumps({'success': True, 'newValue': ', '.join([serie.name for serie in book.series])}),
mimetype='application/json')
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 = Response(json.dumps({'success': True,
'newValue': ', '.join([publisher.name for publisher in book.publishers])}),
mimetype='application/json')
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 = Response(json.dumps({'success': False,
'msg': 'Invalid languages in request: {}'.format(','.join(invalid))}),
mimetype='application/json')
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 = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}),
mimetype='application/json')
ret = make_response(jsonify(success=True, newValue=', '.join(lang_names)))
elif param == 'author_sort':
book.author_sort = vals['value']
ret = Response(json.dumps({'success': True, 'newValue': book.author_sort}),
mimetype='application/json')
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 = Response(json.dumps({'success': True, 'newValue': book.title}),
mimetype='application/json')
ret = make_response(jsonify(success=True, newValue=book.title))
else:
ret = Response(json.dumps({'success': False,
'msg': rename_error}),
mimetype='application/json')
ret = make_response(jsonify(success=False, msg=rename_error))
elif param == 'sort':
book.sort = vals['value']
ret = Response(json.dumps({'success': True, 'newValue': book.sort}),
mimetype='application/json')
ret = make_response(jsonify(success=True,newValue=book.sort))
elif param == 'comments':
edit_book_comments(vals['value'], book)
ret = Response(json.dumps({'success': True, 'newValue': book.comments[0].text}),
mimetype='application/json')
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 = Response(json.dumps({
'success': True,
'newValue': ' & '.join([author.replace('|', ',') for author in input_authors])}),
mimetype='application/json')
ret = make_response(jsonify(
success=True,
newValue=' & '.join([author.replace('|', ',') for author in input_authors])))
else:
ret = Response(json.dumps({'success': False,
'msg': rename_error}),
mimetype='application/json')
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']))
@ -301,8 +287,7 @@ def edit_list_book(param):
if vals['value'] in ["True", "False"]:
ret = ""
else:
ret = Response(json.dumps({'success': True, 'newValue': vals['value']}),
mimetype='application/json')
ret = make_response(jsonify(success=True, newValue=vals['value']))
else:
return _("Parameter not found"), 400
book.last_modified = datetime.now(timezone.utc)
@ -315,9 +300,8 @@ def edit_list_book(param):
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')
ret = make_response(jsonify(success=False,
msg='Database error: {}'.format(e.orig if hasattr(e, "orig") else e)))
return ret
@ -328,13 +312,13 @@ def get_sorted_entry(field, bookid):
book = calibre_db.get_filtered_book(bookid)
if book:
if field == 'title':
return json.dumps({'sort': book.sort})
return make_response(jsonify(sort=book.sort))
elif field == 'authors':
return json.dumps({'author_sort': book.author_sort})
return make_response(jsonify(author_sort=book.author_sort))
if field == 'sort':
return json.dumps({'sort': book.title})
return make_response(jsonify(sort=book.title))
if field == 'author_sort':
return json.dumps({'authors': " & ".join([a.name for a in calibre_db.order_authors([book])])})
return make_response(jsonify(authors=" & ".join([a.name for a in calibre_db.order_authors([book])])))
return ""
@ -350,7 +334,7 @@ def simulate_merge_list_book():
from_book = []
for book_id in vals:
from_book.append(calibre_db.get_book(book_id).title)
return json.dumps({'to': to_book, 'from': from_book})
return make_response(jsonify({'to': to_book, 'from': from_book}))
return ""
@ -388,7 +372,7 @@ def merge_list_book():
element.uncompressed_size,
to_name))
delete_book_from_table(from_book.id, "", True)
return json.dumps({'success': True})
return make_response(jsonify(success=True))
return ""
@ -428,11 +412,11 @@ def table_xchange_author_title():
except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e))
return json.dumps({'success': False})
return make_response(jsonify(success=False))
if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal()
return json.dumps({'success': True})
return make_response(jsonify(success=True))
return ""
@ -560,7 +544,7 @@ def do_edit_book(book_id, upload_formats=None):
if upload_formats:
resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)}
return Response(json.dumps(resp), mimetype='application/json')
return make_response(jsonify(resp))
if "detail_view" in to_save:
return redirect(url_for('web.show_book', book_id=book.id))
@ -769,17 +753,17 @@ def file_handling_on_upload(requested_file):
if config.config_check_extensions and allowed_extensions != ['']:
if not validate_mime_type(requested_file, allowed_extensions):
flash(_("File type isn't allowed to be uploaded to this server"), category="error")
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
return None, make_response(jsonify(location=url_for("web.index")))
if '.' in requested_file.filename:
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
if file_ext not in allowed_extensions and '' not in allowed_extensions:
flash(
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
ext=file_ext), category="error")
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
return None, make_response(jsonify(location=url_for("web.index")))
else:
flash(_('File to be uploaded must have an extension'), category="error")
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
return None, make_response(jsonify(location=url_for("web.index")))
# extract metadata from file
try:
@ -788,7 +772,7 @@ def file_handling_on_upload(requested_file):
log.error("File %s could not saved to temp dir", requested_file.filename)
flash(_("File %(filename)s could not saved to temp dir",
filename=requested_file.filename), category="error")
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
return None, make_response(jsonify(location=url_for("web.index")))
return meta, None
@ -860,7 +844,7 @@ def delete_whole_book(book_id, book):
def render_delete_book_result(book_format, json_response, warning, book_id, location=""):
if book_format:
if json_response:
return json.dumps([warning, {"location": url_for("edit-book.show_edit_book", book_id=book_id),
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')}])
@ -869,7 +853,7 @@ def render_delete_book_result(book_format, json_response, warning, book_id, loca
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
else:
if json_response:
return json.dumps([warning, {"location": get_redirect_location(location, "web.index"),
return jsonify([warning, {"location": get_redirect_location(location, "web.index"),
"type": "success",
"format": book_format,
"message": _('Book Successfully Deleted')}])
@ -887,7 +871,7 @@ def delete_book_from_table(book_id, book_format, json_response, location=""):
result, error = helper.delete_book(book, config.get_book_path(), book_format=book_format.upper())
if not result:
if json_response:
return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": error}])
@ -914,7 +898,7 @@ def delete_book_from_table(book_id, book_format, json_response, location=""):
log.error_or_exception(ex)
calibre_db.session.rollback()
if json_response:
return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": ex}])
@ -928,7 +912,7 @@ def delete_book_from_table(book_id, book_format, json_response, location=""):
return render_delete_book_result(book_format, json_response, warning, book_id, location)
message = _("You are missing permissions to delete books")
if json_response:
return json.dumps({"location": url_for("edit-book.show_edit_book", book_id=book_id),
return jsonify({"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": message})

View File

@ -34,6 +34,15 @@ except ImportError as e:
error = "Cannot import python-magic, checking uploaded file metadata will not work: {}".format(e)
def get_mimetype(ext):
# overwrite some mimetypes for proper file detection
mimes = {".fb2": "text/xml",
".cbz": "application/zip",
".cbr": "application/x-rar"
}
return mimes.get(ext, mimetypes.types_map[ext])
def get_temp_dir():
tmp_dir = os.path.join(gettempdir(), 'calibre_web')
if not os.path.isdir(tmp_dir):
@ -54,7 +63,7 @@ def validate_mime_type(file_buffer, allowed_extensions):
allowed_mimetypes = list()
for x in allowed_extensions:
try:
allowed_mimetypes.append(mimetypes.types_map["." + x])
allowed_mimetypes.append(get_mimetype("." + x))
except KeyError:
log.error("Unkown mimetype for Extension: {}".format(x))
tmp_mime_type = mime.from_buffer(file_buffer.read())

View File

@ -30,7 +30,7 @@ import requests
import unidecode
from uuid import uuid4
from flask import send_from_directory, make_response, abort, url_for, Response
from flask import send_from_directory, make_response, abort, url_for, Response, request
from flask_babel import gettext as _
from flask_babel import lazy_gettext as N_
from flask_babel import get_locale
@ -974,7 +974,8 @@ def do_download_file(book, book_format, client, data, headers):
# ToDo Check headers parameter
for element in headers:
response.headers[element[0]] = element[1]
log.info('Downloading file: {}'.format(os.path.join(filename, book_name + "." + book_format)))
log.info('Downloading file: \'%s\' by %s - %s', format(os.path.join(filename, book_name + "." + book_format)),
current_user.name, request.headers.get('X-Forwarded-For', request.remote_addr))
return response

View File

@ -21,10 +21,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
import json
# import json
from urllib.parse import unquote_plus
from flask import Blueprint, request, render_template, make_response, abort, Response, g
from flask import Blueprint, request, render_template, make_response, abort, g, jsonify
from flask_babel import get_locale
from flask_babel import gettext as _
@ -451,7 +451,7 @@ def get_database_stats():
stat['authors'] = calibre_db.session.query(db.Authors).count()
stat['categories'] = calibre_db.session.query(db.Tags).count()
stat['series'] = calibre_db.session.query(db.Series).count()
return Response(json.dumps(stat), mimetype="application/json")
return make_response(jsonify(stat))
@opds.route("/opds/thumb_240_240/<book_id>")

View File

@ -23,7 +23,7 @@ import json
import os
import sys
from flask import Blueprint, Response, request, url_for
from flask import Blueprint, request, url_for, make_response, jsonify
from .cw_login import current_user
from flask_babel import get_locale
from sqlalchemy.exc import InvalidRequestError, OperationalError
@ -89,7 +89,7 @@ def metadata_provider():
provider.append(
{"name": c.__name__, "active": ac, "initial": ac, "id": c.__id__}
)
return Response(json.dumps(provider), mimetype="application/json")
return make_response(jsonify(provider))
@meta.route("/metadata/provider", methods=["POST"])
@ -114,9 +114,7 @@ def metadata_change_active_provider(prov_name):
provider = next((c for c in cl if c.__id__ == prov_name), None)
if provider is not None:
data = provider.search(new_state.get("query", ""))
return Response(
json.dumps([asdict(x) for x in data]), mimetype="application/json"
)
return make_response(jsonify([asdict(x) for x in data]))
return ""
@ -138,4 +136,4 @@ def metadata_search():
}
for future in concurrent.futures.as_completed(meta):
data.extend([asdict(x) for x in future.result() if x])
return Response(json.dumps(data), mimetype="application/json")
return make_response(jsonify(data))

View File

@ -25,8 +25,7 @@ import chardet # dependency of requests
import copy
from importlib.metadata import metadata
from flask import Blueprint, jsonify
from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for, Response
from flask import Blueprint, jsonify, request, redirect, send_from_directory, make_response, flash, abort, url_for
from flask import session as flask_session
from flask_babel import gettext as _
from flask_babel import get_locale
@ -1198,13 +1197,14 @@ def serve_book(book_id, book_format, anyname):
if not data:
return "File not in Database"
range_header = request.headers.get('Range', None)
if not range_header:
log.info('Serving book: \'%s\' to %s - %s', data.name, current_user.name,
request.headers.get('X-Forwarded-For', request.remote_addr))
if config.config_use_google_drive:
try:
headers = Headers()
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
if not range_header:
log.info('Serving book: %s', data.name)
headers['Accept-Ranges'] = 'bytes'
df = getFileFromEbooksFolder(book.path, data.name + "." + book_format)
return do_gdrive_download(df, headers, (book_format.upper() == 'TXT'))
@ -1213,7 +1213,6 @@ def serve_book(book_id, book_format, anyname):
return "File Not Found"
else:
if book_format.upper() == 'TXT':
log.info('Serving book: %s', data.name)
try:
rawdata = open(os.path.join(config.get_book_path(), book.path, data.name + "." + book_format),
"rb").read()
@ -1234,7 +1233,6 @@ def serve_book(book_id, book_format, anyname):
response = make_response(
send_from_directory(os.path.join(config.get_book_path(), book.path), data.name + "." + book_format))
if not range_header:
log.info('Serving book: %s', data.name)
response.headers['Accept-Ranges'] = 'bytes'
return response
@ -1253,8 +1251,7 @@ def download_link(book_id, book_format, anyname):
@download_required
def send_to_ereader(book_id, book_format, convert):
if not config.get_mail_server_configured():
response = [{'type': "danger", 'message': _("Please configure the SMTP mail settings first...")}]
return Response(json.dumps(response), mimetype='application/json')
return make_response(jsonify(type="danger", message=_("Please configure the SMTP mail settings first...")))
elif current_user.kindle_mail:
result = send_mail(book_id, book_format, convert, current_user.kindle_mail, config.get_book_path(),
current_user.name)
@ -1266,7 +1263,7 @@ def send_to_ereader(book_id, book_format, convert):
response = [{'type': "danger", 'message': _("Oops! There was an error sending book: %(res)s", res=result)}]
else:
response = [{'type': "danger", 'message': _("Oops! Please update your profile with a valid eReader Email.")}]
return Response(json.dumps(response), mimetype='application/json')
return make_response(jsonify(response))
# ################################### Login Logout ##################################################################

View File

@ -1,13 +1,13 @@
# GDrive Integration
google-api-python-client>=1.7.11,<2.200.0
gevent>20.6.0,<24.3.0
greenlet>=0.4.17,<3.1.0
greenlet>=0.4.17,<3.2.0
httplib2>=0.9.2,<0.23.0
oauth2client>=4.0.0,<4.1.4
uritemplate>=3.0.0,<4.2.0
pyasn1-modules>=0.0.8,<0.5.0
pyasn1>=0.1.9,<0.7.0
PyDrive2>=1.3.1,<1.20.0
PyDrive2>=1.3.1,<1.22.0
PyYAML>=3.12,<6.1
rsa>=3.4.2,<4.10.0

View File

@ -37,20 +37,20 @@
<div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;">
<p class='text-justify attribute'><strong>Start Time: </strong>2024-11-29 20:17:45</p>
<p class='text-justify attribute'><strong>Start Time: </strong>2024-12-06 17:23:58</p>
</div>
</div>
<div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3">
<p class='text-justify attribute'><strong>Stop Time: </strong>2024-11-30 03:36:53</p>
<p class='text-justify attribute'><strong>Stop Time: </strong>2024-12-07 00:45:19</p>
</div>
</div>
<div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3">
<p class='text-justify attribute'><strong>Duration: </strong>6h 12 min</p>
<p class='text-justify attribute'><strong>Duration: </strong>6h 11 min</p>
</div>
</div>
</div>
@ -1023,13 +1023,13 @@
<tr id="su" class="skipClass">
<tr id="su" class="passClass">
<td>TestEditAdditionalBooks</td>
<td class="text-center">18</td>
<td class="text-center">17</td>
<td class="text-center">18</td>
<td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center">1</td>
<td class="text-center">
<a onclick="showClassDetail('c12', 18)">Detail</a>
</td>
@ -1172,11 +1172,11 @@
<tr id='st12.16' class='none bg-warning'>
<tr id='pt12.16' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestEditAdditionalBooks - test_xss_author_edit</div>
</td>
<td colspan='6' align='center'>SKIP</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -2074,11 +2074,11 @@ IndexError: list index out of range</pre>
<tr id="su" class="passClass">
<tr id="su" class="failClass">
<td>TestEditBooksOnGdrive</td>
<td class="text-center">18</td>
<td class="text-center">18</td>
<td class="text-center">0</td>
<td class="text-center">16</td>
<td class="text-center">2</td>
<td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center">
@ -2205,11 +2205,31 @@ IndexError: list index out of range</pre>
<tr id='pt19.14' class='hiddenRow bg-success'>
<tr id="ft19.14" class="none bg-danger">
<td>
<div class='testcase'>TestEditBooksOnGdrive - test_edit_rating</div>
</td>
<td colspan='6' align='center'>PASS</td>
<td colspan='6'>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft19.14')">FAIL</a>
</div>
<!--css div popup start-->
<div id="div_ft19.14" class="popup_window test_output" style="display:block;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
onclick="document.getElementById('div_ft19.14').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py&#34;, line 632, in test_edit_rating
self.assertEqual(4, values[&#39;rating&#39;])
AssertionError: 4 != 0</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr>
@ -2241,11 +2261,31 @@ IndexError: list index out of range</pre>
<tr id='pt19.18' class='hiddenRow bg-success'>
<tr id="ft19.18" class="none bg-danger">
<td>
<div class='testcase'>TestEditBooksOnGdrive - test_watch_metadata</div>
</td>
<td colspan='6' align='center'>PASS</td>
<td colspan='6'>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft19.18')">FAIL</a>
</div>
<!--css div popup start-->
<div id="div_ft19.18" class="popup_window test_output" style="display:block;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
onclick="document.getElementById('div_ft19.18').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py&#34;, line 976, in test_watch_metadata
self.assertNotIn(&#39;series&#39;, book)
AssertionError: &#39;series&#39; unexpectedly found in {&#39;id&#39;: 5, &#39;reader&#39;: [], &#39;title&#39;: &#39;testbook&#39;, &#39;author&#39;: [&#39;John Döe&#39;], &#39;rating&#39;: 0, &#39;languages&#39;: [&#39;English&#39;], &#39;identifier&#39;: [], &#39;cover&#39;: &#39;/cover/5/og?c=1733511155&#39;, &#39;tag&#39;: [], &#39;publisher&#39;: [&#39;Randomhäus&#39;], &#39;pubdate&#39;: &#39;Jan 19, 2017&#39;, &#39;comment&#39;: &#39;Lorem ipsum dolor sit amet, consectetuer adipiscing elit.Aenean commodo ligula eget dolor.Aenean massa.Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.Nulla consequat massa quis enim.Donec pede justo, fringilla vel, aliquet nec, vulputate&#39;, &#39;add_shelf&#39;: [], &#39;del_shelf&#39;: [], &#39;edit_enable&#39;: True, &#39;kindle&#39;: None, &#39;kindlebtn&#39;: None, &#39;download&#39;: [&#39;EPUB\n (6.7 kB)&#39;], &#39;read&#39;: False, &#39;archived&#39;: False, &#39;series_all&#39;: &#39;Book 1 of test&#39;, &#39;series_index&#39;: &#39;1&#39;, &#39;series&#39;: &#39;test&#39;, &#39;cust_columns&#39;: []}</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr>
@ -5801,10 +5841,10 @@ IndexError: list index out of range</pre>
<tr id='total_row' class="text-center bg-grey">
<td>Total</td>
<td>523</td>
<td>514</td>
<td>0</td>
<td>513</td>
<td>2</td>
<td>1</td>
<td>8</td>
<td>7</td>
<td>&nbsp;</td>
</tr>
</table>
@ -6348,7 +6388,7 @@ IndexError: list index out of range</pre>
</div>
<script>
drawCircle(514, 0, 1, 8);
drawCircle(513, 2, 1, 7);
showCase(5);
</script>