Compare commits

...

10 Commits

Author SHA1 Message Date
Ozzie Isaacs 97380b4b3f Updated teststatus 2 months ago
Ozzie Isaacs 4fbd064b85 Merge remote-tracking branch 'origin/back' into Develop 2 months ago
Ozzie Isaacs abbd9a5888 Revert logging line termination feature 2 months ago
Ozzie Isaacs e860b4e097 Back function implemented for delete book and edit book 2 months ago
Ozzie Isaacs 23a8a4657d Merge branch 'master' into Develop
(Fix for #3005 and #2993)
2 months ago
Ozzie Isaacs b38a1b2298 Admin can now force full sync for users (fix for #2993 2 months ago
Ozzie Isaacs 0ebfba8d05 Added blobs to csp for reader page (fix for #3005) 2 months ago
Ozzie Isaacs 990ad8d72d Update Requirements 2 months ago
Ozzie Isaacs c3fc125501 Added command line option or overwriting limiter backend
Added logger functions to remove newlines in messages
CalibreTask has now a default name
2 months ago
Ozzie Isaacs 3c4ed0de1a Added ratelimiterbackends 2 months ago

@ -103,7 +103,7 @@ web_server = WebServer()
updater_thread = Updater()
if limiter_present:
limiter = Limiter(key_func=True, headers_enabled=True, auto_check=False, swallow_errors=True)
limiter = Limiter(key_func=True, headers_enabled=True, auto_check=False, swallow_errors=False)
else:
limiter = None
@ -196,8 +196,18 @@ def create_app():
config.config_use_goodreads)
config.store_calibre_uuid(calibre_db, db.Library_Id)
# Configure rate limiter
# https://limits.readthedocs.io/en/stable/storage.html
app.config.update(RATELIMIT_ENABLED=config.config_ratelimiter)
limiter.init_app(app)
if config.config_limiter_uri != "" and not cli_param.memory_backend:
app.config.update(RATELIMIT_STORAGE_URI=config.config_limiter_uri)
if config.config_limiter_options != "":
app.config.update(RATELIMIT_STORAGE_OPTIONS=config.config_limiter_options)
try:
limiter.init_app(app)
except Exception as e:
log.error('Wrong Flask Limiter configuration, falling back to default: {}'.format(e))
app.config.update(RATELIMIT_STORAGE_URI=None)
limiter.init_app(app)
# Register scheduled tasks
from .schedule import register_scheduled_tasks, register_startup_tasks

@ -917,11 +917,15 @@ def list_restriction(res_type, user_id):
@admi.route("/ajax/fullsync", methods=["POST"])
@login_required
def ajax_fullsync():
count = ub.session.query(ub.KoboSyncedBooks).filter(current_user.id == 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')
def ajax_self_fullsync():
return do_full_kobo_sync(current_user.id)
@admi.route("/ajax/fullsync/<int:userid>", methods=["POST"])
@login_required
@admin_required
def ajax_fullsync(userid):
return do_full_kobo_sync(userid)
@admi.route("/ajax/pathchooser/")
@ -931,6 +935,13 @@ def ajax_pathchooser():
return pathchooser()
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')
def check_valid_read_column(column):
if column != "0":
if not calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.id == column) \
@ -1706,7 +1717,7 @@ def _db_configuration_update_helper():
return _db_configuration_result('{}'.format(ex), gdrive_error)
if db_change or not db_valid or not config.db_configured \
or config.config_calibre_dir != to_save["config_calibre_dir"]:
or config.config_calibre_dir != to_save["config_calibre_dir"]:
if not os.path.exists(metadata_db) or not to_save['config_calibre_dir']:
return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdrive_error)
else:
@ -1830,6 +1841,8 @@ def _configuration_update_helper():
return _configuration_result(_('Password length has to be between 1 and 40'))
reboot_required |= _config_int(to_save, "config_session")
reboot_required |= _config_checkbox(to_save, "config_ratelimiter")
reboot_required |= _config_string(to_save, "config_limiter_uri")
reboot_required |= _config_string(to_save, "config_limiter_options")
# Rarfile Content configuration
_config_string(to_save, "config_rarfile_location")

@ -52,6 +52,7 @@ class CliParameter(object):
parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-Web',
version=version_info())
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
parser.add_argument('-m', action='store_true', help='Use Memory-backend as limiter backend, use this parameter in case of miss configured backend')
parser.add_argument('-s', metavar='user:pass',
help='Sets specific username to new password and exits Calibre-Web')
parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version')
@ -98,6 +99,8 @@ class CliParameter(object):
if args.k == "":
self.keyfilepath = ""
# overwrite limiter backend
self.memory_backend = args.m or None
# dry run updater
self.dry_run = args.d or None
# enable reconnect endpoint for docker database reconnect

