Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e732b3b614 | ||
|
|
69f8767be5 | ||
|
|
6cbba6d170 | ||
|
|
888ab4dc8f | ||
|
|
753c19e700 | ||
|
|
4f9390332a | ||
|
|
ffddd85800 | ||
|
|
652b0f20e0 | ||
|
|
598f08b916 | ||
|
|
55cedba693 | ||
|
|
10caf68d8b | ||
|
|
f4ed816f6e | ||
|
|
ba77f41325 | ||
|
|
ea567fcbf0 | ||
|
|
edb05abcbe | ||
|
|
46811219ca | ||
|
|
0aab0cac73 | ||
|
|
3257a36892 | ||
|
|
f10b680b0e | ||
|
|
c2b1950cd5 | ||
|
|
00c8ff8ac4 | ||
|
|
0ca6e86177 | ||
|
|
ab17992ebb | ||
|
|
570371cc82 | ||
|
|
674b47bdbd | ||
|
|
8cff413c4d | ||
|
|
fd744af75d | ||
|
|
6208d5e264 | ||
|
|
42dc36cc10 | ||
|
|
c23d35db4a | ||
|
|
fca580505c | ||
|
|
19da54a7ae | ||
|
|
9c87f0fbde | ||
|
|
e21f943712 | ||
|
|
4bb553f51d | ||
|
|
0615637f58 | ||
|
|
a6c55deac6 | ||
|
|
2de8df7e0d | ||
|
|
4c1d7a9f6b | ||
|
|
9489905602 | ||
|
|
5e7c0c9d34 | ||
|
|
b5da0df42a | ||
|
|
0959f84fd5 | ||
|
|
84777319d7 | ||
|
|
d85bef6c38 | ||
|
|
8ad9f4e3b7 | ||
|
|
cde3888e17 | ||
|
|
387678a771 | ||
|
|
c451daad3c | ||
|
|
224915bba1 | ||
|
|
d598054195 | ||
|
|
088778969d | ||
|
|
0ed4d81da2 | ||
|
|
9478325c23 | ||
|
|
7c715f34dc | ||
|
|
00686ec1f2 | ||
|
|
83d331c89b | ||
|
|
f6338a9fda | ||
|
|
73efef17bd | ||
|
|
74725b68b0 | ||
|
|
efbdd0b6e3 | ||
|
|
1134f54be4 | ||
|
|
2d4ca23d0c | ||
|
|
6157f5027c | ||
|
|
5e48a64b15 | ||
|
|
36a7ff19bc | ||
|
|
1b4f0d0967 | ||
|
|
181117f916 | ||
|
|
32741be6c8 | ||
|
|
f64fe5d835 | ||
|
|
0db93a6036 | ||
|
|
a4bf028537 | ||
|
|
47625a616d | ||
|
|
87635a5ace | ||
|
|
a9713bd497 | ||
|
|
8cef72b847 | ||
|
|
fbed304df8 | ||
|
|
e3bf369ad6 | ||
|
|
d229f71151 | ||
|
|
3aef161cb8 | ||
|
|
cabcace3f0 | ||
|
|
424e493946 | ||
|
|
848302f69e | ||
|
|
8bdd95fc45 | ||
|
|
9c7a834cec | ||
|
|
51a2f36966 | ||
|
|
b64645e5f5 | ||
|
|
bed8957eae | ||
|
|
0ce8c19e88 | ||
|
|
315584690d | ||
|
|
a91724edb0 | ||
|
|
5ad6b7fa64 | ||
|
|
c1be944a48 |
+3
-2
@@ -25,7 +25,7 @@ import sys
|
||||
import os
|
||||
import mimetypes
|
||||
|
||||
from flask import Flask, request
|
||||
from flask import Flask
|
||||
from flask.sessions import SecureCookieSessionInterface
|
||||
from .MyLoginManager import MyLoginManager
|
||||
from flask_principal import Principal
|
||||
@@ -111,7 +111,8 @@ web_server = WebServer()
|
||||
updater_thread = Updater()
|
||||
|
||||
if limiter_present:
|
||||
limiter = Limiter(key_func=True, headers_enabled=True, auto_check=False, swallow_errors=False)
|
||||
limiter = Limiter(key_func=True, headers_enabled=True, in_memory_fallback_enabled=True, default_limits=[],
|
||||
swallow_errors=True)
|
||||
else:
|
||||
limiter = None
|
||||
|
||||
|
||||
+2
-1
@@ -349,8 +349,9 @@ def list_users():
|
||||
if sort not in ub.User.__table__.columns.keys():
|
||||
sort = "id"
|
||||
order = request.args.get("order", "").lower()
|
||||
|
||||
if sort != "state" and order:
|
||||
if not order in ["asc", "desc"]:
|
||||
order = "asc"
|
||||
order = text(sort + " " + order)
|
||||
elif not state:
|
||||
order = ub.User.id.asc()
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from cps.pagination import Pagination
|
||||
from flask import Blueprint
|
||||
from flask_babel import gettext as _
|
||||
from flask_babel import get_locale
|
||||
|
||||
+7
-2
@@ -35,8 +35,13 @@ def clean_string(unsafe_text, book_id=0):
|
||||
try:
|
||||
if bleach:
|
||||
allowed_tags = list(ALLOWED_TAGS)
|
||||
allowed_tags.extend(["p", "span", "div", "pre", "br", "h1", "h2", "h3", "h4", "h5", "h6"])
|
||||
safe_text = clean_html(unsafe_text, tags=set(allowed_tags))
|
||||
allowed_tags.extend(["p", "span", "div", "pre", "br", "h1", "h2", "h3", "h4", "h5", "h6", "img"])
|
||||
allowed_attributes = {
|
||||
"*": ["class", "style"],
|
||||
"a": ["href", "title", "rel"],
|
||||
"img": ["src", "alt", "title", "width", "height"],
|
||||
}
|
||||
safe_text = clean_html(unsafe_text, tags=set(allowed_tags), attributes=allowed_attributes)
|
||||
else:
|
||||
safe_text = clean_html(unsafe_text)
|
||||
except ParserError as e:
|
||||
|
||||
+7
-4
@@ -84,8 +84,8 @@ class _Settings(_Base):
|
||||
config_authors_max = Column(Integer, default=0)
|
||||
config_read_column = Column(Integer, default=0)
|
||||
config_title_regex = Column(String,
|
||||
default=r'^(A|The|An|Der|Die|Das|Den|Ein|Eine'
|
||||
r'|Einen|Dem|Des|Einem|Eines|Le|La|Les|L\'|Un|Une)\s+')
|
||||
default=r"^(A|The|An|Der|Die|Das|Den|Ein|Eine"
|
||||
r"|Einen|Dem|Des|Einem|Eines|Le|La|Les|L'|Un|Une)(\s+|(?<='))")
|
||||
config_theme = Column(Integer, default=0)
|
||||
|
||||
config_log_level = Column(SmallInteger, default=logger.DEFAULT_LOG_LEVEL)
|
||||
@@ -326,7 +326,9 @@ class ConfigSQL(object):
|
||||
def to_dict(self):
|
||||
storage = {}
|
||||
for k, v in self.__dict__.items():
|
||||
if k[0] != '_' and not k.endswith("_e") and not k == "cli" and 'api' not in k.lower():
|
||||
if k[0] != '_' and not k.endswith("_e") and not k == "cli" \
|
||||
and 'api' not in k.lower() and 'token' not in k.lower() \
|
||||
and 'secret' not in k.lower():
|
||||
storage[k] = v
|
||||
return storage
|
||||
|
||||
@@ -403,7 +405,7 @@ class ConfigSQL(object):
|
||||
self.save()
|
||||
|
||||
def get_book_path(self):
|
||||
return self.config_calibre_split_dir if self.config_calibre_split_dir else self.config_calibre_dir
|
||||
return self.config_calibre_split_dir if self.config_calibre_split else self.config_calibre_dir
|
||||
|
||||
def store_calibre_uuid(self, calibre_db, Library_table):
|
||||
from . import app
|
||||
@@ -583,6 +585,7 @@ def get_encryption_key(key_path):
|
||||
try:
|
||||
with open(key_file, "wb") as f:
|
||||
f.write(key)
|
||||
os.chmod(key_file, 0o600)
|
||||
except PermissionError as e:
|
||||
error = e
|
||||
return key, error
|
||||
|
||||
+1
-1
@@ -175,7 +175,7 @@ BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, d
|
||||
'series_id, languages, publisher, pubdate, identifiers')
|
||||
|
||||
# python build process likes to have x.y.zbw -> b for beta and w a counting number
|
||||
STABLE_VERSION = '0.6.26'
|
||||
STABLE_VERSION = '0.6.27b'
|
||||
|
||||
NIGHTLY_VERSION = dict()
|
||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||
|
||||
@@ -449,7 +449,14 @@ class Books(Base):
|
||||
|
||||
@property
|
||||
def atom_timestamp(self):
|
||||
return self.timestamp.strftime('%Y-%m-%dT%H:%M:%S+00:00') or ''
|
||||
# OPDS atom:updated is defined as "the most recent instant in time
|
||||
# when the entry was modified". Books.timestamp is the date added and
|
||||
# never changes after import, so metadata and cover edits were
|
||||
# invisible to OPDS sync clients. Use last_modified, which Calibre
|
||||
# updates on every metadata or cover change; fall back to timestamp
|
||||
# only if last_modified happens to be missing.
|
||||
t = self.last_modified or self.timestamp
|
||||
return t.strftime('%Y-%m-%dT%H:%M:%S+00:00') if t else ''
|
||||
|
||||
|
||||
class CustomColumns(Base):
|
||||
@@ -640,8 +647,8 @@ class CalibreDB:
|
||||
connect_args={'check_same_thread': False},
|
||||
poolclass=StaticPool)
|
||||
with check_engine.begin() as connection:
|
||||
connection.execute(text("attach database '{}' as calibre;".format(dbpath)))
|
||||
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path)))
|
||||
connection.execute(text("attach database '{}' as calibre;".format(dbpath.replace("'", "''"))))
|
||||
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path.replace("'", "''"))))
|
||||
local_session = scoped_session(sessionmaker())
|
||||
local_session.configure(bind=connection)
|
||||
database_uuid = local_session().query(Library_Id).one_or_none()
|
||||
@@ -694,8 +701,8 @@ class CalibreDB:
|
||||
poolclass=StaticPool)
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text('PRAGMA cache_size = 10000;'))
|
||||
connection.execute(text("attach database '{}' as calibre;".format(dbpath)))
|
||||
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path)))
|
||||
connection.execute(text("attach database '{}' as calibre;".format(dbpath.replace("'", "''"))))
|
||||
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path.replace("'", "''"))))
|
||||
|
||||
conn = engine.connect()
|
||||
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
|
||||
@@ -746,7 +753,8 @@ class CalibreDB:
|
||||
.filter(self.common_filters(allow_show_archived)).first())
|
||||
|
||||
def get_book_by_uuid(self, book_uuid):
|
||||
return self.session.query(Books).filter(Books.uuid == book_uuid).first()
|
||||
return self.session.query(Books).filter(Books.uuid == book_uuid). \
|
||||
filter(self.common_filters()).first()
|
||||
|
||||
def get_book_format(self, book_id, file_format):
|
||||
return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == file_format).first()
|
||||
@@ -944,16 +952,13 @@ class CalibreDB:
|
||||
def get_typeahead(self, database, query, replace=('', ''), tag_filter=true()):
|
||||
query = query or ''
|
||||
self.create_functions()
|
||||
# self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
||||
entries = self.session.query(database).filter(tag_filter). \
|
||||
filter(func.lower(database.name).ilike("%" + query + "%")).all()
|
||||
# json_dumps = json.dumps([dict(name=escape(r.name.replace(*replace))) for r in entries])
|
||||
json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries])
|
||||
return json_dumps
|
||||
|
||||
def check_exists_book(self, authr, title):
|
||||
self.create_functions()
|
||||
# self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
||||
q = list()
|
||||
author_terms = re.split(r'\s*&\s*', authr)
|
||||
for author_term in author_terms:
|
||||
@@ -1121,7 +1126,7 @@ class CalibreDB:
|
||||
.filter(self.common_filters())
|
||||
.count())
|
||||
if no_lang_count:
|
||||
tags.append([Category(_("None"), "none"), no_lang_count])
|
||||
tags.append([Category(_("None"), "None", "none"), no_lang_count])
|
||||
return sorted(tags, key=lambda x: x[0].name.lower(), reverse=reverse_order)
|
||||
else:
|
||||
if not languages:
|
||||
@@ -1175,14 +1180,25 @@ def lcase(s):
|
||||
return s.lower()
|
||||
|
||||
|
||||
def title_sort(title, config):
|
||||
# calibre sort stuff
|
||||
title_pat = re.compile(config.config_title_regex, re.IGNORECASE)
|
||||
match = title_pat.search(title)
|
||||
if match:
|
||||
prep = match.group(1)
|
||||
title = title[len(prep):] + ', ' + prep
|
||||
return strip_whitespaces(title)
|
||||
|
||||
class Category:
|
||||
name = None
|
||||
sort = None
|
||||
id = None
|
||||
count = None
|
||||
rating = None
|
||||
|
||||
def __init__(self, name, cat_id, rating=None):
|
||||
self.name = name
|
||||
self.sort = name
|
||||
self.id = cat_id
|
||||
self.rating = rating
|
||||
self.count = 1
|
||||
|
||||
+2
-2
@@ -1611,7 +1611,7 @@ def add_objects(db_book_object, db_object, db_session, db_type, add_elements):
|
||||
if db_type == 'author':
|
||||
new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')))
|
||||
elif db_type == 'series':
|
||||
new_element = db_object(add_element, add_element)
|
||||
new_element = db_object(add_element, db.title_sort(add_element, config))
|
||||
elif db_type == 'custom':
|
||||
new_element = db_object(value=add_element)
|
||||
elif db_type == 'publisher':
|
||||
@@ -1642,7 +1642,7 @@ def create_objects_for_addition(db_element, add_element, db_type):
|
||||
elif db_type == 'series':
|
||||
if db_element.name != add_element:
|
||||
db_element.name = add_element
|
||||
db_element.sort = add_element
|
||||
db_element.sort = db.title_sort(add_element, config)
|
||||
elif db_type == 'author':
|
||||
if db_element.name != add_element:
|
||||
db_element.name = add_element
|
||||
|
||||
+2
-1
@@ -36,7 +36,8 @@ def do_calibre_export(book_id, book_format):
|
||||
if config.config_calibre_split:
|
||||
my_env['CALIBRE_OVERRIDE_DATABASE_PATH'] = os.path.join(config.config_calibre_dir, "metadata.db")
|
||||
library_path = config.get_book_path()
|
||||
opf_command = [calibredb_binarypath, 'export', '--dont-write-opf', '--with-library', library_path,
|
||||
opf_command = [calibredb_binarypath, 'export', '--dont-write-opf', '--dont-save-cover',
|
||||
'--with-library', library_path,
|
||||
'--to-dir', tmp_dir, '--formats', book_format, "--template", "{}".format(temp_file_name),
|
||||
str(book_id)]
|
||||
p = process_open(opf_command, quotes, my_env)
|
||||
|
||||
+1
-1
@@ -175,7 +175,7 @@ def parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path):
|
||||
for cs in cover_section:
|
||||
if cs.endswith('.xhtml') or cs.endswith('.html'):
|
||||
markup = epub_zip.read(os.path.join(cover_path, cs))
|
||||
markup_tree = etree.fromstring(markup)
|
||||
markup_tree = etree.fromstring(markup, parser=etree.XMLParser(resolve_entities=False, no_network=True))
|
||||
# no matter xhtml or html with no namespace
|
||||
img_src = markup_tree.xpath("//*[local-name() = 'img']/@src")
|
||||
# Alternative image source
|
||||
|
||||
+6
-2
@@ -53,16 +53,20 @@ def updateEpub(src, dest, filename, data, ):
|
||||
zf.writestr(filename, data)
|
||||
|
||||
|
||||
# Safe parser: disable entity resolution and network access to prevent XXE attacks
|
||||
_safe_parser = etree.XMLParser(resolve_entities=False, no_network=True)
|
||||
|
||||
|
||||
def get_content_opf(file_path, ns=None):
|
||||
if ns is None:
|
||||
ns = default_ns
|
||||
epubZip = zipfile.ZipFile(file_path)
|
||||
txt = epubZip.read('META-INF/container.xml')
|
||||
tree = etree.fromstring(txt)
|
||||
tree = etree.fromstring(txt, parser=_safe_parser)
|
||||
cf_name = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0]
|
||||
cf = epubZip.read(cf_name)
|
||||
|
||||
return etree.fromstring(cf), cf_name
|
||||
return etree.fromstring(cf, parser=_safe_parser), cf_name
|
||||
|
||||
|
||||
def create_new_metadata_backup(book, custom_columns, export_language, translated_cover_name, lang_type=3):
|
||||
|
||||
+35
-3
@@ -18,15 +18,22 @@
|
||||
|
||||
import traceback
|
||||
|
||||
from flask import render_template
|
||||
from flask import render_template, request, flash, make_response
|
||||
from flask_limiter import RateLimitExceeded
|
||||
from flask_babel import gettext as _
|
||||
from werkzeug.exceptions import default_exceptions
|
||||
|
||||
from .cw_login import current_user
|
||||
try:
|
||||
from werkzeug.exceptions import FailedDependency
|
||||
except ImportError:
|
||||
from werkzeug.exceptions import UnprocessableEntity as FailedDependency
|
||||
|
||||
from . import config, app, logger, services
|
||||
|
||||
from .render_template import render_title_template
|
||||
from .web import render_login
|
||||
from .usermanagement import auth
|
||||
from cps.string_helper import strip_whitespaces
|
||||
|
||||
log = logger.create()
|
||||
|
||||
@@ -57,6 +64,13 @@ def internal_error(error):
|
||||
error_stack="",
|
||||
instance=config.config_calibre_web_title
|
||||
), 500
|
||||
log.error("500 Internal Server Error: %s", traceback.format_exc())
|
||||
error_stack = ""
|
||||
try:
|
||||
if current_user.is_authenticated and current_user.role_admin():
|
||||
error_stack = traceback.format_exc().split("\n")
|
||||
except Exception:
|
||||
pass
|
||||
return render_template('http_error.html',
|
||||
error_code="500 Internal Server Error",
|
||||
error_name='The server encountered an internal error and was unable to complete your '
|
||||
@@ -64,7 +78,7 @@ def internal_error(error):
|
||||
issue=True,
|
||||
goto_admin=False,
|
||||
unconfigured=False,
|
||||
error_stack=traceback.format_exc().split("\n"),
|
||||
error_stack=error_stack,
|
||||
instance=config.config_calibre_web_title
|
||||
), 500
|
||||
|
||||
@@ -85,3 +99,21 @@ def init_errorhandler():
|
||||
log.debug('LDAP server not accessible while trying to login to opds feed')
|
||||
return error_http(FailedDependency())
|
||||
|
||||
|
||||
@app.errorhandler(RateLimitExceeded)
|
||||
def handle_rate_limit(__):
|
||||
log.error("Rate limit exceeded {}".format(request.endpoint))
|
||||
if "register" in request.endpoint:
|
||||
flash(_(u"Please wait one minute to register next user"), category="error")
|
||||
return render_title_template('register.html', config=config, title=_("Register"), page="register")
|
||||
elif "login" in request.endpoint:
|
||||
form = request.form.to_dict()
|
||||
username = strip_whitespaces(form.get('username', "")).lower().replace("\n", "").replace("\r", "")
|
||||
flash(_("Please wait one minute before next login"), category="error")
|
||||
return render_login(username, form.get("password", ""))
|
||||
elif "opds" in request.endpoint:
|
||||
return auth.auth_error_callback(429)
|
||||
else:
|
||||
return make_response('', 429)
|
||||
|
||||
|
||||
|
||||
+4
-1
@@ -20,6 +20,9 @@ from lxml import etree
|
||||
|
||||
from .constants import BookMeta
|
||||
|
||||
# Safe parser: disable entity resolution and network access to prevent XXE attacks
|
||||
_safe_parser = etree.XMLParser(resolve_entities=False, no_network=True)
|
||||
|
||||
|
||||
def get_fb2_info(tmp_file_path, original_file_extension):
|
||||
|
||||
@@ -29,7 +32,7 @@ def get_fb2_info(tmp_file_path, original_file_extension):
|
||||
}
|
||||
|
||||
fb2_file = open(tmp_file_path, encoding="utf-8")
|
||||
tree = etree.fromstring(fb2_file.read().encode())
|
||||
tree = etree.fromstring(fb2_file.read().encode(), parser=_safe_parser)
|
||||
|
||||
authors = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:author', namespaces=ns)
|
||||
|
||||
|
||||
+1
-1
@@ -140,7 +140,7 @@ try:
|
||||
if response:
|
||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
|
||||
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
||||
and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec
|
||||
and response['file']['md5Checksum'] != hashlib.md5(dbpath).hexdigest(): # nosec
|
||||
tmp_dir = get_temp_dir()
|
||||
|
||||
log.info('Database file updated')
|
||||
|
||||
+18
-2
@@ -30,7 +30,7 @@ import requests
|
||||
import unidecode
|
||||
from uuid import uuid4
|
||||
|
||||
from flask import send_from_directory, make_response, abort, url_for, Response, request
|
||||
from flask import send_from_directory, make_response, abort, url_for, Response, request, after_this_request
|
||||
from flask_babel import gettext as _
|
||||
from flask_babel import lazy_gettext as N_
|
||||
from flask_babel import get_locale
|
||||
@@ -956,6 +956,17 @@ def do_download_file(book, book_format, client, data, headers):
|
||||
else:
|
||||
download_name = book_name
|
||||
|
||||
# Clean up staged copies in /tmp/calibre_web after the response is sent
|
||||
# (kepubify / calibre-export branches) so the temp dir does not grow unbounded.
|
||||
if filename == get_temp_dir():
|
||||
_tmp_path = os.path.join(filename, download_name + "." + book_format)
|
||||
@after_this_request
|
||||
def _cleanup_staged_download(resp):
|
||||
try:
|
||||
os.remove(_tmp_path)
|
||||
except OSError as ex:
|
||||
log.warning('Failed to remove staged download %s: %s', _tmp_path, ex)
|
||||
return resp
|
||||
response = make_response(send_from_directory(filename, download_name + "." + book_format))
|
||||
# ToDo Check headers parameter
|
||||
for element in headers:
|
||||
@@ -1091,8 +1102,13 @@ def get_download_link(book_id, book_format, client):
|
||||
file_name = book.title
|
||||
if len(book.authors) > 0:
|
||||
file_name = file_name + ' - ' + book.authors[0].name
|
||||
original_name = file_name
|
||||
file_name = get_valid_filename(file_name, replace_whitespace=False, force_unidecode=True)
|
||||
quoted_file_name = file_name if client == "kindle" else quote(file_name)
|
||||
if client == "kindle":
|
||||
quoted_file_name = file_name
|
||||
else:
|
||||
native_name = get_valid_filename(original_name, replace_whitespace=False, force_unidecode=False)
|
||||
quoted_file_name = quote(native_name)
|
||||
headers = Headers()
|
||||
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
||||
headers["Content-Disposition"] = ('attachment; filename="{}.{}"; filename*=UTF-8\'\'{}.{}').format(
|
||||
|
||||
@@ -30,6 +30,7 @@ from uuid import uuid4
|
||||
from flask import Blueprint, request, url_for, g
|
||||
from flask_babel import format_date
|
||||
from .cw_login import current_user
|
||||
from .clean_html import clean_string as html_clean_string
|
||||
|
||||
from . import constants, logger
|
||||
|
||||
@@ -181,3 +182,9 @@ def contains_music(book_formats):
|
||||
if format.format.lower() in g.constants.EXTENSIONS_AUDIO:
|
||||
result = True
|
||||
return result
|
||||
|
||||
@jinjia.app_template_filter('clean_string')
|
||||
def clean_string(unsafe_text):
|
||||
return html_clean_string(unsafe_text)
|
||||
|
||||
|
||||
|
||||
+35
-12
@@ -25,6 +25,7 @@ import zipfile
|
||||
from time import gmtime, strftime
|
||||
import json
|
||||
from urllib.parse import unquote
|
||||
import requests
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
@@ -41,10 +42,9 @@ from werkzeug.datastructures import Headers
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.sql.expression import and_, or_
|
||||
from sqlalchemy.exc import StatementError
|
||||
import requests
|
||||
|
||||
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
||||
from . import isoLanguages
|
||||
from . import isoLanguages, limiter
|
||||
from .epub import get_epub_layout
|
||||
from .constants import COVER_THUMBNAIL_SMALL, COVER_THUMBNAIL_MEDIUM, COVER_THUMBNAIL_LARGE, BASE_DIR
|
||||
from .helper import get_download_link
|
||||
@@ -140,7 +140,6 @@ def convert_to_kobo_timestamp_string(timestamp):
|
||||
|
||||
@kobo.route("/v1/library/sync")
|
||||
@requires_kobo_auth
|
||||
# @download_required
|
||||
def HandleSyncRequest():
|
||||
if not current_user.role_download():
|
||||
log.info("Users need download permissions for syncing library to Kobo reader")
|
||||
@@ -368,7 +367,7 @@ def get_download_url_for_book(book_id, book_format):
|
||||
)
|
||||
return url_for(
|
||||
"kobo.download_book",
|
||||
auth_token=kobo_auth.get_auth_token(),
|
||||
auth_token=get_auth_token(),
|
||||
book_id=book_id,
|
||||
book_format=book_format.lower(),
|
||||
_external=True,
|
||||
@@ -503,6 +502,7 @@ def get_metadata(book):
|
||||
return metadata
|
||||
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
|
||||
@requires_kobo_auth
|
||||
@@ -811,6 +811,8 @@ def HandleStateRequest(book_uuid):
|
||||
|
||||
ub.session.merge(kobo_reading_state)
|
||||
ub.session_commit()
|
||||
update_results_response["LastModified"] = convert_to_kobo_timestamp_string(kobo_reading_state.last_modified)
|
||||
update_results_response["PriorityTimestamp"] = convert_to_kobo_timestamp_string(kobo_reading_state.priority_timestamp)
|
||||
return jsonify({
|
||||
"RequestResult": "Success",
|
||||
"UpdateResults": [update_results_response],
|
||||
@@ -887,14 +889,21 @@ def get_statistics_response(statistics):
|
||||
return resp
|
||||
|
||||
|
||||
def _clean_progress(value):
|
||||
"""Return progress as int if it's a whole number, preserving Kobo device expectations."""
|
||||
if value is not None and value == int(value):
|
||||
return int(value)
|
||||
return value
|
||||
|
||||
|
||||
def get_current_bookmark_response(current_bookmark):
|
||||
resp = {
|
||||
"LastModified": convert_to_kobo_timestamp_string(current_bookmark.last_modified),
|
||||
}
|
||||
if current_bookmark.progress_percent:
|
||||
resp["ProgressPercent"] = current_bookmark.progress_percent
|
||||
if current_bookmark.content_source_progress_percent:
|
||||
resp["ContentSourceProgressPercent"] = current_bookmark.content_source_progress_percent
|
||||
if current_bookmark.progress_percent is not None:
|
||||
resp["ProgressPercent"] = _clean_progress(current_bookmark.progress_percent)
|
||||
if current_bookmark.content_source_progress_percent is not None:
|
||||
resp["ContentSourceProgressPercent"] = _clean_progress(current_bookmark.content_source_progress_percent)
|
||||
if current_bookmark.location_value:
|
||||
resp["Location"] = {
|
||||
"Value": current_bookmark.location_value,
|
||||
@@ -962,6 +971,7 @@ def HandleBookDeletionRequest(book_uuid):
|
||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET", "POST"])
|
||||
@kobo.route("/v1/library/<dummy>/preview", methods=["POST"])
|
||||
def HandleUnimplementedRequest(dummy=None):
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
log.debug("Unimplemented Library Request received: %s (request is forwarded to kobo if configured)",
|
||||
request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
@@ -976,6 +986,9 @@ def HandleUnimplementedRequest(dummy=None):
|
||||
@kobo.route("/v1/analytics/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/assets", methods=["GET"])
|
||||
def HandleUserRequest(dummy=None):
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
log.error("Key: {}".format(limiter.current_limit.key))
|
||||
log.error("Remaining: {}".format(limiter.current_limit.remaining))
|
||||
log.debug("Unimplemented User Request received: %s (request is forwarded to kobo if configured)", request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
@@ -983,6 +996,7 @@ def HandleUserRequest(dummy=None):
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/user/loyalty/benefits", methods=["GET"])
|
||||
def handle_benefits():
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
if config.config_kobo_proxy:
|
||||
return redirect_or_proxy_request()
|
||||
else:
|
||||
@@ -992,6 +1006,7 @@ def handle_benefits():
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/analytics/gettests", methods=["GET", "POST"])
|
||||
def handle_getests():
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
if config.config_kobo_proxy:
|
||||
return redirect_or_proxy_request()
|
||||
else:
|
||||
@@ -1018,6 +1033,7 @@ def handle_getests():
|
||||
@kobo.route("/v1/affiliate", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/deals", methods=["GET", "POST"])
|
||||
def HandleProductsRequest(dummy=None):
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
log.debug("Unimplemented Products Request received: %s (request is forwarded to kobo if configured)",
|
||||
request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
@@ -1047,6 +1063,8 @@ def make_calibre_web_auth_response():
|
||||
@kobo.route("/v1/auth/device", methods=["POST"])
|
||||
@requires_kobo_auth
|
||||
def HandleAuthRequest():
|
||||
log.error(limiter.current_limit)
|
||||
log.error(limiter.current_limit)
|
||||
log.debug('Kobo Auth request')
|
||||
if config.config_kobo_proxy:
|
||||
try:
|
||||
@@ -1088,7 +1106,7 @@ def HandleInitRequest():
|
||||
kobo_resources["image_host"] = calibre_web_url
|
||||
kobo_resources["image_url_quality_template"] = unquote(calibre_web_url +
|
||||
url_for("kobo.HandleCoverImageRequest",
|
||||
auth_token=kobo_auth.get_auth_token(),
|
||||
auth_token=get_auth_token(),
|
||||
book_uuid="{ImageId}",
|
||||
width="{width}",
|
||||
height="{height}",
|
||||
@@ -1096,15 +1114,17 @@ def HandleInitRequest():
|
||||
isGreyscale='isGreyscale'))
|
||||
kobo_resources["image_url_template"] = unquote(calibre_web_url +
|
||||
url_for("kobo.HandleCoverImageRequest",
|
||||
auth_token=kobo_auth.get_auth_token(),
|
||||
auth_token=get_auth_token(),
|
||||
book_uuid="{ImageId}",
|
||||
width="{width}",
|
||||
height="{height}",
|
||||
isGreyscale='false'))
|
||||
kobo_resources["library_sync"] = calibre_web_url + url_for("kobo.HandleSyncRequest",
|
||||
auth_token=kobo_auth.get_auth_token())
|
||||
else:
|
||||
kobo_resources["image_host"] = url_for("web.index", _external=True).strip("/")
|
||||
kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest",
|
||||
auth_token=kobo_auth.get_auth_token(),
|
||||
auth_token=get_auth_token(),
|
||||
book_uuid="{ImageId}",
|
||||
width="{width}",
|
||||
height="{height}",
|
||||
@@ -1112,12 +1132,15 @@ def HandleInitRequest():
|
||||
isGreyscale='isGreyscale',
|
||||
_external=True))
|
||||
kobo_resources["image_url_template"] = unquote(url_for("kobo.HandleCoverImageRequest",
|
||||
auth_token=kobo_auth.get_auth_token(),
|
||||
auth_token=get_auth_token(),
|
||||
book_uuid="{ImageId}",
|
||||
width="{width}",
|
||||
height="{height}",
|
||||
isGreyscale='false',
|
||||
_external=True))
|
||||
kobo_resources["library_sync"] = url_for("kobo.HandleSyncRequest",
|
||||
auth_token=kobo_auth.get_auth_token(),
|
||||
_external=True)
|
||||
|
||||
response = make_response(jsonify({"Resources": kobo_resources}))
|
||||
response.headers["x-kobo-apitoken"] = "e30="
|
||||
|
||||
+5
-9
@@ -67,7 +67,6 @@ from functools import wraps
|
||||
from flask import g, Blueprint, abort, request
|
||||
from .cw_login import login_user, current_user
|
||||
from flask_babel import gettext as _
|
||||
from flask_limiter import RateLimitExceeded
|
||||
|
||||
from . import logger, config, calibre_db, db, helper, ub, lm, limiter
|
||||
from .render_template import render_title_template
|
||||
@@ -82,6 +81,8 @@ kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
|
||||
@kobo_auth.route("/generate_auth_token/<int:user_id>")
|
||||
@user_login_required
|
||||
def generate_auth_token(user_id):
|
||||
if current_user.id != user_id and not current_user.role_admin():
|
||||
abort(403)
|
||||
warning = False
|
||||
host_list = request.host.rsplit(':')
|
||||
if len(host_list) == 1:
|
||||
@@ -124,6 +125,8 @@ def generate_auth_token(user_id):
|
||||
@kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
|
||||
@user_login_required
|
||||
def delete_auth_token(user_id):
|
||||
if current_user.id != user_id and not current_user.role_admin():
|
||||
abort(403)
|
||||
# Invalidate any previously generated Kobo Auth token for this user
|
||||
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
||||
.filter(ub.RemoteAuthToken.token_type==1).delete()
|
||||
@@ -154,13 +157,6 @@ def requires_kobo_auth(f):
|
||||
def inner(*args, **kwargs):
|
||||
auth_token = get_auth_token()
|
||||
if auth_token is not None:
|
||||
try:
|
||||
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)
|
||||
@@ -169,7 +165,7 @@ def requires_kobo_auth(f):
|
||||
)
|
||||
if user is not None:
|
||||
login_user(user)
|
||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
return f(*args, **kwargs)
|
||||
log.debug("Received Kobo request without a recognizable auth token.")
|
||||
return abort(401)
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@ from flask import request
|
||||
|
||||
|
||||
def request_username():
|
||||
return request.authorization.username
|
||||
return request.authorization.username if request.authorization else ""
|
||||
|
||||
|
||||
def main():
|
||||
@@ -66,8 +66,8 @@ def main():
|
||||
app.register_blueprint(tasks)
|
||||
app.register_blueprint(web)
|
||||
app.register_blueprint(basic)
|
||||
app.register_blueprint(opds)
|
||||
limiter.limit("3/minute", key_func=request_username)(opds)
|
||||
app.register_blueprint(opds)
|
||||
app.register_blueprint(jinjia)
|
||||
app.register_blueprint(about)
|
||||
app.register_blueprint(shelf)
|
||||
@@ -77,9 +77,9 @@ def main():
|
||||
app.register_blueprint(gdrive)
|
||||
app.register_blueprint(editbook)
|
||||
if kobo_available:
|
||||
limiter.limit("3/minute", key_func=get_remote_address)(kobo)
|
||||
app.register_blueprint(kobo)
|
||||
app.register_blueprint(kobo_auth)
|
||||
limiter.limit("3/minute", key_func=get_remote_address)(kobo)
|
||||
if oauth_available:
|
||||
app.register_blueprint(oauth)
|
||||
success = web_server.start()
|
||||
|
||||
@@ -57,7 +57,7 @@ class ComicVine(Metadata):
|
||||
result.raise_for_status()
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
return None
|
||||
return []
|
||||
for result in result.json()["results"]:
|
||||
match = self._parse_search_result(
|
||||
result=result, generic_cover=generic_cover, locale=locale
|
||||
|
||||
@@ -155,7 +155,7 @@ class Douban(Metadata):
|
||||
r.raise_for_status()
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
return None
|
||||
return []
|
||||
|
||||
match = MetaRecord(
|
||||
id=id,
|
||||
|
||||
@@ -134,6 +134,14 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider
|
||||
oauth_entry = query.first()
|
||||
# already bind with user, just login
|
||||
if oauth_entry.user:
|
||||
# If a user is already logged in and it's a different account, reject the link
|
||||
# to prevent account takeover via shared OAuth identities
|
||||
if current_user and current_user.is_authenticated and oauth_entry.user_id != current_user.id:
|
||||
flash(_("This %(oauth)s account is already linked to a different user",
|
||||
oauth=provider_name), category="error")
|
||||
log.warning("User %s tried to link OAuth account already bound to user %s",
|
||||
current_user.id, oauth_entry.user_id)
|
||||
return redirect(url_for('web.profile'))
|
||||
login_user(oauth_entry.user)
|
||||
log.debug("You are now logged in as: '%s'", oauth_entry.user.name)
|
||||
flash(_("Success! You are now logged in as: %(nickname)s", nickname=oauth_entry.user.name),
|
||||
|
||||
@@ -258,7 +258,6 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
||||
|
||||
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
|
||||
calibre_db.create_functions()
|
||||
# calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase)
|
||||
query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
|
||||
q = query.outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book)\
|
||||
.outerjoin(db.Series)\
|
||||
|
||||
@@ -135,5 +135,5 @@ def metadata_search():
|
||||
if active.get(c.__id__, True)
|
||||
}
|
||||
for future in concurrent.futures.as_completed(meta):
|
||||
data.extend([asdict(x) for x in future.result() if x])
|
||||
data.extend([asdict(x) for x in (future.result() or []) if x])
|
||||
return make_response(jsonify(data))
|
||||
|
||||
+6
-4
@@ -41,7 +41,7 @@ except ImportError:
|
||||
VERSION = 'Tornado ' + _version
|
||||
_GEVENT = False
|
||||
|
||||
from . import logger
|
||||
from . import logger, constants
|
||||
|
||||
|
||||
log = logger.create()
|
||||
@@ -216,9 +216,10 @@ class WebServer(object):
|
||||
try:
|
||||
sock, output = self._make_gevent_listener()
|
||||
log.info('Starting Gevent server on %s', output)
|
||||
# Also print to stdout so interactive terminals show a clear success message
|
||||
try:
|
||||
print(f"Calibre-Web: server started on {output}")
|
||||
# Also print to stdout so interactive terminals show a clear success message
|
||||
if constants.APP_MODE not in ['development', 'test']:
|
||||
print(f"Calibre-Web: server started on {output}")
|
||||
except Exception:
|
||||
print(f"Calibre-Web: error {output}")
|
||||
pass
|
||||
@@ -274,7 +275,8 @@ class WebServer(object):
|
||||
log.info('Starting Tornado server on %s', output)
|
||||
# Also print to stdout so interactive terminals show a clear success message
|
||||
try:
|
||||
print(f"Calibre-Web: server started on {output}")
|
||||
if constants.APP_MODE not in ['development', 'test']:
|
||||
print(f"Calibre-Web: server started on {output}")
|
||||
except Exception:
|
||||
print(f"Calibre-Web: error {output}")
|
||||
pass
|
||||
|
||||
@@ -92,7 +92,7 @@ class my_GoodreadsRequest(GoodreadsRequest):
|
||||
if resp.status_code != 200:
|
||||
raise GoodreadsRequestException(resp.reason, self.path)
|
||||
if self.req_format == 'xml':
|
||||
root = etree.fromstring(resp.content)
|
||||
root = etree.fromstring(resp.content, parser=etree.XMLParser(resolve_entities=False, no_network=True))
|
||||
data_dict = etree_to_dict(root)
|
||||
|
||||
return data_dict['GoodreadsResponse']
|
||||
|
||||
@@ -31,6 +31,16 @@ except ImportError:
|
||||
log = logger.create()
|
||||
|
||||
|
||||
def _escape_ldap_filter(s):
|
||||
"""Escape special characters for safe use in LDAP filter strings (RFC 4515)."""
|
||||
s = s.replace('\\', '\\5c')
|
||||
s = s.replace('*', '\\2a')
|
||||
s = s.replace('(', '\\28')
|
||||
s = s.replace(')', '\\29')
|
||||
s = s.replace('\x00', '\\00')
|
||||
return s
|
||||
|
||||
|
||||
class LDAPLogger(object):
|
||||
|
||||
@staticmethod
|
||||
@@ -148,9 +158,11 @@ def bind_user(username, password):
|
||||
|
||||
:returns: True if login succeeded, False if login failed, None if server unavailable.
|
||||
'''
|
||||
# Escape LDAP special characters to prevent LDAP injection in search filters
|
||||
safe_username = _escape_ldap_filter(username)
|
||||
try:
|
||||
if _ldap.get_object_details(username):
|
||||
result = _ldap.bind_user(username, password)
|
||||
if _ldap.get_object_details(safe_username):
|
||||
result = _ldap.bind_user(safe_username, password)
|
||||
log.debug("LDAP login '%s': %r", username, result)
|
||||
return result is not None, None
|
||||
return None, None # User not found
|
||||
|
||||
@@ -308,6 +308,9 @@ def order_shelf(shelf_id):
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
if shelf and check_shelf_view_permissions(shelf):
|
||||
if request.method == "POST":
|
||||
if not check_shelf_edit_permissions(shelf):
|
||||
flash(_("Sorry you are not allowed to edit this shelf"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
to_save = request.form.to_dict()
|
||||
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by(
|
||||
ub.BookShelf.order.asc()).all()
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
#add-to-shelves.dropdown-menu, #remove-from-shelves.dropdown-menu {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tooltip.bottom .tooltip-inner {
|
||||
font-size: 13px;
|
||||
|
||||
@@ -103,11 +103,11 @@ $(function () {
|
||||
});
|
||||
}
|
||||
else {
|
||||
$("#meta-info").html("<p class=\"text-danger\">" + msg.no_result + "!</p>" + $("#meta-info")[0].innerHTML)
|
||||
$("#meta-info").html("<p class=\"text-danger\">" + msg.no_result + "</p>" + $("#meta-info")[0].innerHTML)
|
||||
}
|
||||
},
|
||||
error: function error() {
|
||||
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
|
||||
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "</p>" + $("#meta-info")[0].innerHTML);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<h2>Details</h2>
|
||||
|
||||
{% if entry.series|length > 0 %}
|
||||
<p>{{ _("Book %(index)s of %(range)s", index=entry.series_index | formatfloat(2), range=(entry.series[0].name)|safe) }}</p>
|
||||
<p>{{ _("Book %(index)s of %(range)s", index=entry.series_index|formatfloat(2), range=entry.series[0].name|e) }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.languages|length > 0 %}
|
||||
@@ -74,7 +74,7 @@
|
||||
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0 %}
|
||||
<div>
|
||||
<h2 id="decription">{{ _('Description:') }}</h2>
|
||||
{{ entry.comments[0].text|safe }}
|
||||
{{ entry.comments[0].text|clean_string|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -232,7 +232,7 @@
|
||||
{% elif c.datatype == 'datetime' %}
|
||||
{{ column.value|formatdate }}
|
||||
{% elif c.datatype == 'comments' %}
|
||||
{{ column.value|safe }}
|
||||
{{ column.value|clean_string|safe }}
|
||||
{% elif c.datatype == 'series' %}
|
||||
{{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }}
|
||||
{% elif c.datatype == 'text' %}
|
||||
@@ -284,7 +284,7 @@
|
||||
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0 %}
|
||||
<div class="comments">
|
||||
<h3 id="decription">{{ _('Description:') }}</h3>
|
||||
{{ entry.comments[0].text|safe }}
|
||||
{{ entry.comments[0].text|clean_string|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -294,7 +294,7 @@
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if current_user.shelf.all() or g.shelves_access %}
|
||||
<div id="shelf-actions" class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group" aria-label="Add to shelves">
|
||||
<div class="btn-group dropup" role="group" aria-label="Add to shelves">
|
||||
<button id="add-to-shelf" type="button"
|
||||
class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
{% elif c.datatype == 'datetime' %}
|
||||
{{ column.value|formatdate }}
|
||||
{% elif c.datatype == 'comments' %}
|
||||
{{ column.value|safe }}
|
||||
{{ column.value|clean_string|safe }}
|
||||
{% elif c.datatype == 'series' %}
|
||||
{{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }}
|
||||
{% elif c.datatype == 'text' %}
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
{% elif c.datatype == 'datetime' %}
|
||||
{{ column.value|formatdate }}
|
||||
{% elif c.datatype == 'comments' %}
|
||||
{{column.value|safe}}
|
||||
{{column.value|clean_string|safe}}
|
||||
{% elif c.datatype == 'series' %}
|
||||
{{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }}
|
||||
{% elif c.datatype == 'text' %}
|
||||
@@ -175,7 +175,7 @@
|
||||
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0%}
|
||||
<div class="comments">
|
||||
<h3 id="decription">{{_('Description:')}}</h3>
|
||||
{{entry.comments[0].text|safe}}
|
||||
{{entry.comments[0].text|clean_string|safe}}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -537,7 +537,7 @@ class RemoteAuthToken(Base):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.auth_token = (hexlify(os.urandom(4))).decode('utf-8')
|
||||
self.auth_token = (hexlify(os.urandom(16))).decode('utf-8')
|
||||
self.expiration = datetime.now() + timedelta(minutes=10) # 10 min from now
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
@@ -43,14 +43,14 @@ def verify_password(username, password):
|
||||
if config.config_login_type == constants.LOGIN_LDAP and services.ldap:
|
||||
login_result, error = services.ldap.bind_user(user.name, password)
|
||||
if login_result:
|
||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
return user
|
||||
if error is not None:
|
||||
log.error(error)
|
||||
else:
|
||||
limiter.check()
|
||||
# limiter.check()
|
||||
if check_password_hash(str(user.password), password):
|
||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
return user
|
||||
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
|
||||
log.warning('OPDS Login failed for user "%s" IP-address: %s', username, ip_address)
|
||||
@@ -120,7 +120,7 @@ def load_user_from_reverse_proxy_header(req):
|
||||
if rp_header_username:
|
||||
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == rp_header_username.lower()).first()
|
||||
if user:
|
||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
return user
|
||||
return None
|
||||
|
||||
|
||||
+9
-27
@@ -30,7 +30,6 @@ from flask import session as flask_session
|
||||
from flask_babel import gettext as _
|
||||
from flask_babel import get_locale
|
||||
from .cw_login import login_user, logout_user, current_user
|
||||
from flask_limiter import RateLimitExceeded
|
||||
from flask_limiter.util import get_remote_address
|
||||
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
|
||||
from sqlalchemy.sql.expression import text, func, false, not_, and_, or_
|
||||
@@ -39,7 +38,7 @@ from sqlalchemy.sql.functions import coalesce
|
||||
from werkzeug.datastructures import Headers
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
from . import constants, logger, isoLanguages, services
|
||||
from . import constants, logger, isoLanguages, services, limiter
|
||||
from . import db, ub, config, app
|
||||
from . import calibre_db, kobo_sync_status
|
||||
from .search import render_search_results, render_adv_search_results
|
||||
@@ -55,7 +54,6 @@ from .usermanagement import login_required_if_no_ano
|
||||
from .kobo_sync_status import remove_synced_book
|
||||
from .render_template import render_title_template
|
||||
from .kobo_sync_status import change_archived_books
|
||||
from . import limiter
|
||||
from .services.worker import WorkerThread
|
||||
from .tasks_status import render_task_status
|
||||
from .usermanagement import user_login_required
|
||||
@@ -303,7 +301,6 @@ def get_matching_tags():
|
||||
tag_dict = {'tags': []}
|
||||
q = calibre_db.session.query(db.Books).filter(calibre_db.common_filters(True))
|
||||
calibre_db.create_functions()
|
||||
# calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase)
|
||||
author_input = request.args.get('authors') or ''
|
||||
title_input = request.args.get('title') or ''
|
||||
include_tag_inputs = request.args.getlist('include_tag') or ''
|
||||
@@ -714,7 +711,7 @@ def render_language_books(page, name, order):
|
||||
lang_name = _("None")
|
||||
except KeyError:
|
||||
abort(404)
|
||||
if name == "none":
|
||||
if name.lower() == "none":
|
||||
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
|
||||
db.Books,
|
||||
db.Languages.lang_code == None,
|
||||
@@ -845,7 +842,8 @@ def list_books():
|
||||
order = request.args.get("order", "").lower()
|
||||
state = None
|
||||
join = tuple()
|
||||
|
||||
if not order in ["asc", "desc", ""]:
|
||||
order = "asc"
|
||||
if sort_param == "state":
|
||||
state = json.loads(request.args.get("state", "[]"))
|
||||
elif sort_param == "tags":
|
||||
@@ -1028,7 +1026,7 @@ def series_list():
|
||||
.count())
|
||||
if no_series_count:
|
||||
entries.append([db.Category(_("None"), "-1"), no_series_count])
|
||||
entries = sorted(entries, key=lambda x: x[0].name.lower(), reverse=not order_no)
|
||||
entries = sorted(entries, key=lambda x: (x[0].sort or x[0].name).lower(), reverse=not order_no)
|
||||
return render_title_template('list.html',
|
||||
entries=entries,
|
||||
folder='web.books_list',
|
||||
@@ -1197,7 +1195,9 @@ def get_robots():
|
||||
@viewer_required
|
||||
def serve_book(book_id, book_format, anyname):
|
||||
book_format = book_format.split(".")[0]
|
||||
book = calibre_db.get_book(book_id)
|
||||
book = calibre_db.get_filtered_book(book_id)
|
||||
if not book:
|
||||
return "File not in Database"
|
||||
data = calibre_db.get_book_format(book_id, book_format.upper())
|
||||
if not data:
|
||||
return "File not in Database"
|
||||
@@ -1285,15 +1285,6 @@ def register_post():
|
||||
if not config.config_public_reg:
|
||||
abort(404)
|
||||
to_save = request.form.to_dict()
|
||||
try:
|
||||
limiter.check()
|
||||
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():
|
||||
@@ -1358,7 +1349,7 @@ def register():
|
||||
def handle_login_user(user, remember, message, category):
|
||||
login_user(user, remember=remember)
|
||||
flash(message, category=category)
|
||||
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
|
||||
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
|
||||
return redirect(get_redirect_location(request.form.get('next', None), "web.index"))
|
||||
|
||||
|
||||
@@ -1392,15 +1383,6 @@ def login():
|
||||
def login_post():
|
||||
form = request.form.to_dict()
|
||||
username = strip_whitespaces(form.get('username', "")).lower().replace("\n","").replace("\r","")
|
||||
try:
|
||||
limiter.check()
|
||||
except RateLimitExceeded:
|
||||
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'))
|
||||
if config.config_login_type == constants.LOGIN_LDAP and not services.ldap:
|
||||
|
||||
+232
-230
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
# GDrive Integration
|
||||
google-api-python-client>=2.73.00,<2.200.0
|
||||
gevent>20.6.0,<24.12.0
|
||||
greenlet>=0.4.17,<3.3.0
|
||||
httplib2>=0.9.2,<0.23.0
|
||||
gevent>20.6.0,<25.9.2
|
||||
greenlet>=0.4.17,<3.4.0
|
||||
httplib2>=0.9.2,<0.32.0
|
||||
oauth2client>=4.0.0,<4.1.4
|
||||
uritemplate>=3.0.0,<4.3.0
|
||||
pyasn1-modules>=0.0.8,<0.7.0
|
||||
@@ -21,19 +21,19 @@ python-Levenshtein>=0.12.0,<0.28.0
|
||||
|
||||
# ldap login
|
||||
python-ldap>=3.0.0,<3.5.0
|
||||
Flask-SimpleLDAP>=1.4.0,<2.1.0
|
||||
Flask-SimpleLDAP>=1.4.0,<2.2.0
|
||||
|
||||
# oauth
|
||||
Flask-Dance>=2.0.0,<7.2.0
|
||||
SQLAlchemy-Utils>=0.33.5,<0.42.0
|
||||
SQLAlchemy-Utils>=0.33.5,<0.43.0
|
||||
|
||||
# metadata extraction
|
||||
rarfile>=3.2,<5.0
|
||||
scholarly>=1.2.0,<1.8
|
||||
markdown2>=2.0.0,<2.6.0
|
||||
html2text>=2020.1.16,<2025.2.26
|
||||
html2text>=2020.1.16,<2025.4.16
|
||||
python-dateutil>=2.1,<2.10.0
|
||||
beautifulsoup4>=4.0.1,<4.14.0
|
||||
beautifulsoup4>=4.0.1,<4.15.0
|
||||
faust-cchardet>=2.1.18,<2.1.20
|
||||
py7zr>=0.15.0,<0.21.0
|
||||
mutagen>=1.40.0,<1.50.0
|
||||
|
||||
+21
-22
@@ -7,10 +7,10 @@ name = "calibreweb"
|
||||
description = "Web app for browsing, reading and downloading eBooks stored in a Calibre database."
|
||||
authors = [{name = "@OzzieIsaacs", email = "Ozzie.Fernandez.Isaacs@googlemail.com"}]
|
||||
maintainers = [{name = "@OzzieIsaacs"}]
|
||||
license = {text = "GPLv3+"}
|
||||
license = "GPL-3.0-or-later"
|
||||
license-files = ["LICENSE"]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"License :: OSI Approved :: GNU Affero General Public License v3",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
@@ -18,6 +18,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
keywords = [
|
||||
@@ -33,27 +34,27 @@ dependencies = [
|
||||
"Flask-Principal>=0.3.2,<0.5.1",
|
||||
"Flask>=1.0.2,<3.2.0",
|
||||
"iso-639>=0.4.5,<0.5.0;python_version<'3.12'",
|
||||
"pycountry>=20.0.0,<25.0.0;python_version>='3.12'",
|
||||
"PyPDF>=6.1.3,<6.5.0",
|
||||
"pycountry>=20.0.0,<27.0.0;python_version>='3.12'",
|
||||
"PyPDF>=6.1.3,<6.11.0",
|
||||
"pytz>=2016.10",
|
||||
"requests>=2.32.0,<2.33.0",
|
||||
"SQLAlchemy>=1.3.0,<2.1.0",
|
||||
"tornado>=6.4.2,<6.6",
|
||||
"Wand>=0.4.4,<0.7.0",
|
||||
"unidecode>=0.04.19,<1.4.0",
|
||||
"lxml>=4.9.1,<5.4.0",
|
||||
"Wand>=0.4.4,<0.8.0",
|
||||
"unidecode>=0.04.19,<1.5.0",
|
||||
"lxml>=4.9.1,<6.2.0",
|
||||
"flask-wtf>=0.14.2,<1.3.0",
|
||||
"chardet>=3.0.0,<5.3.0",
|
||||
"netifaces-plus>=0.12.0,<0.13.0",
|
||||
"urllib3>=1.22,<3.0",
|
||||
"Flask-Limiter>=2.3.0,<3.13.0",
|
||||
"regex>=2022.3.2,<2025.3.20",
|
||||
"bleach>=6.0.0,<6.3.0",
|
||||
"Flask-Limiter>=2.3.0,<4.2.0",
|
||||
"regex>=2022.3.2,<2026.1.16",
|
||||
"bleach>=6.0.0,<6.4.0",
|
||||
"python-magic>=0.4.27,<0.5.0",
|
||||
"python-magic-bin>=0.4.0,<0.5.0;sys_platform=='win32'",
|
||||
"flask-httpAuth>=4.4.0,<5.0.0",
|
||||
"cryptography>=39.0.0,<45.0.0",
|
||||
"certifi>=2024.7.4,<2025.8.24",
|
||||
"cryptography>=39.0.0,<48.0.0",
|
||||
"certifi>=2024.7.4,<2026.1.5",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
@@ -71,9 +72,9 @@ content-type = "text/markdown"
|
||||
[project.optional-dependencies]
|
||||
gdrive = [
|
||||
"google-api-python-client>=2.73.00,<2.200.0",
|
||||
"gevent>20.6.0,<24.12.0",
|
||||
"greenlet>=0.4.17,<3.3.0",
|
||||
"httplib2>=0.9.2,<0.23.0",
|
||||
"gevent>20.6.0,<25.9.2",
|
||||
"greenlet>=0.4.17,<3.4.0",
|
||||
"httplib2>=0.9.2,<0.32.0",
|
||||
"oauth2client>=4.0.0,<4.1.4",
|
||||
"uritemplate>=3.0.0,<4.3.0",
|
||||
"pyasn1-modules>=0.0.8,<0.7.0",
|
||||
@@ -92,19 +93,19 @@ goodreads = [
|
||||
]
|
||||
ldap = [
|
||||
"python-ldap>=3.0.0,<3.5.0",
|
||||
"Flask-SimpleLDAP>=1.4.0,<2.1.0",
|
||||
"Flask-SimpleLDAP>=1.4.0,<2.2.0",
|
||||
]
|
||||
oauth = [
|
||||
"Flask-Dance>=2.0.0,<7.2.0",
|
||||
"SQLAlchemy-Utils>=0.33.5,<0.42.0",
|
||||
"SQLAlchemy-Utils>=0.33.5,<0.43.0",
|
||||
]
|
||||
metadata = [
|
||||
"rarfile>=3.2,<5.0",
|
||||
"scholarly>=1.2.0,<1.8",
|
||||
"markdown2>=2.0.0,<2.6.0",
|
||||
"html2text>=2020.1.16,<2025.2.26",
|
||||
"html2text>=2020.1.16,<2025.4.16",
|
||||
"python-dateutil>=2.1,<2.10.0",
|
||||
"beautifulsoup4>=4.0.1,<4.14.0",
|
||||
"beautifulsoup4>=4.0.1,<4.15.0",
|
||||
"faust-cchardet>=2.1.18,<2.1.20",
|
||||
"py7zr>=0.15.0,<0.21.0",
|
||||
"mutagen>=1.40.0,<1.50.0",
|
||||
@@ -119,12 +120,10 @@ kobo = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
cps = "calibreweb:main"
|
||||
cps = "calibreweb.__main__:main"
|
||||
|
||||
[tool.setuptools]
|
||||
include-package-data = true
|
||||
license-files = ["LICENSE"]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "calibreweb.cps.constants.STABLE_VERSION"}
|
||||
|
||||
|
||||
+10
-10
@@ -4,24 +4,24 @@ Flask-Babel>=3.0.0,<4.1.0
|
||||
Flask-Principal>=0.3.2,<0.5.1
|
||||
Flask>=1.0.2,<3.2.0
|
||||
iso-639>=0.4.5,<0.5.0;python_version<'3.12'
|
||||
pycountry>=20.0.0,<25.0.0;python_version>='3.12'
|
||||
PyPDF>=6.1.3,<6.5.0
|
||||
pycountry>=20.0.0,<27.0.0;python_version>='3.12'
|
||||
PyPDF>=6.1.3,<6.11.0
|
||||
pytz>=2016.10
|
||||
requests>=2.32.0,<2.33.0
|
||||
SQLAlchemy>=1.3.0,<2.1.0
|
||||
tornado>=6.4.2,<6.6
|
||||
Wand>=0.4.4,<0.7.0
|
||||
unidecode>=0.04.19,<1.4.0
|
||||
lxml>=4.9.1,<5.4.0
|
||||
Wand>=0.4.4,<0.8.0
|
||||
unidecode>=0.04.19,<1.5.0
|
||||
lxml>=4.9.1,<6.2.0
|
||||
flask-wtf>=0.14.2,<1.3.0
|
||||
chardet>=3.0.0,<5.3.0
|
||||
netifaces-plus>=0.12.0,<0.13.0
|
||||
urllib3>=1.22,<3.0
|
||||
Flask-Limiter>=2.3.0,<3.13.0
|
||||
regex>=2022.3.2,<2025.3.20
|
||||
bleach>=6.0.0,<6.3.0
|
||||
Flask-Limiter>=2.3.0,<4.2.0
|
||||
regex>=2022.3.2,<2026.1.16
|
||||
bleach>=6.0.0,<6.4.0
|
||||
python-magic>=0.4.27,<0.5.0
|
||||
python-magic-bin>=0.4.0,<0.5.0;sys_platform=='win32'
|
||||
flask-httpAuth>=4.4.0,<5.0.0
|
||||
cryptography>=39.0.0,<45.0.0
|
||||
certifi>=2024.7.4,<2025.8.24
|
||||
cryptography>=39.0.0,<48.0.0
|
||||
certifi>=2024.7.4,<2026.1.5
|
||||
|
||||
@@ -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>2026-01-24 20:50:12</p>
|
||||
<p class='text-justify attribute'><strong>Start Time: </strong>2026-05-16 12:48:02</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>2026-01-25 04:15:44</p>
|
||||
<p class='text-justify attribute'><strong>Stop Time: </strong>2026-05-16 20:11:29</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 13 min</p>
|
||||
<p class='text-justify attribute'><strong>Duration: </strong>6h 9 min</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2070,7 +2070,7 @@
|
||||
</div>
|
||||
<div class="text-left pull-left">
|
||||
<pre class="text-left">Traceback (most recent call last):
|
||||
File "/home/ozzie/Development/calibre-web-test/test/test_edit_books_metadata.py", line 87, in test_load_metadata
|
||||
File "/home/ozzie/Development/calibre-web-test/test/test_edit_books_metadata.py", line 90, in test_load_metadata
|
||||
if results[cont]['source'] == 'https://comicvine.gamespot.com/':
|
||||
~~~~~~~^^^^^^
|
||||
IndexError: list index out of range</pre>
|
||||
@@ -2434,11 +2434,11 @@ AssertionError: 30 != 20</pre>
|
||||
|
||||
|
||||
|
||||
<tr id="su" class="passClass">
|
||||
<tr id="su" class="failClass">
|
||||
<td>TestEmbedMetadata</td>
|
||||
<td class="text-center">6</td>
|
||||
<td class="text-center">6</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">4</td>
|
||||
<td class="text-center">2</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">
|
||||
@@ -2466,11 +2466,31 @@ AssertionError: 30 != 20</pre>
|
||||
|
||||
|
||||
|
||||
<tr id='pt23.3' class='hiddenRow bg-success'>
|
||||
<tr id="ft23.3" class="none bg-danger">
|
||||
<td>
|
||||
<div class='testcase'>TestEmbedMetadata - test_download_check_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_ft23.3')">FAIL</a>
|
||||
</div>
|
||||
<!--css div popup start-->
|
||||
<div id="div_ft23.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_ft23.3').style.display='none'"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="text-left pull-left">
|
||||
<pre class="text-left">Traceback (most recent call last):
|
||||
File "/home/ozzie/Development/calibre-web-test/test/test_embed_metadata.py", line 84, in test_download_check_metadata
|
||||
self.assertEqual(20746, len(epub_content))
|
||||
AssertionError: 20746 != 6972</pre>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<!--css div popup end-->
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
@@ -2484,11 +2504,31 @@ AssertionError: 30 != 20</pre>
|
||||
|
||||
|
||||
|
||||
<tr id='pt23.5' class='hiddenRow bg-success'>
|
||||
<tr id="ft23.5" class="none bg-danger">
|
||||
<td>
|
||||
<div class='testcase'>TestEmbedMetadata - test_download_permissions_missing_file</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_ft23.5')">FAIL</a>
|
||||
</div>
|
||||
<!--css div popup start-->
|
||||
<div id="div_ft23.5" 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_ft23.5').style.display='none'"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="text-left pull-left">
|
||||
<pre class="text-left">Traceback (most recent call last):
|
||||
File "/home/ozzie/Development/calibre-web-test/test/test_embed_metadata.py", line 108, in test_download_permissions_missing_file
|
||||
self.assertEqual(20746, len(epub_content))
|
||||
AssertionError: 20746 != 6972</pre>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<!--css div popup end-->
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
@@ -3060,13 +3100,13 @@ AssertionError: 30 != 20</pre>
|
||||
|
||||
|
||||
|
||||
<tr id="su" class="passClass">
|
||||
<tr id="su" class="skipClass">
|
||||
<td>TestSecurity</td>
|
||||
<td class="text-center">6</td>
|
||||
<td class="text-center">6</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">5</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('c33', 6)">Detail</a>
|
||||
</td>
|
||||
@@ -3119,22 +3159,22 @@ AssertionError: 30 != 20</pre>
|
||||
|
||||
|
||||
|
||||
<tr id='pt33.6' class='hiddenRow bg-success'>
|
||||
<tr id='st33.6' class='none bg-warning'>
|
||||
<td>
|
||||
<div class='testcase'>TestSecurity - test_x_forwarded_host</div>
|
||||
</td>
|
||||
<td colspan='6' align='center'>PASS</td>
|
||||
<td colspan='6' align='center'>SKIP</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
|
||||
<tr id="su" class="passClass">
|
||||
<tr id="su" class="errorClass">
|
||||
<td>TestCalibreWebListOrders</td>
|
||||
<td class="text-center">16</td>
|
||||
<td class="text-center">16</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">12</td>
|
||||
<td class="text-center">2</td>
|
||||
<td class="text-center">2</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">
|
||||
<a onclick="showClassDetail('c34', 16)">Detail</a>
|
||||
@@ -3188,38 +3228,120 @@ AssertionError: 30 != 20</pre>
|
||||
|
||||
|
||||
|
||||
<tr id='pt34.6' class='hiddenRow bg-success'>
|
||||
<tr id="ft34.6" class="none bg-danger">
|
||||
<td>
|
||||
<div class='testcase'>TestCalibreWebListOrders - test_language_click_none</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_ft34.6')">FAIL</a>
|
||||
</div>
|
||||
<!--css div popup start-->
|
||||
<div id="div_ft34.6" 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_ft34.6').style.display='none'"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="text-left pull-left">
|
||||
<pre class="text-left">Traceback (most recent call last):
|
||||
File "/home/ozzie/Development/calibre-web-test/test/test_list_orders.py", line 548, in test_language_click_none
|
||||
self.assertEqual(int(element['count']), len(books[1]))
|
||||
AssertionError: 5 != 0</pre>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<!--css div popup end-->
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
<tr id='pt34.7' class='hiddenRow bg-success'>
|
||||
<tr id="et34.7" class="none bg-info">
|
||||
<td>
|
||||
<div class='testcase'>TestCalibreWebListOrders - test_order_authors_all_links</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_et34.7')">ERROR</a>
|
||||
</div>
|
||||
<!--css div popup start-->
|
||||
<div id="div_et34.7" 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_et34.7').style.display='none'"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="text-left pull-left">
|
||||
<pre class="text-left">Traceback (most recent call last):
|
||||
File "/home/ozzie/Development/calibre-web-test/test/test_list_orders.py", line 434, in test_order_authors_all_links
|
||||
self.check_element_on_page((By.ID, "asc")).click()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
AttributeError: 'bool' object has no attribute 'click'</pre>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<!--css div popup end-->
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
<tr id='pt34.8' class='hiddenRow bg-success'>
|
||||
<tr id="ft34.8" class="none bg-danger">
|
||||
<td>
|
||||
<div class='testcase'>TestCalibreWebListOrders - test_order_series_all_links</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_ft34.8')">FAIL</a>
|
||||
</div>
|
||||
<!--css div popup start-->
|
||||
<div id="div_ft34.8" 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_ft34.8').style.display='none'"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="text-left pull-left">
|
||||
<pre class="text-left">Traceback (most recent call last):
|
||||
File "/home/ozzie/Development/calibre-web-test/test/test_list_orders.py", line 367, in test_order_series_all_links
|
||||
self.assertTrue(list)
|
||||
AssertionError: False is not true</pre>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<!--css div popup end-->
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
<tr id='pt34.9' class='hiddenRow bg-success'>
|
||||
<tr id="et34.9" class="none bg-info">
|
||||
<td>
|
||||
<div class='testcase'>TestCalibreWebListOrders - test_publisher_click_none</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_et34.9')">ERROR</a>
|
||||
</div>
|
||||
<!--css div popup start-->
|
||||
<div id="div_et34.9" 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_et34.9').style.display='none'"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="text-left pull-left">
|
||||
<pre class="text-left">Traceback (most recent call last):
|
||||
File "/home/ozzie/Development/calibre-web-test/test/test_list_orders.py", line 557, in test_publisher_click_none
|
||||
self.assertTrue(int(element['count']) > 0)
|
||||
^^^^^^^
|
||||
UnboundLocalError: cannot access local variable 'element' where it is not associated with a value</pre>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<!--css div popup end-->
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
@@ -4040,43 +4162,45 @@ AssertionError: 30 != 20</pre>
|
||||
|
||||
|
||||
|
||||
<tr id="su" class="passClass">
|
||||
<td>TestPipInstall</td>
|
||||
<td class="text-center">3</td>
|
||||
<td class="text-center">3</td>
|
||||
<tr id="su" class="errorClass">
|
||||
<td>_ErrorHolder</td>
|
||||
<td class="text-center">1</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('c42', 3)">Detail</a>
|
||||
<a onclick="showClassDetail('c42', 1)">Detail</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
<tr id='pt42.1' class='hiddenRow bg-success'>
|
||||
<tr id="et42.1" class="none bg-info">
|
||||
<td>
|
||||
<div class='testcase'>TestPipInstall - test_command_start</div>
|
||||
<div class='testcase'>setUpClass (test_pip_install)</div>
|
||||
</td>
|
||||
<td colspan='6' align='center'>PASS</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
<tr id='pt42.2' class='hiddenRow bg-success'>
|
||||
<td>
|
||||
<div class='testcase'>TestPipInstall - test_foldername_database_location</div>
|
||||
<td colspan='6'>
|
||||
<div class="text-center">
|
||||
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et42.1')">ERROR</a>
|
||||
</div>
|
||||
<!--css div popup start-->
|
||||
<div id="div_et42.1" 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_et42.1').style.display='none'"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="text-left pull-left">
|
||||
<pre class="text-left">Traceback (most recent call last):
|
||||
File "/home/ozzie/Development/calibre-web-test/test/test_pip_install.py", line 42, in setUpClass
|
||||
raise FileNotFoundError("Whl file not found for pip, aborting pip install test")
|
||||
FileNotFoundError: Whl file not found for pip, aborting pip install test</pre>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<!--css div popup end-->
|
||||
</td>
|
||||
<td colspan='6' align='center'>PASS</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
<tr id='pt42.3' class='hiddenRow bg-success'>
|
||||
<td>
|
||||
<div class='testcase'>TestPipInstall - test_module_start</div>
|
||||
</td>
|
||||
<td colspan='6' align='center'>PASS</td>
|
||||
</tr>
|
||||
|
||||
|
||||
@@ -4684,11 +4808,11 @@ AssertionError: 30 != 20</pre>
|
||||
|
||||
|
||||
|
||||
<tr id="su" class="failClass">
|
||||
<tr id="su" class="skipClass">
|
||||
<td>TestThumbnails</td>
|
||||
<td class="text-center">8</td>
|
||||
<td class="text-center">6</td>
|
||||
<td class="text-center">1</td>
|
||||
<td class="text-center">7</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">0</td>
|
||||
<td class="text-center">1</td>
|
||||
<td class="text-center">
|
||||
@@ -4761,31 +4885,11 @@ AssertionError: 30 != 20</pre>
|
||||
|
||||
|
||||
|
||||
<tr id="ft52.8" class="none bg-danger">
|
||||
<tr id='pt52.8' class='hiddenRow bg-success'>
|
||||
<td>
|
||||
<div class='testcase'>TestThumbnails - test_sideloaded_book</div>
|
||||
</td>
|
||||
<td colspan='6'>
|
||||
<div class="text-center">
|
||||
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft52.8')">FAIL</a>
|
||||
</div>
|
||||
<!--css div popup start-->
|
||||
<div id="div_ft52.8" 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_ft52.8').style.display='none'"><span
|
||||
aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="text-left pull-left">
|
||||
<pre class="text-left">Traceback (most recent call last):
|
||||
File "/home/ozzie/Development/calibre-web-test/test/test_thumbnails.py", line 317, in test_sideloaded_book
|
||||
self.assertAlmostEqual(diff(BytesIO(list_cover), BytesIO(old_list_cover), delete_diff_file=True), 0.0,
|
||||
AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182775 difference)</pre>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<!--css div popup end-->
|
||||
</td>
|
||||
<td colspan='6' align='center'>PASS</td>
|
||||
</tr>
|
||||
|
||||
|
||||
@@ -5991,11 +6095,11 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr id='total_row' class="text-center bg-grey">
|
||||
<td>Total</td>
|
||||
<td>538</td>
|
||||
<td>528</td>
|
||||
<td>2</td>
|
||||
<td>1</td>
|
||||
<td>7</td>
|
||||
<td>536</td>
|
||||
<td>519</td>
|
||||
<td>5</td>
|
||||
<td>4</td>
|
||||
<td>8</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -6023,7 +6127,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>Platform</th>
|
||||
<td>Linux 6.8.0-90-generic #91-Ubuntu SMP PREEMPT_DYNAMIC Tue Nov 18 14:14:30 UTC 2025 x86_64 x86_64</td>
|
||||
<td>Linux 6.17.0-29-generic #29~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon May 11 10:30:58 UTC 2 x86_64 x86_64</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
@@ -6041,19 +6145,19 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>babel</th>
|
||||
<td>2.17.0</td>
|
||||
<td>2.18.0</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>bleach</th>
|
||||
<td>6.2.0</td>
|
||||
<td>6.3.0</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>certifi</th>
|
||||
<td>2025.8.3</td>
|
||||
<td>2026.1.4</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
@@ -6065,13 +6169,13 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>cryptography</th>
|
||||
<td>44.0.3</td>
|
||||
<td>47.0.0</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Flask</th>
|
||||
<td>3.1.2</td>
|
||||
<td>3.1.3</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
@@ -6083,13 +6187,13 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>Flask-HTTPAuth</th>
|
||||
<td>4.8.0</td>
|
||||
<td>4.8.1</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Flask-Limiter</th>
|
||||
<td>3.12</td>
|
||||
<td>4.1.1</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
@@ -6107,7 +6211,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>greenlet</th>
|
||||
<td>3.3.1</td>
|
||||
<td>3.5.0</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
@@ -6119,7 +6223,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>lxml</th>
|
||||
<td>5.3.2</td>
|
||||
<td>6.1.0</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
@@ -6131,13 +6235,13 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>pycountry</th>
|
||||
<td>24.6.1</td>
|
||||
<td>26.2.16</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>pypdf</th>
|
||||
<td>6.4.2</td>
|
||||
<td>6.10.2</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
@@ -6149,13 +6253,13 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>pytz</th>
|
||||
<td>2025.2</td>
|
||||
<td>2026.2</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>regex</th>
|
||||
<td>2024.11.6</td>
|
||||
<td>2026.1.15</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
@@ -6167,43 +6271,43 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>SQLAlchemy</th>
|
||||
<td>2.0.46</td>
|
||||
<td>2.0.49</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>tornado</th>
|
||||
<td>6.5.4</td>
|
||||
<td>6.5.5</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Unidecode</th>
|
||||
<td>1.3.8</td>
|
||||
<td>1.4.0</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>urllib3</th>
|
||||
<td>2.6.3</td>
|
||||
<td>2.7.0</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Wand</th>
|
||||
<td>0.6.13</td>
|
||||
<td>0.7.0</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Werkzeug</th>
|
||||
<td>3.1.5</td>
|
||||
<td>3.1.8</td>
|
||||
<td>Basic</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.196.0</td>
|
||||
<td>TestBackupMetadataGdrive</td>
|
||||
</tr>
|
||||
|
||||
@@ -6233,7 +6337,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.196.0</td>
|
||||
<td>TestCliGdrivedb</td>
|
||||
</tr>
|
||||
|
||||
@@ -6263,7 +6367,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.196.0</td>
|
||||
<td>TestEbookConvertCalibreGDrive</td>
|
||||
</tr>
|
||||
|
||||
@@ -6293,7 +6397,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.196.0</td>
|
||||
<td>TestEbookConvertGDriveKepubify</td>
|
||||
</tr>
|
||||
|
||||
@@ -6335,7 +6439,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.196.0</td>
|
||||
<td>TestEditAuthorsGdrive</td>
|
||||
</tr>
|
||||
|
||||
@@ -6371,7 +6475,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.196.0</td>
|
||||
<td>TestEditBooksOnGdrive</td>
|
||||
</tr>
|
||||
|
||||
@@ -6413,7 +6517,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.196.0</td>
|
||||
<td>TestEmbedMetadataGdrive</td>
|
||||
</tr>
|
||||
|
||||
@@ -6443,7 +6547,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>google-api-python-client</th>
|
||||
<td>2.188.0</td>
|
||||
<td>2.196.0</td>
|
||||
<td>TestSetupGdrive</td>
|
||||
</tr>
|
||||
|
||||
@@ -6496,8 +6600,8 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Flask-SimpleLDAP</th>
|
||||
<td>2.0.0</td>
|
||||
<th>flask-simpleldap</th>
|
||||
<td>2.1.0</td>
|
||||
<td>TestLdapLogin</td>
|
||||
</tr>
|
||||
|
||||
@@ -6509,7 +6613,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
|
||||
<tr>
|
||||
<th>python-ldap</th>
|
||||
<td>3.4.5</td>
|
||||
<td>3.4.6</td>
|
||||
<td>TestLdapLogin</td>
|
||||
</tr>
|
||||
|
||||
@@ -6545,7 +6649,7 @@ AssertionError: 0.01731210309182775 != 0.0 within 0.0001 delta (0.01731210309182
|
||||
</div>
|
||||
|
||||
<script>
|
||||
drawCircle(528, 2, 1, 7);
|
||||
drawCircle(519, 5, 4, 8);
|
||||
showCase(5);
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user