1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-11-30 21:40:00 +00:00

Merge branch 'master' into Develop

This commit is contained in:
Ozzieisaacs 2021-05-01 20:54:01 +02:00
commit b34672ed19
72 changed files with 14114 additions and 8455 deletions

View File

@ -31,13 +31,13 @@ from datetime import datetime, timedelta
from babel import Locale as LC
from babel.dates import format_datetime
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
from flask_login import login_required, current_user, logout_user, confirm_login
from flask_babel import gettext as _
from sqlalchemy import and_
from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
from sqlalchemy.sql.expression import func, or_
from sqlalchemy.sql.expression import func, or_, text
from . import constants, logger, helper, services
from .cli import filepicker
@ -46,7 +46,7 @@ from .helper import check_valid_domain, send_test_mail, reset_password, generate
valid_email, check_username
from .gdriveutils import is_gdrive_ready, gdrive_support
from .render_template import render_title_template, get_sidebar_config
from . import debug_info
from . import debug_info, _BABEL_TRANSLATIONS
try:
from functools import wraps
@ -224,11 +224,23 @@ def edit_user_table():
languages = calibre_db.speaking_language()
translations = babel.list_translations() + [LC('en')]
allUser = ub.session.query(ub.User)
tags = calibre_db.session.query(db.Tags)\
.join(db.books_tags_link)\
.join(db.Books)\
.filter(calibre_db.common_filters()) \
.group_by(text('books_tags_link.tag'))\
.order_by(db.Tags.name).all()
if config.config_restricted_column:
custom_values = calibre_db.session.query(db.cc_classes[config.config_restricted_column]).all()
else:
custom_values = []
if not config.config_anonbrowse:
allUser = allUser.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS)
return render_title_template("user_table.html",
users=allUser.all(),
tags=tags,
custom_values=custom_values,
translations=translations,
languages=languages,
visiblility=visibility,
@ -237,26 +249,41 @@ def edit_user_table():
title=_(u"Edit Users"),
page="usertable")
@admi.route("/ajax/listusers")
@login_required
@admin_required
def list_users():
off = request.args.get("offset") or 0
limit = request.args.get("limit") or 10
off = int(request.args.get("offset") or 0)
limit = int(request.args.get("limit") or 10)
search = request.args.get("search")
sort = request.args.get("sort", "id")
order = request.args.get("order", "").lower()
state = None
if sort == "state":
state = json.loads(request.args.get("state", "[]"))
if sort != "state" and order:
order = text(sort + " " + order)
elif not state:
order = ub.User.id.asc()
all_user = ub.session.query(ub.User)
if not config.config_anonbrowse:
all_user = all_user.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS)
total_count = all_user.count()
total_count = filtered_count = all_user.count()
if search:
users = all_user.filter(or_(func.lower(ub.User.name).ilike("%" + search + "%"),
all_user = all_user.filter(or_(func.lower(ub.User.name).ilike("%" + search + "%"),
func.lower(ub.User.kindle_mail).ilike("%" + search + "%"),
func.lower(ub.User.email).ilike("%" + search + "%")))\
.offset(off).limit(limit).all()
filtered_count = len(users)
func.lower(ub.User.email).ilike("%" + search + "%")))
if state:
users = calibre_db.get_checkbox_sorted(all_user.all(), state, off, limit, request.args.get("order", "").lower())
else:
users = all_user.offset(off).limit(limit).all()
filtered_count = total_count
users = all_user.order_by(order).offset(off).limit(limit).all()
if search:
filtered_count = len(users)
for user in users:
if user.default_language == "all":
@ -270,12 +297,38 @@ def list_users():
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@admi.route("/ajax/deleteuser")
@admi.route("/ajax/deleteuser", methods=['POST'])
@login_required
@admin_required
def delete_user():
# ToDo User delete check also not last one
return ""
user_ids = request.form.to_dict(flat=False)
users = None
if "userid[]" in user_ids:
users = ub.session.query(ub.User).filter(ub.User.id.in_(user_ids['userid[]'])).all()
elif "userid" in user_ids:
users = ub.session.query(ub.User).filter(ub.User.id == user_ids['userid'][0]).all()
count = 0
errors = list()
success = list()
if not users:
log.error("User not found")
return Response(json.dumps({'type': "danger", 'message': _("User not found")}), mimetype='application/json')
for user in users:
try:
message = _delete_user(user)
count += 1
except Exception as ex:
log.error(ex)
errors.append({'type': "danger", 'message': str(ex)})
if count == 1:
log.info("User {} deleted".format(user_ids))
success = [{'type': "success", 'message': message}]
elif count > 1:
log.info("Users {} deleted".format(user_ids))
success = [{'type': "success", 'message': _("{} users deleted successfully").format(count)}]
success.extend(errors)
return Response(json.dumps(success), mimetype='application/json')
@admi.route("/ajax/getlocale")
@login_required
@ -295,7 +348,7 @@ def table_get_locale():
def table_get_default_lang():
languages = calibre_db.speaking_language()
ret = list()
ret.append({'value':'all','text':_('Show All')})
ret.append({'value': 'all', 'text': _('Show All')})
for lang in languages:
ret.append({'value': lang.lang_code, 'text': lang.name})
return json.dumps(ret)
@ -316,15 +369,21 @@ def edit_list_user(param):
if "pk[]" in vals:
users = all_user.filter(ub.User.id.in_(vals['pk[]'])).all()
else:
return ""
return _("Malformed request"), 400
if 'field_index' in vals:
vals['field_index'] = vals['field_index'][0]
if 'value' in vals:
vals['value'] = vals['value'][0]
else:
return ""
elif not ('value[]' in vals):
return _("Malformed request"), 400
for user in users:
try:
if param in ['denied_tags', 'allowed_tags', 'allowed_column_value', 'denied_column_value']:
if 'value[]' in vals:
setattr(user, param, prepare_tags(user, vals['action'][0], param, vals['value[]']))
else:
setattr(user, param, vals['value'].strip())
else:
vals['value'] = vals['value'].strip()
if param == 'name':
if user.name == "Guest":
@ -334,34 +393,65 @@ def edit_list_user(param):
user.email = check_email(vals['value'])
elif param == 'kindle_mail':
user.kindle_mail = valid_email(vals['value']) if vals['value'] else ""
elif param == 'role':
elif param.endswith('role'):
value = int(vals['field_index'])
if user.name == "Guest" and value in \
[constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]:
raise Exception(_("Guest can't have this role"))
# check for valid value, last on checks for power of 2 value
if value > 0 and value <= constants.ROLE_VIEWER and (value & value-1 == 0 or value == 1):
if vals['value'] == 'true':
user.role |= int(vals['field_index'])
else:
if int(vals['field_index']) == constants.ROLE_ADMIN:
user.role |= value
elif vals['value'] == 'false':
if value == constants.ROLE_ADMIN:
if not ub.session.query(ub.User).\
filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
ub.User.id != user.id).count():
return _(u"No admin user remaining, can't remove admin role", nick=user.name), 400
user.role &= ~int(vals['field_index'])
elif param == 'sidebar_view':
if vals['value'] == 'true':
user.sidebar_view |= int(vals['field_index'])
return Response(
json.dumps([{'type': "danger",
'message':_(u"No admin user remaining, can't remove admin role",
nick=user.name)}]), mimetype='application/json')
user.role &= ~value
else:
user.sidebar_view &= ~int(vals['field_index'])
elif param == 'denied_tags':
user.denied_tags = vals['value']
elif param == 'allowed_tags':
user.allowed_tags = vals['value']
elif param == 'allowed_column_value':
user.allowed_column_value = vals['value']
elif param == 'denied_column_value':
user.denied_column_value = vals['value']
raise Exception(_("Value has to be true or false"))
else:
raise Exception(_("Invalid role"))
elif param.startswith('sidebar'):
value = int(vals['field_index'])
if user.name == "Guest" and value == constants.SIDEBAR_READ_AND_UNREAD:
raise Exception(_("Guest can't have this view"))
# check for valid value, last on checks for power of 2 value
if value > 0 and value <= constants.SIDEBAR_LIST and (value & value-1 == 0 or value == 1):
if vals['value'] == 'true':
user.sidebar_view |= value
elif vals['value'] == 'false':
user.sidebar_view &= ~value
else:
raise Exception(_("Value has to be true or false"))
else:
raise Exception(_("Invalid view"))
elif param == 'locale':
if user.name == "Guest":
raise Exception(_("Guest's Locale is determined automatically and can't be set"))
if vals['value'] in _BABEL_TRANSLATIONS:
user.locale = vals['value']
else:
raise Exception(_("No Valid Locale Given"))
elif param == 'default_language':
languages = calibre_db.session.query(db.Languages) \
.join(db.books_languages_link) \
.join(db.Books) \
.filter(calibre_db.common_filters()) \
.group_by(text('books_languages_link.lang_code')).all()
lang_codes = [lang.lang_code for lang in languages] + ["all"]
if vals['value'] in lang_codes:
user.default_language = vals['value']
else:
raise Exception(_("No Valid Book Language Given"))
else:
return _("Parameter not found"), 400
except Exception as ex:
log.debug_or_exception(ex)
return str(ex), 400
ub.session_commit()
return ""
@ -383,6 +473,21 @@ def update_table_settings():
return "Invalid request", 400
return ""
def check_valid_read_column(column):
if column != "0":
if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \
.filter(and_(db.Custom_Columns.datatype == 'bool', db.Custom_Columns.mark_for_delete == 0)).all():
return False
return True
def check_valid_restricted_column(column):
if column != "0":
if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \
.filter(and_(db.Custom_Columns.datatype == 'text', db.Custom_Columns.mark_for_delete == 0)).all():
return False
return True
@admi.route("/admin/viewconfig", methods=["POST"])
@login_required
@ -398,12 +503,23 @@ def update_view_configuration():
if _config_string("config_title_regex"):
calibre_db.update_title_sort(config)
if not check_valid_read_column(to_save.get("config_read_column", "0")):
flash(_(u"Invalid Read Column"), category="error")
log.debug("Invalid Read column")
return view_configuration()
_config_int("config_read_column")
if not check_valid_restricted_column(to_save.get("config_restricted_column", "0")):
flash(_(u"Invalid Restricted Column"), category="error")
log.debug("Invalid Restricted Column")
return view_configuration()
_config_int("config_restricted_column")
_config_int("config_theme")
_config_int("config_random_books")
_config_int("config_books_per_page")
_config_int("config_authors_max")
_config_int("config_restricted_column")
config.config_default_role = constants.selected_roles(to_save)
config.config_default_role &= ~constants.ROLE_ANONYMOUS
@ -414,6 +530,7 @@ def update_view_configuration():
config.save()
flash(_(u"Calibre-Web configuration updated"), category="success")
log.debug("Calibre-Web configuration updated")
before_request()
return view_configuration()
@ -437,6 +554,8 @@ def load_dialogtexts(element_id):
texts["main"] = _('Are you sure you want to change visible book languages for selected user(s)?')
elif element_id == "role":
texts["main"] = _('Are you sure you want to change the selected role for the selected user(s)?')
elif element_id == "restrictions":
texts["main"] = _('Are you sure you want to change the selected restrictions for the selected user(s)?')
elif element_id == "sidebar_view":
texts["main"] = _('Are you sure you want to change the selected visibility restrictions for the selected user(s)?')
return json.dumps(texts)
@ -583,6 +702,26 @@ def restriction_deletion(element, list_func):
return ','.join(elementlist)
def prepare_tags(user, action, tags_name, id_list):
if "tags" in tags_name:
tags = calibre_db.session.query(db.Tags).filter(db.Tags.id.in_(id_list)).all()
if not tags:
raise Exception(_("Tag not found"))
new_tags_list = [x.name for x in tags]
else:
tags = calibre_db.session.query(db.cc_classes[config.config_restricted_column])\
.filter(db.cc_classes[config.config_restricted_column].id.in_(id_list)).all()
new_tags_list = [x.value for x in tags]
saved_tags_list = user.__dict__[tags_name].split(",") if len(user.__dict__[tags_name]) else []
if action == "remove":
saved_tags_list = [x for x in saved_tags_list if x not in new_tags_list]
elif action == "add":
saved_tags_list.extend(x for x in new_tags_list if x not in saved_tags_list)
else:
raise Exception(_("Invalid Action"))
return ",".join(saved_tags_list)
@admi.route("/ajax/addrestriction/<int:res_type>", defaults={"user_id": 0}, methods=['POST'])
@admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
@login_required
@ -610,10 +749,10 @@ def add_restriction(res_type, user_id):
usr = current_user
if 'submit_allow' in element:
usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags)
ub.session_commit("Changed allowed tags of user {} to {}".format(usr.name, usr.list_allowed_tags))
ub.session_commit("Changed allowed tags of user {} to {}".format(usr.name, usr.list_allowed_tags()))
elif 'submit_deny' in element:
usr.denied_tags = restriction_addition(element, usr.list_denied_tags)
ub.session_commit("Changed denied tags of user {} to {}".format(usr.name, usr.list_denied_tags))
ub.session_commit("Changed denied tags of user {} to {}".format(usr.name, usr.list_denied_tags()))
if res_type == 3: # CustomC per user
if isinstance(user_id, int):
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
@ -622,11 +761,11 @@ def add_restriction(res_type, user_id):
if 'submit_allow' in element:
usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values)
ub.session_commit("Changed allowed columns of user {} to {}".format(usr.name,
usr.list_allowed_column_values))
usr.list_allowed_column_values()))
elif 'submit_deny' in element:
usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values)
ub.session_commit("Changed denied columns of user {} to {}".format(usr.name,
usr.list_denied_column_values))
usr.list_denied_column_values()))
return ""
@ -824,6 +963,7 @@ def pathchooser():
def basic_configuration():
logout_user()
if request.method == "POST":
log.debug("Basic Configuration send")
return _configuration_update_helper(configured=filepicker)
return _configuration_result(configured=filepicker)
@ -862,7 +1002,10 @@ def _configuration_gdrive_helper(to_save):
)
# always show google drive settings, but in case of error deny support
config.config_use_google_drive = (not gdrive_error) and ("config_use_google_drive" in to_save)
new_gdrive_value = (not gdrive_error) and ("config_use_google_drive" in to_save)
if config.config_use_google_drive and not new_gdrive_value:
config.config_google_drive_watch_changes_response = {}
config.config_use_google_drive = new_gdrive_value
if _config_string(to_save, "config_google_drive_folder"):
gdriveutils.deleteDatabaseOnChange()
return gdrive_error
@ -1079,7 +1222,8 @@ def _configuration_update_helper(configured):
return _configuration_result(unrar_status, gdrive_error, configured)
except (OperationalError, InvalidRequestError):
ub.session.rollback()
_configuration_result(_(u"Settings DB is not Writeable"), gdrive_error, configured)
log.error("Settings DB is not Writeable")
_configuration_result(_("Settings DB is not Writeable"), gdrive_error, configured)
try:
metadata_db = os.path.join(config.config_calibre_dir, "metadata.db")
@ -1111,15 +1255,16 @@ def _configuration_result(error_flash=None, gdrive_error=None, configured=True):
if gdrive_error is None:
gdrive_error = gdriveutils.get_error_text()
if gdrive_error:
log.error(gdrive_error)
gdrive_error = _(gdrive_error)
else:
# if config.config_use_google_drive and\
if not gdrive_authenticate and gdrive_support:
gdrivefolders = gdriveutils.listRootFolders()
show_back_button = current_user.is_authenticated
show_login_button = config.db_configured and not current_user.is_authenticated
if error_flash:
log.error(error_flash)
config.load()
flash(error_flash, category="error")
show_login_button = False
@ -1172,30 +1317,46 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
ub.session.add(content)
ub.session.commit()
flash(_(u"User '%(user)s' created", user=content.name), category="success")
log.debug("User {} created".format(content.name))
return redirect(url_for('admin.admin'))
except IntegrityError:
ub.session.rollback()
flash(_(u"Found an existing account for this e-mail address or name."), category="error")
log.error("Found an existing account for {} or {}".format(content.name, content.email))
flash(_("Found an existing account for this e-mail address or name."), category="error")
except OperationalError:
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error")
def _delete_user(content):
if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
ub.User.id != content.id).count():
if content.name != "Guest":
ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
ub.session_commit()
log.info(u"User {} deleted".format(content.name))
return(_(u"User '%(nick)s' deleted", nick=content.name))
else:
log.warning(_(u"Can't delete Guest User"))
raise Exception(_(u"Can't delete Guest User"))
else:
log.warning(u"No admin user remaining, can't delete user")
raise Exception(_(u"No admin user remaining, can't delete user"))
def _handle_edit_user(to_save, content, languages, translations, kobo_support):
if to_save.get("delete"):
if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
ub.User.id != content.id).count():
ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
ub.session_commit()
flash(_(u"User '%(nick)s' deleted", nick=content.name), category="success")
return redirect(url_for('admin.admin'))
else:
flash(_(u"No admin user remaining, can't delete user", nick=content.name), category="error")
try:
flash(_delete_user(content), category="success")
except Exception as ex:
log.error(ex)
flash(str(ex), category="error")
return redirect(url_for('admin.admin'))
else:
if not ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
ub.User.id != content.id).count() and 'admin_role' not in to_save:
flash(_(u"No admin user remaining, can't remove admin role", nick=content.name), category="error")
log.warning("No admin user remaining, can't remove admin role from {}".format(content.name))
flash(_("No admin user remaining, can't remove admin role"), category="error")
return redirect(url_for('admin.admin'))
if to_save.get("password"):
content.password = generate_password_hash(to_save["password"])
@ -1235,6 +1396,7 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
if to_save.get("kindle_mail") != content.kindle_mail:
content.kindle_mail = valid_email(to_save["kindle_mail"]) if to_save["kindle_mail"] else ""
except Exception as ex:
log.error(ex)
flash(str(ex), category="error")
return render_title_template("user_edit.html",
translations=translations,
@ -1249,12 +1411,15 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
try:
ub.session_commit()
flash(_(u"User '%(nick)s' updated", nick=content.name), category="success")
except IntegrityError:
except IntegrityError as ex:
ub.session.rollback()
flash(_(u"An unknown error occured."), category="error")
log.error("An unknown error occurred while changing user: {}".format(str(ex)))
flash(_(u"An unknown error occurred."), category="error")
except OperationalError:
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error")
return ""
@admi.route("/admin/user/new", methods=["GET", "POST"])
@ -1318,7 +1483,8 @@ def update_mailsettings():
config.save()
except (OperationalError, InvalidRequestError):
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error")
return edit_mailsettings()
if to_save.get("test"):
@ -1350,7 +1516,9 @@ def edit_user(user_id):
kobo_support = feature_support['kobo'] and config.config_kobo_sync
if request.method == "POST":
to_save = request.form.to_dict()
_handle_edit_user(to_save, content, languages, translations, kobo_support)
resp = _handle_edit_user(to_save, content, languages, translations, kobo_support)
if resp:
return resp
return render_title_template("user_edit.html",
translations=translations,
languages=languages,

View File

@ -71,7 +71,7 @@ if args.c:
if os.path.isfile(args.c):
certfilepath = args.c
else:
print("Certfilepath is invalid. Exiting...")
print("Certfile path is invalid. Exiting...")
sys.exit(1)
if args.c == "":
@ -81,7 +81,7 @@ if args.k:
if os.path.isfile(args.k):
keyfilepath = args.k
else:
print("Keyfilepath is invalid. Exiting...")
print("Keyfile path is invalid. Exiting...")
sys.exit(1)
if (args.k and not args.c) or (not args.k and args.c):
@ -91,29 +91,29 @@ if (args.k and not args.c) or (not args.k and args.c):
if args.k == "":
keyfilepath = ""
# handle and check ipadress argument
ipadress = args.i or None
if ipadress:
# handle and check ip address argument
ip_address = args.i or None
if ip_address:
try:
# try to parse the given ip address with socket
if hasattr(socket, 'inet_pton'):
if ':' in ipadress:
socket.inet_pton(socket.AF_INET6, ipadress)
if ':' in ip_address:
socket.inet_pton(socket.AF_INET6, ip_address)
else:
socket.inet_pton(socket.AF_INET, ipadress)
socket.inet_pton(socket.AF_INET, ip_address)
else:
# on windows python < 3.4, inet_pton is not available
# inet_atom only handles IPv4 addresses
socket.inet_aton(ipadress)
socket.inet_aton(ip_address)
except socket.error as err:
print(ipadress, ':', err)
print(ip_address, ':', err)
sys.exit(1)
# handle and check user password argument
user_credentials = args.s or None
if user_credentials and ":" not in user_credentials:
print("No valid username:password format")
print("No valid 'username:password' format")
sys.exit(3)
# Handles enableing of filepicker
# Handles enabling of filepicker
filepicker = args.f or None

View File

@ -20,16 +20,18 @@
from __future__ import division, print_function, unicode_literals
import os
import sys
import json
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
from sqlalchemy import Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
from sqlalchemy.exc import OperationalError
from sqlalchemy.sql.expression import text
try:
# Compability with sqlalchemy 2.0
# Compatibility with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
from . import constants, cli, logger, ub
from . import constants, cli, logger
log = logger.create()
@ -190,7 +192,7 @@ class _ConfigSQL(object):
@staticmethod
def get_config_ipaddress():
return cli.ipadress or ""
return cli.ip_address or ""
def _has_role(self, role_flag):
return constants.has_flag(self.config_default_role, role_flag)
@ -260,7 +262,6 @@ class _ConfigSQL(object):
"""
new_value = dictionary.get(field, default)
if new_value is None:
# log.debug("_ConfigSQL set_from_dictionary field '%s' not found", field)
return False
if field not in self.__dict__:
@ -277,7 +278,6 @@ class _ConfigSQL(object):
if current_value == new_value:
return False
# log.debug("_ConfigSQL set_from_dictionary '%s' = %r (was %r)", field, new_value, current_value)
setattr(self, field, new_value)
return True
@ -358,7 +358,7 @@ def _migrate_table(session, orm_class):
if column_name[0] != '_':
try:
session.query(column).first()
except exc.OperationalError as err:
except OperationalError as err:
log.debug("%s: %s", column_name, err.args[0])
if column.default is not None:
if sys.version_info < (3, 0):
@ -368,20 +368,23 @@ def _migrate_table(session, orm_class):
column_default = ""
else:
if isinstance(column.default.arg, bool):
column_default = ("DEFAULT %r" % int(column.default.arg))
column_default = "DEFAULT {}".format(int(column.default.arg))
else:
column_default = ("DEFAULT '%r'" % column.default.arg)
column_default = "DEFAULT `{}`".format(column.default.arg)
if isinstance(column.type, JSON):
column_type = "JSON"
else:
column_type = column.type
alter_table = "ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
alter_table = text("ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
column_name,
column_type,
column_default)
column_default))
log.debug(alter_table)
session.execute(alter_table)
changed = True
except json.decoder.JSONDecodeError as e:
log.error("Database corrupt column: {}".format(column_name))
log.debug(e)
if changed:
try:

View File

@ -33,7 +33,7 @@ from sqlalchemy.orm.collections import InstrumentedList
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.exc import OperationalError
try:
# Compability with sqlalchemy 2.0
# Compatibility with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
@ -44,6 +44,7 @@ from flask_login import current_user
from babel import Locale as LC
from babel.core import UnknownLocaleError
from flask_babel import gettext as _
from flask import flash
from . import logger, ub, isoLanguages
from .pagination import Pagination
@ -121,6 +122,8 @@ class Identifiers(Base):
return u"Douban"
elif format_type == "goodreads":
return u"Goodreads"
elif format_type == "babelio":
return u"Babelio"
elif format_type == "google":
return u"Google Books"
elif format_type == "kobo":
@ -148,6 +151,8 @@ class Identifiers(Base):
return u"https://dx.doi.org/{0}".format(self.val)
elif format_type == "goodreads":
return u"https://www.goodreads.com/book/show/{0}".format(self.val)
elif format_type == "babelio":
return u"https://www.babelio.com/livres/titre/{0}".format(self.val)
elif format_type == "douban":
return u"https://book.douban.com/subject/{0}".format(self.val)
elif format_type == "google":
@ -393,7 +398,7 @@ class AlchemyEncoder(json.JSONEncoder):
if isinstance(o.__class__, DeclarativeMeta):
# an SQLAlchemy class
fields = {}
for field in [x for x in dir(o) if not x.startswith('_') and x != 'metadata']:
for field in [x for x in dir(o) if not x.startswith('_') and x != 'metadata' and x!="password"]:
if field == 'books':
continue
data = o.__getattribute__(field)
@ -602,6 +607,7 @@ class CalibreDB():
neg_content_tags_filter = false() if negtags_list == [''] else Books.tags.any(Tags.name.in_(negtags_list))
pos_content_tags_filter = true() if postags_list == [''] else Books.tags.any(Tags.name.in_(postags_list))
if self.config.config_restricted_column:
try:
pos_cc_list = current_user.allowed_column_value.split(',')
pos_content_cc_filter = true() if pos_cc_list == [''] else \
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
@ -610,12 +616,37 @@ class CalibreDB():
neg_content_cc_filter = false() if neg_cc_list == [''] else \
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list))
except (KeyError, AttributeError):
pos_content_cc_filter = false()
neg_content_cc_filter = true()
log.error(u"Custom Column No.%d is not existing in calibre database",
self.config.config_restricted_column)
flash(_("Custom Column No.%(column)d is not existing in calibre database",
column=self.config.config_restricted_column),
category="error")
else:
pos_content_cc_filter = true()
neg_content_cc_filter = false()
return and_(lang_filter, pos_content_tags_filter, ~neg_content_tags_filter,
pos_content_cc_filter, ~neg_content_cc_filter, archived_filter)
@staticmethod
def get_checkbox_sorted(inputlist, state, offset, limit, order):
outcome = list()
elementlist = {ele.id: ele for ele in inputlist}
for entry in state:
try:
outcome.append(elementlist[entry])
except KeyError:
pass
del elementlist[entry]
for entry in elementlist:
outcome.append(elementlist[entry])
if order == "asc":
outcome.reverse()
return outcome[offset:offset + limit]
# Fill indexpage with all requested data from database
def fill_indexpage(self, page, pagesize, database, db_filter, order, *join):
return self.fill_indexpage_with_archived_books(page, pagesize, database, db_filter, order, False, *join)
@ -689,23 +720,33 @@ class CalibreDB():
return self.session.query(Books) \
.filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first()
# read search results from calibre-database and return it (function is used for feed and simple search
def get_search_results(self, term, offset=None, order=None, limit=None):
order = order or [Books.sort]
pagination = None
def search_query(self, term, *join):
term.strip().lower()
self.session.connection().connection.connection.create_function("lower", 1, lcase)
q = list()
authorterms = re.split("[, ]+", term)
for authorterm in authorterms:
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%")))
result = self.session.query(Books).filter(self.common_filters(True)).filter(
query = self.session.query(Books)
if len(join) == 3:
query = query.outerjoin(join[0], join[1]).outerjoin(join[2])
elif len(join) == 2:
query = query.outerjoin(join[0], join[1])
elif len(join) == 1:
query = query.outerjoin(join[0])
return query.filter(self.common_filters(True)).filter(
or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")),
Books.series.any(func.lower(Series.name).ilike("%" + term + "%")),
Books.authors.any(and_(*q)),
Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")),
func.lower(Books.title).ilike("%" + term + "%")
)).order_by(*order).all()
))
# read search results from calibre-database and return it (function is used for feed and simple search
def get_search_results(self, term, offset=None, order=None, limit=None, *join):
order = order or [Books.sort]
pagination = None
result = self.search_query(term, *join).order_by(*order).all()
result_count = len(result)
if offset != None and limit != None:
offset = int(offset)

View File

@ -229,14 +229,14 @@ def modify_identifiers(input_identifiers, db_identifiers, db_session):
@editbook.route("/ajax/delete/<int:book_id>")
@login_required
def delete_book_from_details(book_id):
return Response(delete_book(book_id,"", True), mimetype='application/json')
return Response(delete_book(book_id, "", True), mimetype='application/json')
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""})
@editbook.route("/delete/<int:book_id>/<string:book_format>")
@login_required
def delete_book_ajax(book_id, book_format):
return delete_book(book_id,book_format, False)
return delete_book(book_id, book_format, False)
def delete_whole_book(book_id, book):
@ -315,19 +315,19 @@ def delete_book(book_id, book_format, jsonResponse):
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
if not result:
if jsonResponse:
return json.dumps({"location": url_for("editbook.edit_book"),
"type": "alert",
return json.dumps([{"location": url_for("editbook.edit_book", book_id=book_id),
"type": "danger",
"format": "",
"error": error}),
"message": error}])
else:
flash(error, category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id))
if error:
if jsonResponse:
warning = {"location": url_for("editbook.edit_book"),
warning = {"location": url_for("editbook.edit_book", book_id=book_id),
"type": "warning",
"format": "",
"error": error}
"message": error}
else:
flash(error, category="warning")
if not book_format:
@ -339,6 +339,15 @@ def delete_book(book_id, book_format, jsonResponse):
except Exception as ex:
log.debug_or_exception(ex)
calibre_db.session.rollback()
if jsonResponse:
return json.dumps([{"location": url_for("editbook.edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": ex}])
else:
flash(str(ex), category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id))
else:
# book not found
log.error('Book with id "%s" could not be deleted: not found', book_id)
@ -1176,6 +1185,6 @@ def merge_list_book():
element.format,
element.uncompressed_size,
to_name))
delete_book(from_book.id,"", True) # json_resp =
delete_book(from_book.id,"", True)
return json.dumps({'success': True})
return ""

View File

@ -29,7 +29,7 @@ from sqlalchemy import Column, UniqueConstraint
from sqlalchemy import String, Integer
from sqlalchemy.orm import sessionmaker, scoped_session
try:
# Compability with sqlalchemy 2.0
# Compatibility with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
@ -221,7 +221,7 @@ def listRootFolders():
drive = getDrive(Gdrive.Instance().drive)
folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false"
fileList = drive.ListFile({'q': folder}).GetList()
except (ServerNotFoundError, ssl.SSLError) as e:
except (ServerNotFoundError, ssl.SSLError, RefreshError) as e:
log.info("GDrive Error %s" % e)
fileList = []
return fileList
@ -257,7 +257,12 @@ def getEbooksFolderId(drive=None):
log.error('Error gDrive, root ID not found')
gDriveId.path = '/'
session.merge(gDriveId)
try:
session.commit()
except OperationalError as ex:
log.error("gdrive.db DB is not Writeable")
log.debug('Database error: %s', ex)
session.rollback()
return gDriveId.gdrive_id
@ -272,6 +277,7 @@ def getFile(pathId, fileName, drive):
def getFolderId(path, drive):
# drive = getDrive(drive)
try:
currentFolderId = getEbooksFolderId(drive)
sqlCheckPath = path if path[-1] == '/' else path + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first()
@ -303,6 +309,10 @@ def getFolderId(path, drive):
session.commit()
else:
currentFolderId = storedPathName.gdrive_id
except OperationalError as ex:
log.error("gdrive.db DB is not Writeable")
log.debug('Database error: %s', ex)
session.rollback()
return currentFolderId
@ -346,7 +356,7 @@ def moveGdriveFolderRemote(origin_file, target_folder):
addParents=gFileTargetDir['id'],
removeParents=previous_parents,
fields='id, parents').execute()
# if previous_parents has no childs anymore, delete original fileparent
# if previous_parents has no children anymore, delete original fileparent
if len(children['items']) == 1:
deleteDatabaseEntry(previous_parents)
drive.auth.service.files().delete(fileId=previous_parents).execute()
@ -507,9 +517,10 @@ def deleteDatabaseOnChange():
try:
session.query(GdriveId).delete()
session.commit()
except (OperationalError, InvalidRequestError):
except (OperationalError, InvalidRequestError) as ex:
session.rollback()
log.info(u"GDrive DB is not Writeable")
log.debug('Database error: %s', ex)
log.error(u"GDrive DB is not Writeable")
def updateGdriveCalibreFromLocal():
@ -524,13 +535,23 @@ def updateDatabaseOnEdit(ID,newPath):
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
if storedPathName:
storedPathName.path = sqlCheckPath
try:
session.commit()
except OperationalError as ex:
log.error("gdrive.db DB is not Writeable")
log.debug('Database error: %s', ex)
session.rollback()
# Deletes the hashes in database of deleted book
def deleteDatabaseEntry(ID):
session.query(GdriveId).filter(GdriveId.gdrive_id == ID).delete()
try:
session.commit()
except OperationalError as ex:
log.error("gdrive.db DB is not Writeable")
log.debug('Database error: %s', ex)
session.rollback()
# Gets cover file from gdrive
@ -547,7 +568,12 @@ def get_cover_via_gdrive(cover_path):
permissionAdded = PermissionAdded()
permissionAdded.gdrive_id = df['id']
session.add(permissionAdded)
try:
session.commit()
except OperationalError as ex:
log.error("gdrive.db DB is not Writeable")
log.debug('Database error: %s', ex)
session.rollback()
return df.metadata.get('webContentLink')
else:
return None

View File

@ -693,6 +693,7 @@ def do_download_file(book, book_format, client, data, headers):
# ToDo Check headers parameter
for element in headers:
response.headers[element[0]] = element[1]
log.info('Downloading file: {}'.format(os.path.join(filename, data.name + "." + book_format)))
return response
##################################
@ -732,7 +733,6 @@ def json_serial(obj):
'seconds': obj.seconds,
'microseconds': obj.microseconds,
}
# return obj.isoformat()
raise TypeError("Type %s not serializable" % type(obj))
@ -795,8 +795,8 @@ def tags_filters():
# checks if domain is in database (including wildcards)
# example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name;
# from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/
# in all calls the email address is checked for validity
def check_valid_domain(domain_text):
# domain_text = domain_text.split('@', 1)[-1].lower()
sql = "SELECT * FROM registration WHERE (:domain LIKE domain and allow = 1);"
result = ub.session.query(ub.Registration).from_statement(text(sql)).params(domain=domain_text).all()
if not len(result):
@ -830,6 +830,7 @@ def get_download_link(book_id, book_format, client):
if book:
data1 = calibre_db.get_book_format(book.id, book_format.upper())
else:
log.error("Book id {} not found for downloading".format(book_id))
abort(404)
if data1:
# collect downloaded books only for registered user and not for anonymous user

View File

@ -62,11 +62,11 @@ class _Logger(logging.Logger):
def debug_no_auth(self, message, *args, **kwargs):
message = message.strip("\r\n")
if message.startswith("send: AUTH"):
self.debug(message[:16], stacklevel=2, *args, **kwargs)
self.debug(message[:16], *args, **kwargs)
else:
self.debug(message, stacklevel=2, *args, **kwargs)
self.debug(message, *args, **kwargs)
def get(name=None):
@ -153,11 +153,11 @@ def setup(log_file, log_level=None):
file_handler.baseFilename = log_file
else:
try:
file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2, encoding='utf-8')
file_handler = RotatingFileHandler(log_file, maxBytes=100000, backupCount=2, encoding='utf-8')
except IOError:
if log_file == DEFAULT_LOG_FILE:
raise
file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=50000, backupCount=2, encoding='utf-8')
file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=100000, backupCount=2, encoding='utf-8')
log_file = ""
file_handler.setFormatter(FORMATTER)

View File

@ -30,6 +30,7 @@ from flask_babel import gettext as _
from flask_dance.consumer import oauth_authorized, oauth_error
from flask_dance.contrib.github import make_github_blueprint, github
from flask_dance.contrib.google import make_google_blueprint, google
from oauthlib.oauth2 import TokenExpiredError, InvalidGrantError
from flask_login import login_user, current_user, login_required
from sqlalchemy.orm.exc import NoResultFound
@ -146,6 +147,7 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider
ub.session.add(oauth_entry)
ub.session.commit()
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
log.info("Link to {} Succeeded".format(provider_name))
return redirect(url_for('web.profile'))
except Exception as ex:
log.debug_or_exception(ex)
@ -194,6 +196,7 @@ def unlink_oauth(provider):
ub.session.commit()
logout_oauth_user()
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
log.info("Unlink to {} Succeeded".format(oauth_check[provider]))
except Exception as ex:
log.debug_or_exception(ex)
ub.session.rollback()
@ -257,11 +260,13 @@ if ub.oauth_support:
def github_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with GitHub."), category="error")
log.error("Failed to log in with GitHub")
return False
resp = blueprint.session.get("/user")
if not resp.ok:
flash(_(u"Failed to fetch user info from GitHub."), category="error")
log.error("Failed to fetch user info from GitHub")
return False
github_info = resp.json()
@ -273,11 +278,13 @@ if ub.oauth_support:
def google_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with Google."), category="error")
log.error("Failed to log in with Google")
return False
resp = blueprint.session.get("/oauth2/v2/userinfo")
if not resp.ok:
flash(_(u"Failed to fetch user info from Google."), category="error")
log.error("Failed to fetch user info from Google")
return False
google_info = resp.json()
@ -318,11 +325,16 @@ if ub.oauth_support:
def github_login():
if not github.authorized:
return redirect(url_for('github.login'))
try:
account_info = github.get('/user')
if account_info.ok:
account_info_json = account_info.json()
return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github')
flash(_(u"GitHub Oauth error, please retry later."), category="error")
log.error("GitHub Oauth error, please retry later")
except (InvalidGrantError, TokenExpiredError) as e:
flash(_(u"GitHub Oauth error: {}").format(e), category="error")
log.error(e)
return redirect(url_for('web.login'))
@ -337,11 +349,16 @@ def github_login_unlink():
def google_login():
if not google.authorized:
return redirect(url_for("google.login"))
try:
resp = google.get("/oauth2/v2/userinfo")
if resp.ok:
account_info_json = resp.json()
return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google')
flash(_(u"Google Oauth error, please retry later."), category="error")
log.error("Google Oauth error, please retry later")
except (InvalidGrantError, TokenExpiredError) as e:
flash(_(u"Google Oauth error: {}").format(e), category="error")
log.error(e)
return redirect(url_for('web.login'))

View File

@ -546,8 +546,8 @@ def check_auth(username, password):
if bool(user and check_password_hash(str(user.password), password)):
return True
else:
ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr)
log.warning('OPDS Login failed for user "%s" IP-address: %s', username.decode('utf-8'), ipAdress)
ip_Address = request.headers.get('X-Forwarded-For', request.remote_addr)
log.warning('OPDS Login failed for user "%s" IP-address: %s', username.decode('utf-8'), ip_Address)
return False

View File

@ -49,5 +49,5 @@ except ImportError as err:
try:
from . import gmail
except ImportError as err:
log.debug("Cannot import Gmail, sending books via G-Mail Accounts will not work: %s", err)
log.debug("Cannot import gmail, sending books via Gmail Oauth2 Verification will not work: %s", err)
gmail = None

View File

@ -53,6 +53,7 @@ def setup_gmail(token):
'expiry': creds.expiry.isoformat(),
'email': user_info
}
return {}
def get_user_info(credentials):
user_info_service = build(serviceName='oauth2', version='v2',credentials=credentials)
@ -60,6 +61,7 @@ def get_user_info(credentials):
return user_info.get('email', "")
def send_messsage(token, msg):
log.debug("Start sending email via Gmail")
creds = Credentials(
token=token['token'],
refresh_token=token['refresh_token'],
@ -78,3 +80,4 @@ def send_messsage(token, msg):
body = {'raw': raw}
(service.users().messages().send(userId='me', body=body).execute())
log.debug("Email send successfully via Gmail")

View File

@ -45,7 +45,7 @@ class ImprovedQueue(queue.Queue):
with self.mutex:
return list(self.queue)
#Class for all worker tasks in the background
# Class for all worker tasks in the background
class WorkerThread(threading.Thread):
_instance = None
@ -69,6 +69,7 @@ class WorkerThread(threading.Thread):
def add(cls, user, task):
ins = cls.getInstance()
ins.num += 1
log.debug("Add Task for user: {}: {}".format(user, task))
ins.queue.put(QueuedTask(
num=ins.num,
user=user,

View File

@ -99,12 +99,14 @@ def add_to_shelf(shelf_id, book_id):
ub.session.commit()
except (OperationalError, InvalidRequestError):
ub.session.rollback()
log.error("Settings DB is not Writeable")
flash(_(u"Settings DB is not Writeable"), category="error")
if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"])
else:
return redirect(url_for('web.index'))
if not xhr:
log.debug("Book has been added to shelf: {}".format(shelf.name))
flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"])
@ -123,6 +125,7 @@ def search_to_shelf(shelf_id):
return redirect(url_for('web.index'))
if not check_shelf_edit_permissions(shelf):
log.warning("You are not allowed to add a book to the the shelf: {}".format(shelf.name))
flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error")
return redirect(url_for('web.index'))
@ -140,7 +143,7 @@ def search_to_shelf(shelf_id):
books_for_shelf = ub.searched_ids[current_user.id]
if not books_for_shelf:
log.error("Books are already part of %s", shelf.name)
log.error("Books are already part of {}".format(shelf.name))
flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
return redirect(url_for('web.index'))
@ -156,8 +159,10 @@ def search_to_shelf(shelf_id):
flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
except (OperationalError, InvalidRequestError):
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error")
else:
log.error("Could not add books to shelf: {}".format(shelf.name))
flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
return redirect(url_for('web.index'))
@ -168,7 +173,7 @@ def remove_from_shelf(shelf_id, book_id):
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
if shelf is None:
log.error("Invalid shelf specified: %s", shelf_id)
log.error("Invalid shelf specified: {}".format(shelf_id))
if not xhr:
return redirect(url_for('web.index'))
return "Invalid shelf specified", 400
@ -197,7 +202,8 @@ def remove_from_shelf(shelf_id, book_id):
ub.session.commit()
except (OperationalError, InvalidRequestError):
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error")
if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"])
else:
@ -211,6 +217,7 @@ def remove_from_shelf(shelf_id, book_id):
return "", 204
else:
if not xhr:
log.warning("You are not allowed to remove a book from shelf: {}".format(shelf.name))
flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name),
category="error")
return redirect(url_for('web.index'))
@ -258,7 +265,8 @@ def create_edit_shelf(shelf, title, page, shelf_id=False):
except (OperationalError, InvalidRequestError) as ex:
ub.session.rollback()
log.debug_or_exception(ex)
flash(_(u"Settings DB is not Writeable"), category="error")
log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error")
except Exception as ex:
ub.session.rollback()
log.debug_or_exception(ex)
@ -278,6 +286,7 @@ def check_shelf_is_unique(shelf, to_save, shelf_id=False):
.first() is None
if not is_shelf_name_unique:
log.error("A public shelf with the name '{}' already exists.".format(to_save["title"]))
flash(_(u"A public shelf with the name '%(title)s' already exists.", title=to_save["title"]),
category="error")
else:
@ -288,6 +297,7 @@ def check_shelf_is_unique(shelf, to_save, shelf_id=False):
.first() is None
if not is_shelf_name_unique:
log.error("A private shelf with the name '{}' already exists.".format(to_save["title"]))
flash(_(u"A private shelf with the name '%(title)s' already exists.", title=to_save["title"]),
category="error")
return is_shelf_name_unique
@ -311,7 +321,8 @@ def delete_shelf(shelf_id):
delete_shelf_helper(cur_shelf)
except InvalidRequestError:
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error")
return redirect(url_for('web.index'))
@ -345,7 +356,8 @@ def order_shelf(shelf_id):
ub.session.commit()
except (OperationalError, InvalidRequestError):
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error")
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
result = list()
@ -415,7 +427,8 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
ub.session.commit()
except (OperationalError, InvalidRequestError):
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error("Settings DB is not Writeable")
flash(_("Settings DB is not Writeable"), category="error")
return render_title_template(page,
entries=result,

View File

@ -84,15 +84,24 @@ body {
#progress .bar-load,
#progress .bar-read {
display: flex;
align-items: flex-end;
justify-content: flex-end;
position: absolute;
top: 0;
left: 0;
bottom: 0;
transition: width 150ms ease-in-out;
}
#progress .from-left {
left: 0;
align-items: flex-end;
justify-content: flex-end;
}
#progress .from-right {
right: 0;
align-items: flex-start;
justify-content: flex-start;
}
#progress .bar-load {
color: #000;
background-color: #ccc;

View File

@ -15,6 +15,10 @@ body {
overflow: hidden;
}
.myselect {
overflow: visible !important;
}
#main {
position: absolute;
width: 100%;

View File

@ -22,7 +22,21 @@ $(function() {
});
$("#have_read_cb").on("change", function() {
$(this).closest("form").submit();
$.post({
url: this.closest("form").action,
error: function(response) {
var data = [{type:"danger", message:response.responseText}]
$("#flash_success").remove();
$("#flash_danger").remove();
if (!jQuery.isEmptyObject(data)) {
data.forEach(function (item) {
$(".navbar").after('<div class="row-fluid text-center" style="margin-top: -20px;">' +
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
'</div>');
});
}
}
});
});
$(function() {

View File

@ -171,7 +171,10 @@ kthoom.ImageFile = function(file) {
function initProgressClick() {
$("#progress").click(function(e) {
var page = Math.max(1, Math.ceil((e.offsetX / $(this).width()) * totalImages)) - 1;
var offset = $(this).offset();
var x = e.pageX - offset.left;
var rate = settings.direction === 0 ? x / $(this).width() : 1 - x / $(this).width();
var page = Math.max(1, Math.ceil(rate * totalImages)) - 1;
currentImage = page;
updatePage();
});
@ -285,6 +288,22 @@ function updatePage() {
}
function updateProgress(loadPercentage) {
if (settings.direction === 0) {
$("#progress .bar-read")
.removeClass("from-right")
.addClass("from-left");
$("#progress .bar-load")
.removeClass("from-right")
.addClass("from-left");
} else {
$("#progress .bar-read")
.removeClass("from-left")
.addClass("from-right");
$("#progress .bar-load")
.removeClass("from-left")
.addClass("from-right");
}
// Set the load/unzip progress if it's passed in
if (loadPercentage) {
$("#progress .bar-load").css({ width: loadPercentage + "%" });
@ -526,18 +545,17 @@ function keyHandler(evt) {
break;
case kthoom.Key.SPACE:
var container = $("#mainContent");
var atTop = container.scrollTop() === 0;
var atBottom = container.scrollTop() >= container[0].scrollHeight - container.height();
// var atTop = container.scrollTop() === 0;
// var atBottom = container.scrollTop() >= container[0].scrollHeight - container.height();
if (evt.shiftKey && atTop) {
if (evt.shiftKey) {
evt.preventDefault();
// If it's Shift + Space and the container is at the top of the page
showLeftPage();
} else if (!evt.shiftKey && atBottom) {
showPrevPage();
} else {
evt.preventDefault();
// If you're at the bottom of the page and you only pressed space
showRightPage();
container.scrollTop(0);
showNextPage();
}
break;
default:

View File

@ -114,18 +114,22 @@ $(document).ready(function() {
}
});
$(".session").click(function() {
window.sessionStorage.setItem("back", window.location.pathname);
});
$("#back").click(function() {
var loc = sessionStorage.getItem("back");
if (!loc) {
loc = $(this).data("back");
}
sessionStorage.removeItem("back");
window.location.href = loc;
});
function confirmDialog(id, dialogid, dataValue, yesFn, noFn) {
var $confirm = $("#" + dialogid);
$confirm.modal('show');
$.ajax({
method:"get",
dataType: "json",
url: getPath() + "/ajax/loaddialogtexts/" + id,
success: function success(data) {
$("#header-"+ dialogid).html(data.header);
$("#text-"+ dialogid).html(data.main);
}
});
$("#btnConfirmYes-"+ dialogid).off('click').click(function () {
yesFn(dataValue);
$confirm.modal("hide");
@ -136,16 +140,27 @@ function confirmDialog(id, dialogid, dataValue, yesFn, noFn) {
}
$confirm.modal("hide");
});
$.ajax({
method:"get",
dataType: "json",
url: getPath() + "/ajax/loaddialogtexts/" + id,
success: function success(data) {
$("#header-"+ dialogid).html(data.header);
$("#text-"+ dialogid).html(data.main);
}
});
$confirm.modal('show');
}
$("#delete_confirm").click(function() {
//get data-id attribute of the clicked element
var deleteId = $(this).data("delete-id");
var bookFormat = $(this).data("delete-format");
var ajaxResponse = $(this).data("ajax");
if (bookFormat) {
window.location.href = getPath() + "/delete/" + deleteId + "/" + bookFormat;
} else {
if ($(this).data("delete-format")) {
if (ajaxResponse) {
path = getPath() + "/ajax/delete/" + deleteId;
$.ajax({
method:"get",
@ -163,6 +178,19 @@ $("#delete_confirm").click(function() {
}
});
$("#books-table").bootstrapTable("refresh");
/*$.ajax({
method:"get",
url: window.location.pathname + "/../../ajax/listbooks",
async: true,
timeout: 900,
success:function(data) {
$("#book-table").bootstrapTable("load", data);
loadSuccess();
}
});*/
}
});
} else {
@ -187,6 +215,7 @@ $("#deleteModal").on("show.bs.modal", function(e) {
}
$(e.currentTarget).find("#delete_confirm").data("delete-id", bookId);
$(e.currentTarget).find("#delete_confirm").data("delete-format", bookfomat);
$(e.currentTarget).find("#delete_confirm").data("ajax", $(e.relatedTarget).data("ajax"));
});

View File

@ -19,9 +19,9 @@
/* global getPath, confirmDialog */
var selections = [];
var reload = false;
$(function() {
$("#books-table").on("check.bs.table check-all.bs.table uncheck.bs.table uncheck-all.bs.table",
function (e, rowsAfter, rowsBefore) {
var rows = rowsAfter;
@ -117,6 +117,7 @@ $(function() {
$("#books-table").bootstrapTable({
sidePagination: "server",
queryParams: queryParams,
pagination: true,
paginationLoop: false,
paginationDetailHAlign: " hidden",
@ -176,7 +177,6 @@ $(function() {
},
});
$("#domain_allow_submit").click(function(event) {
event.preventDefault();
$("#domain_add_allow").ajaxForm();
@ -318,7 +318,6 @@ $(function() {
},
url: getPath() + "/ajax/listrestriction/" + type + "/" + userId,
rowStyle: function(row) {
// console.log('Reihe :' + row + " Index :" + index);
if (row.id.charAt(0) === "a") {
return {classes: "bg-primary"};
} else {
@ -387,7 +386,6 @@ $(function() {
var target = $(e.relatedTarget).attr('id');
var dataId;
$(e.relatedTarget).one('focus', function(e){$(this).blur();});
//$(e.relatedTarget).blur();
if ($(e.relatedTarget).hasClass("button_head")) {
dataId = $('#user-table').bootstrapTable('getSelections').map(a => a.id);
} else {
@ -422,6 +420,7 @@ $(function() {
$("#user-table").bootstrapTable({
sidePagination: "server",
queryParams: queryParams,
pagination: true,
paginationLoop: false,
paginationDetailHAlign: " hidden",
@ -452,38 +451,13 @@ $(function() {
$(this).next().text(elText);
});
},
onLoadSuccess: function () {
var guest = $(".editable[data-name='name'][data-value='Guest']");
guest.editable("disable");
$(".editable[data-name='locale'][data-pk='"+guest.data("pk")+"']").editable("disable");
$("input[data-name='admin_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$("input[data-name='passwd_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$("input[data-name='edit_shelf_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
// ToDo: Disable delete
onPostHeader () {
move_header_elements();
},
// eslint-disable-next-line no-unused-vars
/*onEditableSave: function (field, row, oldvalue, $el) {
if (field === "title" || field === "authors") {
$.ajax({
method:"get",
dataType: "json",
url: window.location.pathname + "/../../ajax/sort_value/" + field + "/" + row.id,
success: function success(data) {
var key = Object.keys(data)[0];
$("#books-table").bootstrapTable("updateCellByUniqueId", {
id: row.id,
field: key,
value: data[key]
});
// console.log(data);
}
});
}
},*/
// eslint-disable-next-line no-unused-vars
onColumnSwitch: function (field, checked) {
onLoadSuccess: function () {
loadSuccess();
},
onColumnSwitch: function () {
var visible = $("#user-table").bootstrapTable("getVisibleColumns");
var hidden = $("#user-table").bootstrapTable("getHiddenColumns");
var st = "";
@ -501,31 +475,10 @@ $(function() {
url: window.location.pathname + "/../../ajax/user_table_settings",
data: "{" + st + "}",
});
handle_header_buttons();
},
});
$("#user_delete_selection").click(function() {
$("#user-table").bootstrapTable("uncheckAll");
});
function user_handle (userId) {
$.ajax({
method:"post",
url: window.location.pathname + "/../../ajax/deleteuser",
data: {"userid":userId}
});
$.ajax({
method:"get",
url: window.location.pathname + "/../../ajax/listusers",
async: true,
timeout: 900,
success:function(data) {
$("#user-table").bootstrapTable("load", data);
}
});
}
$("#user-table").on("click-cell.bs.table", function (field, value, row, $element) {
if (value === "denied_column_value") {
ConfirmDialog("btndeluser", "GeneralDeleteModal", $element.id, user_handle);
@ -545,6 +498,11 @@ $(function() {
});
var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference";
selections = window._[func](selections, ids);
handle_header_buttons();
});
});
function handle_header_buttons () {
if (selections.length < 1) {
$("#user_delete_selection").addClass("disabled");
$("#user_delete_selection").attr("aria-disabled", true);
@ -553,6 +511,10 @@ $(function() {
$(".check_head").prop('checked', false);
$(".button_head").attr("aria-disabled", true);
$(".button_head").addClass("disabled");
$(".multi_head").attr("aria-disabled", true);
$(".multi_head").addClass("hidden");
$(".multi_selector").attr("aria-disabled", true);
$(".multi_selector").attr("disabled", true);
$(".header_select").attr("disabled", true);
} else {
$("#user_delete_selection").removeClass("disabled");
@ -561,13 +523,14 @@ $(function() {
$(".check_head").removeAttr("disabled");
$(".button_head").attr("aria-disabled", false);
$(".button_head").removeClass("disabled");
$(".multi_head").attr("aria-disabled", false);
$(".multi_head").removeClass("hidden");
$(".multi_selector").attr("aria-disabled", false);
$(".multi_selector").removeAttr("disabled");
$('.multi_selector').selectpicker('refresh');
$(".header_select").removeAttr("disabled");
}
});
});
}
/* Function for deleting domain restrictions */
function TableActions (value, row) {
return [
@ -578,10 +541,6 @@ function TableActions (value, row) {
].join("");
}
function editEntry(param)
{
console.log(param);
}
/* Function for deleting domain restrictions */
function RestrictionActions (value, row) {
return [
@ -600,10 +559,10 @@ function EbookActions (value, row) {
].join("");
}
/* Function for deleting books */
/* Function for deleting Users */
function UserActions (value, row) {
return [
"<div class=\"user-remove\" data-target=\"#GeneralDeleteModal\" title=\"Remove\">",
"<div class=\"user-remove\" data-value=\"delete\" onclick=\"deleteUser(this, '" + row.id + "')\" data-pk=\"" + row.id + "\" title=\"Remove\">",
"<i class=\"glyphicon glyphicon-trash\"></i>",
"</div>"
].join("");
@ -618,46 +577,160 @@ function responseHandler(res) {
}
function singleUserFormatter(value, row) {
return '<a class="btn btn-default" href="/admin/user/' + row.id + '">' + this.buttontext + '</a>'
return '<a class="btn btn-default" onclick="storeLocation()" href="' + window.location.pathname + '/../../admin/user/' + row.id + '">' + this.buttontext + '</a>'
}
function checkboxFormatter(value, row, index){
if(value & this.column)
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.name + '" checked onchange="checkboxChange(this, ' + row.id + ', \'' + this.field + '\', ' + this.column + ')">';
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.field + '" checked onchange="checkboxChange(this, ' + row.id + ', \'' + this.name + '\', ' + this.column + ')">';
else
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.name + '" onchange="checkboxChange(this, ' + row.id + ', \'' + this.field + '\', ' + this.column + ')">';
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.field + '" onchange="checkboxChange(this, ' + row.id + ', \'' + this.name + '\', ' + this.column + ')">';
}
/* Do some hiding disabling after user list is loaded */
function loadSuccess() {
var guest = $(".editable[data-name='name'][data-value='Guest']");
guest.editable("disable");
$("input:radio.check_head:checked").each(function() {
$(this).prop('checked', false);
});
$(".header_select").each(function() {
$(this).prop("selectedIndex", 0);
});
$(".header_select").each(function() {
$(this).prop("selectedIndex", 0);
});
$('.multi_selector').selectpicker('deselectAll');
$('.multi_selector').selectpicker('refresh');
$(".editable[data-name='locale'][data-pk='"+guest.data("pk")+"']").editable("disable");
$(".editable[data-name='locale'][data-pk='"+guest.data("pk")+"']").hide();
$("input[data-name='admin_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$("input[data-name='passwd_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$("input[data-name='edit_shelf_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$("input[data-name='sidebar_read_and_unread'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$(".user-remove[data-pk='"+guest.data("pk")+"']").hide();
}
function move_header_elements() {
$(".header_select").each(function () {
var item = $(this).parent();
var parent = item.parent().parent();
if (parent.prop('nodeName') === "TH") {
item.prependTo(parent);
}
});
$(".form-check").each(function () {
var item = $(this).parent();
var parent = item.parent().parent();
if (parent.prop('nodeName') === "TH") {
item.prependTo(parent);
}
});
$(".multi_select").each(function () {
var item = $(this);
var parent = item.parent().parent();
if (parent.prop('nodeName') === "TH") {
item.prependTo(parent);
item.addClass("myselect");
}
});
$(".multi_selector").selectpicker();
if (! $._data($(".multi_head").get(0), "events") ) {
// Functions have to be here, otherwise the callbacks are not fired if visible columns are changed
$(".multi_head").on("click", function () {
var val = $(this).data("set");
var field = $(this).data("name");
var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id);
var values = $("#" + field).val();
confirmDialog(
"restrictions",
"GeneralChangeModal",
0,
function () {
$.ajax({
method: "post",
url: window.location.pathname + "/../../ajax/editlistusers/" + field,
data: {"pk": result, "value": values, "action": val},
success: function (data) {
handleListServerResponse(data);
},
error: function (data) {
handleListServerResponse([{type: "danger", message: data.responseText}])
},
});
}
);
});
}
$("#user_delete_selection").click(function () {
$("#user-table").bootstrapTable("uncheckAll");
});
$("#select_locale").on("change", function () {
selectHeader(this, "locale");
});
$("#select_default_language").on("change", function () {
selectHeader(this, "default_language");
});
if (! $._data($(".check_head").get(0), "events") ) {
$(".check_head").on("change", function () {
var val = $(this).data("set");
var name = $(this).data("name");
var data = $(this).data("val");
checkboxHeader(val, name, data);
});
}
if (! $._data($(".button_head").get(0), "events") ) {
$(".button_head").on("click", function () {
var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id);
confirmDialog(
"btndeluser",
"GeneralDeleteModal",
0,
function () {
$.ajax({
method: "post",
url: window.location.pathname + "/../../ajax/deleteuser",
data: {"userid": result},
success: function (data) {
selections = selections.filter((el) => !result.includes(el));
handleListServerResponse(data);
},
error: function (data) {
handleListServerResponse([{type: "danger", message: data.responseText}])
},
});
}
);
});
}
}
function handleListServerResponse (data) {
$("#flash_success").remove();
$("#flash_danger").remove();
if (!jQuery.isEmptyObject(data)) {
data.forEach(function(item) {
$(".navbar").after('<div class="row-fluid text-center" style="margin-top: -20px;">' +
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
'</div>');
});
}
$("#user-table").bootstrapTable("refresh");
}
function checkboxChange(checkbox, userId, field, field_index) {
$.ajax({
method:"post",
method: "post",
url: window.location.pathname + "/../../ajax/editlistusers/" + field,
data: {"pk":userId, "field_index":field_index, "value": checkbox.checked}
/*<div className="editable-buttons">
<button type="button" className="btn btn-default btn-sm editable-cancel"><i
className="glyphicon glyphicon-remove"></i></button>
</div>*/
/*<div className="editable-error-block help-block" style="">Text to show</div>*/
data: {"pk": userId, "field_index": field_index, "value": checkbox.checked},
error: function(data) {
handleListServerResponse([{type:"danger", message:data.responseText}])
},
success: handleListServerResponse
});
$.ajax({
method:"get",
url: window.location.pathname + "/../../ajax/listusers",
async: true,
timeout: 900,
success:function(data) {
$("#user-table").bootstrapTable("load", data);
}
});
}
function deactivateHeaderButtons(e) {
$("#user_delete_selection").addClass("disabled");
$("#user_delete_selection").attr("aria-disabled", true);
$(".check_head").attr("aria-disabled", true);
$(".check_head").attr("disabled", true);
$(".check_head").prop('checked', false);
$(".button_head").attr("aria-disabled", true);
$(".button_head").addClass("disabled");
$(".header_select").attr("disabled", true);
}
function selectHeader(element, field) {
@ -668,19 +741,13 @@ function selectHeader(element, field) {
method: "post",
url: window.location.pathname + "/../../ajax/editlistusers/" + field,
data: {"pk": result, "value": element.value},
success: function () {
$.ajax({
method: "get",
url: window.location.pathname + "/../../ajax/listusers",
async: true,
timeout: 900,
success: function (data) {
$("#user-table").bootstrapTable("load", data);
deactivateHeaderButtons();
}
});
}
error: function (data) {
handleListServerResponse([{type:"danger", message:data.responseText}])
},
success: handleListServerResponse,
});
},function() {
$(element).prop("selectedIndex", 0);
});
}
}
@ -692,27 +759,51 @@ function checkboxHeader(CheckboxState, field, field_index) {
method: "post",
url: window.location.pathname + "/../../ajax/editlistusers/" + field,
data: {"pk": result, "field_index": field_index, "value": CheckboxState},
success: function () {
$.ajax({
method: "get",
url: window.location.pathname + "/../../ajax/listusers",
async: true,
timeout: 900,
error: function (data) {
handleListServerResponse([{type:"danger", message:data.responseText}])
},
success: function (data) {
$("#user-table").bootstrapTable("load", data);
$("#user_delete_selection").addClass("disabled");
$("#user_delete_selection").attr("aria-disabled", true);
$(".check_head").attr("aria-disabled", true);
$(".check_head").attr("disabled", true);
$(".check_head").prop('checked', false);
$(".button_head").attr("aria-disabled", true);
$(".button_head").addClass("disabled");
$(".header_select").attr("disabled", true);
}
handleListServerResponse (data, true)
},
});
},function() {
$("input:radio.check_head:checked").each(function() {
$(this).prop('checked', false);
});
});
}
function deleteUser(a,id){
confirmDialog(
"btndeluser",
"GeneralDeleteModal",
0,
function() {
$.ajax({
method:"post",
url: window.location.pathname + "/../../ajax/deleteuser",
data: {"userid":id},
success: function (data) {
userId = parseInt(id, 10);
selections = selections.filter(item => item !== userId);
handleListServerResponse(data);
},
error: function (data) {
handleListServerResponse([{type:"danger", message:data.responseText}])
},
});
}
});
});
);
}
function queryParams(params)
{
params.state = JSON.stringify(selections);
return params;
}
function storeLocation() {
window.sessionStorage.setItem("back", window.location.pathname);
}
function user_handle (userId) {
@ -721,17 +812,5 @@ function user_handle (userId) {
url: window.location.pathname + "/../../ajax/deleteuser",
data: {"userid":userId}
});
$.ajax({
method:"get",
url: window.location.pathname + "/../../ajax/listusers",
async: true,
timeout: 900,
success:function(data) {
$("#user-table").bootstrapTable("load", data);
}
});
}
function test(){
console.log("hello");
$("#user-table").bootstrapTable("refresh");
}

View File

@ -134,9 +134,9 @@ class TaskEmail(CalibreTask):
return message
def run(self, worker_thread):
try:
# create MIME message
msg = self.prepare_message()
try:
if self.settings['mail_server_type'] == 0:
self.send_standard_email(msg)
else:
@ -173,6 +173,7 @@ class TaskEmail(CalibreTask):
org_smtpstderr = smtplib.stderr
smtplib.stderr = logger.StderrLogger('worker.smtp')
log.debug("Start sending email")
if use_ssl == 2:
self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"],
timeout=timeout)
@ -195,11 +196,11 @@ class TaskEmail(CalibreTask):
self.asyncSMTP.sendmail(self.settings["mail_from"], self.recipent, fp.getvalue())
self.asyncSMTP.quit()
self._handleSuccess()
log.debug("Email send successfully")
if sys.version_info < (3, 0):
smtplib.stderr = org_smtpstderr
def send_gmail_email(self, message):
return gmail.send_messsage(self.settings.get('mail_gmail_token', None), message)
@ -258,3 +259,6 @@ class TaskEmail(CalibreTask):
@property
def name(self):
return "Email"
def __str__(self):
return "{}, {}".format(self.name, self.subject)

View File

@ -26,7 +26,7 @@
{% for user in allUser %}
{% if not user.role_anonymous() or config.config_anonbrowse %}
<tr>
<td><a href="{{url_for('admin.edit_user', user_id=user.id)}}">{{user.name}}</a></td>
<td><a class="session" href="{{url_for('admin.edit_user', user_id=user.id)}}">{{user.name}}</a></td>
<td>{{user.email}}</td>
<td>{{user.kindle_mail}}</td>
<td>{{user.downloads.count()}}</td>

View File

@ -1,6 +1,7 @@
{% extends "layout.html" %}
{% macro text_table_row(parameter, edit_text, show_text, validate) -%}
<th data-field="{{ parameter }}" id="{{ parameter }}" data-sortable="true"
{% macro text_table_row(parameter, edit_text, show_text, validate, sort) -%}
<th data-field="{{ parameter }}" id="{{ parameter }}"
{% if sort %}data-sortable="true" {% endif %}
data-visible = "{{visiblility.get(parameter)}}"
{% if g.user.role_edit() %}
data-editable-type="text"
@ -43,16 +44,16 @@
<th data-field="state" data-checkbox="true" data-sortable="true"></th>
{% endif %}
<th data-field="id" id="id" data-visible="false" data-switchable="false"></th>
{{ text_table_row('title', _('Enter Title'),_('Title'), true) }}
{{ text_table_row('sort', _('Enter Title Sort'),_('Title Sort'), false) }}
{{ text_table_row('author_sort', _('Enter Author Sort'),_('Author Sort'), false) }}
{{ text_table_row('authors', _('Enter Authors'),_('Authors'), true) }}
{{ text_table_row('tags', _('Enter Categories'),_('Categories'), false) }}
{{ text_table_row('series', _('Enter Series'),_('Series'), false) }}
{{ text_table_row('title', _('Enter Title'),_('Title'), true, true) }}
{{ text_table_row('sort', _('Enter Title Sort'),_('Title Sort'), false, true) }}
{{ text_table_row('author_sort', _('Enter Author Sort'),_('Author Sort'), false, true) }}
{{ text_table_row('authors', _('Enter Authors'),_('Authors'), true, true) }}
{{ text_table_row('tags', _('Enter Categories'),_('Categories'), false, true) }}
{{ text_table_row('series', _('Enter Series'),_('Series'), false, true) }}
<th data-field="series_index" id="series_index" data-visible="{{visiblility.get('series_index')}}" data-edit-validate="{{ _('This Field is Required') }}" data-sortable="true" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.01" data-editable-min="0" data-editable-url="{{ url_for('editbook.edit_list_book', param='series_index')}}" data-edit="true" data-editable-title="{{_('Enter title')}}"{% endif %}>{{_('Series Index')}}</th>
{{ text_table_row('languages', _('Enter Languages'),_('Languages'), false) }}
{{ text_table_row('languages', _('Enter Languages'),_('Languages'), false, true) }}
<!--th data-field="pubdate" data-type="date" data-visible="{{visiblility.get('pubdate')}}" data-viewformat="dd.mm.yyyy" id="pubdate" data-sortable="true">{{_('Publishing Date')}}</th-->
{{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false) }}
{{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false, true) }}
{% if g.user.role_delete_books() and g.user.role_edit()%}
<th data-align="right" data-formatter="EbookActions" data-switchable="false">{{_('Delete')}}</th>
{% endif %}

View File

@ -12,7 +12,7 @@
<label for="mail_server_type">{{_('Choose Server Type')}}</label>
<select name="mail_server_type" id="config_email_type" class="form-control" data-control="email-settings">
<option value="0" {% if content.mail_server_type == 0 %}selected{% endif %}>{{_('Use Standard E-Mail Account')}}</option>
<option value="1" {% if content.mail_server_type == 1 %}selected{% endif %}>{{_('G-Mail Account with OAuth2 Verfification')}}</option>
<option value="1" {% if content.mail_server_type == 1 %}selected{% endif %}>{{_('Gmail Account with OAuth2 Verfification')}}</option>
</select>
</div>
<div data-related="email-settings-1">
@ -20,7 +20,7 @@
{% if content.mail_gmail_token == {} %}
<button type="submit" id="gmail_server" name="gmail" value="submit" class="btn btn-default">{{_('Setup Gmail Account as E-Mail Server')}}</button>
{% else %}
<button type="submit" id="invalidate_server" name="invalidate" value="submit" class="btn btn-danger">{{_('Revoke G-Mail Access')}}</button>
<button type="submit" id="invalidate_server" name="invalidate" value="submit" class="btn btn-danger">{{_('Revoke Gmail Access')}}</button>
{% endif %}
</div>
</div>
@ -66,7 +66,7 @@
{% if feature_support['gmail'] %}
</div>
{% endif %}
<a href="{{ url_for('admin.admin') }}" id="back" class="btn btn-default">{{_('Back')}}</a>
<a href="{{ url_for('admin.admin') }}" id="email_back" class="btn btn-default">{{_('Back')}}</a>
</form>
{% if g.allow_registration %}
<div class="col-md-10 col-lg-6">

View File

@ -92,7 +92,7 @@
{% for message in get_flashed_messages(with_categories=True) %}
{%if message[0] == "error" %}
<div class="row-fluid text-center" style="margin-top: -20px;">
<div id="flash_alert" class="alert alert-danger">{{ message[1] }}</div>
<div id="flash_danger" class="alert alert-danger">{{ message[1] }}</div>
</div>
{%endif%}
{%if message[0] == "info" %}

View File

@ -60,12 +60,12 @@
<a id="fullscreen" class="icon-resize-full">Fullscreen</a>
</div>
<div id="progress" role="progressbar" class="loading">
<div class="bar-load">
<div class="bar-load from-left">
<div class="text load">
Loading...
</div>
</div>
<div class="bar-read">
<div class="bar-read from-left">
<div class="text page"></div>
</div>
</div>

View File

@ -129,7 +129,7 @@
<div class="col-sm-12">
<div id="user_submit" class="btn btn-default">{{_('Save')}}</div>
{% if not profile %}
<a href="{{ url_for('admin.admin') }}" id="back" class="btn btn-default">{{_('Cancel')}}</a>
<div class="btn btn-default" data-back="{{ url_for('admin.admin') }}" id="back">{{_('Cancel')}}</div>
{% endif %}
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
<div class="btn btn-danger" id="btndeluser" data-value="{{ content.id }}" data-remote="false" >{{_('Delete User')}}</div>

View File

@ -1,5 +1,5 @@
{% extends "layout.html" %}
{% macro user_table_row(parameter, edit_text, show_text, validate, button=False, id=0) -%}
{% macro user_table_row(parameter, edit_text, show_text, validate, elements=False) -%}
<th data-field="{{ parameter }}" id="{{ parameter }}"
data-name="{{ parameter }}"
data-visible="{{visiblility.get(parameter)}}"
@ -11,8 +11,22 @@
data-sortable="true"
{% endif %}
{% if validate %}data-edit-validate="{{ _('This Field is Required') }}"{% endif %}>
{% if button %}
<!--div><button data-id="{{id}}" data-toggle="modal" data-target="#restrictModal" class="btn btn-default button_head disabled" aria-disabled="true">{{edit_text}}</button></div--><br>
{% if elements %}
<div class="multi_select">
<select class="multi_selector" id="{{ parameter }}" data-live-search="true" data-style="btn-default" data-dropup-auto="false" aria-disabled="true" multiple disabled>
{% for tag in elements %}
<option class="tags_click" value="{{tag.id}}">{% if tag.name %}{{tag.name}}{% else %}{{tag.value}}{% endif %}</option>
{% endfor %}
</select>
<div class="btn-group btn-group-justified" role="group">
<div class="btn-group" role="group">
<div class="multi_head btn btn-default hidden" data-set="remove" data-name="{{parameter}}" aria-disabled="true">{{_('Remove')}}</div>
</div>
<div class="btn-group" role="group">
<div class="multi_head btn btn-default hidden" data-set="add" data-name="{{parameter}}" aria-disabled="true">{{_('Add')}}</div>
</div>
</div>
</div>
{% endif %}
{{ show_text }}
</th>
@ -24,14 +38,16 @@
data-column="{{value.get(array_field)}}"
data-formatter="checkboxFormatter">
<div class="form-check">
<label>
<input type="radio" class="check_head" name="options_{{array_field}}" onchange="checkboxHeader('false', '{{parameter}}', {{value.get(array_field)}})" disabled>{{_('Deny')}}
</label>
<div>
<input type="radio" class="check_head" data-set="false" data-val={{value.get(array_field)}} name="options_{{array_field}}" id="false_{{array_field}}" data-name="{{parameter}}" disabled>{{_('Deny')}}
</div>
<div>
<input type="radio" class="check_head" data-set="true" data-val={{value.get(array_field)}} name="options_{{array_field}}" data-name="{{parameter}}" disabled>{{_('Allow')}}
</div>
<div class="form-check">
<label>
<input type="radio" class="check_head" name="options_{{array_field}}" onchange="checkboxHeader('true', '{{parameter}}', {{value.get(array_field)}})" disabled>{{_('Allow')}}
</label>
</div>
{{show_text}}
</th>
@ -48,14 +64,14 @@
data-editable-source={{url}}
{% if validate %}data-edit-validate="{{ _('This Field is Required') }}"{% endif %}>
<div>
<select id="select_{{ parameter }}" class="header_select" onchange="selectHeader(this, '{{parameter}}')" disabled="">
<select id="select_{{ parameter }}" class="header_select" disabled="">
<option value="none">{{ _('Select...') }}</option>
<option value="all">{{ _('Show All') }}</option>
{% for language in languages %}
<option value="{{language.lang_code}}">{{language.name}}</option>
{% endfor %}
</select>
</div><br>
</div>
{{ show_text }}
</th>
{%- endmacro %}
@ -71,13 +87,13 @@
data-editable-source={{url}}
{% if validate %}data-edit-validate="{{ _('This Field is Required') }}"{% endif %}>
<div>
<select id="select_{{ parameter }}" class="header_select" onchange="selectHeader(this, '{{parameter}}')" disabled="">
<select id="select_{{ parameter }}" class="header_select" disabled="">
<option value="None">{{_('Select...')}}</option>
{% for translation in translations %}
<option value="{{translation}}">{{translation.display_name|capitalize}}</option>
{% endfor %}
</select>
</div><br>
</div>
{{ show_text }}
</th>
{%- endmacro %}
@ -86,6 +102,7 @@
{% block header %}
<link href="{{ url_for('static', filename='css/libs/bootstrap-table.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/libs/bootstrap-editable.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/libs/bootstrap-select.min.css') }}" rel="stylesheet" >
{% endblock %}
{% block body %}
<h2 class="{{page}}">{{_(title)}}</h2>
@ -106,20 +123,21 @@
{{ user_table_row('kindle_mail', _('Enter Kindle E-mail Address'), _('Kindle E-mail'), false) }}
{{ user_select_translations('locale', url_for('admin.table_get_locale'), _('Locale'), true) }}
{{ user_select_languages('default_language', url_for('admin.table_get_default_lang'), _('Visible Book Languages'), true) }}
{{ user_table_row('denied_tags', _("Edit Denied Tags"), _("Denied Tags"), false, true, 0) }}
{{ user_table_row('allowed_tags', _("Edit Allowed Tags"), _("Allowed Tags"), false, true, 1) }}
{{ user_table_row('allowed_column_value', _("Edit Allowed Column Values"), _("Allowed Column Values"), false, true, 2) }}
{{ user_table_row('denied_column_value', _("Edit Denied Column Values"), _("Denied Columns Values"), false, true, 3) }}
{{ user_table_row('denied_tags', _("Edit Denied Tags"), _("Denied Tags"), false, tags) }}
{{ user_table_row('allowed_tags', _("Edit Allowed Tags"), _("Allowed Tags"), false, tags) }}
{{ user_table_row('allowed_column_value', _("Edit Allowed Column Values"), _("Allowed Column Values"), false, custom_values) }}
{{ user_table_row('denied_column_value', _("Edit Denied Column Values"), _("Denied Columns Values"), false, custom_values) }}
{{ user_checkbox_row("role", "admin_role", _('Admin'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "download_role",_('Upload'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "upload_role", _('Download'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "edit_role", _('Edit'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "passwd_role", _('Change Password'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "edit_shelf_role", _('Edit Public Shelfs'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "delete_role", _('Delete'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "upload_role",_('Upload'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "download_role", _('Download'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "viewer_role", _('View'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "edit_role", _('Edit'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "delete_role", _('Delete'), visiblility, all_roles)}}
{{ user_checkbox_row("role", "edit_shelf_role", _('Edit Public Shelfs'), visiblility, all_roles)}}
{{ user_checkbox_row("sidebar_view", "detail_random", _('Show Random Books in Detail View'), visiblility, sidebar_settings)}}
{{ user_checkbox_row("sidebar_view", "sidebar_language", _('Show language selection'), visiblility, sidebar_settings)}}
{{ user_checkbox_row("sidebar_view", "sidebar_read_and_unread", _('Show read/unread selection'), visiblility, sidebar_settings)}}
{{ user_checkbox_row("sidebar_view", "sidebar_series", _('Show series selection'), visiblility, sidebar_settings)}}
{{ user_checkbox_row("sidebar_view", "sidebar_category", _('Show category selection'), visiblility, sidebar_settings)}}
{{ user_checkbox_row("sidebar_view", "sidebar_random", _('Show random books'), visiblility, sidebar_settings)}}
@ -136,6 +154,9 @@
</tr>
</thead>
</table>
<div class="errorlink">
<div class="btn btn-default" data-back="{{ url_for('admin.admin') }}" id="back">{{_('Back')}}</div>
</div>
{% endblock %}
{% block modal %}
{{ delete_confirm_modal() }}
@ -146,7 +167,7 @@
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-select.min.js')}}"></script>
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
<script>
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -44,7 +44,7 @@ from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float,
from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.sql.expression import func
try:
# Compability with sqlalchemy 2.0
# Compatibility with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
@ -713,9 +713,12 @@ def init_db(app_db_path):
create_anonymous_user(session)
if cli.user_credentials:
username, password = cli.user_credentials.split(':')
username, password = cli.user_credentials.split(':', 1)
user = session.query(User).filter(func.lower(User.name) == username.lower()).first()
if user:
if not password:
print("Empty password is not allowed")
sys.exit(4)
user.password = generate_password_hash(password)
if session_commit() == "":
print("Password for user '{}' changed".format(username))

View File

@ -103,20 +103,21 @@ class Updater(threading.Thread):
time.sleep(2)
return True
except requests.exceptions.HTTPError as ex:
log.info(u'HTTP Error %s', ex)
log.error(u'HTTP Error %s', ex)
self.status = 8
except requests.exceptions.ConnectionError:
log.info(u'Connection error')
log.error(u'Connection error')
self.status = 9
except requests.exceptions.Timeout:
log.info(u'Timeout while establishing connection')
log.error(u'Timeout while establishing connection')
self.status = 10
except (requests.exceptions.RequestException, zipfile.BadZipFile):
self.status = 11
log.info(u'General error')
except (IOError, OSError):
log.error(u'General error')
except (IOError, OSError) as ex:
self.status = 12
log.info(u'Update File Could Not be Saved in Temp Dir')
log.error(u'Possible Reason for error: update file could not be saved in temp dir')
log.debug_or_exception(ex)
self.pause()
return False
@ -182,39 +183,50 @@ class Updater(threading.Thread):
@classmethod
def moveallfiles(cls, root_src_dir, root_dst_dir):
change_permissions = True
new_permissions = os.stat(root_dst_dir)
if sys.platform == "win32" or sys.platform == "darwin":
change_permissions = False
else:
log.debug('Update on OS-System : %s', sys.platform)
log.debug('Performing Update on OS-System: %s', sys.platform)
change_permissions = (sys.platform == "win32" or sys.platform == "darwin")
for src_dir, __, files in os.walk(root_src_dir):
dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
if not os.path.exists(dst_dir):
try:
os.makedirs(dst_dir)
log.debug('Create-Dir: %s', dst_dir)
log.debug('Create directory: {}', dst_dir)
except OSError as e:
log.error('Failed creating folder: {} with error {}'.format(dst_dir, e))
if change_permissions:
# print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid))
try:
os.chown(dst_dir, new_permissions.st_uid, new_permissions.st_gid)
except OSError as e:
old_permissions = os.stat(dst_dir)
log.error('Failed changing permissions of %s. Before: %s:%s After %s:%s error: %s',
dst_dir, old_permissions.st_uid, old_permissions.st_gid,
new_permissions.st_uid, new_permissions.st_gid, e)
for file_ in files:
src_file = os.path.join(src_dir, file_)
dst_file = os.path.join(dst_dir, file_)
if os.path.exists(dst_file):
if change_permissions:
permission = os.stat(dst_file)
log.debug('Remove file before copy: %s', dst_file)
try:
os.remove(dst_file)
log.debug('Remove file before copy: %s', dst_file)
except OSError as e:
log.error('Failed removing file: {} with error {}'.format(dst_file, e))
else:
if change_permissions:
permission = new_permissions
try:
shutil.move(src_file, dst_dir)
log.debug('Move File %s to %s', src_file, dst_dir)
except OSError as ex:
log.error('Failed moving file from {} to {} with error {}'.format(src_file, dst_dir, ex))
if change_permissions:
try:
os.chown(dst_file, permission.st_uid, permission.st_gid)
except OSError as e:
old_permissions = os.stat(dst_file)
log.debug('Fail change permissions of %s. Before: %s:%s After %s:%s error: %s',
log.error('Failed changing permissions of %s. Before: %s:%s After %s:%s error: %s',
dst_file, old_permissions.st_uid, old_permissions.st_gid,
permission.st_uid, permission.st_gid, e)
return
@ -266,9 +278,8 @@ class Updater(threading.Thread):
shutil.rmtree(item_path, ignore_errors=True)
else:
try:
log.debug("Delete file %s", item_path)
# log_from_thread("Delete file " + item_path)
os.remove(item_path)
log.debug("Delete file %s", item_path)
except OSError:
log.debug("Could not remove: %s", item_path)
shutil.rmtree(source, ignore_errors=True)
@ -283,11 +294,13 @@ class Updater(threading.Thread):
@classmethod
def _nightly_version_info(cls):
if is_sha1(constants.NIGHTLY_VERSION[0]) and len(constants.NIGHTLY_VERSION[1]) > 0:
log.debug("Nightly version: {}, {}".format(constants.NIGHTLY_VERSION[0], constants.NIGHTLY_VERSION[1]))
return {'version': constants.NIGHTLY_VERSION[0], 'datetime': constants.NIGHTLY_VERSION[1]}
return False
@classmethod
def _stable_version_info(cls):
log.debug("Stable version: {}".format(constants.STABLE_VERSION))
return constants.STABLE_VERSION # Current version
@staticmethod
@ -381,6 +394,7 @@ class Updater(threading.Thread):
# if 'committer' in update_data and 'message' in update_data:
try:
log.debug("A new update is available.")
status['success'] = True
status['message'] = _(
u'A new update is available. Click on the button below to update to the latest version.')
@ -401,6 +415,7 @@ class Updater(threading.Thread):
except (IndexError, KeyError):
status['success'] = False
status['message'] = _(u'Could not fetch update information')
log.error("Could not fetch update information")
return json.dumps(status)
return ''
@ -468,6 +483,7 @@ class Updater(threading.Thread):
# we are already on newest version, no update available
if 'tag_name' not in commit[0]:
status['message'] = _(u'Unexpected data while reading update information')
log.error("Unexpected data while reading update information")
return json.dumps(status)
if commit[0]['tag_name'] == version:
status.update({

View File

@ -75,8 +75,9 @@ def load_user_from_auth_header(header_val):
basic_username = basic_password = '' # nosec
try:
header_val = base64.b64decode(header_val).decode('utf-8')
basic_username = header_val.split(':')[0]
basic_password = header_val.split(':')[1]
# Users with colon are invalid: rfc7617 page 4
basic_username = header_val.split(':', 1)[0]
basic_password = header_val.split(':', 1)[1]
except (TypeError, UnicodeDecodeError, binascii.Error):
pass
user = _fetch_user_by_name(basic_username)

View File

@ -26,6 +26,7 @@ from datetime import datetime
import json
import mimetypes
import chardet # dependency of requests
import copy
from babel.dates import format_date
from babel import Locale as LC
@ -183,11 +184,12 @@ def toggle_read(book_id):
calibre_db.session.add(new_cc)
calibre_db.session.commit()
except (KeyError, AttributeError):
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 existing in calibre database", config.config_read_column)
return "Custom Column No.{} is not existing in calibre database".format(config.config_read_column), 400
except (OperationalError, InvalidRequestError) as e:
calibre_db.session.rollback()
log.error(u"Read status could not set: %e", e)
return "Read status could not set: {}".format(e), 400
return ""
@web.route("/ajax/togglearchived/<int:book_id>", methods=['POST'])
@ -753,20 +755,50 @@ def books_table():
@web.route("/ajax/listbooks")
@login_required
def list_books():
off = request.args.get("offset") or 0
limit = request.args.get("limit") or config.config_books_per_page
# sort = request.args.get("sort")
if request.args.get("order") == 'desc':
order = [db.Books.timestamp.desc()]
else:
order = [db.Books.timestamp.asc()]
off = int(request.args.get("offset") or 0)
limit = int(request.args.get("limit") or config.config_books_per_page)
search = request.args.get("search")
total_count = calibre_db.session.query(db.Books).count()
sort = request.args.get("sort", "id")
order = request.args.get("order", "").lower()
state = None
join = tuple()
if sort == "state":
state = json.loads(request.args.get("state", "[]"))
elif sort == "tags":
order = [db.Tags.name.asc()] if order == "asc" else [db.Tags.name.desc()]
join = db.books_tags_link,db.Books.id == db.books_tags_link.c.book, db.Tags
elif sort == "series":
order = [db.Series.name.asc()] if order == "asc" else [db.Series.name.desc()]
join = db.books_series_link,db.Books.id == db.books_series_link.c.book, db.Series
elif sort == "publishers":
order = [db.Publishers.name.asc()] if order == "asc" else [db.Publishers.name.desc()]
join = db.books_publishers_link,db.Books.id == db.books_publishers_link.c.book, db.Publishers
elif sort == "authors":
order = [db.Authors.name.asc()] if order == "asc" else [db.Authors.name.desc()]
join = db.books_authors_link,db.Books.id == db.books_authors_link.c.book, db.Authors
elif sort == "languages":
order = [db.Languages.lang_code.asc()] if order == "asc" else [db.Languages.lang_code.desc()]
join = db.books_languages_link,db.Books.id == db.books_languages_link.c.book, db.Languages
elif order and sort in ["sort", "title", "authors_sort", "series_index"]:
order = [text(sort + " " + order)]
elif not state:
order = [db.Books.timestamp.desc()]
total_count = filtered_count = calibre_db.session.query(db.Books).count()
if state:
if search:
entries, filtered_count, __ = calibre_db.get_search_results(search, off, order, limit)
books = calibre_db.search_query(search).all()
filtered_count = len(books)
else:
entries, __, __ = calibre_db.fill_indexpage((int(off) / (int(limit)) + 1), limit, db.Books, True, order)
filtered_count = total_count
books = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).all()
entries = calibre_db.get_checkbox_sorted(books, state, off, limit,order)
elif search:
entries, filtered_count, __ = calibre_db.get_search_results(search, off, order, limit, *join)
else:
entries, __, __ = calibre_db.fill_indexpage((int(off) / (int(limit)) + 1), limit, db.Books, True, order, *join)
for entry in entries:
for index in range(0, len(entry.languages)):
try:
@ -816,9 +848,12 @@ def author_list():
charlist = calibre_db.session.query(func.upper(func.substr(db.Authors.sort, 1, 1)).label('char')) \
.join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(func.upper(func.substr(db.Authors.sort, 1, 1))).all()
for entry in entries:
# If not creating a copy, readonly databases can not display authornames with "|" in it as changing the name
# starts a change session
autor_copy = copy.deepcopy(entries)
for entry in autor_copy:
entry.Authors.name = entry.Authors.name.replace('|', ',')
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
return render_title_template('list.html', entries=autor_copy, folder='web.books_list', charlist=charlist,
title=u"Authors", page="authorlist", data='author', order=order_no)
else:
abort(404)
@ -1083,12 +1118,19 @@ def adv_search_ratings(q, rating_high, rating_low):
def adv_search_read_status(q, read_status):
if read_status:
if config.config_read_column:
try:
if read_status == "True":
q = q.join(db.cc_classes[config.config_read_column], isouter=True) \
.filter(db.cc_classes[config.config_read_column].value == True)
else:
q = q.join(db.cc_classes[config.config_read_column], isouter=True) \
.filter(coalesce(db.cc_classes[config.config_read_column].value, False) != True)
except (KeyError, AttributeError):
log.error(u"Custom Column No.%d is not existing in calibre database", config.config_read_column)
flash(_("Custom Column No.%(column)d is not existing in calibre database",
column=config.config_read_column),
category="error")
return q
else:
if read_status == "True":
q = q.join(ub.ReadBook, db.Books.id == ub.ReadBook.book_id, isouter=True) \
@ -1453,23 +1495,23 @@ def login():
log.info(error)
flash(_(u"Could not login: %(message)s", message=error), category="error")
else:
ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr)
log.warning('LDAP Login failed for user "%s" IP-address: %s', form['username'], ipAdress)
ip_Address = request.headers.get('X-Forwarded-For', request.remote_addr)
log.warning('LDAP Login failed for user "%s" IP-address: %s', form['username'], ip_Address)
flash(_(u"Wrong Username or Password"), category="error")
else:
ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr)
ip_Address = request.headers.get('X-Forwarded-For', request.remote_addr)
if 'forgot' in form and form['forgot'] == 'forgot':
if user != None and user.name != "Guest":
ret, __ = reset_password(user.id)
if ret == 1:
flash(_(u"New Password was send to your email address"), category="info")
log.info('Password reset for user "%s" IP-address: %s', form['username'], ipAdress)
log.info('Password reset for user "%s" IP-address: %s', form['username'], ip_Address)
else:
log.error(u"An unknown error occurred. Please try again later")
flash(_(u"An unknown error occurred. Please try again later."), category="error")
else:
flash(_(u"Please enter valid username to reset password"), category="error")
log.warning('Username missing for password reset IP-address: %s', ipAdress)
log.warning('Username missing for password reset IP-address: %s', ip_Address)
else:
if user and check_password_hash(str(user.password), form['password']) and user.name != "Guest":
login_user(user, remember=bool(form.get('remember_me')))
@ -1478,7 +1520,7 @@ def login():
config.config_is_initial = False
return redirect_back(url_for("web.index"))
else:
log.warning('Login failed for user "%s" IP-address: %s', form['username'], ipAdress)
log.warning('Login failed for user "%s" IP-address: %s', form['username'], ip_Address)
flash(_(u"Wrong Username or Password"), category="error")
next_url = request.args.get('next', default=url_for("web.index"), type=str)

File diff suppressed because it is too large Load Diff

View File

@ -26,8 +26,8 @@ python-ldap>=3.0.0,<3.4.0
Flask-SimpleLDAP>=1.4.0,<1.5.0
#oauth
Flask-Dance>=1.4.0,<3.1.0
SQLAlchemy-Utils>=0.33.5,<0.37.0
Flask-Dance>=1.4.0,<4.1.0
SQLAlchemy-Utils>=0.33.5,<0.38.0
# extracting metadata
lxml>=3.8.0,<4.7.0

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,4 @@
output_list = Array();
/* Level - 0: Summary; 1: Failed; 2: All; 3: Skipped 4: Error*/
@ -24,9 +25,9 @@ function showCase(level) {
row.classList.add('hiddenRow');
}
}
// Show skipped if all or skipped or summary problems selected
// Show skipped if all or skipped selected
if (id.substr(0,2) == 'st') {
if (level ==2 || level ==3 || level == 5) {
if (level ==2 || level ==3) {
row.classList.remove('hiddenRow');
}
else {