Merge branch 'master' into Develop

# Conflicts:
#	cps/editbooks.py
pull/1409/head
Ozzieisaacs 4 years ago
commit 48f4b12c0e

@ -36,10 +36,6 @@ from flask_principal import Principal
from . import config_sql, logger, cache_buster, cli, ub, db
from .reverseproxy import ReverseProxied
from .server import WebServer
try:
from werkzeug.middleware.proxy_fix import ProxyFix
except ImportError:
from werkzeug.contrib.fixers import ProxyFix
mimetypes.init()
mimetypes.add_type('application/xhtml+xml', '.xhtml')
@ -80,10 +76,7 @@ log = logger.create()
from . import services
def create_app():
try:
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))
app.wsgi_app = ReverseProxied(app.wsgi_app)
# For python2 convert path to unicode
if sys.version_info < (3, 0):
app.static_folder = app.static_folder.decode('utf-8')
@ -95,7 +88,7 @@ def create_app():
log.info('Starting Calibre Web...')
Principal(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)
db.setup_db(config)

@ -22,7 +22,7 @@ import os
import json
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 . import constants, cli, logger, ub
@ -31,6 +31,15 @@ from . import constants, cli, logger, ub
log = logger.create()
_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
# (application settings)
@ -304,7 +313,7 @@ def _migrate_table(session, orm_class):
log.debug("%s: %s", column_name, err.args[0])
if column.default is not None:
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')
if column.default is None:
column_default = ""
@ -340,6 +349,7 @@ def _migrate_database(session):
# make sure the table is created, if it does not exist
_Base.metadata.create_all(session.bind)
_migrate_table(session, _Settings)
_migrate_table(session, _Flask_Settings)
def load_configuration(session):
@ -357,3 +367,11 @@ def load_configuration(session):
update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False)
session.commit()
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

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