@ -168,6 +168,8 @@ class _Settings(_Base):
config_password_special = Column(Boolean, default=True)
config_session = Column(Integer, default=1)
config_ratelimiter = Column(Boolean, default=True)
config_limiter_uri = Column(String, default="")
config_limiter_options = Column(String, default="")
def __repr__(self):
return self.__class__.__name__

@ -159,7 +159,7 @@ EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'kepub', 'mobi', 'azw', 'azw3', 'cbr'
_extension = ""
if sys.platform == "win32":
_extension = ".exe"
SUPPORTED_CALIBRE_BINARIES = {binary:binary + _extension for binary in ["ebook-convert", "calibredb", "ebook-meta"]}
SUPPORTED_CALIBRE_BINARIES = {binary:binary + _extension for binary in ["ebook-convert", "calibredb"]}
def has_flag(value, bit_flag):

@ -60,6 +60,7 @@ from .tasks.upload import TaskUpload
from .render_template import render_title_template
from .usermanagement import login_required_if_no_ano
from .kobo_sync_status import change_archived_books
from .redirect import get_redirect_location
editbook = Blueprint('edit-book', __name__)
@ -96,7 +97,7 @@ def delete_book_from_details(book_id):
@editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
@login_required
def delete_book_ajax(book_id, book_format):
return delete_book_from_table(book_id, book_format, False)
return delete_book_from_table(book_id, book_format, False, request.form.to_dict().get('location', ""))
@editbook.route("/admin/book/<int:book_id>", methods=['GET'])
@ -823,7 +824,7 @@ 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):
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),
@ -835,16 +836,16 @@ def render_delete_book_result(book_format, json_response, warning, book_id):
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
else:
if json_response:
return json.dumps([warning, {"location": url_for('web.index'),
return json.dumps([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(url_for('web.index'))
return redirect(get_redirect_location(location, "web.index"))
def delete_book_from_table(book_id, book_format, json_response):
def delete_book_from_table(book_id, book_format, json_response, location=""):
warning = {}
if current_user.role_delete_books():
book = calibre_db.get_book(book_id)
@ -891,7 +892,7 @@ def delete_book_from_table(book_id, book_format, json_response):
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)
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),

@ -692,15 +692,15 @@ def valid_password(check_password):
if config.config_password_policy:
verify = ""
if config.config_password_min_length > 0:
verify += "^(?=.{" + str(config.config_password_min_length) + ",}$)"
verify += r"^(?=.{" + str(config.config_password_min_length) + ",}$)"
if config.config_password_number:
verify += "(?=.*?\d)"
verify += r"(?=.*?\d)"
if config.config_password_lower:
verify += "(?=.*?[a-z])"
verify += r"(?=.*?[a-z])"
if config.config_password_upper:
verify += "(?=.*?[A-Z])"
verify += r"(?=.*?[A-Z])"
if config.config_password_special:
verify += "(?=.*?[^A-Za-z\s0-9])"
verify += r"(?=.*?[^A-Za-z\s0-9])"
match = re.match(verify, check_password)
if not match:
raise Exception(_("Password doesn't comply with password validation rules"))
@ -1039,7 +1039,7 @@ def check_calibre(calibre_location):
binaries_available = [os.path.isfile(binary_path) for binary_path in supported_binary_paths]
binaries_executable = [os.access(binary_path, os.X_OK) for binary_path in supported_binary_paths]
if all(binaries_available) and all(binaries_executable):
values = [process_wait([binary_path, "--version"], pattern='\(calibre (.*)\)')
values = [process_wait([binary_path, "--version"], pattern=r'\(calibre (.*)\)')
for binary_path in supported_binary_paths]
if all(values):
version = values[0].group(1)

@ -156,6 +156,9 @@ def requires_kobo_auth(f):
limiter.check()
except RateLimitExceeded:
return abort(429)
except (ConnectionError, Exception) as e:
log.error("Connection error to limiter backend: %s", e)
return abort(429)
user = (
ub.session.query(ub.User)
.join(ub.RemoteAuthToken)

@ -44,9 +44,9 @@ def remove_prefix(text, prefix):
return ""
def redirect_back(endpoint, **values):
target = request.form.get('next', None) or url_for(endpoint, **values)
def get_redirect_location(next, endpoint, **values):
target = next or url_for(endpoint, **values)
adapter = current_app.url_map.bind(urlparse(request.host_url).netloc)
if not len(adapter.allowed_methods(remove_prefix(target, request.environ.get('HTTP_X_SCRIPT_NAME',"")))):
target = url_for(endpoint, **values)
return redirect(target)
return target

@ -266,3 +266,6 @@ class CalibreTask:
def _handleSuccess(self):
self.stat = STAT_FINISH_SUCCESS
self.progress = 1
def __str__(self):
return self.name

@ -20,7 +20,7 @@ function getPath() {
return jsFileLocation.substr(0, jsFileLocation.search("/static/js/libs/jquery.min.js")); // the js folder path
}
function postButton(event, action){
function postButton(event, action, location=""){
event.preventDefault();
var newForm = jQuery('<form>', {
"action": action,
@ -30,7 +30,14 @@ function postButton(event, action){
'name': 'csrf_token',
'value': $("input[name=\'csrf_token\']").val(),
'type': 'hidden'
})).appendTo('body');
})).appendTo('body')
if(location !== "") {
newForm.append(jQuery('<input>', {
'name': 'location',
'value': location,
'type': 'hidden'
})).appendTo('body');
}
newForm.submit();
}
@ -212,17 +219,20 @@ $("#delete_confirm").click(function(event) {
$( ".navbar" ).after( '<div class="row-fluid text-center" >' +
'<div id="flash_'+item.type+'" class="alert alert-'+item.type+'">'+item.message+'</div>' +
'</div>');
}
});
$("#books-table").bootstrapTable("refresh");
}
});
} else {
postButton(event, getPath() + "/delete/" + deleteId);
var loc = sessionStorage.getItem("back");
if (!loc) {
loc = $(this).data("back");
}
sessionStorage.removeItem("back");
postButton(event, getPath() + "/delete/" + deleteId, location=loc);
}
}
});
//triggered when modal is about to be shown
@ -541,6 +551,7 @@ $(function() {
$.get(e.relatedTarget.href).done(function(content) {
$modalBody.html(content);
preFilters.remove(useCache);
$("#back").remove();
});
})
.on("hidden.bs.modal", function() {
@ -621,8 +632,12 @@ $(function() {
"btnfullsync",
"GeneralDeleteModal",
$(this).data('value'),
function(value){
path = getPath() + "/ajax/fullsync"
function(userid) {
if (userid) {
path = getPath() + "/ajax/fullsync/" + userid
} else {
path = getPath() + "/ajax/fullsync"
}
$.ajax({
method:"post",
url: path,

@ -316,9 +316,9 @@ class TaskConvert(CalibreTask):
def __str__(self):
if self.ereader_mail:
return "Convert {} {}".format(self.book_id, self.ereader_mail)
return "Convert Book {} and mail it to {}".format(self.book_id, self.ereader_mail)
else:
return "Convert {}".format(self.book_id)
return "Convert Book {}".format(self.book_id)
@property
def is_cancellable(self):

@ -32,7 +32,7 @@
</div>
<div class="row display-flex">
{% for entry in entries %}
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book session">
<div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
<span class="img" title="{{entry.Books.title}}">
@ -99,7 +99,7 @@
<h3>{{_("More by")}} {{ author.name.replace('|',',') }}</h3>
<div class="row">
{% for entry in other_books %}
<div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="col-sm-3 col-lg-2 col-xs-6 book session">
<div class="cover">
<a href="https://www.goodreads.com/book/show/{{ entry.gid['#text'] }}" target="_blank" rel="noopener">
<img title="{{entry.title}}" src="{{ entry.image_url }}" />

@ -372,6 +372,16 @@
<input type="checkbox" id="config_ratelimiter" name="config_ratelimiter" {% if config.config_ratelimiter %}checked{% endif %}>
<label for="config_ratelimiter">{{_('Limit failed login attempts')}}</label>
</div>
<div data-related="ratelimiter_settings">
<div class="form-group" style="margin-left:10px;">
<label for="config_calibre">{{_('Configure Backend for Limiter')}}</label>
<input type="text" class="form-control" id="config_limiter_uri" name="config_limiter_uri" value="{% if config.config_limiter_uri != None %}{{ config.config_limiter_uri }}{% endif %}" autocomplete="off">
</div>
<div class="form-group" style="margin-left:10px;">
<label for="config_calibre">{{_('Options for Limiter')}}</label>
<input type="text" class="form-control" id="config_limiter_options" name="config_limiter_options" value="{% if config.config_limiter_options != None %}{{ config.config_limiter_options }}{% endif %}" autocomplete="off">
</div>
</div>
<div class="form-group">
<label for="config_session">{{_('Session protection')}}</label>
<select name="config_session" id="config_session" class="form-control">

@ -333,15 +333,15 @@
{% endif %}
{% if current_user.role_edit() %}
<div class="btn-toolbar" role="toolbar">
<div class="col-sm-12">
<div class="btn-group" role="group" aria-label="Edit/Delete book">
<a href="{{ url_for('edit-book.show_edit_book', book_id=entry.id) }}"
class="btn btn-sm btn-primary" id="edit_book" role="button"><span
class="glyphicon glyphicon-edit"></span> {{ _('Edit Metadata') }}</a>
</div>
</div>
<div class="btn btn-default" data-back="{{ url_for('web.index') }}" id="back">{{_('Cancel')}}</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
@ -367,4 +367,3 @@
</script>
{% endblock %}

@ -6,7 +6,7 @@
<h2 class="random-books">{{_('Discover (Random Books)')}}</h2>
<div class="row display-flex">
{% for entry in random %}
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books_rand">
<div class="col-sm-3 col-lg-2 col-xs-6 book session" id="books_rand">
<div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
<span class="img" title="{{ entry.Books.title }}">
@ -89,7 +89,7 @@
<div class="row display-flex">
{% if entries[0] %}
{% for entry in entries %}
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books">
<div class="col-sm-3 col-lg-2 col-xs-6 book session" id="books">
<div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
<span class="img" title="{{ entry.Books.title }}">

@ -41,7 +41,7 @@
<div class="row display-flex">
{% for entry in entries %}
<div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="col-sm-3 col-lg-2 col-xs-6 book session">
<div class="cover">
{% if entry.Books.has_cover is defined %}
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>

@ -31,7 +31,7 @@
{% endif %}
<div class="row display-flex">
{% for entry in entries %}
<div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="col-sm-3 col-lg-2 col-xs-6 book session">
<div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.Books.id) }}" {% if simple==false %}data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"{% endif %}>
<span class="img" title="{{entry.Books.title}}" >

@ -67,7 +67,7 @@
<div class="btn btn-danger" id="config_delete_kobo_token" data-value="{{ content.id }}" data-remote="false" {% if not content.remote_auth_token.first() %} style="display: none;" {% endif %}>{{_('Delete')}}</div>
</div>
<div class="form-group col">
<div class="btn btn-default" id="kobo_full_sync" data-value="{{ content.id }}" {% if not content.remote_auth_token.first() %} style="display: none;" {% endif %}>{{_('Force full kobo sync')}}</div>
<div class="btn btn-default" id="kobo_full_sync" data-value="{% if current_user.role_admin() %}{{ content.id }}{% else %}0{% endif %}" {% if not content.remote_auth_token.first() %} style="display: none;" {% endif %}>{{_('Force full kobo sync')}}</div>
</div>
{% endif %}
<div class="col-sm-6">

@ -50,7 +50,7 @@ from .helper import check_valid_domain, check_email, check_username, \
send_registration_mail, check_send_to_ereader, check_read_formats, tags_filters, reset_password, valid_email, \
edit_book_read_status, valid_password
from .pagination import Pagination
from .redirect import redirect_back
from .redirect import get_redirect_location
from .babel import get_available_locale
from .usermanagement import login_required_if_no_ano
from .kobo_sync_status import remove_synced_book
@ -86,9 +86,13 @@ except ImportError:
@app.after_request
def add_security_headers(resp):
csp = "default-src 'self'"
csp += ''.join([' ' + host for host in config.config_trustedhosts.strip().split(',')])
csp += " 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self'"
default_src = ([host.strip() for host in config.config_trustedhosts.split(',') if host] +
["'self'", "'unsafe-inline'", "'unsafe-eval'"])
csp = "default-src " + ' '.join(default_src) + "; "
csp += "font-src 'self' data:"
if request.endpoint == "web.read_book":
csp += " blob:"
csp += "; img-src 'self'"
if request.path.startswith("/author/") and config.config_use_goodreads:
csp += " images.gr-assets.com i.gr-assets.com s.gr-assets.com"
csp += " data:"
@ -1272,6 +1276,10 @@ def register_post():
except RateLimitExceeded:
flash(_(u"Please wait one minute to register next user"), category="error")
return render_title_template('register.html', config=config, title=_("Register"), page="register")
except (ConnectionError, Exception) as e:
log.error("Connection error to limiter backend: %s", e)
flash(_("Connection error to limiter backend, please contact your administrator"), category="error")
return render_title_template('register.html', config=config, title=_("Register"), page="register")
if current_user is not None and current_user.is_authenticated:
return redirect(url_for('web.index'))
if not config.get_mail_server_configured():
@ -1334,7 +1342,7 @@ def handle_login_user(user, remember, message, category):
ub.store_user_session()
flash(message, category=category)
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
return redirect_back("web.index")
return redirect(get_redirect_location(request.form.get('next', None), "web.index"))
def render_login(username="", password=""):
@ -1370,7 +1378,11 @@ def login_post():
try:
limiter.check()
except RateLimitExceeded:
flash(_(u"Please wait one minute before next login"), category="error")
flash(_("Please wait one minute before next login"), category="error")
return render_login(username, form.get("password", ""))
except (ConnectionError, Exception) as e:
log.error("Connection error to limiter backend: %s", e)
flash(_("Connection error to limiter backend, please contact your administrator"), category="error")
return render_login(username, form.get("password", ""))
if current_user is not None and current_user.is_authenticated:
return redirect(url_for('web.index'))

@ -1,23 +1,23 @@
# GDrive Integration
google-api-python-client>=1.7.11,<2.108.0
gevent>20.6.0,<24.0.0
google-api-python-client>=1.7.11,<2.120.0
gevent>20.6.0,<24.3.0
greenlet>=0.4.17,<3.1.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.4.0
pyasn1>=0.1.9,<0.6.0
PyDrive2>=1.3.1,<1.18.0
PyDrive2>=1.3.1,<1.20.0
PyYAML>=3.12,<6.1
rsa>=3.4.2,<4.10.0
# Gmail
google-auth-oauthlib>=0.4.3,<1.1.0
google-api-python-client>=1.7.11,<2.108.0
google-auth-oauthlib>=0.4.3,<1.3.0
google-api-python-client>=1.7.11,<2.120.0
# goodreads
goodreads>=0.3.2,<0.4.0
python-Levenshtein>=0.12.0,<0.22.0
python-Levenshtein>=0.12.0,<0.26.0
# ldap login
python-ldap>=3.0.0,<3.5.0
@ -28,10 +28,10 @@ Flask-Dance>=2.0.0,<7.1.0
SQLAlchemy-Utils>=0.33.5,<0.42.0
# metadata extraction
rarfile>=3.2
rarfile>=3.2,<4.2
scholarly>=1.2.0,<1.8
markdown2>=2.0.0,<2.5.0
html2text>=2020.1.16,<2022.1.1
html2text>=2020.1.16,<2024.2.26
python-dateutil>=2.1,<2.9.0
beautifulsoup4>=4.0.1,<4.13.0
faust-cchardet>=2.1.18,<2.1.20

@ -2,18 +2,18 @@ Werkzeug<3.0.0
APScheduler>=3.6.3,<3.11.0
Babel>=1.3,<3.0
Flask-Babel>=0.11.1,<4.1.0
Flask-Login>=0.3.2,<0.6.3
Flask-Login>=0.3.2,<0.6.4
Flask-Principal>=0.3.2,<0.5.1
Flask>=1.0.2,<2.4.0
Flask>=1.0.2,<3.1.0
iso-639>=0.4.5,<0.5.0
PyPDF>=3.0.0,<3.16.0
PyPDF>=3.15.6,<4.1.0
pytz>=2016.10
requests>=2.28.0,<2.32.0
SQLAlchemy>=1.3.0,<2.1.0
tornado>=6.3,<6.4
tornado>=6.3,<6.5
Wand>=0.4.4,<0.7.0
unidecode>=0.04.19,<1.4.0
lxml>=3.8.0,<5.0.0
lxml>=3.8.0,<5.2.0
flask-wtf>=0.14.2,<1.3.0
chardet>=3.0.0,<4.1.0
advocate>=1.0.0,<1.1.0

@ -42,18 +42,18 @@ install_requires =
APScheduler>=3.6.3,<3.11.0
Babel>=1.3,<3.0
Flask-Babel>=0.11.1,<4.1.0
Flask-Login>=0.3.2,<0.6.3
Flask-Login>=0.3.2,<0.6.4
Flask-Principal>=0.3.2,<0.5.1
Flask>=1.0.2,<2.4.0
Flask>=1.0.2,<3.1.0
iso-639>=0.4.5,<0.5.0
PyPDF>=3.0.0,<3.16.0
PyPDF>=3.15.6,<4.1.0
pytz>=2016.10
requests>=2.28.0,<2.32.0
SQLAlchemy>=1.3.0,<2.1.0
tornado>=6.3,<6.4
tornado>=6.3,<6.5
Wand>=0.4.4,<0.7.0
unidecode>=0.04.19,<1.4.0
lxml>=3.8.0,<5.0.0
lxml>=3.8.0,<5.2.0
flask-wtf>=0.14.2,<1.3.0
chardet>=3.0.0,<4.1.0
advocate>=1.0.0,<1.1.0
@ -66,23 +66,23 @@ include = cps/services*
[options.extras_require]
gdrive =
google-api-python-client>=1.7.11,<2.108.0
gevent>20.6.0,<24.0.0
google-api-python-client>=1.7.11,<2.120.0
gevent>20.6.0,<24.3.0
greenlet>=0.4.17,<3.1.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.4.0
pyasn1>=0.1.9,<0.6.0
PyDrive2>=1.3.1,<1.18.0
PyDrive2>=1.3.1,<1.20.0
PyYAML>=3.12,<6.1
rsa>=3.4.2,<4.10.0
gmail =
google-auth-oauthlib>=0.4.3,<1.1.0
google-api-python-client>=1.7.11,<2.108.0
google-auth-oauthlib>=0.4.3,<1.3.0
google-api-python-client>=1.7.11,<2.120.0
goodreads =
goodreads>=0.3.2,<0.4.0
python-Levenshtein>=0.12.0,<0.22.0
python-Levenshtein>=0.12.0,<0.26.0
ldap =
python-ldap>=3.0.0,<3.5.0
Flask-SimpleLDAP>=1.4.0,<1.5.0
@ -90,10 +90,10 @@ oauth =
Flask-Dance>=2.0.0,<7.1.0
SQLAlchemy-Utils>=0.33.5,<0.42.0
metadata =
rarfile>=3.2
rarfile>=3.2,<4.2
scholarly>=1.2.0,<1.8
markdown2>=2.0.0,<2.5.0
html2text>=2020.1.16,<2022.1.1
html2text>=2020.1.16,<2024.2.26
python-dateutil>=2.1,<2.9.0
beautifulsoup4>=4.0.1,<4.13.0
faust-cchardet>=2.1.18,<2.1.20

@ -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-02-24 19:10:31</p>
<p class='text-justify attribute'><strong>Start Time: </strong>2024-02-26 20:07:24</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-02-25 02:03:41</p>
<p class='text-justify attribute'><strong>Stop Time: </strong>2024-02-27 03:19:17</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>5h 43 min</p>
<p class='text-justify attribute'><strong>Duration: </strong>6h 0 min</p>
</div>
</div>
</div>
@ -266,31 +266,11 @@
<tr id="ft2.3" class="none bg-danger">
<tr id='pt2.3' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestBackupMetadata - test_backup_change_book_description</div>
</td>
<td colspan='6'>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft2.3')">FAIL</a>
</div>
<!--css div popup start-->
<div id="div_ft2.3" 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_ft2.3').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_backup_metadata.py&#34;, line 339, in test_backup_change_book_description
self.assertEqual(metadata[&#39;description&#39;], &#34;&#34;)
AssertionError: &#39;&lt;p&gt;&lt;strong&gt;Test&lt;/strong&gt;&lt;/p&gt;&#39; != &#39;&#39;</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -349,11 +329,34 @@ AssertionError: &#39;&lt;p&gt;&lt;strong&gt;Test&lt;/strong&gt;&lt;/p&gt;&#39; !
<tr id='pt2.10' class='hiddenRow bg-success'>
<tr id="ft2.10" class="none bg-danger">
<td>
<div class='testcase'>TestBackupMetadata - test_backup_change_book_tags</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_ft2.10')">FAIL</a>
</div>
<!--css div popup start-->
<div id="div_ft2.10" 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_ft2.10').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_backup_metadata.py&#34;, line 243, in test_backup_change_book_tags
self.assertCountEqual(metadata[&#39;tags&#39;], [&#39;Ku&#39;,&#39;kOl&#39;])
AssertionError: Element counts were not equal:
First has 1, Second has 0: &#39;Lo执|1u&#39;
First has 0, Second has 1: &#39;Ku&#39;
First has 0, Second has 1: &#39;kOl&#39;</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr>
@ -1987,12 +1990,12 @@ IndexError: list index out of range</pre>
<tr id="su" class="errorClass">
<tr id="su" class="passClass">
<td>TestEditBooksOnGdrive</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">1</td>
<td class="text-center">0</td>
<td class="text-center">
<a onclick="showClassDetail('c18', 18)">Detail</a>
@ -2010,31 +2013,11 @@ IndexError: list index out of range</pre>
<tr id="et18.2" class="none bg-info">
<tr id='pt18.2' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestEditBooksOnGdrive - test_edit_author</div>
</td>
<td colspan='6'>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et18.2')">ERROR</a>
</div>
<!--css div popup start-->
<div id="div_et18.2" 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_et18.2').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 310, in test_edit_author
self.assertEqual(u&#39;O0ü 执&#39;, values[&#39;author&#39;][0])
IndexError: list index out of range</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -5586,9 +5569,9 @@ AssertionError: False is not true</pre>
<tr id='total_row' class="text-center bg-grey">
<td>Total</td>
<td>492</td>
<td>478</td>
<td>2</td>
<td>479</td>
<td>2</td>
<td>1</td>
<td>10</td>
<td>&nbsp;</td>
</tr>
@ -5671,7 +5654,7 @@ AssertionError: False is not true</pre>
<tr>
<th>Flask-Login</th>
<td>0.6.2</td>
<td>0.6.3</td>
<td>Basic</td>
</tr>
@ -5707,7 +5690,7 @@ AssertionError: False is not true</pre>
<tr>
<th>lxml</th>
<td>4.9.4</td>
<td>5.1.0</td>
<td>Basic</td>
</tr>
@ -5719,7 +5702,7 @@ AssertionError: False is not true</pre>
<tr>
<th>pypdf</th>
<td>3.15.5</td>
<td>4.0.2</td>
<td>Basic</td>
</tr>
@ -5743,7 +5726,7 @@ AssertionError: False is not true</pre>
<tr>
<th>tornado</th>
<td>6.3.3</td>
<td>6.4</td>
<td>Basic</td>
</tr>
@ -6109,7 +6092,7 @@ AssertionError: False is not true</pre>
</div>
<script>
drawCircle(478, 2, 2, 10);
drawCircle(479, 2, 1, 10);
showCase(5);
</script>

Loading…
Cancel
Save