1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-11-24 18:47:23 +00:00

Merge branch 'master' into Develop

# Conflicts:
#	cps/editbooks.py
This commit is contained in:
Ozzieisaacs 2020-05-06 18:47:33 +02:00
commit 48f4b12c0e
12 changed files with 301 additions and 261 deletions

View File

@ -36,10 +36,6 @@ from flask_principal import Principal
from . import config_sql, logger, cache_buster, cli, ub, db from . import config_sql, logger, cache_buster, cli, ub, db
from .reverseproxy import ReverseProxied from .reverseproxy import ReverseProxied
from .server import WebServer from .server import WebServer
try:
from werkzeug.middleware.proxy_fix import ProxyFix
except ImportError:
from werkzeug.contrib.fixers import ProxyFix
mimetypes.init() mimetypes.init()
mimetypes.add_type('application/xhtml+xml', '.xhtml') mimetypes.add_type('application/xhtml+xml', '.xhtml')
@ -80,10 +76,7 @@ log = logger.create()
from . import services from . import services
def create_app(): def create_app():
try: app.wsgi_app = ReverseProxied(app.wsgi_app)
app.wsgi_app = ReverseProxied(ProxyFix(app.wsgi_app, x_for=1, x_host=1))
except (ValueError, TypeError):
app.wsgi_app = ReverseProxied(ProxyFix(app.wsgi_app))
# For python2 convert path to unicode # For python2 convert path to unicode
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
app.static_folder = app.static_folder.decode('utf-8') app.static_folder = app.static_folder.decode('utf-8')
@ -95,7 +88,7 @@ def create_app():
log.info('Starting Calibre Web...') log.info('Starting Calibre Web...')
Principal(app) Principal(app)
lm.init_app(app) lm.init_app(app)
app.secret_key = os.getenv('SECRET_KEY', 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT') app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
web_server.init_app(app, config) web_server.init_app(app, config)
db.setup_db(config) db.setup_db(config)

View File

@ -22,7 +22,7 @@ import os
import json import json
import sys import sys
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from . import constants, cli, logger, ub from . import constants, cli, logger, ub
@ -31,6 +31,15 @@ from . import constants, cli, logger, ub
log = logger.create() log = logger.create()
_Base = declarative_base() _Base = declarative_base()
class _Flask_Settings(_Base):
__tablename__ = 'flask_settings'
id = Column(Integer, primary_key=True)
flask_session_key = Column(BLOB, default="")
def __init__(self, key):
self.flask_session_key = key
# Baseclass for representing settings in app.db with email server settings and Calibre database settings # Baseclass for representing settings in app.db with email server settings and Calibre database settings
# (application settings) # (application settings)
@ -304,7 +313,7 @@ def _migrate_table(session, orm_class):
log.debug("%s: %s", column_name, err.args[0]) log.debug("%s: %s", column_name, err.args[0])
if column.default is not None: if column.default is not None:
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
if isinstance(column.default.arg,unicode): if isinstance(column.default.arg, unicode):
column.default.arg = column.default.arg.encode('utf-8') column.default.arg = column.default.arg.encode('utf-8')
if column.default is None: if column.default is None:
column_default = "" column_default = ""
@ -340,6 +349,7 @@ def _migrate_database(session):
# make sure the table is created, if it does not exist # make sure the table is created, if it does not exist
_Base.metadata.create_all(session.bind) _Base.metadata.create_all(session.bind)
_migrate_table(session, _Settings) _migrate_table(session, _Settings)
_migrate_table(session, _Flask_Settings)
def load_configuration(session): def load_configuration(session):
@ -357,3 +367,11 @@ def load_configuration(session):
update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False) update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False)
session.commit() session.commit()
return conf return conf
def get_flask_session_key(session):
flask_settings = session.query(_Flask_Settings).one_or_none()
if flask_settings == None:
flask_settings = _Flask_Settings(os.urandom(32))
session.add(flask_settings)
session.commit()
return flask_settings.flask_session_key

View File

@ -127,7 +127,7 @@ def selected_roles(dictionary):
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
'series_id, languages') 'series_id, languages')
STABLE_VERSION = {'version': '0.6.7 Beta'} STABLE_VERSION = {'version': '0.6.8 Beta'}
NIGHTLY_VERSION = {} NIGHTLY_VERSION = {}
NIGHTLY_VERSION[0] = '$Format:%H$' NIGHTLY_VERSION[0] = '$Format:%H$'

View File