@ -30,6 +30,7 @@ from uuid import uuid4
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
from flask_babel import gettext as _
from flask_login import current_user, login_required
from sqlalchemy.exc import OperationalError
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper
from . import config, get_locale, db, ub, worker
@ -444,10 +445,16 @@ def upload_single_file(request, book, book_id):
if is_format:
log.warning('Book format %s already existing', file_ext.upper())
else:
db_format = db.Data(book_id, file_ext.upper(), file_size, file_name)
db.session.add(db_format)
db.session.commit()
db.update_title_sort(config)
try:
db_format = db.Data(book_id, file_ext.upper(), file_size, file_name)
db.session.add(db_format)
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
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>")
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):
@ -653,181 +661,188 @@ def upload():
abort(404)
if request.method == 'POST' and 'btn-upload' in request.files:
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:
meta = uploader.upload(requested_file, config.config_rarfile_location)
except (IOError, OSError):
log.error("File %s could not saved to temp dir", requested_file.filename)
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'):
entry = helper.check_exists_book(authr, title)
if entry:
log.info("Uploaded book probably exists in library")
flash(_(u"Uploaded book probably exists in the library, consider to change before upload new: ")
+ Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning")
# handle authors
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())
# 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')
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file
if not os.path.exists(filepath):
# extract metadata from file
try:
os.makedirs(filepath)
except OSError:
log.error("Failed to create path %s (Permission denied)", filepath)
flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error")
meta = uploader.upload(requested_file, config.config_rarfile_location)
except (IOError, OSError):
log.error("File %s could not saved to temp dir", requested_file.filename)
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')
try:
copyfile(meta.file_path, saved_filename)
except OSError:
log.error("Failed to store file %s (Permission denied)", saved_filename)
flash(_(u"Failed to store file %(file)s (Permission denied).", file=saved_filename), category="error")
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:
has_cover = 0
copyfile(os.path.join(constants.STATIC_DIR, 'generic_cover.jpg'),
os.path.join(filepath, "cover.jpg"))
else:
has_cover = 1
# 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
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'):
entry = helper.check_exists_book(authr, title)
if entry:
log.info("Uploaded book probably exists in library")
flash(_(u"Uploaded book probably exists in the library, consider to change before upload new: ")
+ Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning")
# handle authors
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_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:
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>")
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:
os.makedirs(filepath)
except OSError:
log.error("Failed to create path %s (Permission denied)", filepath)
flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
try:
copyfile(meta.file_path, saved_filename)
os.unlink(meta.file_path)
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')
# 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')
if meta.cover is None:
has_cover = 0
copyfile(os.path.join(constants.STATIC_DIR, 'generic_cover.jpg'),
os.path.join(filepath, "cover.jpg"))
else:
resp = {"location": url_for('web.show_book', book_id=db_book.id)}
return Response(json.dumps(resp), mimetype='application/json')
has_cover = 1
# 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')
@editbook.route("/admin/book/convert/<int:book_id>", methods=['POST'])
@login_required_if_no_ano
@edit_required

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

@ -90,15 +90,15 @@
<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>
</div>
<label for="config_certfile">{{_('SSL certfile location (leave it empty for non-SSL Servers)')}}</label>
<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">
<span class="input-group-btn">
<button type="button" id="certfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
</div>
<label for="config_calibre_dir" >{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label>
<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">
<span class="input-group-btn">
<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>
<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>
<label for="config_converterpath">{{_('Path to Calibre E-Book Converter')}}</label>
<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">
<span class="input-group-btn">
<button type="button" id="converter_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
</div>
<label for="config_kepubifypath">{{_('Path to Kepubify E-Book Converter')}}</label>
<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">
<span class="input-group-btn">
<button type="button" id="kepubify_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
</div>
{% if feature_support['rar'] %}
<label for="config_rarfile_location">{{_('Location of Unrar binary')}}</label>
<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">
<span class="input-group-btn">
<button type="button" id="unrar_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>

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

@ -35,12 +35,12 @@
<label for="mail_from">{{_('From E-mail')}}</label>
<input type="text" class="form-control" name="mail_from" id="mail_from" value="{{content.mail_from}}">
</div>
<label for="mail_size">{{_('Attachment Size Limit')}}</label>
<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 %}">
<span class="input-group-btn">
<button type="button" id="certfile_path" class="btn btn-default" disabled>MB</button>
</span>
<span class="input-group-btn">
<button type="button" id="certfile_path" class="btn btn-default" disabled>MB</button>
</span>
</div>
<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>

@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 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"
"Last-Translator: ElQuimm <quimm@webtaste.com>\n"
"Language: it\n"
@ -172,7 +172,7 @@ msgstr "Configurazione del server e-mail aggiornata"
#: cps/admin.py:821
msgid "User not found"
msgstr ""
msgstr "Utente non trovato"
#: cps/admin.py:842
#, python-format
@ -185,7 +185,7 @@ msgstr "Non rimarrebbe nessun utente amministratore, non posso eliminare l'utent
#: cps/admin.py:851
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
msgid "Found an existing account for this e-mail address."
@ -285,11 +285,11 @@ msgstr "non configurato"
#: cps/editbooks.py:239
msgid "Book Format Successfully Deleted"
msgstr ""
msgstr "Il formato del libro è stato eliminato con successo"
#: cps/editbooks.py:242
msgid "Book Successfully Deleted"
msgstr ""
msgstr "Il libro é stato eliminato con successo"
#: cps/editbooks.py:253 cps/editbooks.py:489
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
#, python-format
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
#, python-format
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
msgid "Metadata successfully updated"
@ -362,7 +362,7 @@ msgstr "Il file %(file)s è stato caricato"
#: cps/editbooks.py:833
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
#, python-format
@ -446,17 +446,17 @@ msgstr "Il file richiesto non può essere letto. I permessi sono corretti?"
#: cps/helper.py:299
#, python-format
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
#, python-format
msgid "Deleting book %(id)s failed: %(message)s"
msgstr ""
msgstr "L'eliminazione del libro %(id)s non è riuscita: %(message)s"
#: cps/helper.py:319
#, python-format
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
#, python-format
@ -489,7 +489,7 @@ msgstr "Errore nel creare la cartella per la copertina"
#: cps/helper.py:555
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
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
msgid "Unrar binary file not found"
msgstr ""
msgstr "Non ho trovato il file binario di UnRar"
#: cps/helper.py:635
msgid "Error excecuting UnRar"
msgstr ""
msgstr "Errore nell'eseguire UnRar"
#: cps/helper.py:691
msgid "Waiting"
@ -558,19 +558,19 @@ msgstr "Registra con %(provider)s"
#: cps/oauth_bb.py:154
msgid "Failed to log in with GitHub."
msgstr "Accesso con GitHub non riuscito."
msgstr "Accesso con GitHub non è riuscito."
#: cps/oauth_bb.py:159
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
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
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
#, python-format
@ -584,7 +584,7 @@ msgstr "Collegamento a %(oauth)s avvenuto con successo"
#: cps/oauth_bb.py:241
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
#, python-format
@ -594,7 +594,7 @@ msgstr "Scollegamento da %(oauth)s avvenuto con successo"
#: cps/oauth_bb.py:287
#, python-format
msgid "Unlink to %(oauth)s Failed"
msgstr "Scollegamento da %(oauth)s fallito"
msgstr "Scollegamento da %(oauth)s non riuscito"
#: cps/oauth_bb.py:290
#, python-format
@ -818,11 +818,11 @@ msgstr "Mostra la selezione del formato dei file"
#: cps/ub.py:107 cps/web.py:1150
msgid "Archived Books"
msgstr ""
msgstr "Libri archiviati"
#: cps/ub.py:109
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
msgid "Unexpected data while reading update information"
@ -1360,23 +1360,23 @@ msgstr "Descrizione"
#: cps/templates/book_edit.html:66
msgid "Identifiers"
msgstr ""
msgstr "Identificatori"
#: cps/templates/book_edit.html:70 cps/templates/book_edit.html:308
msgid "Identifier Type"
msgstr ""
msgstr "Tipo di identificatore"
#: cps/templates/book_edit.html:71 cps/templates/book_edit.html:309
msgid "Identifier Value"
msgstr ""
msgstr "Valore dell'identificatore"
#: cps/templates/book_edit.html:72 cps/templates/book_edit.html:310
msgid "Remove"
msgstr ""
msgstr "Rimuovi"
#: cps/templates/book_edit.html:76
msgid "Add Identifier"
msgstr ""
msgstr "Aggiungi un identificatore"
#: cps/templates/book_edit.html:80 cps/templates/search_form.html:33
msgid "Tags"
@ -1453,11 +1453,11 @@ msgstr "e dal disco rigido"
#: cps/templates/book_edit.html:209
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
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
msgid "Keyword"
@ -1775,7 +1775,7 @@ msgstr "Percorso del convertitore"
#: cps/templates/config_edit.html:349
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/login.html:4 cps/templates/login.html:20
@ -1912,15 +1912,15 @@ msgstr "da leggere"
#: cps/templates/detail.html:208
msgid "Restore from archive"
msgstr ""
msgstr "Ripristina dall'archivio"
#: cps/templates/detail.html:208
msgid "Add to archive"
msgstr ""
msgstr "Aggiungi all'archivio"
#: cps/templates/detail.html:209
msgid "Archived"
msgstr ""
msgstr "Archiviato"
#: cps/templates/detail.html:219
msgid "Description:"
@ -2252,7 +2252,7 @@ msgstr "Scuro"
#: cps/templates/readcbr.html:121
msgid "Scale"
msgstr "Adatta"
msgstr "Scala"
#: cps/templates/readcbr.html:124
msgid "Best"
@ -2396,7 +2396,7 @@ msgstr "Cambia ordine"
#: cps/templates/shelf.html:67
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
msgid "Shelf will be deleted for all users"

@ -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_babel import gettext as _
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 werkzeug.exceptions import default_exceptions
from werkzeug.exceptions import default_exceptions, InternalServerError
try:
from werkzeug.exceptions import FailedDependency
except ImportError:
@ -119,9 +119,16 @@ for ex in default_exceptions:
if feature_support['ldap']:
# Only way of catching the LDAPException upon logging in with LDAP server down
@app.errorhandler(services.ldap.LDAPException)
def handle_exception(e):
log.debug('LDAP server not accessible while trying to login to opds feed')
return error_http(FailedDependency())
def handle_LDAP_exception(e):
log.debug('LDAP server not accssible while trying to login to opds feed %s', e)
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__)
@ -435,6 +442,10 @@ def toggle_read(book_id):
db.session.commit()
except KeyError:
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 ""
@web.route("/ajax/togglearchived/<int:book_id>", methods=['POST'])

