Merge branch 'master' into Develop

pull/1999/head
Ozzieisaacs 3 years ago
commit b34672ed19

@ -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,52 +369,89 @@ 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:
vals['value'] = vals['value'].strip()
if param == 'name':
if user.name == "Guest":
raise Exception(_("Guest Name can't be changed"))
user.name = check_username(vals['value'])
elif param =='email':
user.email = check_email(vals['value'])
elif param == 'kindle_mail':
user.kindle_mail = valid_email(vals['value']) if vals['value'] else ""
elif param == 'role':
if vals['value'] == 'true':
user.role |= int(vals['field_index'])
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:
if int(vals['field_index']) == 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'])
setattr(user, param, vals['value'].strip())
else:
vals['value'] = vals['value'].strip()
if param == 'name':
if user.name == "Guest":
raise Exception(_("Guest Name can't be changed"))
user.name = check_username(vals['value'])
elif param =='email':
user.email = check_email(vals['value'])
elif param == 'kindle_mail':
user.kindle_mail = valid_email(vals['value']) if vals['value'] else ""
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 |= 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 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:
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:
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']
elif param == 'locale':
user.locale = vals['value']
elif param == 'default_language':
user.default_language = vals['value']
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 _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():
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()
flash(_(u"User '%(nick)s' deleted", nick=content.name), category="success")
return redirect(url_for('admin.admin'))
log.info(u"User {} deleted".format(content.name))
return(_(u"User '%(nick)s' deleted", nick=content.name))
else:
flash(_(u"No admin user remaining, can't delete user", nick=content.name), category="error")
return redirect(url_for('admin.admin'))
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"):
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,

@ -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

@ -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:

@ -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,20 +607,46 @@ 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:
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)). \
any(cc_classes[self.config.config_restricted_column].value.in_(pos_cc_list))
neg_cc_list = current_user.denied_column_value.split(',')
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))
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)). \
any(cc_classes[self.config.config_restricted_column].value.in_(pos_cc_list))
neg_cc_list = current_user.denied_column_value.split(',')
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)

@ -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 ""

@ -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)
session.commit()
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,37 +277,42 @@ def getFile(pathId, fileName, drive):
def getFolderId(path, drive):
# drive = getDrive(drive)
currentFolderId = getEbooksFolderId(drive)
sqlCheckPath = path if path[-1] == '/' else path + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first()
if not storedPathName:
dbChange = False
s = path.split('/')
for i, x in enumerate(s):
if len(x) > 0:
currentPath = "/".join(s[:i+1])
if currentPath[-1] != '/':
currentPath = currentPath + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == currentPath).first()
if storedPathName:
currentFolderId = storedPathName.gdrive_id
else:
currentFolder = getFolderInFolder(currentFolderId, x, drive)
if currentFolder:
gDriveId = GdriveId()
gDriveId.gdrive_id = currentFolder['id']
gDriveId.path = currentPath
session.merge(gDriveId)
dbChange = True
currentFolderId = currentFolder['id']
try:
currentFolderId = getEbooksFolderId(drive)
sqlCheckPath = path if path[-1] == '/' else path + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first()
if not storedPathName:
dbChange = False
s = path.split('/')
for i, x in enumerate(s):
if len(x) > 0:
currentPath = "/".join(s[:i+1])
if currentPath[-1] != '/':
currentPath = currentPath + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == currentPath).first()
if storedPathName:
currentFolderId = storedPathName.gdrive_id
else:
currentFolderId = None
break
if dbChange:
session.commit()
else:
currentFolderId = storedPathName.gdrive_id
currentFolder = getFolderInFolder(currentFolderId, x, drive)
if currentFolder:
gDriveId = GdriveId()
gDriveId.gdrive_id = currentFolder['id']
gDriveId.path = currentPath
session.merge(gDriveId)
dbChange = True
currentFolderId = currentFolder['id']
else:
currentFolderId = None
break
if dbChange:
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
session.commit()
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()
session.commit()
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)
session.commit()
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

@ -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

@ -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)

@ -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'))
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")
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"))
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")
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'))

@ -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

@ -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

@ -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")

@ -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,

@ -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,

@ -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;

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

@ -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() {

@ -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:

@ -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"));
});

@ -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);
});
},
onPostHeader () {
move_header_elements();
},
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
loadSuccess();
},
// 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) {
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,29 +498,39 @@ $(function() {
});
var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference";
selections = window._[func](selections, ids);
if (selections.length < 1) {
$("#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);
} else {
$("#user_delete_selection").removeClass("disabled");
$("#user_delete_selection").attr("aria-disabled", false);
$(".check_head").attr("aria-disabled", false);
$(".check_head").removeAttr("disabled");
$(".button_head").attr("aria-disabled", false);
$(".button_head").removeClass("disabled");
$(".header_select").removeAttr("disabled");
}
handle_header_buttons();
});
});
function handle_header_buttons () {
if (selections.length < 1) {
$("#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");
$(".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");
$("#user_delete_selection").attr("aria-disabled", false);
$(".check_head").attr("aria-disabled", false);
$(".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 + ')">';
}
function checkboxChange(checkbox, userId, field, field_index) {
$.ajax({
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>*/
/* 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);
});
$.ajax({
method:"get",
url: window.location.pathname + "/../../ajax/listusers",
async: true,
timeout: 900,
success:function(data) {
$("#user-table").bootstrapTable("load", data);
$(".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 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 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",
url: window.location.pathname + "/../../ajax/editlistusers/" + field,
data: {"pk": userId, "field_index": field_index, "value": checkbox.checked},
error: function(data) {
handleListServerResponse([{type:"danger", message:data.responseText}])
},
success: handleListServerResponse
});
}
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,46 +759,58 @@ 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,
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);
}
});
}
error: function (data) {
handleListServerResponse([{type:"danger", message:data.responseText}])
},
success: function (data) {
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) {
$.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);
}
});
}
function test(){
console.log("hello");
$("#user-table").bootstrapTable("refresh");
}

@ -134,9 +134,9 @@ class TaskEmail(CalibreTask):
return message
def run(self, worker_thread):
# create MIME message
msg = self.prepare_message()
try:
# create MIME message
msg = self.prepare_message()
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)

@ -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>

@ -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 %}

@ -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">

@ -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" %}

@ -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>

@ -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>

@ -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>
@ -23,15 +37,17 @@
data-visible="{{element.get(array_field)}}"
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 class="form-check">
<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

@ -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))

@ -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):
os.makedirs(dst_dir)
log.debug('Create-Dir: %s', dst_dir)
try:
os.makedirs(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))
os.chown(dst_dir, new_permissions.st_uid, new_permissions.st_gid)
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)
os.remove(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
shutil.move(src_file, dst_dir)
log.debug('Move File %s to %s', src_file, dst_dir)
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({

@ -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)

@ -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()
if search:
entries, filtered_count, __ = calibre_db.get_search_results(search, off, order, limit)
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:
books = calibre_db.search_query(search).all()
filtered_count = len(books)
else:
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)
filtered_count = total_count
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:
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)
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

@ -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

@ -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 {

Loading…
Cancel
Save