@ -30,6 +30,7 @@ from uuid import uuid4
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import current_user, login_required from flask_login import current_user, login_required
from sqlalchemy.exc import OperationalError
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper from . import constants, logger, isoLanguages, gdriveutils, uploader, helper
from . import config, get_locale, db, ub, worker from . import config, get_locale, db, ub, worker
@ -444,10 +445,16 @@ def upload_single_file(request, book, book_id):
if is_format: if is_format:
log.warning('Book format %s already existing', file_ext.upper()) log.warning('Book format %s already existing', file_ext.upper())
else: else:
db_format = db.Data(book_id, file_ext.upper(), file_size, file_name) try:
db.session.add(db_format) db_format = db.Data(book_id, file_ext.upper(), file_size, file_name)
db.session.commit() db.session.add(db_format)
db.update_title_sort(config) db.session.commit()
db.update_title_sort(config)
except OperationalError as e:
db.session.rollback()
log.error('Database error: %s', e)
flash(_(u"Database error: %(error)s.", error=e), category="error")
return redirect(url_for('web.show_book', book_id=book.id))
# Queue uploader info # Queue uploader info
uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title) uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title)
@ -455,7 +462,8 @@ def upload_single_file(request, book, book_id):
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>") "<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>")
return uploader.process( return uploader.process(
saved_filename, *os.path.splitext(requested_file.filename)) saved_filename, *os.path.splitext(requested_file.filename),
rarExcecutable=config.config_rarfile_location)
def upload_cover(request, book): def upload_cover(request, book):
@ -653,181 +661,188 @@ def upload():
abort(404) abort(404)
if request.method == 'POST' and 'btn-upload' in request.files: if request.method == 'POST' and 'btn-upload' in request.files:
for requested_file in request.files.getlist("btn-upload"): for requested_file in request.files.getlist("btn-upload"):
# create the function for sorting...
db.update_title_sort(config)
db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
# check if file extension is correct
if '.' in requested_file.filename:
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
if file_ext not in constants.EXTENSIONS_UPLOAD:
flash(
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
ext=file_ext), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
else:
flash(_('File to be uploaded must have an extension'), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
# extract metadata from file
try: try:
meta = uploader.upload(requested_file, config.config_rarfile_location) # create the function for sorting...
except (IOError, OSError): db.update_title_sort(config)
log.error("File %s could not saved to temp dir", requested_file.filename) db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
flash(_(u"File %(filename)s could not saved to temp dir",
filename= requested_file.filename), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
title = meta.title
authr = meta.author
tags = meta.tags
series = meta.series
series_index = meta.series_id
if title != _(u'Unknown') and authr != _(u'Unknown'): # check if file extension is correct
entry = helper.check_exists_book(authr, title) if '.' in requested_file.filename:
if entry: file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
log.info("Uploaded book probably exists in library") if file_ext not in constants.EXTENSIONS_UPLOAD:
flash(_(u"Uploaded book probably exists in the library, consider to change before upload new: ") flash(
+ Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning") _("File extension '%(ext)s' is not allowed to be uploaded to this server",
ext=file_ext), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
else:
flash(_('File to be uploaded must have an extension'), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
# handle authors # extract metadata from file
is_author = db.session.query(db.Authors).filter(db.Authors.name == authr).first()
if is_author:
db_author = is_author
authr= is_author.name
else:
db_author = db.Authors(authr, helper.get_sorted_author(authr), "")
db.session.add(db_author)
title_dir = helper.get_valid_filename(title)
author_dir = helper.get_valid_filename(authr)
filepath = os.path.join(config.config_calibre_dir, author_dir, title_dir)
saved_filename = os.path.join(filepath, title_dir + meta.extension.lower())
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file
if not os.path.exists(filepath):
try: try:
os.makedirs(filepath) meta = uploader.upload(requested_file, config.config_rarfile_location)
except OSError: except (IOError, OSError):
log.error("Failed to create path %s (Permission denied)", filepath) log.error("File %s could not saved to temp dir", requested_file.filename)
flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error") flash(_(u"File %(filename)s could not saved to temp dir",
filename= requested_file.filename), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
try: title = meta.title
copyfile(meta.file_path, saved_filename) authr = meta.author
except OSError: tags = meta.tags
log.error("Failed to store file %s (Permission denied)", saved_filename) series = meta.series
flash(_(u"Failed to store file %(file)s (Permission denied).", file=saved_filename), category="error") series_index = meta.series_id
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
try:
os.unlink(meta.file_path)
except OSError:
log.error("Failed to delete file %(file)s (Permission denied)", meta.file_path)
flash(_(u"Failed to delete file %(file)s (Permission denied).", file= meta.file_path),
category="warning")
if meta.cover is None: if title != _(u'Unknown') and authr != _(u'Unknown'):
has_cover = 0 entry = helper.check_exists_book(authr, title)
copyfile(os.path.join(constants.STATIC_DIR, 'generic_cover.jpg'), if entry:
os.path.join(filepath, "cover.jpg")) log.info("Uploaded book probably exists in library")
else: flash(_(u"Uploaded book probably exists in the library, consider to change before upload new: ")
has_cover = 1 + Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning")
# handle series # handle authors
db_series = None is_author = db.session.query(db.Authors).filter(db.Authors.name == authr).first()
is_series = db.session.query(db.Series).filter(db.Series.name == series).first() if is_author:
if is_series: db_author = is_author
db_series = is_series authr= is_author.name
elif series != '':
db_series = db.Series(series, "")
db.session.add(db_series)
# add language actually one value in list
input_language = meta.languages
db_language = None
if input_language != "":
input_language = isoLanguages.get(name=input_language).part3
hasLanguage = db.session.query(db.Languages).filter(db.Languages.lang_code == input_language).first()
if hasLanguage:
db_language = hasLanguage
else: else:
db_language = db.Languages(input_language) db_author = db.Authors(authr, helper.get_sorted_author(authr), "")
db.session.add(db_language) db.session.add(db_author)
# If the language of the file is excluded from the users view, it's not imported, to allow the user to view title_dir = helper.get_valid_filename(title)
# the book it's language is set to the filter language author_dir = helper.get_valid_filename(authr)
if db_language != current_user.filter_language() and current_user.filter_language() != "all": filepath = os.path.join(config.config_calibre_dir, author_dir, title_dir)
db_language = db.session.query(db.Languages).\ saved_filename = os.path.join(filepath, title_dir + meta.extension.lower())
filter(db.Languages.lang_code == current_user.filter_language()).first()
# combine path and normalize path from windows systems # check if file path exists, otherwise create it, copy file to calibre path and delete temp file
path = os.path.join(author_dir, title_dir).replace('\\', '/') if not os.path.exists(filepath):
# Calibre adds books with utc as timezone try:
db_book = db.Books(title, "", db_author.sort, datetime.utcnow(), datetime(101, 1, 1), os.makedirs(filepath)
series_index, datetime.utcnow(), path, has_cover, db_author, [], db_language) except OSError:
db_book.authors.append(db_author) log.error("Failed to create path %s (Permission denied)", filepath)
if db_series: flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error")
db_book.series.append(db_series) return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
if db_language is not None: try:
db_book.languages.append(db_language) copyfile(meta.file_path, saved_filename)
file_size = os.path.getsize(saved_filename) os.unlink(meta.file_path)
db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, title_dir) except OSError as e:
log.error("Failed to move file %s: %s", saved_filename, e)
flash(_(u"Failed to Move File %(file)s: %(error)s", file=saved_filename, error=e), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
# handle tags if meta.cover is None:
input_tags = tags.split(',') has_cover = 0
input_tags = list(map(lambda it: it.strip(), input_tags)) copyfile(os.path.join(constants.STATIC_DIR, 'generic_cover.jpg'),
if input_tags[0] !="": os.path.join(filepath, "cover.jpg"))
modify_database_object(input_tags, db_book.tags, db.Tags, db.session, 'tags')
# flush content, get db_book.id available
db_book.data.append(db_data)
db.session.add(db_book)
db.session.flush()
# add comment
book_id = db_book.id
upload_comment = Markup(meta.description).unescape()
if upload_comment != "":
db.session.add(db.Comments(upload_comment, book_id))
# save data to database, reread data
db.session.commit()
db.update_title_sort(config)
# Reread book. It's important not to filter the result, as it could have language which hide it from
# current users view (tags are not stored/extracted from metadata and could also be limited)
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
# upload book to gdrive if nesseccary and add "(bookid)" to folder name
if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal()
error = helper.update_dir_stucture(book.id, config.config_calibre_dir)
# move cover to final directory, including book id
if has_cover:
move(meta.cover, os.path.join(filepath+ ' ({})'.format(book_id), "cover.jpg"))
db.session.commit()
if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal()
if error:
flash(error, category="error")
uploadText=_(u"File %(file)s uploaded", file=book.title)
worker.add_upload(current_user.nickname,
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>")
# create data for displaying display Full language name instead of iso639.part3language
if db_language is not None:
book.languages[0].language_name = _(meta.languages)
author_names = []
for author in db_book.authors:
author_names.append(author.name)
if len(request.files.getlist("btn-upload")) < 2:
if current_user.role_edit() or current_user.role_admin():
resp = {"location": url_for('editbook.edit_book', book_id=db_book.id)}
return Response(json.dumps(resp), mimetype='application/json')
else: else:
resp = {"location": url_for('web.show_book', book_id=db_book.id)} has_cover = 1
return Response(json.dumps(resp), mimetype='application/json')
# handle series
db_series = None
is_series = db.session.query(db.Series).filter(db.Series.name == series).first()
if is_series:
db_series = is_series
elif series != '':
db_series = db.Series(series, "")
db.session.add(db_series)
# add language actually one value in list
input_language = meta.languages
db_language = None
if input_language != "":
input_language = isoLanguages.get(name=input_language).part3
hasLanguage = db.session.query(db.Languages).filter(db.Languages.lang_code == input_language).first()
if hasLanguage:
db_language = hasLanguage
else:
db_language = db.Languages(input_language)
db.session.add(db_language)
# If the language of the file is excluded from the users view, it's not imported, to allow the user to view
# the book it's language is set to the filter language
if db_language != current_user.filter_language() and current_user.filter_language() != "all":
db_language = db.session.query(db.Languages).\
filter(db.Languages.lang_code == current_user.filter_language()).first()
# combine path and normalize path from windows systems
path = os.path.join(author_dir, title_dir).replace('\\', '/')
# Calibre adds books with utc as timezone
db_book = db.Books(title, "", db_author.sort, datetime.utcnow(), datetime(101, 1, 1),
series_index, datetime.utcnow(), path, has_cover, db_author, [], db_language)
db_book.authors.append(db_author)
if db_series:
db_book.series.append(db_series)
if db_language is not None:
db_book.languages.append(db_language)
file_size = os.path.getsize(saved_filename)
db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, title_dir)
# handle tags
input_tags = tags.split(',')
input_tags = list(map(lambda it: it.strip(), input_tags))
if input_tags[0] !="":
modify_database_object(input_tags, db_book.tags, db.Tags, db.session, 'tags')
# flush content, get db_book.id available
db_book.data.append(db_data)
db.session.add(db_book)
db.session.flush()
# add comment
book_id = db_book.id
upload_comment = Markup(meta.description).unescape()
if upload_comment != "":
db.session.add(db.Comments(upload_comment, book_id))
# save data to database, reread data
db.session.commit()
db.update_title_sort(config)
# Reread book. It's important not to filter the result, as it could have language which hide it from
# current users view (tags are not stored/extracted from metadata and could also be limited)
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
# upload book to gdrive if nesseccary and add "(bookid)" to folder name
if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal()
error = helper.update_dir_stucture(book.id, config.config_calibre_dir)
# move cover to final directory, including book id
if has_cover:
try:
new_coverpath = os.path.join(filepath+ ' ({})'.format(book_id), "cover.jpg")
copyfile(meta.cover, new_coverpath)
os.unlink(meta.cover)
except OSError as e:
log.error("Failed to move cover file %s: %s", new_coverpath, e)
flash(_(u"Failed to Move Cover File %(file)s: %(error)s", file=new_coverpath,
error=e),
category="error")
db.session.commit()
if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal()
if error:
flash(error, category="error")
uploadText=_(u"File %(file)s uploaded", file=book.title)
worker.add_upload(current_user.nickname,
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>")
# create data for displaying display Full language name instead of iso639.part3language
if db_language is not None:
book.languages[0].language_name = _(meta.languages)
author_names = []
for author in db_book.authors:
author_names.append(author.name)
if len(request.files.getlist("btn-upload")) < 2:
if current_user.role_edit() or current_user.role_admin():
resp = {"location": url_for('editbook.edit_book', book_id=db_book.id)}
return Response(json.dumps(resp), mimetype='application/json')
else:
resp = {"location": url_for('web.show_book', book_id=db_book.id)}
return Response(json.dumps(resp), mimetype='application/json')
except OperationalError as e:
db.session.rollback()
log.error("Database error: %s", e)
flash(_(u"Database error: %(error)s.", error=e), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
@editbook.route("/admin/book/convert/<int:book_id>", methods=['POST']) @editbook.route("/admin/book/convert/<int:book_id>", methods=['POST'])
@login_required_if_no_ano @login_required_if_no_ano
@edit_required @edit_required

View File

@ -291,6 +291,7 @@ def delete_book_file(book, calibrepath, book_format=None):
for file in os.listdir(path): for file in os.listdir(path):
if file.upper().endswith("."+book_format): if file.upper().endswith("."+book_format):
os.remove(os.path.join(path, file)) os.remove(os.path.join(path, file))
return True, None
else: else:
if os.path.isdir(path): if os.path.isdir(path):
if len(next(os.walk(path))[1]): if len(next(os.walk(path))[1]):

View File

@ -90,15 +90,15 @@
<label for="config_port">{{_('Server Port')}}</label> <label for="config_port">{{_('Server Port')}}</label>
<input type="number" min="1" max="65535" class="form-control" name="config_port" id="config_port" value="{% if config.config_port != None %}{{ config.config_port }}{% endif %}" autocomplete="off" required> <input type="number" min="1" max="65535" class="form-control" name="config_port" id="config_port" value="{% if config.config_port != None %}{{ config.config_port }}{% endif %}" autocomplete="off" required>
</div> </div>
<label for="config_certfile">{{_('SSL certfile location (leave it empty for non-SSL Servers)')}}</label>
<div class="form-group input-group"> <div class="form-group input-group">
<label for="config_certfile" class="sr-only">{{_('SSL certfile location (leave it empty for non-SSL Servers)')}}</label>
<input type="text" class="form-control" id="config_certfile" name="config_certfile" value="{% if config.config_certfile != None %}{{ config.config_certfile }}{% endif %}" autocomplete="off"> <input type="text" class="form-control" id="config_certfile" name="config_certfile" value="{% if config.config_certfile != None %}{{ config.config_certfile }}{% endif %}" autocomplete="off">
<span class="input-group-btn"> <span class="input-group-btn">
<button type="button" id="certfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button> <button type="button" id="certfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span> </span>
</div> </div>
<label for="config_calibre_dir" >{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label>
<div class="form-group input-group"> <div class="form-group input-group">
<label for="config_calibre_dir" class="sr-only">{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label>
<input type="text" class="form-control" id="config_keyfile" name="config_keyfile" value="{% if config.config_keyfile != None %}{{ config.config_keyfile }}{% endif %}" autocomplete="off"> <input type="text" class="form-control" id="config_keyfile" name="config_keyfile" value="{% if config.config_keyfile != None %}{{ config.config_keyfile }}{% endif %}" autocomplete="off">
<span class="input-group-btn"> <span class="input-group-btn">
<button type="button" id="keyfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button> <button type="button" id="keyfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
@ -349,23 +349,23 @@
<label for="config_calibre">{{_('Calibre E-Book Converter Settings')}}</label> <label for="config_calibre">{{_('Calibre E-Book Converter Settings')}}</label>
<input type="text" class="form-control" id="config_calibre" name="config_calibre" value="{% if config.config_calibre != None %}{{ config.config_calibre }}{% endif %}" autocomplete="off"> <input type="text" class="form-control" id="config_calibre" name="config_calibre" value="{% if config.config_calibre != None %}{{ config.config_calibre }}{% endif %}" autocomplete="off">
</div> </div>
<label for="config_converterpath">{{_('Path to Calibre E-Book Converter')}}</label>
<div class="form-group input-group"> <div class="form-group input-group">
<label for="config_converterpath" class="sr-only">{{_('Path to Calibre E-Book Converter')}}</label>
<input type="text" class="form-control" id="config_converterpath" name="config_converterpath" value="{% if config.config_converterpath != None %}{{ config.config_converterpath }}{% endif %}" autocomplete="off"> <input type="text" class="form-control" id="config_converterpath" name="config_converterpath" value="{% if config.config_converterpath != None %}{{ config.config_converterpath }}{% endif %}" autocomplete="off">
<span class="input-group-btn"> <span class="input-group-btn">
<button type="button" id="converter_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button> <button type="button" id="converter_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span> </span>
</div> </div>
<label for="config_kepubifypath">{{_('Path to Kepubify E-Book Converter')}}</label>
<div class="form-group input-group"> <div class="form-group input-group">
<label for="config_kepubifypath" class="sr-only">{{_('Path to Kepubify E-Book Converter')}}</label>
<input type="text" class="form-control" id="config_kepubifypath" name="config_kepubifypath" value="{% if config.config_kepubifypath != None %}{{ config.config_kepubifypath }}{% endif %}" autocomplete="off"> <input type="text" class="form-control" id="config_kepubifypath" name="config_kepubifypath" value="{% if config.config_kepubifypath != None %}{{ config.config_kepubifypath }}{% endif %}" autocomplete="off">
<span class="input-group-btn"> <span class="input-group-btn">
<button type="button" id="kepubify_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button> <button type="button" id="kepubify_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span> </span>
</div> </div>
{% if feature_support['rar'] %} {% if feature_support['rar'] %}
<label for="config_rarfile_location">{{_('Location of Unrar binary')}}</label>
<div class="form-group input-group"> <div class="form-group input-group">
<label for="config_rarfile_location" class="sr-only">{{_('Location of Unrar binary')}}</label>
<input type="text" class="form-control" id="config_rarfile_location" name="config_rarfile_location" value="{% if config.config_rarfile_location != None %}{{ config.config_rarfile_location }}{% endif %}" autocomplete="off"> <input type="text" class="form-control" id="config_rarfile_location" name="config_rarfile_location" value="{% if config.config_rarfile_location != None %}{{ config.config_rarfile_location }}{% endif %}" autocomplete="off">
<span class="input-group-btn"> <span class="input-group-btn">
<button type="button" id="unrar_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button> <button type="button" id="unrar_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>

View File

@ -6,8 +6,8 @@
{% block body %} {% block body %}
<div class="discover"> <div class="discover">
<h2>{{title}}</h2> <h2>{{title}}</h2>
<form role="form" method="POST" autocomplete="off"> <form role="form" method="POST" autocomplete="off" class="col-md-10 col-lg-6">
<div class="panel-group col-md-10 col-lg-6"> <div class="panel-group">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h4 class="panel-title"> <h4 class="panel-title">

View File

@ -35,12 +35,12 @@
<label for="mail_from">{{_('From E-mail')}}</label> <label for="mail_from">{{_('From E-mail')}}</label>
<input type="text" class="form-control" name="mail_from" id="mail_from" value="{{content.mail_from}}"> <input type="text" class="form-control" name="mail_from" id="mail_from" value="{{content.mail_from}}">
</div> </div>
<label for="mail_size">{{_('Attachment Size Limit')}}</label>
<div class="form-group input-group"> <div class="form-group input-group">
<label for="mail_size" class="sr-only">{{_('Attachment Size Limit')}}</label>
<input type="number" min="1" max="600" class="form-control" name="attachment_size" id="mail_size" value="{% if config.mail_size != None %}{{ config.mail_size }}{% endif %}"> <input type="number" min="1" max="600" class="form-control" name="attachment_size" id="mail_size" value="{% if config.mail_size != None %}{{ config.mail_size }}{% endif %}">
<span class="input-group-btn"> <span class="input-group-btn">
<button type="button" id="certfile_path" class="btn btn-default" disabled>MB</button> <button type="button" id="certfile_path" class="btn btn-default" disabled>MB</button>
</span> </span>
</div> </div>
<button type="submit" name="submit" value="submit" class="btn btn-default">{{_('Save')}}</button> <button type="submit" name="submit" value="submit" class="btn btn-default">{{_('Save')}}</button>
<button type="submit" name="test" value="test" class="btn btn-default">{{_('Save and Send Test E-mail')}}</button> <button type="submit" name="test" value="test" class="btn btn-default">{{_('Save and Send Test E-mail')}}</button>

View File

@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Calibre-Web\n" "Project-Id-Version: Calibre-Web\n"
"Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n"
"POT-Creation-Date: 2020-05-01 17:15+0200\n" "POT-Creation-Date: 2020-05-04 20:19+0200\n"
"PO-Revision-Date: 2017-04-04 15:09+0200\n" "PO-Revision-Date: 2017-04-04 15:09+0200\n"
"Last-Translator: ElQuimm <quimm@webtaste.com>\n" "Last-Translator: ElQuimm <quimm@webtaste.com>\n"
"Language: it\n" "Language: it\n"
@ -172,7 +172,7 @@ msgstr "Configurazione del server e-mail aggiornata"
#: cps/admin.py:821 #: cps/admin.py:821
msgid "User not found" msgid "User not found"
msgstr "" msgstr "Utente non trovato"
#: cps/admin.py:842 #: cps/admin.py:842
#, python-format #, python-format
@ -185,7 +185,7 @@ msgstr "Non rimarrebbe nessun utente amministratore, non posso eliminare l'utent
#: cps/admin.py:851 #: cps/admin.py:851
msgid "No admin user remaining, can't remove admin role" msgid "No admin user remaining, can't remove admin role"
msgstr "" msgstr "Non rimarrebbe nessun utente amministratore, non posso eliminare il ruolo di amministratore"
#: cps/admin.py:887 cps/web.py:1515 #: cps/admin.py:887 cps/web.py:1515
msgid "Found an existing account for this e-mail address." msgid "Found an existing account for this e-mail address."
@ -285,11 +285,11 @@ msgstr "non configurato"
#: cps/editbooks.py:239 #: cps/editbooks.py:239
msgid "Book Format Successfully Deleted" msgid "Book Format Successfully Deleted"
msgstr "" msgstr "Il formato del libro è stato eliminato con successo"
#: cps/editbooks.py:242 #: cps/editbooks.py:242
msgid "Book Successfully Deleted" msgid "Book Successfully Deleted"
msgstr "" msgstr "Il libro é stato eliminato con successo"
#: cps/editbooks.py:253 cps/editbooks.py:489 #: cps/editbooks.py:253 cps/editbooks.py:489
msgid "Error opening eBook. File does not exist or file is not accessible" msgid "Error opening eBook. File does not exist or file is not accessible"
@ -321,12 +321,12 @@ msgstr "Impossibile creare la cartella %(path)s (autorizzazione negata)."
#: cps/editbooks.py:434 #: cps/editbooks.py:434
#, python-format #, python-format
msgid "Failed to store file %(file)s." msgid "Failed to store file %(file)s."
msgstr "Il salvataggio del file %(file)s è fallito." msgstr "Il salvataggio del file %(file)s non è riuscito."
#: cps/editbooks.py:451 #: cps/editbooks.py:451
#, python-format #, python-format
msgid "File format %(ext)s added to %(book)s" msgid "File format %(ext)s added to %(book)s"
msgstr "Ho aggiunto l'estensione %(ext)s al libro %(book)s" msgstr "Ho aggiunto il formato %(ext)s al libro %(book)s"
#: cps/editbooks.py:606 #: cps/editbooks.py:606
msgid "Metadata successfully updated" msgid "Metadata successfully updated"
@ -362,7 +362,7 @@ msgstr "Il file %(file)s è stato caricato"
#: cps/editbooks.py:833 #: cps/editbooks.py:833
msgid "Source or destination format for conversion missing" msgid "Source or destination format for conversion missing"
msgstr "Il formato sorgente o quello di destinazione, necessari alla conversione, mancano" msgstr "Mancano o il formato sorgente o quello di destinazione, necessari alla conversione"
#: cps/editbooks.py:841 #: cps/editbooks.py:841
#, python-format #, python-format
@ -446,17 +446,17 @@ msgstr "Il file richiesto non può essere letto. I permessi sono corretti?"
#: cps/helper.py:299 #: cps/helper.py:299
#, python-format #, python-format
msgid "Deleting book %(id)s failed, path has subfolders: %(path)s" msgid "Deleting book %(id)s failed, path has subfolders: %(path)s"
msgstr "" msgstr "L'eliminazione del libro %(id)s non è riuscita, poiché il percorso ha delle sottocartelle: %(path)s"
#: cps/helper.py:309 #: cps/helper.py:309
#, python-format #, python-format
msgid "Deleting book %(id)s failed: %(message)s" msgid "Deleting book %(id)s failed: %(message)s"
msgstr "" msgstr "L'eliminazione del libro %(id)s non è riuscita: %(message)s"
#: cps/helper.py:319 #: cps/helper.py:319
#, python-format #, python-format
msgid "Deleting book %(id)s failed, book path not valid: %(path)s" msgid "Deleting book %(id)s failed, book path not valid: %(path)s"
msgstr "" msgstr "L'eliminazione del libro %(id)s non è riuscita, poiché il percorso non è valido: %(path)s"
#: cps/helper.py:354 #: cps/helper.py:354
#, python-format #, python-format
@ -489,7 +489,7 @@ msgstr "Errore nel creare la cartella per la copertina"
#: cps/helper.py:555 #: cps/helper.py:555
msgid "Cover-file is not a valid image file, or could not be stored" msgid "Cover-file is not a valid image file, or could not be stored"
msgstr "" msgstr "Il file della copertina non è in un formato immagine valido o non può essere salvato"
#: cps/helper.py:566 #: cps/helper.py:566
msgid "Only jpg/jpeg/png/webp files are supported as coverfile" msgid "Only jpg/jpeg/png/webp files are supported as coverfile"
@ -501,11 +501,11 @@ msgstr "Solamente i file nei formati jpg/jpeg sono supportati per le copertine"
#: cps/helper.py:622 #: cps/helper.py:622
msgid "Unrar binary file not found" msgid "Unrar binary file not found"
msgstr "" msgstr "Non ho trovato il file binario di UnRar"
#: cps/helper.py:635 #: cps/helper.py:635
msgid "Error excecuting UnRar" msgid "Error excecuting UnRar"
msgstr "" msgstr "Errore nell'eseguire UnRar"
#: cps/helper.py:691 #: cps/helper.py:691
msgid "Waiting" msgid "Waiting"
@ -558,19 +558,19 @@ msgstr "Registra con %(provider)s"
#: cps/oauth_bb.py:154 #: cps/oauth_bb.py:154
msgid "Failed to log in with GitHub." msgid "Failed to log in with GitHub."
msgstr "Accesso con GitHub non riuscito." msgstr "Accesso con GitHub non è riuscito."
#: cps/oauth_bb.py:159 #: cps/oauth_bb.py:159
msgid "Failed to fetch user info from GitHub." msgid "Failed to fetch user info from GitHub."
msgstr "Fallito il recupero delle informazioni dell'utente da GitHub." msgstr "Il recupero delle informazioni dell'utente da GitHub non è riuscito."
#: cps/oauth_bb.py:170 #: cps/oauth_bb.py:170
msgid "Failed to log in with Google." msgid "Failed to log in with Google."
msgstr "Fallito l'accesso con Google." msgstr "L'accesso con Google non è riuscito."
#: cps/oauth_bb.py:175 #: cps/oauth_bb.py:175
msgid "Failed to fetch user info from Google." msgid "Failed to fetch user info from Google."
msgstr "Fallito il recupero delle informazioni dell'utente da Google." msgstr "Il recupero delle informazioni dell'utente da Google non è riuscito."
#: cps/oauth_bb.py:225 cps/web.py:1291 cps/web.py:1431 #: cps/oauth_bb.py:225 cps/web.py:1291 cps/web.py:1431
#, python-format #, python-format
@ -584,7 +584,7 @@ msgstr "Collegamento a %(oauth)s avvenuto con successo"
#: cps/oauth_bb.py:241 #: cps/oauth_bb.py:241
msgid "Login failed, No User Linked With OAuth Account" msgid "Login failed, No User Linked With OAuth Account"
msgstr "Accesso fallito, non c'è un utente collegato all'account OAuth" msgstr "Accesso non riuscito, non c'è un utente collegato all'account OAuth"
#: cps/oauth_bb.py:283 #: cps/oauth_bb.py:283
#, python-format #, python-format
@ -594,7 +594,7 @@ msgstr "Scollegamento da %(oauth)s avvenuto con successo"
#: cps/oauth_bb.py:287 #: cps/oauth_bb.py:287
#, python-format #, python-format
msgid "Unlink to %(oauth)s Failed" msgid "Unlink to %(oauth)s Failed"
msgstr "Scollegamento da %(oauth)s fallito" msgstr "Scollegamento da %(oauth)s non riuscito"
#: cps/oauth_bb.py:290 #: cps/oauth_bb.py:290
#, python-format #, python-format
@ -818,11 +818,11 @@ msgstr "Mostra la selezione del formato dei file"
#: cps/ub.py:107 cps/web.py:1150 #: cps/ub.py:107 cps/web.py:1150
msgid "Archived Books" msgid "Archived Books"
msgstr "" msgstr "Libri archiviati"
#: cps/ub.py:109 #: cps/ub.py:109
msgid "Show archived books" msgid "Show archived books"
msgstr "" msgstr "Mostra l'opzione per la selezione dei libri archiviati"
#: cps/updater.py:294 cps/updater.py:305 cps/updater.py:406 cps/updater.py:420 #: cps/updater.py:294 cps/updater.py:305 cps/updater.py:406 cps/updater.py:420
msgid "Unexpected data while reading update information" msgid "Unexpected data while reading update information"
@ -1360,23 +1360,23 @@ msgstr "Descrizione"
#: cps/templates/book_edit.html:66 #: cps/templates/book_edit.html:66
msgid "Identifiers" msgid "Identifiers"
msgstr "" msgstr "Identificatori"
#: cps/templates/book_edit.html:70 cps/templates/book_edit.html:308 #: cps/templates/book_edit.html:70 cps/templates/book_edit.html:308
msgid "Identifier Type" msgid "Identifier Type"
msgstr "" msgstr "Tipo di identificatore"
#: cps/templates/book_edit.html:71 cps/templates/book_edit.html:309 #: cps/templates/book_edit.html:71 cps/templates/book_edit.html:309
msgid "Identifier Value" msgid "Identifier Value"
msgstr "" msgstr "Valore dell'identificatore"
#: cps/templates/book_edit.html:72 cps/templates/book_edit.html:310 #: cps/templates/book_edit.html:72 cps/templates/book_edit.html:310
msgid "Remove" msgid "Remove"
msgstr "" msgstr "Rimuovi"
#: cps/templates/book_edit.html:76 #: cps/templates/book_edit.html:76
msgid "Add Identifier" msgid "Add Identifier"
msgstr "" msgstr "Aggiungi un identificatore"
#: cps/templates/book_edit.html:80 cps/templates/search_form.html:33 #: cps/templates/book_edit.html:80 cps/templates/search_form.html:33
msgid "Tags" msgid "Tags"
@ -1453,11 +1453,11 @@ msgstr "e dal disco rigido"
#: cps/templates/book_edit.html:209 #: cps/templates/book_edit.html:209
msgid "Important Kobo Note: deleted books will remain on any paired Kobo device." msgid "Important Kobo Note: deleted books will remain on any paired Kobo device."
msgstr "" msgstr "Oservazione importante riguardo Kobo: i libri eliminati, rimarranno in ogni lettore Kobo accoppiato."
#: cps/templates/book_edit.html:210 #: cps/templates/book_edit.html:210
msgid "Books must first be archived and the device synced before a book can safely be deleted." msgid "Books must first be archived and the device synced before a book can safely be deleted."
msgstr "" msgstr "Prima di poter elimnare in sicurezza un libro, prima occorre che il libro venga archiviato e che l'apparecchio venga sincronizzato."
#: cps/templates/book_edit.html:232 #: cps/templates/book_edit.html:232
msgid "Keyword" msgid "Keyword"
@ -1775,7 +1775,7 @@ msgstr "Percorso del convertitore"
#: cps/templates/config_edit.html:349 #: cps/templates/config_edit.html:349
msgid "Location of Unrar binary" msgid "Location of Unrar binary"
msgstr "Percorso di UnRar" msgstr "Percorso del file binario di UnRar"
#: cps/templates/config_edit.html:368 cps/templates/layout.html:84 #: cps/templates/config_edit.html:368 cps/templates/layout.html:84
#: cps/templates/login.html:4 cps/templates/login.html:20 #: cps/templates/login.html:4 cps/templates/login.html:20
@ -1912,15 +1912,15 @@ msgstr "da leggere"
#: cps/templates/detail.html:208 #: cps/templates/detail.html:208
msgid "Restore from archive" msgid "Restore from archive"
msgstr "" msgstr "Ripristina dall'archivio"
#: cps/templates/detail.html:208 #: cps/templates/detail.html:208
msgid "Add to archive" msgid "Add to archive"
msgstr "" msgstr "Aggiungi all'archivio"
#: cps/templates/detail.html:209 #: cps/templates/detail.html:209
msgid "Archived" msgid "Archived"
msgstr "" msgstr "Archiviato"
#: cps/templates/detail.html:219 #: cps/templates/detail.html:219
msgid "Description:" msgid "Description:"
@ -2252,7 +2252,7 @@ msgstr "Scuro"
#: cps/templates/readcbr.html:121 #: cps/templates/readcbr.html:121
msgid "Scale" msgid "Scale"
msgstr "Adatta" msgstr "Scala"
#: cps/templates/readcbr.html:124 #: cps/templates/readcbr.html:124
msgid "Best" msgid "Best"
@ -2396,7 +2396,7 @@ msgstr "Cambia ordine"
#: cps/templates/shelf.html:67 #: cps/templates/shelf.html:67
msgid "Are you sure you want to delete this shelf?" msgid "Are you sure you want to delete this shelf?"
msgstr "Vuoi davvero eliminare lo scaffale?" msgstr "Vuoi davvero eliminare questo scaffale?"
#: cps/templates/shelf.html:70 #: cps/templates/shelf.html:70
msgid "Shelf will be deleted for all users" msgid "Shelf will be deleted for all users"

View File

@ -37,9 +37,9 @@ from flask import Blueprint
from flask import render_template, request, redirect, send_from_directory, make_response, g, flash, abort, url_for from flask import render_template, request, redirect, send_from_directory, make_response, g, flash, abort, url_for
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import login_user, logout_user, login_required, current_user from flask_login import login_user, logout_user, login_required, current_user
from sqlalchemy.exc import IntegrityError, InvalidRequestError from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
from sqlalchemy.sql.expression import text, func, true, false, not_, and_, or_ from sqlalchemy.sql.expression import text, func, true, false, not_, and_, or_
from werkzeug.exceptions import default_exceptions from werkzeug.exceptions import default_exceptions, InternalServerError
try: try:
from werkzeug.exceptions import FailedDependency from werkzeug.exceptions import FailedDependency
except ImportError: except ImportError:
@ -119,9 +119,16 @@ for ex in default_exceptions:
if feature_support['ldap']: if feature_support['ldap']:
# Only way of catching the LDAPException upon logging in with LDAP server down # Only way of catching the LDAPException upon logging in with LDAP server down
@app.errorhandler(services.ldap.LDAPException) @app.errorhandler(services.ldap.LDAPException)
def handle_exception(e): def handle_LDAP_exception(e):
log.debug('LDAP server not accessible while trying to login to opds feed') log.debug('LDAP server not accssible while trying to login to opds feed %s', e)
return error_http(FailedDependency()) return error_http(e)
# @app.errorhandler(InvalidRequestError)
#@app.errorhandler(OperationalError)
#def handle_db_exception(e):
# db.session.rollback()
# log.error('Database request error: %s',e)
# return internal_error(InternalServerError(e))
web = Blueprint('web', __name__) web = Blueprint('web', __name__)
@ -435,6 +442,10 @@ def toggle_read(book_id):
db.session.commit() db.session.commit()
except KeyError: except KeyError:
log.error(u"Custom Column No.%d is not exisiting in calibre database", config.config_read_column) log.error(u"Custom Column No.%d is not exisiting in calibre database", config.config_read_column)
except OperationalError as e:
db.session.rollback()
log.error(u"Read status could not set: %e", e)
return "" return ""
@web.route("/ajax/togglearchived/<int:book_id>", methods=['POST']) @web.route("/ajax/togglearchived/<int:book_id>", methods=['POST'])

66
test/Calibre-Web TestSummary.html Normal file → Executable file
View File

@ -36,17 +36,17 @@
<div class="col-xs-12 col-sm-6"> <div class="col-xs-12 col-sm-6">
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;"> <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>2020-05-01 13:35:57</p> <p class='text-justify attribute'><strong>Start Time: </strong>2020-05-05 19:02:03</p>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3"> <div class="col-xs-6 col-md-6 col-sm-offset-3">
<p class='text-justify attribute'><strong>Stop Time: </strong>2020-05-01 14:32:26</p> <p class='text-justify attribute'><strong>Stop Time: </strong>2020-05-05 19:58:37</p>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3"> <div class="col-xs-6 col-md-6 col-sm-offset-3">
<p class='text-justify attribute'><strong>Duration: </strong>47:49 min</p> <p class='text-justify attribute'><strong>Duration: </strong>47:42 min</p>
</div> </div>
</div> </div>
</div> </div>
@ -1829,8 +1829,8 @@ AssertionError: False is not true : logfile config value is not empty after rese
<tr class="result['header']['style']"> <tr class="result['header']['style']">
<td>test_updater.test_updater</td> <td>test_updater.test_updater</td>
<td class="text-center">7</td> <td class="text-center">7</td>
<td class="text-center">6</td> <td class="text-center">5</td>
<td class="text-center">0</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">1</td>
<td class="text-center"> <td class="text-center">
@ -1867,11 +1867,33 @@ AssertionError: False is not true : logfile config value is not empty after rese
<tr id='pt18.4' class='hiddenRow bg-success'> <tr id='ft18.4' class='none bg-danger'>
<td> <td>
<div class='testcase'>test_check_update_stable_versions</div> <div class='testcase'>test_check_update_stable_versions</div>
</td> </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_ft18.4')">FAIL</a>
</div>
<!--css div popup start-->
<div id='div_ft18.4' class="popup_window test_output" style="display:none;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus='this.blur();'
onclick="document.getElementById('div_ft18.4').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 "/home/matthias/Entwicklung/calibre-web-test/test/test_updater.py", line 150, in test_check_update_stable_versions
self.check_updater('latest version installed', "alert-warning")
File "/home/matthias/Entwicklung/calibre-web-test/test/test_updater.py", line 60, in check_updater
self.assertTrue(self.check_element_on_page((By.CLASS_NAME, className)))
AssertionError: False is not true</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr> </tr>
@ -1924,8 +1946,8 @@ AssertionError: False is not true : logfile config value is not empty after rese
<tr class="result['header']['style']"> <tr class="result['header']['style']">
<td>test_user_template.test_user_template</td> <td>test_user_template.test_user_template</td>
<td class="text-center">19</td> <td class="text-center">19</td>
<td class="text-center">18</td> <td class="text-center">19</td>
<td class="text-center">1</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center"> <td class="text-center">
@ -2088,31 +2110,11 @@ AssertionError: False is not true : logfile config value is not empty after rese
<tr id='ft19.18' class='none bg-danger'> <tr id='pt19.18' class='hiddenRow bg-success'>
<td> <td>
<div class='testcase'>test_series_user_template</div> <div class='testcase'>test_series_user_template</div>
</td> </td>
<td colspan='6'> <td colspan='6' align='center'>PASS</td>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft19.18')">FAIL</a>
</div>
<!--css div popup start-->
<div id='div_ft19.18' class="popup_window test_output" style="display:none;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus='this.blur();'
onclick="document.getElementById('div_ft19.18').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File "/home/matthias/Entwicklung/calibre-web-test/test/test_user_template.py", line 193, in test_series_user_template
self.assertTrue(self.check_element_on_page((By.ID, "nav_hot")))
AssertionError: False is not true</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr> </tr>
@ -2574,7 +2576,7 @@ AssertionError: False is not true</pre>
<tr> <tr>
<th>SQLAlchemy-Utils</th> <th>SQLAlchemy-Utils</th>
<td>0.36.4</td> <td>0.36.5</td>
<td>test_OAuth_login</td> <td>test_OAuth_login</td>
</tr> </tr>