@ -36,17 +36,17 @@
<div class="col-xs-12 col-sm-6">
<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>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 class="row">
<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 class="row">
<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>
@ -1829,8 +1829,8 @@ AssertionError: False is not true : logfile config value is not empty after rese
<tr class="result['header']['style']">
<td>test_updater.test_updater</td>
<td class="text-center">7</td>
<td class="text-center">6</td>
<td class="text-center">0</td>
<td class="text-center">5</td>
<td class="text-center">1</td>
<td class="text-center">0</td>
<td class="text-center">1</td>
<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>
<div class='testcase'>test_check_update_stable_versions</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_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>
@ -1924,8 +1946,8 @@ AssertionError: False is not true : logfile config value is not empty after rese
<tr class="result['header']['style']">
<td>test_user_template.test_user_template</td>
<td class="text-center">19</td>
<td class="text-center">18</td>
<td class="text-center">1</td>
<td class="text-center">19</td>
<td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center">0</td>
<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>
<div class='testcase'>test_series_user_template</div>
</td>
<td colspan='6'>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft19.18')">FAIL</a>
</div>
<!--css div popup start-->
<div id='div_ft19.18' class="popup_window test_output" style="display: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>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -2574,7 +2576,7 @@ AssertionError: False is not true</pre>
<tr>
<th>SQLAlchemy-Utils</th>
<td>0.36.4</td>
<td>0.36.5</td>
<td>test_OAuth_login</td>
</tr>

Loading…
Cancel
Save