Merge branch 'development' into master

pull/1927/head
Ozzie Isaacs 3 years ago
commit 947154dc9c

@ -127,7 +127,7 @@ def get_locale():
user = getattr(g, 'user', None) user = getattr(g, 'user', None)
# user = None # user = None
if user is not None and hasattr(user, "locale"): if user is not None and hasattr(user, "locale"):
if user.nickname != 'Guest': # if the account is the guest account bypass the config lang settings if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
return user.locale return user.locale
preferred = list() preferred = list()

@ -35,13 +35,15 @@ from flask import Blueprint, flash, redirect, url_for, abort, request, make_resp
from flask_login import login_required, current_user, logout_user, confirm_login from flask_login import login_required, current_user, logout_user, confirm_login
from flask_babel import gettext as _ from flask_babel import gettext as _
from sqlalchemy import and_ from sqlalchemy import and_
from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
from sqlalchemy.sql.expression import func, or_ from sqlalchemy.sql.expression import func, or_
from . import constants, logger, helper, services from . import constants, logger, helper, services
from .cli import filepicker from .cli import filepicker
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \
valid_email, check_username
from .gdriveutils import is_gdrive_ready, gdrive_support from .gdriveutils import is_gdrive_ready, gdrive_support
from .render_template import render_title_template, get_sidebar_config from .render_template import render_title_template, get_sidebar_config
from . import debug_info from . import debug_info
@ -57,12 +59,12 @@ feature_support = {
'ldap': bool(services.ldap), 'ldap': bool(services.ldap),
'goodreads': bool(services.goodreads_support), 'goodreads': bool(services.goodreads_support),
'kobo': bool(services.kobo), 'kobo': bool(services.kobo),
'updater': constants.UPDATER_AVAILABLE 'updater': constants.UPDATER_AVAILABLE,
'gmail': bool(services.gmail)
} }
try: try:
# pylint: disable=unused-import import rarfile # pylint: disable=unused-import
import rarfile
feature_support['rar'] = True feature_support['rar'] = True
except (ImportError, SyntaxError): except (ImportError, SyntaxError):
feature_support['rar'] = False feature_support['rar'] = False
@ -150,7 +152,7 @@ def shutdown():
else: else:
showtext['text'] = _(u'Performing shutdown of server, please close window') showtext['text'] = _(u'Performing shutdown of server, please close window')
# stop gevent/tornado server # stop gevent/tornado server
web_server.stop(task == 0) web_server.stop(task==0)
return json.dumps(showtext) return json.dumps(showtext)
if task == 2: if task == 2:
@ -185,10 +187,10 @@ def admin():
else: else:
commit = version['version'] commit = version['version']
all_user = ub.session.query(ub.User).all() allUser = ub.session.query(ub.User).all()
email_settings = config.get_mail_settings() email_settings = config.get_mail_settings()
kobo_support = feature_support['kobo'] and config.config_kobo_sync kobo_support = feature_support['kobo'] and config.config_kobo_sync
return render_title_template("admin.html", allUser=all_user, email=email_settings, config=config, commit=commit, return render_title_template("admin.html", allUser=allUser, email=email_settings, config=config, commit=commit,
feature_support=feature_support, kobo_support=kobo_support, feature_support=feature_support, kobo_support=kobo_support,
title=_(u"Admin page"), page="admin") title=_(u"Admin page"), page="admin")
@ -214,6 +216,173 @@ def view_configuration():
restrictColumns=restrict_columns, restrictColumns=restrict_columns,
title=_(u"UI Configuration"), page="uiconfig") title=_(u"UI Configuration"), page="uiconfig")
@admi.route("/admin/usertable")
@login_required
@admin_required
def edit_user_table():
visibility = current_user.view_settings.get('useredit', {})
languages = calibre_db.speaking_language()
translations = babel.list_translations() + [LC('en')]
allUser = ub.session.query(ub.User)
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(),
translations=translations,
languages=languages,
visiblility=visibility,
all_roles=constants.ALL_ROLES,
sidebar_settings=constants.sidebar_settings,
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
search = request.args.get("search")
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()
if search:
users = 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)
else:
users = all_user.offset(off).limit(limit).all()
filtered_count = total_count
for user in users:
if user.default_language == "all":
user.default = _("all")
else:
user.default = LC.parse(user.default_language).get_language_name(get_locale())
table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": users}
js_list = json.dumps(table_entries, cls=db.AlchemyEncoder)
response = make_response(js_list)
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@admi.route("/ajax/deleteuser")
@login_required
@admin_required
def delete_user():
# ToDo User delete check also not last one
return ""
@admi.route("/ajax/getlocale")
@login_required
@admin_required
def table_get_locale():
locale = babel.list_translations() + [LC('en')]
ret = list()
current_locale = get_locale()
for loc in locale:
ret.append({'value': str(loc), 'text': loc.get_language_name(current_locale)})
return json.dumps(ret)
@admi.route("/ajax/getdefaultlanguage")
@login_required
@admin_required
def table_get_default_lang():
languages = calibre_db.speaking_language()
ret = list()
ret.append({'value':'all','text':_('Show All')})
for lang in languages:
ret.append({'value': lang.lang_code, 'text': lang.name})
return json.dumps(ret)
@admi.route("/ajax/editlistusers/<param>", methods=['POST'])
@login_required
@admin_required
def edit_list_user(param):
vals = request.form.to_dict(flat=False)
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)
# only one user is posted
if "pk" in vals:
users = [all_user.filter(ub.User.id == vals['pk'][0]).one_or_none()]
else:
if "pk[]" in vals:
users = all_user.filter(ub.User.id.in_(vals['pk[]'])).all()
else:
return ""
if 'field_index' in vals:
vals['field_index'] = vals['field_index'][0]
if 'value' in vals:
vals['value'] = vals['value'][0]
else:
return ""
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'])
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'])
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']
except Exception as ex:
return str(ex), 400
ub.session_commit()
return ""
@admi.route("/ajax/user_table_settings", methods=['POST'])
@login_required
@admin_required
def update_table_settings():
current_user.view_settings['useredit'] = json.loads(request.data)
try:
try:
flag_modified(current_user, "view_settings")
except AttributeError:
pass
ub.session.commit()
except (InvalidRequestError, OperationalError):
log.error("Invalid request received: {}".format(request))
return "Invalid request", 400
return ""
@admi.route("/admin/viewconfig", methods=["POST"]) @admi.route("/admin/viewconfig", methods=["POST"])
@login_required @login_required
@ -262,6 +431,14 @@ def load_dialogtexts(element_id):
texts["main"] = _('Do you really want to delete this user?') texts["main"] = _('Do you really want to delete this user?')
elif element_id == "delete_shelf": elif element_id == "delete_shelf":
texts["main"] = _('Are you sure you want to delete this shelf?') texts["main"] = _('Are you sure you want to delete this shelf?')
elif element_id == "select_locale":
texts["main"] = _('Are you sure you want to change locales of selected user(s)?')
elif element_id == "select_default_language":
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 == "sidebar_view":
texts["main"] = _('Are you sure you want to change the selected visibility restrictions for the selected user(s)?')
return json.dumps(texts) return json.dumps(texts)
@ -348,7 +525,7 @@ def edit_restriction(res_type, user_id):
elementlist = usr.list_allowed_tags() elementlist = usr.list_allowed_tags()
elementlist[int(element['id'][1:])] = element['Element'] elementlist[int(element['id'][1:])] = element['Element']
usr.allowed_tags = ','.join(elementlist) usr.allowed_tags = ','.join(elementlist)
ub.session_commit("Changed allowed tags of user {} to {}".format(usr.nickname, usr.allowed_tags)) ub.session_commit("Changed allowed tags of user {} to {}".format(usr.name, usr.allowed_tags))
if res_type == 3: # CColumn per user if res_type == 3: # CColumn per user
if isinstance(user_id, int): if isinstance(user_id, int):
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
@ -357,7 +534,7 @@ def edit_restriction(res_type, user_id):
elementlist = usr.list_allowed_column_values() elementlist = usr.list_allowed_column_values()
elementlist[int(element['id'][1:])] = element['Element'] elementlist[int(element['id'][1:])] = element['Element']
usr.allowed_column_value = ','.join(elementlist) usr.allowed_column_value = ','.join(elementlist)
ub.session_commit("Changed allowed columns of user {} to {}".format(usr.nickname, usr.allowed_column_value)) ub.session_commit("Changed allowed columns of user {} to {}".format(usr.name, usr.allowed_column_value))
if element['id'].startswith('d'): if element['id'].startswith('d'):
if res_type == 0: # Tags as template if res_type == 0: # Tags as template
elementlist = config.list_denied_tags() elementlist = config.list_denied_tags()
@ -377,7 +554,7 @@ def edit_restriction(res_type, user_id):
elementlist = usr.list_denied_tags() elementlist = usr.list_denied_tags()
elementlist[int(element['id'][1:])] = element['Element'] elementlist[int(element['id'][1:])] = element['Element']
usr.denied_tags = ','.join(elementlist) usr.denied_tags = ','.join(elementlist)
ub.session_commit("Changed denied tags of user {} to {}".format(usr.nickname, usr.denied_tags)) ub.session_commit("Changed denied tags of user {} to {}".format(usr.name, usr.denied_tags))
if res_type == 3: # CColumn per user if res_type == 3: # CColumn per user
if isinstance(user_id, int): if isinstance(user_id, int):
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
@ -386,7 +563,7 @@ def edit_restriction(res_type, user_id):
elementlist = usr.list_denied_column_values() elementlist = usr.list_denied_column_values()
elementlist[int(element['id'][1:])] = element['Element'] elementlist[int(element['id'][1:])] = element['Element']
usr.denied_column_value = ','.join(elementlist) usr.denied_column_value = ','.join(elementlist)
ub.session_commit("Changed denied columns of user {} to {}".format(usr.nickname, usr.denied_column_value)) ub.session_commit("Changed denied columns of user {} to {}".format(usr.name, usr.denied_column_value))
return "" return ""
@ -433,10 +610,10 @@ def add_restriction(res_type, user_id):
usr = current_user usr = current_user
if 'submit_allow' in element: if 'submit_allow' in element:
usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags) usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags)
ub.session_commit("Changed allowed tags of user {} to {}".format(usr.nickname, 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: elif 'submit_deny' in element:
usr.denied_tags = restriction_addition(element, usr.list_denied_tags) usr.denied_tags = restriction_addition(element, usr.list_denied_tags)
ub.session_commit("Changed denied tags of user {} to {}".format(usr.nickname, 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 res_type == 3: # CustomC per user
if isinstance(user_id, int): if isinstance(user_id, int):
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
@ -444,11 +621,11 @@ def add_restriction(res_type, user_id):
usr = current_user usr = current_user
if 'submit_allow' in element: if 'submit_allow' in element:
usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values) usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values)
ub.session_commit("Changed allowed columns of user {} to {}".format(usr.nickname, 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: elif 'submit_deny' in element:
usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values) usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values)
ub.session_commit("Changed denied columns of user {} to {}".format(usr.nickname, ub.session_commit("Changed denied columns of user {} to {}".format(usr.name,
usr.list_denied_column_values)) usr.list_denied_column_values))
return "" return ""
@ -480,10 +657,10 @@ def delete_restriction(res_type, user_id):
usr = current_user usr = current_user
if element['id'].startswith('a'): if element['id'].startswith('a'):
usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags) usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags)
ub.session_commit("Deleted allowed tags of user {}: {}".format(usr.nickname, usr.list_allowed_tags)) ub.session_commit("Deleted allowed tags of user {}: {}".format(usr.name, usr.list_allowed_tags))
elif element['id'].startswith('d'): elif element['id'].startswith('d'):
usr.denied_tags = restriction_deletion(element, usr.list_denied_tags) usr.denied_tags = restriction_deletion(element, usr.list_denied_tags)
ub.session_commit("Deleted denied tags of user {}: {}".format(usr.nickname, usr.list_allowed_tags)) ub.session_commit("Deleted denied tags of user {}: {}".format(usr.name, usr.list_allowed_tags))
elif res_type == 3: # Columns per user elif res_type == 3: # Columns per user
if isinstance(user_id, int): if isinstance(user_id, int):
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
@ -491,12 +668,12 @@ def delete_restriction(res_type, user_id):
usr = current_user usr = current_user
if element['id'].startswith('a'): if element['id'].startswith('a'):
usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values) usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values)
ub.session_commit("Deleted allowed columns of user {}: {}".format(usr.nickname, ub.session_commit("Deleted allowed columns of user {}: {}".format(usr.name,
usr.list_allowed_column_values)) usr.list_allowed_column_values))
elif element['id'].startswith('d'): elif element['id'].startswith('d'):
usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values) usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values)
ub.session_commit("Deleted denied columns of user {}: {}".format(usr.nickname, ub.session_commit("Deleted denied columns of user {}: {}".format(usr.name,
usr.list_denied_column_values)) usr.list_denied_column_values))
return "" return ""
@ -602,7 +779,6 @@ def pathchooser():
folders = [] folders = []
files = [] files = []
# locale = get_locale()
for f in folders: for f in folders:
try: try:
data = {"name": f, "fullpath": os.path.join(cwd, f)} data = {"name": f, "fullpath": os.path.join(cwd, f)}
@ -730,13 +906,35 @@ def _configuration_logfile_helper(to_save, gdrive_error):
return reboot_required, None return reboot_required, None
def _configuration_ldap_check(reboot_required, to_save, gdrive_error): def _configuration_ldap_helper(to_save, gdrive_error):
reboot_required = False
reboot_required |= _config_string(to_save, "config_ldap_provider_url")
reboot_required |= _config_int(to_save, "config_ldap_port")
reboot_required |= _config_int(to_save, "config_ldap_authentication")
reboot_required |= _config_string(to_save, "config_ldap_dn")
reboot_required |= _config_string(to_save, "config_ldap_serv_username")
reboot_required |= _config_string(to_save, "config_ldap_user_object")
reboot_required |= _config_string(to_save, "config_ldap_group_object_filter")
reboot_required |= _config_string(to_save, "config_ldap_group_members_field")
reboot_required |= _config_string(to_save, "config_ldap_member_user_object")
reboot_required |= _config_checkbox(to_save, "config_ldap_openldap")
reboot_required |= _config_int(to_save, "config_ldap_encryption")
reboot_required |= _config_string(to_save, "config_ldap_cacert_path")
reboot_required |= _config_string(to_save, "config_ldap_cert_path")
reboot_required |= _config_string(to_save, "config_ldap_key_path")
_config_string(to_save, "config_ldap_group_name")
if to_save.get("config_ldap_serv_password", "") != "":
reboot_required |= 1
config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode, encode='UTF-8')
config.save()
if not config.config_ldap_provider_url \ if not config.config_ldap_provider_url \
or not config.config_ldap_port \ or not config.config_ldap_port \
or not config.config_ldap_dn \ or not config.config_ldap_dn \
or not config.config_ldap_user_object: or not config.config_ldap_user_object:
return reboot_required, _configuration_result(_('Please Enter a LDAP Provider, ' return reboot_required, _configuration_result(_('Please Enter a LDAP Provider, '
'Port, DN and User Object Identifier'), gdrive_error) 'Port, DN and User Object Identifier'), gdrive_error)
if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS: if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS:
if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE: if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE:
if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password): if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password):
@ -746,14 +944,6 @@ def _configuration_ldap_check(reboot_required, to_save, gdrive_error):
if not config.config_ldap_serv_username: if not config.config_ldap_serv_username:
return reboot_required, _configuration_result('Please Enter a LDAP Service Account', gdrive_error) return reboot_required, _configuration_result('Please Enter a LDAP Service Account', gdrive_error)
if config.config_ldap_group_object_filter:
if config.config_ldap_group_object_filter.count("%s") != 1:
return reboot_required, \
_configuration_result(_('LDAP Group Object Filter Needs to Have One "%s" Format Identifier'),
gdrive_error)
if config.config_ldap_group_object_filter.count("(") != config.config_ldap_group_object_filter.count(")"):
return reboot_required, _configuration_result(_('LDAP Group Object Filter Has Unmatched Parenthesis'),
gdrive_error)
if config.config_ldap_group_object_filter: if config.config_ldap_group_object_filter:
if config.config_ldap_group_object_filter.count("%s") != 1: if config.config_ldap_group_object_filter.count("%s") != 1:
return reboot_required, \ return reboot_required, \
@ -771,7 +961,7 @@ def _configuration_ldap_check(reboot_required, to_save, gdrive_error):
return reboot_required, _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis'), return reboot_required, _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis'),
gdrive_error) gdrive_error)
if "ldap_import_user_filter" in to_save and to_save["ldap_import_user_filter"] == '0': if to_save.get("ldap_import_user_filter") == '0':
config.config_ldap_member_user_object = "" config.config_ldap_member_user_object = ""
else: else:
if config.config_ldap_member_user_object.count("%s") != 1: if config.config_ldap_member_user_object.count("%s") != 1:
@ -793,31 +983,6 @@ def _configuration_ldap_check(reboot_required, to_save, gdrive_error):
return reboot_required, None return reboot_required, None
def _configuration_ldap_helper(to_save, gdrive_error):
reboot_required = False
reboot_required |= _config_string(to_save, "config_ldap_provider_url")
reboot_required |= _config_int(to_save, "config_ldap_port")
reboot_required |= _config_int(to_save, "config_ldap_authentication")
reboot_required |= _config_string(to_save, "config_ldap_dn")
reboot_required |= _config_string(to_save, "config_ldap_serv_username")
reboot_required |= _config_string(to_save, "config_ldap_user_object")
reboot_required |= _config_string(to_save, "config_ldap_group_object_filter")
reboot_required |= _config_string(to_save, "config_ldap_group_members_field")
reboot_required |= _config_string(to_save, "config_ldap_member_user_object")
reboot_required |= _config_checkbox(to_save, "config_ldap_openldap")
reboot_required |= _config_int(to_save, "config_ldap_encryption")
reboot_required |= _config_string(to_save, "config_ldap_cacert_path")
reboot_required |= _config_string(to_save, "config_ldap_cert_path")
reboot_required |= _config_string(to_save, "config_ldap_key_path")
_config_string(to_save, "config_ldap_group_name")
if "config_ldap_serv_password" in to_save and to_save["config_ldap_serv_password"] != "":
reboot_required |= 1
config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode, encode='UTF-8')
config.save()
return _configuration_ldap_check(reboot_required, to_save, gdrive_error)
def _configuration_update_helper(configured): def _configuration_update_helper(configured):
reboot_required = False reboot_required = False
db_change = False db_change = False
@ -921,8 +1086,8 @@ def _configuration_update_helper(configured):
if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db): if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db):
gdriveutils.downloadFile(None, "metadata.db", metadata_db) gdriveutils.downloadFile(None, "metadata.db", metadata_db)
db_change = True db_change = True
except Exception as e: except Exception as ex:
return _configuration_result('%s' % e, gdrive_error, configured) return _configuration_result('%s' % ex, gdrive_error, configured)
if db_change: if db_change:
if not calibre_db.setup_db(config, ub.app_DB_path): if not calibre_db.setup_db(config, ub.app_DB_path):
@ -974,7 +1139,6 @@ def _configuration_result(error_flash=None, gdrive_error=None, configured=True):
def _handle_new_user(to_save, content, languages, translations, kobo_support): def _handle_new_user(to_save, content, languages, translations, kobo_support):
content.default_language = to_save["default_language"] content.default_language = to_save["default_language"]
# content.mature_content = "Show_mature_content" in to_save
content.locale = to_save.get("locale", content.locale) content.locale = to_save.get("locale", content.locale)
content.sidebar_view = sum(int(key[5:]) for key in to_save if key.startswith('show_')) content.sidebar_view = sum(int(key[5:]) for key in to_save if key.startswith('show_'))
@ -982,28 +1146,21 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
content.sidebar_view |= constants.DETAIL_RANDOM content.sidebar_view |= constants.DETAIL_RANDOM
content.role = constants.selected_roles(to_save) content.role = constants.selected_roles(to_save)
if not to_save["nickname"] or not to_save["email"] or not to_save["password"]:
flash(_(u"Please fill out all fields!"), category="error")
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
registered_oauth=oauth_check, kobo_support=kobo_support,
title=_(u"Add new user"))
content.password = generate_password_hash(to_save["password"]) content.password = generate_password_hash(to_save["password"])
existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower()) \ try:
.first() if not to_save["name"] or not to_save["email"] or not to_save["password"]:
existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()) \ log.info("Missing entries on new user")
.first() raise Exception(_(u"Please fill out all fields!"))
if not existing_user and not existing_email: content.email = check_email(to_save["email"])
content.nickname = to_save["nickname"] # Query User name, if not existing, change
if config.config_public_reg and not check_valid_domain(to_save["email"]): content.name = check_username(to_save["name"])
flash(_(u"E-mail is not from valid domain"), category="error") if to_save.get("kindle_mail"):
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, content.kindle_mail = valid_email(to_save["kindle_mail"])
registered_oauth=oauth_check, kobo_support=kobo_support, if config.config_public_reg and not check_valid_domain(content.email):
title=_(u"Add new user")) log.info("E-mail: {} for new user is not from valid domain".format(content.email))
else: raise Exception(_(u"E-mail is not from valid domain"))
content.email = to_save["email"] except Exception as ex:
else: flash(str(ex), category="error")
flash(_(u"Found an existing account for this e-mail address or nickname."), category="error")
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
languages=languages, title=_(u"Add new user"), page="newuser", languages=languages, title=_(u"Add new user"), page="newuser",
kobo_support=kobo_support, registered_oauth=oauth_check) kobo_support=kobo_support, registered_oauth=oauth_check)
@ -1014,49 +1171,33 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
content.denied_column_value = config.config_denied_column_value content.denied_column_value = config.config_denied_column_value
ub.session.add(content) ub.session.add(content)
ub.session.commit() ub.session.commit()
flash(_(u"User '%(user)s' created", user=content.nickname), category="success") flash(_(u"User '%(user)s' created", user=content.name), category="success")
return redirect(url_for('admin.admin')) return redirect(url_for('admin.admin'))
except IntegrityError: except IntegrityError:
ub.session.rollback() ub.session.rollback()
flash(_(u"Found an existing account for this e-mail address or nickname."), category="error") flash(_(u"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")
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():
ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
ub.session_commit()
flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success")
return redirect(url_for('admin.admin'))
else:
flash(_(u"No admin user remaining, can't delete user", nick=content.nickname), category="error")
return redirect(url_for('admin.admin'))
def save_edited_user(content):
try:
ub.session_commit()
flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success")
except IntegrityError:
ub.session.rollback()
flash(_(u"An unknown error occured."), category="error")
except OperationalError: except OperationalError:
ub.session.rollback() ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error") flash(_(u"Settings DB is not Writeable"), category="error")
def _handle_edit_user(to_save, content, languages, translations, kobo_support): def _handle_edit_user(to_save, content, languages, translations, kobo_support):
if "delete" in to_save: if to_save.get("delete"):
return 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():
ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
ub.session_commit()
flash(_(u"User '%(nick)s' deleted", nick=content.name), category="success")
return redirect(url_for('admin.admin'))
else:
flash(_(u"No admin user remaining, can't delete user", nick=content.name), category="error")
return redirect(url_for('admin.admin'))
else: else:
if not ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, 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: 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.nickname), category="error") flash(_(u"No admin user remaining, can't remove admin role", nick=content.name), category="error")
return redirect(url_for('admin.admin')) return redirect(url_for('admin.admin'))
if to_save.get("password"):
if "password" in to_save and to_save["password"]:
content.password = generate_password_hash(to_save["password"]) content.password = generate_password_hash(to_save["password"])
anonymous = content.is_anonymous anonymous = content.is_anonymous
content.role = constants.selected_roles(to_save) content.role = constants.selected_roles(to_save)
@ -1074,50 +1215,46 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
elif value not in val and content.check_visibility(value): elif value not in val and content.check_visibility(value):
content.sidebar_view &= ~value content.sidebar_view &= ~value
if "Show_detail_random" in to_save: if to_save.get("Show_detail_random"):
content.sidebar_view |= constants.DETAIL_RANDOM content.sidebar_view |= constants.DETAIL_RANDOM
else: else:
content.sidebar_view &= ~constants.DETAIL_RANDOM content.sidebar_view &= ~constants.DETAIL_RANDOM
if "default_language" in to_save: if to_save.get("default_language"):
content.default_language = to_save["default_language"] content.default_language = to_save["default_language"]
if "locale" in to_save and to_save["locale"]: if to_save.get("locale"):
content.locale = to_save["locale"] content.locale = to_save["locale"]
if to_save["email"] and to_save["email"] != content.email: try:
existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()) \ if to_save.get("email", content.email) != content.email:
.first() content.email = check_email(to_save["email"])
if not existing_email: # Query User name, if not existing, change
content.email = to_save["email"] if to_save.get("name", content.name) != content.name:
else: if to_save.get("name") == "Guest":
flash(_(u"Found an existing account for this e-mail address."), category="error") raise Exception(_("Guest Name can't be changed"))
return render_title_template("user_edit.html", content.name = check_username(to_save["name"])
translations=translations, if to_save.get("kindle_mail") != content.kindle_mail:
languages=languages, content.kindle_mail = valid_email(to_save["kindle_mail"]) if to_save["kindle_mail"] else ""
mail_configured=config.get_mail_server_configured(), except Exception as ex:
kobo_support=kobo_support, flash(str(ex), category="error")
new_user=0, return render_title_template("user_edit.html",
content=content, translations=translations,
registered_oauth=oauth_check, languages=languages,
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") mail_configured=config.get_mail_server_configured(),
if "nickname" in to_save and to_save["nickname"] != content.nickname: kobo_support=kobo_support,
# Query User nickname, if not existing, change new_user=0,
if not ub.session.query(ub.User).filter(ub.User.nickname == to_save["nickname"]).scalar(): content=content,
content.nickname = to_save["nickname"] registered_oauth=oauth_check,
else: title=_(u"Edit User %(nick)s", nick=content.name),
flash(_(u"This username is already taken"), category="error") page="edituser")
return render_title_template("user_edit.html", try:
translations=translations, ub.session_commit()
languages=languages, flash(_(u"User '%(nick)s' updated", nick=content.name), category="success")
mail_configured=config.get_mail_server_configured(), except IntegrityError:
new_user=0, content=content, ub.session.rollback()
registered_oauth=oauth_check, flash(_(u"An unknown error occured."), category="error")
kobo_support=kobo_support, except OperationalError:
title=_(u"Edit User %(nick)s", nick=content.nickname), ub.session.rollback()
page="edituser") flash(_(u"Settings DB is not Writeable"), category="error")
if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail:
content.kindle_mail = to_save["kindle_mail"]
return save_edited_user(content)
@admi.route("/admin/user/new", methods=["GET", "POST"]) @admi.route("/admin/user/new", methods=["GET", "POST"])
@ -1145,7 +1282,7 @@ def new_user():
def edit_mailsettings(): def edit_mailsettings():
content = config.get_mail_settings() content = config.get_mail_settings()
return render_title_template("email_edit.html", content=content, title=_(u"Edit E-mail Server Settings"), return render_title_template("email_edit.html", content=content, title=_(u"Edit E-mail Server Settings"),
page="mailset") page="mailset", feature_support=feature_support)
@admi.route("/admin/mailsettings", methods=["POST"]) @admi.route("/admin/mailsettings", methods=["POST"])
@ -1153,14 +1290,30 @@ def edit_mailsettings():
@admin_required @admin_required
def update_mailsettings(): def update_mailsettings():
to_save = request.form.to_dict() to_save = request.form.to_dict()
_config_int(to_save, "mail_server_type")
if to_save.get("invalidate"):
config.mail_gmail_token = {}
try:
flag_modified(config, "mail_gmail_token")
except AttributeError:
pass
elif to_save.get("gmail"):
try:
config.mail_gmail_token = services.gmail.setup_gmail(config.mail_gmail_token)
flash(_(u"G-Mail Account Verification Successful"), category="success")
except Exception as ex:
flash(str(ex), category="error")
log.error(ex)
return edit_mailsettings()
_config_string(to_save, "mail_server") else:
_config_int(to_save, "mail_port") _config_string(to_save, "mail_server")
_config_int(to_save, "mail_use_ssl") _config_int(to_save, "mail_port")
_config_string(to_save, "mail_login") _config_int(to_save, "mail_use_ssl")
_config_string(to_save, "mail_password") _config_string(to_save, "mail_login")
_config_string(to_save, "mail_from") _config_string(to_save, "mail_password")
_config_int(to_save, "mail_size", lambda y: int(y)*1024*1024) _config_string(to_save, "mail_from")
_config_int(to_save, "mail_size", lambda y: int(y)*1024*1024)
try: try:
config.save() config.save()
except (OperationalError, InvalidRequestError): except (OperationalError, InvalidRequestError):
@ -1170,10 +1323,10 @@ def update_mailsettings():
if to_save.get("test"): if to_save.get("test"):
if current_user.email: if current_user.email:
result = send_test_mail(current_user.email, current_user.nickname) result = send_test_mail(current_user.email, current_user.name)
if result is None: if result is None:
flash(_(u"Test e-mail queued for sending to %(email)s, please check Tasks for result", email=current_user.email), flash(_(u"Test e-mail queued for sending to %(email)s, please check Tasks for result",
category="info") email=current_user.email), category="info")
else: else:
flash(_(u"There was an error sending the Test e-mail: %(res)s", res=result), category="error") flash(_(u"There was an error sending the Test e-mail: %(res)s", res=result), category="error")
else: else:
@ -1189,7 +1342,7 @@ def update_mailsettings():
@admin_required @admin_required
def edit_user(user_id): def edit_user(user_id):
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
if not content or (not config.config_anonbrowse and content.nickname == "Guest"): if not content or (not config.config_anonbrowse and content.name == "Guest"):
flash(_(u"User not found"), category="error") flash(_(u"User not found"), category="error")
return redirect(url_for('admin.admin')) return redirect(url_for('admin.admin'))
languages = calibre_db.speaking_language() languages = calibre_db.speaking_language()
@ -1206,7 +1359,8 @@ def edit_user(user_id):
registered_oauth=oauth_check, registered_oauth=oauth_check,
mail_configured=config.get_mail_server_configured(), mail_configured=config.get_mail_server_configured(),
kobo_support=kobo_support, kobo_support=kobo_support,
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") title=_(u"Edit User %(nick)s", nick=content.name),
page="edituser")
@admi.route("/admin/resetpassword/<int:user_id>") @admi.route("/admin/resetpassword/<int:user_id>")
@ -1328,18 +1482,15 @@ def get_updater_status():
return '' return ''
def create_ldap_user(user, user_data, config): def ldap_import_create_user(user, user_data):
imported = 0
showtext = None
user_login_field = extract_dynamic_field_from_filter(user, config.config_ldap_user_object) user_login_field = extract_dynamic_field_from_filter(user, config.config_ldap_user_object)
username = user_data[user_login_field][0].decode('utf-8') username = user_data[user_login_field][0].decode('utf-8')
# check for duplicate username # check for duplicate username
if ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first(): if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).first():
# if ub.session.query(ub.User).filter(ub.User.nickname == username).first(): # if ub.session.query(ub.User).filter(ub.User.name == username).first():
log.warning("LDAP User %s Already in Database", user_data) log.warning("LDAP User %s Already in Database", user_data)
return imported, showtext return 0, None
kindlemail = '' kindlemail = ''
if 'mail' in user_data: if 'mail' in user_data:
@ -1350,13 +1501,15 @@ def create_ldap_user(user, user_data, config):
else: else:
log.debug('No Mail Field Found in LDAP Response') log.debug('No Mail Field Found in LDAP Response')
useremail = username + '@email.com' useremail = username + '@email.com'
# check for duplicate email
if ub.session.query(ub.User).filter(func.lower(ub.User.email) == useremail.lower()).first():
log.warning("LDAP Email %s Already in Database", user_data)
return imported, showtext
try:
# check for duplicate email
useremail = check_email(useremail)
except Exception as ex:
log.warning("LDAP Email Error: {}, {}".format(user_data, ex))
return 0, None
content = ub.User() content = ub.User()
content.nickname = username content.name = username
content.password = '' # dummy password which will be replaced by ldap one content.password = '' # dummy password which will be replaced by ldap one
content.email = useremail content.email = useremail
content.kindle_mail = kindlemail content.kindle_mail = kindlemail
@ -1369,12 +1522,12 @@ def create_ldap_user(user, user_data, config):
ub.session.add(content) ub.session.add(content)
try: try:
ub.session.commit() ub.session.commit()
imported = 1 return 1, None # increase no of users
except Exception as e: except Exception as ex:
log.warning("Failed to create LDAP user: %s - %s", user, e) log.warning("Failed to create LDAP user: %s - %s", user, ex)
ub.session.rollback() ub.session.rollback()
showtext = _(u'Failed to Create at Least One LDAP User') message = _(u'Failed to Create at Least One LDAP User')
return imported, showtext return 0, message
@admi.route('/import_ldap_users') @admi.route('/import_ldap_users')
@ -1404,23 +1557,23 @@ def import_ldap_users():
query_filter = config.config_ldap_user_object query_filter = config.config_ldap_user_object
try: try:
user_identifier = extract_user_identifier(user, query_filter) user_identifier = extract_user_identifier(user, query_filter)
except Exception as e: except Exception as ex:
log.warning(e) log.warning(ex)
continue continue
else: else:
user_identifier = user user_identifier = user
query_filter = None query_filter = None
try: try:
user_data = services.ldap.get_object_details(user=user_identifier, query_filter=query_filter) user_data = services.ldap.get_object_details(user=user_identifier, query_filter=query_filter)
except AttributeError as e: except AttributeError as ex:
log.debug_or_exception(e) log.debug_or_exception(ex)
continue continue
if user_data: if user_data:
success, txt = create_ldap_user(user, user_data, config) user_count, message = ldap_import_create_user(user, user_data)
# In case of error store text for showing it if message:
if txt: showtext['text'] = message
showtext['text'] = txt else:
imported += success imported += user_count
else: else:
log.warning("LDAP User: %s Not Found", user) log.warning("LDAP User: %s Not Found", user)
showtext['text'] = _(u'At Least One LDAP User Not Found in Database') showtext['text'] = _(u'At Least One LDAP User Not Found in Database')

@ -105,8 +105,8 @@ def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecu
if extension in COVER_EXTENSIONS: if extension in COVER_EXTENSIONS:
cover_data = cf.read(name) cover_data = cf.read(name)
break break
except Exception as e: except Exception as ex:
log.debug('Rarfile failed with error: %s', e) log.debug('Rarfile failed with error: %s', ex)
return cover_data return cover_data

@ -23,7 +23,11 @@ import sys
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
from sqlalchemy.exc import OperationalError from sqlalchemy.exc import OperationalError
from sqlalchemy.ext.declarative import declarative_base try:
# Compability 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, ub
@ -35,7 +39,7 @@ class _Flask_Settings(_Base):
__tablename__ = 'flask_settings' __tablename__ = 'flask_settings'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
flask_session_key = Column(BLOB, default="") flask_session_key = Column(BLOB, default=b"")
def __init__(self, key): def __init__(self, key):
self.flask_session_key = key self.flask_session_key = key
@ -54,6 +58,8 @@ class _Settings(_Base):
mail_password = Column(String, default='mypassword') mail_password = Column(String, default='mypassword')
mail_from = Column(String, default='automailer <mail@example.com>') mail_from = Column(String, default='automailer <mail@example.com>')
mail_size = Column(Integer, default=25*1024*1024) mail_size = Column(Integer, default=25*1024*1024)
mail_server_type = Column(SmallInteger, default=0)
mail_gmail_token = Column(JSON, default={})
config_calibre_dir = Column(String) config_calibre_dir = Column(String)
config_port = Column(Integer, default=constants.DEFAULT_PORT) config_port = Column(Integer, default=constants.DEFAULT_PORT)
@ -242,15 +248,16 @@ class _ConfigSQL(object):
return {k:v for k, v in self.__dict__.items() if k.startswith('mail_')} return {k:v for k, v in self.__dict__.items() if k.startswith('mail_')}
def get_mail_server_configured(self): def get_mail_server_configured(self):
return not bool(self.mail_server == constants.DEFAULT_MAIL_SERVER) return bool((self.mail_server != constants.DEFAULT_MAIL_SERVER and self.mail_server_type == 0)
or (self.mail_gmail_token != {} and self.mail_server_type == 1))
def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None): def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None):
'''Possibly updates a field of this object. """Possibly updates a field of this object.
The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor. The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor.
:returns: `True` if the field has changed value :returns: `True` if the field has changed value
''' """
new_value = dictionary.get(field, default) new_value = dictionary.get(field, default)
if new_value is None: if new_value is None:
# log.debug("_ConfigSQL set_from_dictionary field '%s' not found", field) # log.debug("_ConfigSQL set_from_dictionary field '%s' not found", field)
@ -301,8 +308,11 @@ class _ConfigSQL(object):
have_metadata_db = os.path.isfile(db_file) have_metadata_db = os.path.isfile(db_file)
self.db_configured = have_metadata_db self.db_configured = have_metadata_db
constants.EXTENSIONS_UPLOAD = [x.lstrip().rstrip().lower() for x in self.config_upload_formats.split(',')] constants.EXTENSIONS_UPLOAD = [x.lstrip().rstrip().lower() for x in self.config_upload_formats.split(',')]
# pylint: disable=access-member-before-definition if os.environ.get('FLASK_DEBUG'):
logfile = logger.setup(self.config_logfile, self.config_log_level) logfile = logger.setup(logger.LOG_TO_STDOUT, logger.logging.DEBUG)
else:
# pylint: disable=access-member-before-definition
logfile = logger.setup(self.config_logfile, self.config_log_level)
if logfile != self.config_logfile: if logfile != self.config_logfile:
log.warning("Log path %s not valid, falling back to default", self.config_logfile) log.warning("Log path %s not valid, falling back to default", self.config_logfile)
self.config_logfile = logfile self.config_logfile = logfile
@ -360,10 +370,14 @@ def _migrate_table(session, orm_class):
if isinstance(column.default.arg, bool): if isinstance(column.default.arg, bool):
column_default = ("DEFAULT %r" % int(column.default.arg)) column_default = ("DEFAULT %r" % int(column.default.arg))
else: else:
column_default = ("DEFAULT %r" % column.default.arg) column_default = ("DEFAULT '%r'" % 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 = "ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
column_name, column_name,
column.type, column_type,
column_default) column_default)
log.debug(alter_table) log.debug(alter_table)
session.execute(alter_table) session.execute(alter_table)
@ -426,12 +440,12 @@ def load_configuration(session):
session.commit() session.commit()
conf = _ConfigSQL(session) conf = _ConfigSQL(session)
# Migrate from global restrictions to user based restrictions # Migrate from global restrictions to user based restrictions
if bool(conf.config_default_show & constants.MATURE_CONTENT) and conf.config_denied_tags == "": #if bool(conf.config_default_show & constants.MATURE_CONTENT) and conf.config_denied_tags == "":
conf.config_denied_tags = conf.config_mature_content_tags # conf.config_denied_tags = conf.config_mature_content_tags
conf.save() # conf.save()
session.query(ub.User).filter(ub.User.mature_content != True). \ # session.query(ub.User).filter(ub.User.mature_content != True). \
update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False) # update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False)
session.commit() # session.commit()
return conf return conf
def get_flask_session_key(session): def get_flask_session_key(session):

@ -21,8 +21,10 @@ import sys
import os import os
from collections import namedtuple from collections import namedtuple
# if installed via pip this variable is set to true # if installed via pip this variable is set to true (empty file with name .HOMEDIR present)
HOME_CONFIG = False HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file__)), '.HOMEDIR'))
#In executables updater is not available, so variable is set to False there
UPDATER_AVAILABLE = True UPDATER_AVAILABLE = True
# Base dir is parent of current file, necessary if called from different folder # Base dir is parent of current file, necessary if called from different folder
@ -86,6 +88,26 @@ SIDEBAR_ARCHIVED = 1 << 15
SIDEBAR_DOWNLOAD = 1 << 16 SIDEBAR_DOWNLOAD = 1 << 16
SIDEBAR_LIST = 1 << 17 SIDEBAR_LIST = 1 << 17
sidebar_settings = {
"detail_random": DETAIL_RANDOM,
"sidebar_language": SIDEBAR_LANGUAGE,
"sidebar_series": SIDEBAR_SERIES,
"sidebar_category": SIDEBAR_CATEGORY,
"sidebar_random": SIDEBAR_RANDOM,
"sidebar_author": SIDEBAR_AUTHOR,
"sidebar_best_rated": SIDEBAR_BEST_RATED,
"sidebar_read_and_unread": SIDEBAR_READ_AND_UNREAD,
"sidebar_recent": SIDEBAR_RECENT,
"sidebar_sorted": SIDEBAR_SORTED,
"sidebar_publisher": SIDEBAR_PUBLISHER,
"sidebar_rating": SIDEBAR_RATING,
"sidebar_format": SIDEBAR_FORMAT,
"sidebar_archived": SIDEBAR_ARCHIVED,
"sidebar_download": SIDEBAR_DOWNLOAD,
"sidebar_list": SIDEBAR_LIST,
}
ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS
ADMIN_USER_SIDEBAR = (SIDEBAR_LIST << 1) - 1 ADMIN_USER_SIDEBAR = (SIDEBAR_LIST << 1) - 1

@ -30,7 +30,13 @@ from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float
from sqlalchemy.orm import relationship, sessionmaker, scoped_session from sqlalchemy.orm import relationship, sessionmaker, scoped_session
from sqlalchemy.orm.collections import InstrumentedList from sqlalchemy.orm.collections import InstrumentedList
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.exc import OperationalError
try:
# Compability with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
from sqlalchemy.sql.expression import and_, true, false, text, func, or_ from sqlalchemy.sql.expression import and_, true, false, text, func, or_
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
@ -326,7 +332,6 @@ class Books(Base):
has_cover = Column(Integer, default=0) has_cover = Column(Integer, default=0)
uuid = Column(String) uuid = Column(String)
isbn = Column(String(collation='NOCASE'), default="") isbn = Column(String(collation='NOCASE'), default="")
# Iccn = Column(String(collation='NOCASE'), default="")
flags = Column(Integer, nullable=False, default=1) flags = Column(Integer, nullable=False, default=1)
authors = relationship('Authors', secondary=books_authors_link, backref='books') authors = relationship('Authors', secondary=books_authors_link, backref='books')
@ -437,12 +442,80 @@ class CalibreDB():
self.instances.add(self) self.instances.add(self)
def initSession(self, expire_on_commit=True): def initSession(self, expire_on_commit=True):
self.session = self.session_factory() self.session = self.session_factory()
self.session.expire_on_commit = expire_on_commit self.session.expire_on_commit = expire_on_commit
self.update_title_sort(self.config) self.update_title_sort(self.config)
@classmethod
def setup_db_cc_classes(self, cc):
cc_ids = []
books_custom_column_links = {}
for row in cc:
if row.datatype not in cc_exceptions:
if row.datatype == 'series':
dicttable = {'__tablename__': 'books_custom_column_' + str(row.id) + '_link',
'id': Column(Integer, primary_key=True),
'book': Column(Integer, ForeignKey('books.id'),
primary_key=True),
'map_value': Column('value', Integer,
ForeignKey('custom_column_' +
str(row.id) + '.id'),
primary_key=True),
'extra': Column(Float),
'asoc': relationship('custom_column_' + str(row.id), uselist=False),
'value': association_proxy('asoc', 'value')
}
books_custom_column_links[row.id] = type(str('books_custom_column_' + str(row.id) + '_link'),
(Base,), dicttable)
else:
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link',
Base.metadata,
Column('book', Integer, ForeignKey('books.id'),
primary_key=True),
Column('value', Integer,
ForeignKey('custom_column_' +
str(row.id) + '.id'),
primary_key=True)
)
cc_ids.append([row.id, row.datatype])
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True)}
if row.datatype == 'float':
ccdict['value'] = Column(Float)
elif row.datatype == 'int':
ccdict['value'] = Column(Integer)
elif row.datatype == 'bool':
ccdict['value'] = Column(Boolean)
else:
ccdict['value'] = Column(String)
if row.datatype in ['float', 'int', 'bool']:
ccdict['book'] = Column(Integer, ForeignKey('books.id'))
cc_classes[row.id] = type(str('custom_column_' + str(row.id)), (Base,), ccdict)
for cc_id in cc_ids:
if (cc_id[1] == 'bool') or (cc_id[1] == 'int') or (cc_id[1] == 'float'):
setattr(Books,
'custom_column_' + str(cc_id[0]),
relationship(cc_classes[cc_id[0]],
primaryjoin=(
Books.id == cc_classes[cc_id[0]].book),
backref='books'))
elif (cc_id[1] == 'series'):
setattr(Books,
'custom_column_' + str(cc_id[0]),
relationship(books_custom_column_links[cc_id[0]],
backref='books'))
else:
setattr(Books,
'custom_column_' + str(cc_id[0]),
relationship(cc_classes[cc_id[0]],
secondary=books_custom_column_links[cc_id[0]],
backref='books'))
return cc_classes
@classmethod @classmethod
def setup_db(cls, config, app_db_path): def setup_db(cls, config, app_db_path):
cls.config = config cls.config = config
@ -465,84 +538,24 @@ class CalibreDB():
isolation_level="SERIALIZABLE", isolation_level="SERIALIZABLE",
connect_args={'check_same_thread': False}, connect_args={'check_same_thread': False},
poolclass=StaticPool) poolclass=StaticPool)
cls.engine.execute("attach database '{}' as calibre;".format(dbpath)) with cls.engine.begin() as connection:
cls.engine.execute("attach database '{}' as app_settings;".format(app_db_path)) connection.execute(text("attach database '{}' as calibre;".format(dbpath)))
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path)))
conn = cls.engine.connect() conn = cls.engine.connect()
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302 # conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
except Exception as e: except Exception as ex:
config.invalidate(e) config.invalidate(ex)
return False return False
config.db_configured = True config.db_configured = True
if not cc_classes: if not cc_classes:
cc = conn.execute("SELECT id, datatype FROM custom_columns") try:
cc = conn.execute("SELECT id, datatype FROM custom_columns")
cc_ids = [] cls.setup_db_cc_classes(cc)
books_custom_column_links = {} except OperationalError as e:
for row in cc: log.debug_or_exception(e)
if row.datatype not in cc_exceptions:
if row.datatype == 'series':
dicttable = {'__tablename__': 'books_custom_column_' + str(row.id) + '_link',
'id': Column(Integer, primary_key=True),
'book': Column(Integer, ForeignKey('books.id'),
primary_key=True),
'map_value': Column('value', Integer,
ForeignKey('custom_column_' +
str(row.id) + '.id'),
primary_key=True),
'extra': Column(Float),
'asoc': relationship('custom_column_' + str(row.id), uselist=False),
'value': association_proxy('asoc', 'value')
}
books_custom_column_links[row.id] = type(str('books_custom_column_' + str(row.id) + '_link'),
(Base,), dicttable)
else:
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link',
Base.metadata,
Column('book', Integer, ForeignKey('books.id'),
primary_key=True),
Column('value', Integer,
ForeignKey('custom_column_' +
str(row.id) + '.id'),
primary_key=True)
)
cc_ids.append([row.id, row.datatype])
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True)}
if row.datatype == 'float':
ccdict['value'] = Column(Float)
elif row.datatype == 'int':
ccdict['value'] = Column(Integer)
elif row.datatype == 'bool':
ccdict['value'] = Column(Boolean)
else:
ccdict['value'] = Column(String)
if row.datatype in ['float', 'int', 'bool']:
ccdict['book'] = Column(Integer, ForeignKey('books.id'))
cc_classes[row.id] = type(str('custom_column_' + str(row.id)), (Base,), ccdict)
for cc_id in cc_ids:
if (cc_id[1] == 'bool') or (cc_id[1] == 'int') or (cc_id[1] == 'float'):
setattr(Books,
'custom_column_' + str(cc_id[0]),
relationship(cc_classes[cc_id[0]],
primaryjoin=(
Books.id == cc_classes[cc_id[0]].book),
backref='books'))
elif (cc_id[1] == 'series'):
setattr(Books,
'custom_column_' + str(cc_id[0]),
relationship(books_custom_column_links[cc_id[0]],
backref='books'))
else:
setattr(Books,
'custom_column_' + str(cc_id[0]),
relationship(cc_classes[cc_id[0]],
secondary=books_custom_column_links[cc_id[0]],
backref='books'))
cls.session_factory = scoped_session(sessionmaker(autocommit=False, cls.session_factory = scoped_session(sessionmaker(autocommit=False,
autoflush=True, autoflush=True,
@ -614,13 +627,18 @@ class CalibreDB():
randm = self.session.query(Books) \ randm = self.session.query(Books) \
.filter(self.common_filters(allow_show_archived)) \ .filter(self.common_filters(allow_show_archived)) \
.order_by(func.random()) \ .order_by(func.random()) \
.limit(self.config.config_random_books) .limit(self.config.config_random_books).all()
else: else:
randm = false() randm = false()
off = int(int(pagesize) * (page - 1)) off = int(int(pagesize) * (page - 1))
query = self.session.query(database) \ query = self.session.query(database)
.join(*join, isouter=True) \ if len(join) == 3:
.filter(db_filter) \ 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])
query = query.filter(db_filter)\
.filter(self.common_filters(allow_show_archived)) .filter(self.common_filters(allow_show_archived))
entries = list() entries = list()
pagination = list() pagination = list()
@ -628,8 +646,8 @@ class CalibreDB():
pagination = Pagination(page, pagesize, pagination = Pagination(page, pagesize,
len(query.all())) len(query.all()))
entries = query.order_by(*order).offset(off).limit(pagesize).all() entries = query.order_by(*order).offset(off).limit(pagesize).all()
except Exception as e: except Exception as ex:
log.debug_or_exception(e) log.debug_or_exception(ex)
#for book in entries: #for book in entries:
# book = self.order_authors(book) # book = self.order_authors(book)
return entries, randm, pagination return entries, randm, pagination
@ -773,7 +791,7 @@ class CalibreDB():
def lcase(s): def lcase(s):
try: try:
return unidecode.unidecode(s.lower()) return unidecode.unidecode(s.lower())
except Exception as e: except Exception as ex:
log = logger.create() log = logger.create()
log.debug_or_exception(e) log.debug_or_exception(ex)
return s.lower() return s.lower()

@ -27,6 +27,8 @@ import json
from shutil import copyfile from shutil import copyfile
from uuid import uuid4 from uuid import uuid4
from babel import Locale as LC
from babel.core import UnknownLocaleError
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import current_user, login_required from flask_login import current_user, login_required
@ -68,17 +70,7 @@ def edit_required(f):
return inner return inner
def search_objects_remove(db_book_object, db_type, input_elements):
# Modifies different Database objects, first check if elements have to be added to database, than check
# if elements have to be deleted, because they are no longer used
def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type):
# passing input_elements not as a list may lead to undesired results
if not isinstance(input_elements, list):
raise TypeError(str(input_elements) + " should be passed as a list")
changed = False
input_elements = [x for x in input_elements if x != '']
# we have all input element (authors, series, tags) names now
# 1. search for elements to remove
del_elements = [] del_elements = []
for c_elements in db_book_object: for c_elements in db_book_object:
found = False found = False
@ -96,7 +88,10 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
# if the element was not found in the new list, add it to remove list # if the element was not found in the new list, add it to remove list
if not found: if not found:
del_elements.append(c_elements) del_elements.append(c_elements)
# 2. search for elements that need to be added return del_elements
def search_objects_add(db_book_object, db_type, input_elements):
add_elements = [] add_elements = []
for inp_element in input_elements: for inp_element in input_elements:
found = False found = False
@ -112,64 +107,96 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
break break
if not found: if not found:
add_elements.append(inp_element) add_elements.append(inp_element)
# if there are elements to remove, we remove them now return add_elements
def remove_objects(db_book_object, db_session, del_elements):
changed = False
if len(del_elements) > 0: if len(del_elements) > 0:
for del_element in del_elements: for del_element in del_elements:
db_book_object.remove(del_element) db_book_object.remove(del_element)
changed = True changed = True
if len(del_element.books) == 0: if len(del_element.books) == 0:
db_session.delete(del_element) db_session.delete(del_element)
# if there are elements to add, we add them now! return changed
if len(add_elements) > 0:
if db_type == 'languages': def add_objects(db_book_object, db_object, db_session, db_type, add_elements):
db_filter = db_object.lang_code changed = False
if db_type == 'languages':
db_filter = db_object.lang_code
elif db_type == 'custom':
db_filter = db_object.value
else:
db_filter = db_object.name
for add_element in add_elements:
# check if a element with that name exists
db_element = db_session.query(db_object).filter(db_filter == add_element).first()
# if no element is found add it
# if new_element is None:
if db_type == 'author':
new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')), "")
elif db_type == 'series':
new_element = db_object(add_element, add_element)
elif db_type == 'custom': elif db_type == 'custom':
db_filter = db_object.value new_element = db_object(value=add_element)
elif db_type == 'publisher':
new_element = db_object(add_element, None)
else: # db_type should be tag or language
new_element = db_object(add_element)
if db_element is None:
changed = True
db_session.add(new_element)
db_book_object.append(new_element)
else: else:
db_filter = db_object.name db_element = create_objects_for_addition(db_element, add_element, db_type)
for add_element in add_elements: changed = True
# check if a element with that name exists # add element to book
db_element = db_session.query(db_object).filter(db_filter == add_element).first() changed = True
# if no element is found add it db_book_object.append(db_element)
# if new_element is None: return changed
if db_type == 'author':
new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')), "")
elif db_type == 'series': def create_objects_for_addition(db_element, add_element, db_type):
new_element = db_object(add_element, add_element) if db_type == 'custom':
elif db_type == 'custom': if db_element.value != add_element:
new_element = db_object(value=add_element) db_element.value = add_element # ToDo: Before new_element, but this is not plausible
elif db_type == 'publisher': elif db_type == 'languages':
new_element = db_object(add_element, None) if db_element.lang_code != add_element:
else: # db_type should be tag or language db_element.lang_code = add_element
new_element = db_object(add_element) elif db_type == 'series':
if db_element is None: if db_element.name != add_element:
changed = True db_element.name = add_element
db_session.add(new_element) db_element.sort = add_element
db_book_object.append(new_element) elif db_type == 'author':
else: if db_element.name != add_element:
if db_type == 'custom': db_element.name = add_element
if db_element.value != add_element: db_element.sort = add_element.replace('|', ',')
new_element.value = add_element elif db_type == 'publisher':
elif db_type == 'languages': if db_element.name != add_element:
if db_element.lang_code != add_element: db_element.name = add_element
db_element.lang_code = add_element db_element.sort = None
elif db_type == 'series': elif db_element.name != add_element:
if db_element.name != add_element: db_element.name = add_element
db_element.name = add_element return db_element
db_element.sort = add_element
elif db_type == 'author':
if db_element.name != add_element: # Modifies different Database objects, first check if elements if elements have to be deleted,
db_element.name = add_element # because they are no longer used, than check if elements have to be added to database
db_element.sort = add_element.replace('|', ',') def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type):
elif db_type == 'publisher': # passing input_elements not as a list may lead to undesired results
if db_element.name != add_element: if not isinstance(input_elements, list):
db_element.name = add_element raise TypeError(str(input_elements) + " should be passed as a list")
db_element.sort = None input_elements = [x for x in input_elements if x != '']
elif db_element.name != add_element: # we have all input element (authors, series, tags) names now
db_element.name = add_element # 1. search for elements to remove
# add element to book del_elements = search_objects_remove(db_book_object, db_type, input_elements)
changed = True # 2. search for elements that need to be added
db_book_object.append(db_element) add_elements = search_objects_add(db_book_object, db_type, input_elements)
# if there are elements to remove, we remove them now
changed = remove_objects(db_book_object, db_session, del_elements)
# if there are elements to add, we add them now!
if len(add_elements) > 0:
changed |= add_objects(db_book_object, db_object, db_session, db_type, add_elements)
return changed return changed
@ -309,8 +336,8 @@ def delete_book(book_id, book_format, jsonResponse):
calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\ calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
filter(db.Data.format == book_format).delete() filter(db.Data.format == book_format).delete()
calibre_db.session.commit() calibre_db.session.commit()
except Exception as e: except Exception as ex:
log.debug_or_exception(e) log.debug_or_exception(ex)
calibre_db.session.rollback() calibre_db.session.rollback()
else: else:
# book not found # book not found
@ -422,7 +449,7 @@ def edit_book_comments(comments, book):
return modif_date return modif_date
def edit_book_languages(languages, book, upload=False): def edit_book_languages(languages, book, upload=False, invalid=None):
input_languages = languages.split(',') input_languages = languages.split(',')
unknown_languages = [] unknown_languages = []
if not upload: if not upload:
@ -431,7 +458,10 @@ def edit_book_languages(languages, book, upload=False):
input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages) input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages)
for l in unknown_languages: for l in unknown_languages:
log.error('%s is not a valid language', l) log.error('%s is not a valid language', l)
flash(_(u"%(langname)s is not a valid language", langname=l), category="warning") if isinstance(invalid, list):
invalid.append(l)
else:
flash(_(u"%(langname)s is not a valid language", langname=l), category="warning")
# ToDo: Not working correct # ToDo: Not working correct
if upload and len(input_l) == 1: if upload and len(input_l) == 1:
# If the language of the file is excluded from the users view, it's not imported, to allow the user to view # If the language of the file is excluded from the users view, it's not imported, to allow the user to view
@ -597,7 +627,7 @@ def upload_single_file(request, book, book_id):
# Queue uploader info # Queue uploader info
uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title) uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title)
WorkerThread.add(current_user.nickname, TaskUpload( WorkerThread.add(current_user.name, TaskUpload(
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>")) "<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>"))
return uploader.process( return uploader.process(
@ -621,6 +651,46 @@ def upload_cover(request, book):
return None return None
def handle_title_on_edit(book, book_title):
# handle book title
book_title = book_title.rstrip().strip()
if book.title != book_title:
if book_title == '':
book_title = _(u'Unknown')
book.title = book_title
return True
return False
def handle_author_on_edit(book, author_name, update_stored=True):
# handle author(s)
input_authors = author_name.split('&')
input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
# Remove duplicates in authors list
input_authors = helper.uniq(input_authors)
# we have all author names now
if input_authors == ['']:
input_authors = [_(u'Unknown')] # prevent empty Author
change = modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
# Search for each author if author is in database, if not, author name and sorted author name is generated new
# everything then is assembled for sorted author field in database
sort_authors_list = list()
for inp in input_authors:
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
if not stored_author:
stored_author = helper.get_sorted_author(inp)
else:
stored_author = stored_author.sort
sort_authors_list.append(helper.get_sorted_author(stored_author))
sort_authors = ' & '.join(sort_authors_list)
if book.author_sort != sort_authors and update_stored:
book.author_sort = sort_authors
change = True
return input_authors, change
@editbook.route("/admin/book/<int:book_id>", methods=['GET', 'POST']) @editbook.route("/admin/book/<int:book_id>", methods=['GET', 'POST'])
@login_required_if_no_ano @login_required_if_no_ano
@edit_required @edit_required
@ -638,7 +708,6 @@ def edit_book(book_id):
if request.method != 'POST': if request.method != 'POST':
return render_edit_book(book_id) return render_edit_book(book_id)
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
# Book not found # Book not found
@ -656,39 +725,12 @@ def edit_book(book_id):
# Update book # Update book
edited_books_id = None edited_books_id = None
#handle book title # handle book title
if book.title != to_save["book_title"].rstrip().strip(): title_change = handle_title_on_edit(book, to_save["book_title"])
if to_save["book_title"] == '':
to_save["book_title"] = _(u'Unknown')
book.title = to_save["book_title"].rstrip().strip()
edited_books_id = book.id
modif_date = True
# handle author(s) input_authors, authorchange = handle_author_on_edit(book, to_save["author_name"])
input_authors = to_save["author_name"].split('&') if authorchange or title_change:
input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
# Remove duplicates in authors list
input_authors = helper.uniq(input_authors)
# we have all author names now
if input_authors == ['']:
input_authors = [_(u'Unknown')] # prevent empty Author
modif_date |= modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
# Search for each author if author is in database, if not, authorname and sorted authorname is generated new
# everything then is assembled for sorted author field in database
sort_authors_list = list()
for inp in input_authors:
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
if not stored_author:
stored_author = helper.get_sorted_author(inp)
else:
stored_author = stored_author.sort
sort_authors_list.append(helper.get_sorted_author(stored_author))
sort_authors = ' & '.join(sort_authors_list)
if book.author_sort != sort_authors:
edited_books_id = book.id edited_books_id = book.id
book.author_sort = sort_authors
modif_date = True modif_date = True
if config.config_use_google_drive: if config.config_use_google_drive:
@ -715,10 +757,8 @@ def edit_book(book_id):
# Add default series_index to book # Add default series_index to book
modif_date |= edit_book_series_index(to_save["series_index"], book) modif_date |= edit_book_series_index(to_save["series_index"], book)
# Handle book comments/description # Handle book comments/description
modif_date |= edit_book_comments(to_save["description"], book) modif_date |= edit_book_comments(to_save["description"], book)
# Handle identifiers # Handle identifiers
input_identifiers = identifier_list(to_save, book) input_identifiers = identifier_list(to_save, book)
modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session) modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session)
@ -727,30 +767,25 @@ def edit_book(book_id):
modif_date |= modification modif_date |= modification
# Handle book tags # Handle book tags
modif_date |= edit_book_tags(to_save['tags'], book) modif_date |= edit_book_tags(to_save['tags'], book)
# Handle book series # Handle book series
modif_date |= edit_book_series(to_save["series"], book) modif_date |= edit_book_series(to_save["series"], book)
if to_save["pubdate"]:
try:
book.pubdate = datetime.strptime(to_save["pubdate"], "%Y-%m-%d")
except ValueError:
book.pubdate = db.Books.DEFAULT_PUBDATE
else:
book.pubdate = db.Books.DEFAULT_PUBDATE
# handle book publisher # handle book publisher
modif_date |= edit_book_publisher(to_save['publisher'], book) modif_date |= edit_book_publisher(to_save['publisher'], book)
# handle book languages # handle book languages
modif_date |= edit_book_languages(to_save['languages'], book) modif_date |= edit_book_languages(to_save['languages'], book)
# handle book ratings # handle book ratings
modif_date |= edit_book_ratings(to_save, book) modif_date |= edit_book_ratings(to_save, book)
# handle cc data # handle cc data
modif_date |= edit_cc_data(book_id, book, to_save) modif_date |= edit_cc_data(book_id, book, to_save)
if to_save["pubdate"]:
try:
book.pubdate = datetime.strptime(to_save["pubdate"], "%Y-%m-%d")
except ValueError:
book.pubdate = db.Books.DEFAULT_PUBDATE
else:
book.pubdate = db.Books.DEFAULT_PUBDATE
if modif_date: if modif_date:
book.last_modified = datetime.utcnow() book.last_modified = datetime.utcnow()
calibre_db.session.merge(book) calibre_db.session.merge(book)
@ -766,8 +801,8 @@ def edit_book(book_id):
calibre_db.session.rollback() calibre_db.session.rollback()
flash(error, category="error") flash(error, category="error")
return render_edit_book(book_id) return render_edit_book(book_id)
except Exception as e: except Exception as ex:
log.debug_or_exception(e) log.debug_or_exception(ex)
calibre_db.session.rollback() calibre_db.session.rollback()
flash(_("Error editing book, please check logfile for details"), category="error") flash(_("Error editing book, please check logfile for details"), category="error")
return redirect(url_for('web.show_book', book_id=book.id)) return redirect(url_for('web.show_book', book_id=book.id))
@ -883,6 +918,48 @@ def create_book_on_upload(modif_date, meta):
calibre_db.session.flush() calibre_db.session.flush()
return db_book, input_authors, title_dir return db_book, input_authors, title_dir
def file_handling_on_upload(requested_file):
# check if file extension is correct
if '.' in requested_file.filename:
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD:
flash(
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
ext=file_ext), category="error")
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
else:
flash(_('File to be uploaded must have an extension'), category="error")
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
# extract metadata from file
try:
meta = uploader.upload(requested_file, config.config_rarfile_location)
except (IOError, OSError):
log.error("File %s could not saved to temp dir", requested_file.filename)
flash(_(u"File %(filename)s could not saved to temp dir",
filename=requested_file.filename), category="error")
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
return meta, None
def move_coverfile(meta, db_book):
# move cover to final directory, including book id
if meta.cover:
coverfile = meta.cover
else:
coverfile = os.path.join(constants.STATIC_DIR, 'generic_cover.jpg')
new_coverpath = os.path.join(config.config_calibre_dir, db_book.path, "cover.jpg")
try:
copyfile(coverfile, new_coverpath)
if meta.cover:
os.unlink(meta.cover)
except OSError as e:
log.error("Failed to move cover file %s: %s", new_coverpath, e)
flash(_(u"Failed to Move Cover File %(file)s: %(error)s", file=new_coverpath,
error=e),
category="error")
@editbook.route("/upload", methods=["GET", "POST"]) @editbook.route("/upload", methods=["GET", "POST"])
@login_required_if_no_ano @login_required_if_no_ano
@upload_required @upload_required
@ -897,30 +974,13 @@ def upload():
calibre_db.update_title_sort(config) calibre_db.update_title_sort(config)
calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4())) calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
# check if file extension is correct meta, error = file_handling_on_upload(requested_file)
if '.' in requested_file.filename: if error:
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() return error
if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD:
flash(
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
ext=file_ext), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
else:
flash(_('File to be uploaded must have an extension'), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
# extract metadata from file
try:
meta = uploader.upload(requested_file, config.config_rarfile_location)
except (IOError, OSError):
log.error("File %s could not saved to temp dir", requested_file.filename)
flash(_(u"File %(filename)s could not saved to temp dir",
filename= requested_file.filename), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
db_book, input_authors, title_dir = create_book_on_upload(modif_date, meta) db_book, input_authors, title_dir = create_book_on_upload(modif_date, meta)
# Comments needs book id therfore only possible after flush # Comments needs book id therefore only possible after flush
modif_date |= edit_book_comments(Markup(meta.description).unescape(), db_book) modif_date |= edit_book_comments(Markup(meta.description).unescape(), db_book)
book_id = db_book.id book_id = db_book.id
@ -932,21 +992,7 @@ def upload():
meta.file_path, meta.file_path,
title_dir + meta.extension) title_dir + meta.extension)
# move cover to final directory, including book id move_coverfile(meta, db_book)
if meta.cover:
coverfile = meta.cover
else:
coverfile = os.path.join(constants.STATIC_DIR, 'generic_cover.jpg')
new_coverpath = os.path.join(config.config_calibre_dir, db_book.path, "cover.jpg")
try:
copyfile(coverfile, new_coverpath)
if meta.cover:
os.unlink(meta.cover)
except OSError as e:
log.error("Failed to move cover file %s: %s", new_coverpath, e)
flash(_(u"Failed to Move Cover File %(file)s: %(error)s", file=new_coverpath,
error=e),
category="error")
# save data to database, reread data # save data to database, reread data
calibre_db.session.commit() calibre_db.session.commit()
@ -956,7 +1002,7 @@ def upload():
if error: if error:
flash(error, category="error") flash(error, category="error")
uploadText=_(u"File %(file)s uploaded", file=title) uploadText=_(u"File %(file)s uploaded", file=title)
WorkerThread.add(current_user.nickname, TaskUpload( WorkerThread.add(current_user.name, TaskUpload(
"<a href=\"" + url_for('web.show_book', book_id=book_id) + "\">" + uploadText + "</a>")) "<a href=\"" + url_for('web.show_book', book_id=book_id) + "\">" + uploadText + "</a>"))
if len(request.files.getlist("btn-upload")) < 2: if len(request.files.getlist("btn-upload")) < 2:
@ -986,7 +1032,7 @@ def convert_bookformat(book_id):
log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to) log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to)
rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(), rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(),
book_format_to.upper(), current_user.nickname) book_format_to.upper(), current_user.name)
if rtn is None: if rtn is None:
flash(_(u"Book successfully queued for converting to %(book_format)s", flash(_(u"Book successfully queued for converting to %(book_format)s",
@ -996,6 +1042,7 @@ def convert_bookformat(book_id):
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error") flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id)) return redirect(url_for('editbook.edit_book', book_id=book_id))
@editbook.route("/ajax/editbooks/<param>", methods=['POST']) @editbook.route("/ajax/editbooks/<param>", methods=['POST'])
@login_required_if_no_ano @login_required_if_no_ano
@edit_required @edit_required
@ -1005,71 +1052,79 @@ def edit_list_book(param):
ret = "" ret = ""
if param =='series_index': if param =='series_index':
edit_book_series_index(vals['value'], book) edit_book_series_index(vals['value'], book)
ret = Response(json.dumps({'success':True, 'newValue': book.series_index}), mimetype='application/json') ret = Response(json.dumps({'success': True, 'newValue': book.series_index}), mimetype='application/json')
elif param =='tags': elif param =='tags':
edit_book_tags(vals['value'], book) edit_book_tags(vals['value'], book)
ret = Response(json.dumps({'success':True, 'newValue': ', '.join([tag.name for tag in book.tags])}), ret = Response(json.dumps({'success': True, 'newValue': ', '.join([tag.name for tag in book.tags])}),
mimetype='application/json') mimetype='application/json')
elif param =='series': elif param =='series':
edit_book_series(vals['value'], book) edit_book_series(vals['value'], book)
ret = Response(json.dumps({'success':True, 'newValue': ', '.join([serie.name for serie in book.series])}), ret = Response(json.dumps({'success': True, 'newValue': ', '.join([serie.name for serie in book.series])}),
mimetype='application/json') mimetype='application/json')
elif param =='publishers': elif param =='publishers':
vals['publisher'] = vals['value'] edit_book_publisher(vals['value'], book)
edit_book_publisher(vals, book) ret = Response(json.dumps({'success': True,
ret = Response(json.dumps({'success':True, 'newValue': book.publishers}), 'newValue': ', '.join([publisher.name for publisher in book.publishers])}),
mimetype='application/json') mimetype='application/json')
elif param =='languages': elif param =='languages':
edit_book_languages(vals['value'], book) invalid = list()
# ToDo: Not working edit_book_languages(vals['value'], book, invalid=invalid)
ret = Response(json.dumps({'success':True, 'newValue': ', '.join([lang.name for lang in book.languages])}), if invalid:
mimetype='application/json') ret = Response(json.dumps({'success': False,
'msg': 'Invalid languages in request: {}'.format(','.join(invalid))}),
mimetype='application/json')
else:
lang_names = list()
for lang in book.languages:
try:
lang_names.append(LC.parse(lang.lang_code).get_language_name(get_locale()))
except UnknownLocaleError:
lang_names.append(_(isoLanguages.get(part3=lang.lang_code).name))
ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}),
mimetype='application/json')
elif param =='author_sort': elif param =='author_sort':
book.author_sort = vals['value'] book.author_sort = vals['value']
ret = Response(json.dumps({'success':True, 'newValue': book.author_sort}), ret = Response(json.dumps({'success': True, 'newValue': book.author_sort}),
mimetype='application/json') mimetype='application/json')
elif param =='title': elif param == 'title':
book.title = vals['value'] sort = book.sort
handle_title_on_edit(book, vals.get('value', ""))
helper.update_dir_stucture(book.id, config.config_calibre_dir) helper.update_dir_stucture(book.id, config.config_calibre_dir)
ret = Response(json.dumps({'success':True, 'newValue': book.title}), ret = Response(json.dumps({'success': True, 'newValue': book.title}),
mimetype='application/json') mimetype='application/json')
elif param =='sort': elif param =='sort':
book.sort = vals['value'] book.sort = vals['value']
ret = Response(json.dumps({'success':True, 'newValue': book.sort}), ret = Response(json.dumps({'success': True, 'newValue': book.sort}),
mimetype='application/json') mimetype='application/json')
# ToDo: edit books
elif param =='authors': elif param =='authors':
input_authors = vals['value'].split('&') input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true")
input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
sort_authors_list = list()
for inp in input_authors:
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
if not stored_author:
stored_author = helper.get_sorted_author(inp)
else:
stored_author = stored_author.sort
sort_authors_list.append(helper.get_sorted_author(stored_author))
sort_authors = ' & '.join(sort_authors_list)
if book.author_sort != sort_authors:
book.author_sort = sort_authors
helper.update_dir_stucture(book.id, config.config_calibre_dir, input_authors[0]) helper.update_dir_stucture(book.id, config.config_calibre_dir, input_authors[0])
ret = Response(json.dumps({'success':True, 'newValue': ', '.join([author.name for author in book.authors])}), ret = Response(json.dumps({'success': True,
'newValue': ' & '.join([author.replace('|',',') for author in input_authors])}),
mimetype='application/json') mimetype='application/json')
book.last_modified = datetime.utcnow() book.last_modified = datetime.utcnow()
calibre_db.session.commit() calibre_db.session.commit()
# revert change for sort if automatic fields link is deactivated
if param == 'title' and vals.get('checkT') == "false":
book.sort = sort
calibre_db.session.commit()
return ret return ret
@editbook.route("/ajax/sort_value/<field>/<int:bookid>") @editbook.route("/ajax/sort_value/<field>/<int:bookid>")
@login_required @login_required
def get_sorted_entry(field, bookid): def get_sorted_entry(field, bookid):
if field == 'title' or field == 'authors': if field in ['title', 'authors', 'sort', 'author_sort']:
book = calibre_db.get_filtered_book(bookid) book = calibre_db.get_filtered_book(bookid)
if book: if book:
if field == 'title': if field == 'title':
return json.dumps({'sort': book.sort}) return json.dumps({'sort': book.sort})
elif field == 'authors': elif field == 'authors':
return json.dumps({'author_sort': book.author_sort}) return json.dumps({'author_sort': book.author_sort})
if field == 'sort':
return json.dumps({'sort': book.title})
if field == 'author_sort':
return json.dumps({'author_sort': book.author})
return "" return ""

@ -87,18 +87,29 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
lang = epub_metadata['language'].split('-', 1)[0].lower() lang = epub_metadata['language'].split('-', 1)[0].lower()
epub_metadata['language'] = isoLanguages.get_lang3(lang) epub_metadata['language'] = isoLanguages.get_lang3(lang)
series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns) epub_metadata = parse_epbub_series(ns, tree, epub_metadata)
if len(series) > 0:
epub_metadata['series'] = series[0]
else:
epub_metadata['series'] = ''
series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns) coverfile = parse_ebpub_cover(ns, tree, epubZip, coverpath, tmp_file_path)
if len(series_id) > 0:
epub_metadata['series_id'] = series_id[0] if not epub_metadata['title']:
title = original_file_name
else: else:
epub_metadata['series_id'] = '1' title = epub_metadata['title']
return BookMeta(
file_path=tmp_file_path,
extension=original_file_extension,
title=title.encode('utf-8').decode('utf-8'),
author=epub_metadata['creator'].encode('utf-8').decode('utf-8'),
cover=coverfile,
description=epub_metadata['description'],
tags=epub_metadata['subject'].encode('utf-8').decode('utf-8'),
series=epub_metadata['series'].encode('utf-8').decode('utf-8'),
series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'),
languages=epub_metadata['language'],
publisher="")
def parse_ebpub_cover(ns, tree, epubZip, coverpath, tmp_file_path):
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns) coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
coverfile = None coverfile = None
if len(coversection) > 0: if len(coversection) > 0:
@ -126,21 +137,18 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
coverfile = extractCover(epubZip, filename, "", tmp_file_path) coverfile = extractCover(epubZip, filename, "", tmp_file_path)
else: else:
coverfile = extractCover(epubZip, coversection[0], coverpath, tmp_file_path) coverfile = extractCover(epubZip, coversection[0], coverpath, tmp_file_path)
return coverfile
if not epub_metadata['title']: def parse_epbub_series(ns, tree, epub_metadata):
title = original_file_name series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns)
if len(series) > 0:
epub_metadata['series'] = series[0]
else: else:
title = epub_metadata['title'] epub_metadata['series'] = ''
return BookMeta( series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns)
file_path=tmp_file_path, if len(series_id) > 0:
extension=original_file_extension, epub_metadata['series_id'] = series_id[0]
title=title.encode('utf-8').decode('utf-8'), else:
author=epub_metadata['creator'].encode('utf-8').decode('utf-8'), epub_metadata['series_id'] = '1'
cover=coverfile, return epub_metadata
description=epub_metadata['description'],
tags=epub_metadata['subject'].encode('utf-8').decode('utf-8'),
series=epub_metadata['series'].encode('utf-8').decode('utf-8'),
series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'),
languages=epub_metadata['language'],
publisher="")

@ -155,6 +155,6 @@ def on_received_watch_confirmation():
# prevent error on windows, as os.rename does on existing files, also allow cross hdd move # prevent error on windows, as os.rename does on existing files, also allow cross hdd move
move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath) move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath)
calibre_db.reconnect_db(config, ub.app_DB_path) calibre_db.reconnect_db(config, ub.app_DB_path)
except Exception as e: except Exception as ex:
log.debug_or_exception(e) log.debug_or_exception(ex)
return '' return ''

@ -28,7 +28,11 @@ from sqlalchemy import create_engine
from sqlalchemy import Column, UniqueConstraint from sqlalchemy import Column, UniqueConstraint
from sqlalchemy import String, Integer from sqlalchemy import String, Integer
from sqlalchemy.orm import sessionmaker, scoped_session from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base try:
# Compability with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.exc import OperationalError, InvalidRequestError from sqlalchemy.exc import OperationalError, InvalidRequestError
try: try:
@ -198,8 +202,8 @@ def getDrive(drive=None, gauth=None):
gauth.Refresh() gauth.Refresh()
except RefreshError as e: except RefreshError as e:
log.error("Google Drive error: %s", e) log.error("Google Drive error: %s", e)
except Exception as e: except Exception as ex:
log.debug_or_exception(e) log.debug_or_exception(ex)
else: else:
# Initialize the saved creds # Initialize the saved creds
gauth.Authorize() gauth.Authorize()
@ -493,8 +497,8 @@ def getChangeById (drive, change_id):
except (errors.HttpError) as error: except (errors.HttpError) as error:
log.error(error) log.error(error)
return None return None
except Exception as e: except Exception as ex:
log.error(e) log.error(ex)
return None return None

@ -35,7 +35,7 @@ from babel.units import format_unit
from flask import send_from_directory, make_response, redirect, abort, url_for from flask import send_from_directory, make_response, redirect, abort, url_for
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import current_user from flask_login import current_user
from sqlalchemy.sql.expression import true, false, and_, text from sqlalchemy.sql.expression import true, false, and_, text, func
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
@ -480,8 +480,8 @@ def reset_password(user_id):
password = generate_random_password() password = generate_random_password()
existing_user.password = generate_password_hash(password) existing_user.password = generate_password_hash(password)
ub.session.commit() ub.session.commit()
send_registration_mail(existing_user.email, existing_user.nickname, password, True) send_registration_mail(existing_user.email, existing_user.name, password, True)
return 1, existing_user.nickname return 1, existing_user.name
except Exception: except Exception:
ub.session.rollback() ub.session.rollback()
return 0, None return 0, None
@ -498,11 +498,37 @@ def generate_random_password():
def uniq(inpt): def uniq(inpt):
output = [] output = []
inpt = [ " ".join(inp.split()) for inp in inpt]
for x in inpt: for x in inpt:
if x not in output: if x not in output:
output.append(x) output.append(x)
return output return output
def check_email(email):
email = valid_email(email)
if ub.session.query(ub.User).filter(func.lower(ub.User.email) == email.lower()).first():
log.error(u"Found an existing account for this e-mail address")
raise Exception(_(u"Found an existing account for this e-mail address"))
return email
def check_username(username):
username = username.strip()
if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).scalar():
log.error(u"This username is already taken")
raise Exception (_(u"This username is already taken"))
return username
def valid_email(email):
email = email.strip()
# Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$",
email):
log.error(u"Invalid e-mail address format")
raise Exception(_(u"Invalid e-mail address format"))
return email
# ################################# External interface ################################# # ################################# External interface #################################
@ -550,8 +576,8 @@ def get_book_cover_internal(book, use_generic_cover_on_failure):
else: else:
log.error('%s/cover.jpg not found on Google Drive', book.path) log.error('%s/cover.jpg not found on Google Drive', book.path)
return get_cover_on_failure(use_generic_cover_on_failure) return get_cover_on_failure(use_generic_cover_on_failure)
except Exception as e: except Exception as ex:
log.debug_or_exception(e) log.debug_or_exception(ex)
return get_cover_on_failure(use_generic_cover_on_failure) return get_cover_on_failure(use_generic_cover_on_failure)
else: else:
cover_file_path = os.path.join(config.config_calibre_dir, book.path) cover_file_path = os.path.join(config.config_calibre_dir, book.path)
@ -731,7 +757,7 @@ def format_runtime(runtime):
def render_task_status(tasklist): def render_task_status(tasklist):
renderedtasklist = list() renderedtasklist = list()
for __, user, __, task in tasklist: for __, user, __, task in tasklist:
if user == current_user.nickname or current_user.role_admin(): if user == current_user.name or current_user.role_admin():
ret = {} ret = {}
if task.start_time: if task.start_time:
ret['starttime'] = format_datetime(task.start_time, format='short', locale=get_locale()) ret['starttime'] = format_datetime(task.start_time, format='short', locale=get_locale())

@ -63,11 +63,12 @@ def get_language_codes(locale, language_names, remainder=None):
if v in language_names: if v in language_names:
lang.append(k) lang.append(k)
language_names.remove(v) language_names.remove(v)
if remainder is not None: if remainder is not None and language_names:
remainder.extend(language_names) remainder.extend(language_names)
return lang return lang
def get_valid_language_codes(locale, language_names, remainder=None): def get_valid_language_codes(locale, language_names, remainder=None):
lang = list() lang = list()
if "" in language_names: if "" in language_names:

@ -82,7 +82,7 @@ def formatdate_filter(val):
except AttributeError as e: except AttributeError as e:
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e, log.error('Babel error: %s, Current user locale: %s, Current User: %s', e,
current_user.locale, current_user.locale,
current_user.nickname current_user.name
) )
return val return val

@ -177,7 +177,7 @@ def HandleSyncRequest():
for book in changed_entries: for book in changed_entries:
formats = [data.format for data in book.Books.data] formats = [data.format for data in book.Books.data]
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats: if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.nickname) helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
kobo_reading_state = get_or_create_reading_state(book.Books.id) kobo_reading_state = get_or_create_reading_state(book.Books.id)
entitlement = { entitlement = {
@ -262,8 +262,8 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
extra_headers["x-kobo-sync-mode"] = store_response.headers.get("x-kobo-sync-mode") extra_headers["x-kobo-sync-mode"] = store_response.headers.get("x-kobo-sync-mode")
extra_headers["x-kobo-recent-reads"] = store_response.headers.get("x-kobo-recent-reads") extra_headers["x-kobo-recent-reads"] = store_response.headers.get("x-kobo-recent-reads")
except Exception as e: except Exception as ex:
log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e)) log.error("Failed to receive or parse response from Kobo's sync endpoint: {}".format(ex))
if set_cont: if set_cont:
extra_headers["x-kobo-sync"] = "continue" extra_headers["x-kobo-sync"] = "continue"
sync_token.to_headers(extra_headers) sync_token.to_headers(extra_headers)

@ -155,7 +155,7 @@ def generate_auth_token(user_id):
for book in books: for book in books:
formats = [data.format for data in book.data] formats = [data.format for data in book.data]
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats: if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.nickname) helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
return render_title_template( return render_title_template(
"generate_kobo_auth_url.html", "generate_kobo_auth_url.html",

@ -42,6 +42,7 @@ except NameError:
oauth_check = {} oauth_check = {}
oauthblueprints = []
oauth = Blueprint('oauth', __name__) oauth = Blueprint('oauth', __name__)
log = logger.create() log = logger.create()
@ -87,7 +88,7 @@ def register_user_with_oauth(user=None):
except NoResultFound: except NoResultFound:
# no found, return error # no found, return error
return return
ub.session_commit("User {} with OAuth for provider {} registered".format(user.nickname, oauth_key)) ub.session_commit("User {} with OAuth for provider {} registered".format(user.name, oauth_key))
def logout_oauth_user(): def logout_oauth_user():
@ -133,8 +134,8 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider
# already bind with user, just login # already bind with user, just login
if oauth_entry.user: if oauth_entry.user:
login_user(oauth_entry.user) login_user(oauth_entry.user)
log.debug(u"You are now logged in as: '%s'", oauth_entry.user.nickname) log.debug(u"You are now logged in as: '%s'", oauth_entry.user.name)
flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.nickname), flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.name),
category="success") category="success")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
else: else:
@ -146,8 +147,8 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider
ub.session.commit() ub.session.commit()
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success") flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
return redirect(url_for('web.profile')) return redirect(url_for('web.profile'))
except Exception as e: except Exception as ex:
log.debug_or_exception(e) log.debug_or_exception(ex)
ub.session.rollback() ub.session.rollback()
else: else:
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error") flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
@ -193,8 +194,8 @@ def unlink_oauth(provider):
ub.session.commit() ub.session.commit()
logout_oauth_user() logout_oauth_user()
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success") flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
except Exception as e: except Exception as ex:
log.debug_or_exception(e) log.debug_or_exception(ex)
ub.session.rollback() ub.session.rollback()
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error") flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
except NoResultFound: except NoResultFound:
@ -203,7 +204,6 @@ def unlink_oauth(provider):
return redirect(url_for('web.profile')) return redirect(url_for('web.profile'))
def generate_oauth_blueprints(): def generate_oauth_blueprints():
oauthblueprints = []
if not ub.session.query(ub.OAuthProvider).count(): if not ub.session.query(ub.OAuthProvider).count():
for provider in ("github", "google"): for provider in ("github", "google"):
oauthProvider = ub.OAuthProvider() oauthProvider = ub.OAuthProvider()
@ -299,39 +299,6 @@ if ub.oauth_support:
) # ToDo: Translate ) # ToDo: Translate
flash(msg, category="error") flash(msg, category="error")
@oauth.route('/link/github')
@oauth_required
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")
return redirect(url_for('web.login'))
@oauth.route('/unlink/github', methods=["GET"])
@login_required
def github_login_unlink():
return unlink_oauth(oauthblueprints[0]['id'])
@oauth.route('/link/google')
@oauth_required
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")
return redirect(url_for('web.login'))
@oauth_error.connect_via(oauthblueprints[1]['blueprint']) @oauth_error.connect_via(oauthblueprints[1]['blueprint'])
def google_error(blueprint, error, error_description=None, error_uri=None): def google_error(blueprint, error, error_description=None, error_uri=None):
msg = ( msg = (
@ -346,7 +313,39 @@ if ub.oauth_support:
flash(msg, category="error") flash(msg, category="error")
@oauth.route('/unlink/google', methods=["GET"]) @oauth.route('/link/github')
@login_required @oauth_required
def google_login_unlink(): def github_login():
return unlink_oauth(oauthblueprints[1]['id']) 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")
return redirect(url_for('web.login'))
@oauth.route('/unlink/github', methods=["GET"])
@login_required
def github_login_unlink():
return unlink_oauth(oauthblueprints[0]['id'])
@oauth.route('/link/google')
@oauth_required
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")
return redirect(url_for('web.login'))
@oauth.route('/unlink/google', methods=["GET"])
@login_required
def google_login_unlink():
return unlink_oauth(oauthblueprints[1]['id'])

@ -27,7 +27,7 @@ from functools import wraps
from flask import Blueprint, request, render_template, Response, g, make_response, abort from flask import Blueprint, request, render_template, Response, g, make_response, abort
from flask_login import current_user from flask_login import current_user
from sqlalchemy.sql.expression import func, text, or_, and_ from sqlalchemy.sql.expression import func, text, or_, and_, true
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
@ -97,6 +97,44 @@ def feed_normal_search():
return feed_search(request.args.get("query", "").strip()) return feed_search(request.args.get("query", "").strip())
@opds.route("/opds/books")
@requires_basic_auth_if_no_ano
def feed_booksindex():
shift = 0
off = int(request.args.get("offset") or 0)
entries = calibre_db.session.query(func.upper(func.substr(db.Books.sort, 1, 1)).label('id'))\
.filter(calibre_db.common_filters()).group_by(func.upper(func.substr(db.Books.sort, 1, 1))).all()
elements = []
if off == 0:
elements.append({'id': "00", 'name':_("All")})
shift = 1
for entry in entries[
off + shift - 1:
int(off + int(config.config_books_per_page) - shift)]:
elements.append({'id': entry.id, 'name': entry.id})
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries) + 1)
return render_xml_template('feed.xml',
letterelements=elements,
folder='opds.feed_letter_books',
pagination=pagination)
@opds.route("/opds/books/letter/<book_id>")
@requires_basic_auth_if_no_ano
def feed_letter_books(book_id):
off = request.args.get("offset") or 0
letter = true() if book_id == "00" else func.upper(db.Books.sort).startswith(book_id)
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books,
letter,
[db.Books.sort])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@opds.route("/opds/new") @opds.route("/opds/new")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_new(): def feed_new():
@ -150,14 +188,41 @@ def feed_hot():
@opds.route("/opds/author") @opds.route("/opds/author")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_authorindex(): def feed_authorindex():
shift = 0
off = int(request.args.get("offset") or 0)
entries = calibre_db.session.query(func.upper(func.substr(db.Authors.sort, 1, 1)).label('id'))\
.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()
elements = []
if off == 0:
elements.append({'id': "00", 'name':_("All")})
shift = 1
for entry in entries[
off + shift - 1:
int(off + int(config.config_books_per_page) - shift)]:
elements.append({'id': entry.id, 'name': entry.id})
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries) + 1)
return render_xml_template('feed.xml',
letterelements=elements,
folder='opds.feed_letter_author',
pagination=pagination)
@opds.route("/opds/author/letter/<book_id>")
@requires_basic_auth_if_no_ano
def feed_letter_author(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
letter = true() if book_id == "00" else func.upper(db.Authors.sort).startswith(book_id)
entries = calibre_db.session.query(db.Authors).join(db.books_authors_link).join(db.Books)\ entries = calibre_db.session.query(db.Authors).join(db.books_authors_link).join(db.Books)\
.filter(calibre_db.common_filters())\ .filter(calibre_db.common_filters()).filter(letter)\
.group_by(text('books_authors_link.author'))\ .group_by(text('books_authors_link.author'))\
.order_by(db.Authors.sort).limit(config.config_books_per_page)\ .order_by(db.Authors.sort)
.offset(off)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(calibre_db.session.query(db.Authors).all())) entries.count())
entries = entries.limit(config.config_books_per_page).offset(off).all()
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination) return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination)
@ -201,17 +266,41 @@ def feed_publisher(book_id):
@opds.route("/opds/category") @opds.route("/opds/category")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_categoryindex(): def feed_categoryindex():
shift = 0
off = int(request.args.get("offset") or 0)
entries = calibre_db.session.query(func.upper(func.substr(db.Tags.name, 1, 1)).label('id'))\
.join(db.books_tags_link).join(db.Books).filter(calibre_db.common_filters())\
.group_by(func.upper(func.substr(db.Tags.name, 1, 1))).all()
elements = []
if off == 0:
elements.append({'id': "00", 'name':_("All")})
shift = 1
for entry in entries[
off + shift - 1:
int(off + int(config.config_books_per_page) - shift)]:
elements.append({'id': entry.id, 'name': entry.id})
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries) + 1)
return render_xml_template('feed.xml',
letterelements=elements,
folder='opds.feed_letter_category',
pagination=pagination)
@opds.route("/opds/category/letter/<book_id>")
@requires_basic_auth_if_no_ano
def feed_letter_category(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
letter = true() if book_id == "00" else func.upper(db.Tags.name).startswith(book_id)
entries = calibre_db.session.query(db.Tags)\ entries = calibre_db.session.query(db.Tags)\
.join(db.books_tags_link)\ .join(db.books_tags_link)\
.join(db.Books)\ .join(db.Books)\
.filter(calibre_db.common_filters())\ .filter(calibre_db.common_filters()).filter(letter)\
.group_by(text('books_tags_link.tag'))\ .group_by(text('books_tags_link.tag'))\
.order_by(db.Tags.name)\ .order_by(db.Tags.name)
.offset(off)\
.limit(config.config_books_per_page)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(calibre_db.session.query(db.Tags).all())) entries.count())
entries = entries.offset(off).limit(config.config_books_per_page).all()
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination) return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination)
@ -229,16 +318,40 @@ def feed_category(book_id):
@opds.route("/opds/series") @opds.route("/opds/series")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_seriesindex(): def feed_seriesindex():
shift = 0
off = int(request.args.get("offset") or 0)
entries = calibre_db.session.query(func.upper(func.substr(db.Series.sort, 1, 1)).label('id'))\
.join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters())\
.group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
elements = []
if off == 0:
elements.append({'id': "00", 'name':_("All")})
shift = 1
for entry in entries[
off + shift - 1:
int(off + int(config.config_books_per_page) - shift)]:
elements.append({'id': entry.id, 'name': entry.id})
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries) + 1)
return render_xml_template('feed.xml',
letterelements=elements,
folder='opds.feed_letter_series',
pagination=pagination)
@opds.route("/opds/series/letter/<book_id>")
@requires_basic_auth_if_no_ano
def feed_letter_series(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
letter = true() if book_id == "00" else func.upper(db.Series.sort).startswith(book_id)
entries = calibre_db.session.query(db.Series)\ entries = calibre_db.session.query(db.Series)\
.join(db.books_series_link)\ .join(db.books_series_link)\
.join(db.Books)\ .join(db.Books)\
.filter(calibre_db.common_filters())\ .filter(calibre_db.common_filters()).filter(letter)\
.group_by(text('books_series_link.series'))\ .group_by(text('books_series_link.series'))\
.order_by(db.Series.sort)\ .order_by(db.Series.sort)
.offset(off).all()
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(calibre_db.session.query(db.Series).all())) entries.count())
entries = entries.offset(off).limit(config.config_books_per_page).all()
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination) return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination)
@ -269,7 +382,7 @@ def feed_ratingindex():
len(entries)) len(entries))
element = list() element = list()
for entry in entries: for entry in entries:
element.append(FeedObject(entry[0].id, "{} Stars".format(entry.name))) element.append(FeedObject(entry[0].id, _("{} Stars").format(entry.name)))
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination) return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination)
@ -428,7 +541,7 @@ def check_auth(username, password):
username = username.encode('windows-1252') username = username.encode('windows-1252')
except UnicodeEncodeError: except UnicodeEncodeError:
username = username.encode('utf-8') username = username.encode('utf-8')
user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == user = ub.session.query(ub.User).filter(func.lower(ub.User.name) ==
username.decode('utf-8').lower()).first() username.decode('utf-8').lower()).first()
if bool(user and check_password_hash(str(user.password), password)): if bool(user and check_password_hash(str(user.password), password)):
return True return True

@ -126,11 +126,11 @@ def token_verified():
login_user(user) login_user(user)
ub.session.delete(auth_token) ub.session.delete(auth_token)
ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.nickname)) ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.name))
data['status'] = 'success' data['status'] = 'success'
log.debug(u"Remote Login for userid %s succeded", user.id) log.debug(u"Remote Login for userid %s succeded", user.id)
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success") flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name), category="success")
response = make_response(json.dumps(data, ensure_ascii=False)) response = make_response(json.dumps(data, ensure_ascii=False))
response.headers["Content-Type"] = "application/json; charset=utf-8" response.headers["Content-Type"] = "application/json; charset=utf-8"

@ -42,10 +42,16 @@ def get_sidebar_config(kwargs=None):
sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot", sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot",
"visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot", "visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot",
"show_text": _('Show Hot Books'), "config_show": True}) "show_text": _('Show Hot Books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list', if current_user.role_admin():
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous), sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list',
"page": "download", "show_text": _('Show Downloaded Books'), "id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
"config_show": content}) "page": "download", "show_text": _('Show Downloaded Books'),
"config_show": content})
else:
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
"page": "download", "show_text": _('Show Downloaded Books'),
"config_show": content})
sidebar.append( sidebar.append(
{"glyph": "glyphicon-star", "text": _('Top Rated Books'), "link": 'web.books_list', "id": "rated", {"glyph": "glyphicon-star", "text": _('Top Rated Books'), "link": 'web.books_list', "id": "rated",
"visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated", "visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated",

@ -45,3 +45,9 @@ except ImportError as err:
log.debug("Cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err) log.debug("Cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err)
kobo = None kobo = None
SyncToken = None SyncToken = None
try:
from . import gmail
except ImportError as err:
log.debug("Cannot import Gmail, sending books via G-Mail Accounts will not work: %s", err)
gmail = None

@ -0,0 +1,80 @@
from __future__ import print_function
import os.path
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
from datetime import datetime
import base64
from flask_babel import gettext as _
from ..constants import BASE_DIR
from .. import logger
log = logger.create()
SCOPES = ['openid', 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/userinfo.email']
def setup_gmail(token):
# If there are no (valid) credentials available, let the user log in.
creds = None
if "token" in token:
creds = Credentials(
token=token['token'],
refresh_token=token['refresh_token'],
token_uri=token['token_uri'],
client_id=token['client_id'],
client_secret=token['client_secret'],
scopes=token['scopes'],
)
creds.expiry = datetime.fromisoformat(token['expiry'])
if not creds or not creds.valid:
# don't forget to dump one more time after the refresh
# also, some file-locking routines wouldn't be needless
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
cred_file = os.path.join(BASE_DIR, 'gmail.json')
if not os.path.exists(cred_file):
raise Exception(_("Found no valid gmail.json file with OAuth information"))
flow = InstalledAppFlow.from_client_secrets_file(
os.path.join(BASE_DIR, 'gmail.json'), SCOPES)
creds = flow.run_local_server(port=0)
user_info = get_user_info(creds)
return {
'token': creds.token,
'refresh_token': creds.refresh_token,
'token_uri': creds.token_uri,
'client_id': creds.client_id,
'client_secret': creds.client_secret,
'scopes': creds.scopes,
'expiry': creds.expiry.isoformat(),
'email': user_info
}
def get_user_info(credentials):
user_info_service = build(serviceName='oauth2', version='v2',credentials=credentials)
user_info = user_info_service.userinfo().get().execute()
return user_info.get('email', "")
def send_messsage(token, msg):
creds = Credentials(
token=token['token'],
refresh_token=token['refresh_token'],
token_uri=token['token_uri'],
client_id=token['client_id'],
client_secret=token['client_secret'],
scopes=token['scopes'],
)
creds.expiry = datetime.fromisoformat(token['expiry'])
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
service = build('gmail', 'v1', credentials=creds)
message_as_bytes = msg.as_bytes() # the message should converted from string to bytes.
message_as_base64 = base64.urlsafe_b64encode(message_as_bytes) # encode in base64 (printable letters coding)
raw = message_as_base64.decode() # convert to something JSON serializable
body = {'raw': raw}
(service.users().messages().send(userId='me', body=body).execute())

@ -159,9 +159,9 @@ class CalibreTask:
# catch any unhandled exceptions in a task and automatically fail it # catch any unhandled exceptions in a task and automatically fail it
try: try:
self.run(*args) self.run(*args)
except Exception as e: except Exception as ex:
self._handleError(str(e)) self._handleError(str(ex))
log.debug_or_exception(e) log.debug_or_exception(ex)
self.end_time = datetime.now() self.end_time = datetime.now()

@ -255,13 +255,13 @@ def create_edit_shelf(shelf, title, page, shelf_id=False):
log.info(u"Shelf {} {}".format(to_save["title"], shelf_action)) log.info(u"Shelf {} {}".format(to_save["title"], shelf_action))
flash(flash_text, category="success") flash(flash_text, category="success")
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id)) return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
except (OperationalError, InvalidRequestError) as e: except (OperationalError, InvalidRequestError) as ex:
ub.session.rollback() ub.session.rollback()
log.debug_or_exception(e) log.debug_or_exception(ex)
flash(_(u"Settings DB is not Writeable"), category="error") flash(_(u"Settings DB is not Writeable"), category="error")
except Exception as e: except Exception as ex:
ub.session.rollback() ub.session.rollback()
log.debug_or_exception(e) log.debug_or_exception(ex)
flash(_(u"There was an error"), category="error") flash(_(u"There was an error"), category="error")
return render_title_template('shelf_edit.html', shelf=shelf, title=title, page=page) return render_title_template('shelf_edit.html', shelf=shelf, title=title, page=page)

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -56,6 +56,7 @@ a,
.book-remove, .book-remove,
.editable-empty, .editable-empty,
.editable-empty:hover { color: #45b29d; } .editable-empty:hover { color: #45b29d; }
.book-remove:hover { color: #23527c; }
.user-remove:hover { color: #23527c; } .user-remove:hover { color: #23527c; }
.btn-default a { color: #444; } .btn-default a { color: #444; }
.panel-title > a { text-decoration: none; } .panel-title > a { text-decoration: none; }

@ -15,6 +15,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
var direction = $("#asc").data('order'); // 0=Descending order; 1= ascending order
var $list = $("#list").isotope({ var $list = $("#list").isotope({
itemSelector: ".book", itemSelector: ".book",
layoutMode: "fitRows", layoutMode: "fitRows",
@ -24,6 +26,9 @@ var $list = $("#list").isotope({
}); });
$("#desc").click(function() { $("#desc").click(function() {
if (direction === 0) {
return;
}
var page = $(this).data("id"); var page = $(this).data("id");
$.ajax({ $.ajax({
method:"post", method:"post",
@ -39,6 +44,9 @@ $("#desc").click(function() {
}); });
$("#asc").click(function() { $("#asc").click(function() {
if (direction === 1) {
return;
}
var page = $(this).data("id"); var page = $(this).data("id");
$.ajax({ $.ajax({
method:"post", method:"post",

@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
var direction = 0; // Descending order var direction = $("#asc").data('order'); // 0=Descending order; 1= ascending order
var sort = 0; // Show sorted entries var sort = 0; // Show sorted entries
$("#sort_name").click(function() { $("#sort_name").click(function() {

@ -114,26 +114,23 @@ $(document).ready(function() {
} }
}); });
function confirmDialog(id, dataValue, yesFn, noFn) { function confirmDialog(id, dialogid, dataValue, yesFn, noFn) {
var $confirm = $("#GeneralDeleteModal"); var $confirm = $("#" + dialogid);
// var dataValue= e.data('value'); // target.data('value');
$confirm.modal('show'); $confirm.modal('show');
$.ajax({ $.ajax({
method:"get", method:"get",
dataType: "json", dataType: "json",
url: getPath() + "/ajax/loaddialogtexts/" + id, url: getPath() + "/ajax/loaddialogtexts/" + id,
success: function success(data) { success: function success(data) {
$("#header").html(data.header); $("#header-"+ dialogid).html(data.header);
$("#text").html(data.main); $("#text-"+ dialogid).html(data.main);
} }
}); });
$("#btnConfirmYes-"+ dialogid).off('click').click(function () {
$("#btnConfirmYes").off('click').click(function () {
yesFn(dataValue); yesFn(dataValue);
$confirm.modal("hide"); $confirm.modal("hide");
}); });
$("#btnConfirmNo").off('click').click(function () { $("#btnConfirmNo-"+ dialogid).off('click').click(function () {
if (typeof noFn !== 'undefined') { if (typeof noFn !== 'undefined') {
noFn(dataValue); noFn(dataValue);
} }
@ -485,6 +482,7 @@ $(function() {
$("#config_delete_kobo_token").click(function() { $("#config_delete_kobo_token").click(function() {
confirmDialog( confirmDialog(
$(this).attr('id'), $(this).attr('id'),
"GeneralDeleteModal",
$(this).data('value'), $(this).data('value'),
function (value) { function (value) {
$.ajax({ $.ajax({
@ -513,6 +511,7 @@ $(function() {
$("#btndeluser").click(function() { $("#btndeluser").click(function() {
confirmDialog( confirmDialog(
$(this).attr('id'), $(this).attr('id'),
"GeneralDeleteModal",
$(this).data('value'), $(this).data('value'),
function(value){ function(value){
var subform = $('#user_submit').closest("form"); var subform = $('#user_submit').closest("form");
@ -531,6 +530,7 @@ $(function() {
$("#delete_shelf").click(function() { $("#delete_shelf").click(function() {
confirmDialog( confirmDialog(
$(this).attr('id'), $(this).attr('id'),
"GeneralDeleteModal",
$(this).data('value'), $(this).data('value'),
function(value){ function(value){
window.location.href = window.location.pathname + "/../../shelf/delete/" + value window.location.href = window.location.pathname + "/../../shelf/delete/" + value

@ -95,17 +95,22 @@ $(function() {
mode: "inline", mode: "inline",
emptytext: "<span class='glyphicon glyphicon-plus'></span>", emptytext: "<span class='glyphicon glyphicon-plus'></span>",
success: function (response, __) { success: function (response, __) {
if(!response.success) return response.msg; if (!response.success) return response.msg;
return {newValue: response.newValue}; return {newValue: response.newValue};
},
params: function (params) {
params.checkA = $('#autoupdate_authorsort').prop('checked');
params.checkT = $('#autoupdate_titlesort').prop('checked');
return params
} }
} }
}; };
} var validateText = $(this).attr("data-edit-validate");
var validateText = $(this).attr("data-edit-validate"); if (validateText) {
if (validateText) { element.editable.validate = function (value) {
element.editable.validate = function (value) { if ($.trim(value) === "") return validateText;
if ($.trim(value) === "") return validateText; };
}; }
} }
column.push(element); column.push(element);
}); });
@ -121,7 +126,7 @@ $(function() {
search: true, search: true,
showColumns: true, showColumns: true,
searchAlign: "left", searchAlign: "left",
showSearchButton : false, showSearchButton : true,
searchOnEnterKey: true, searchOnEnterKey: true,
checkboxHeader: false, checkboxHeader: false,
maintainMetaData: true, maintainMetaData: true,
@ -132,7 +137,8 @@ $(function() {
}, },
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
onEditableSave: function (field, row, oldvalue, $el) { onEditableSave: function (field, row, oldvalue, $el) {
if (field === "title" || field === "authors") { if ($.inArray(field, [ "title", "sort" ]) !== -1 && $('#autoupdate_titlesort').prop('checked')
|| $.inArray(field, [ "authors", "author_sort" ]) !== -1 && $('#autoupdate_authorsort').prop('checked')) {
$.ajax({ $.ajax({
method:"get", method:"get",
dataType: "json", dataType: "json",
@ -240,16 +246,16 @@ $(function() {
} }
$("#domain-allow-table").on("click-cell.bs.table", function (field, value, row, $element) { $("#domain-allow-table").on("click-cell.bs.table", function (field, value, row, $element) {
if (value === 2) { if (value === 2) {
confirmDialog("btndeletedomain", $element.id, domainHandle); confirmDialog("btndeletedomain", "GeneralDeleteModal", $element.id, domainHandle);
} }
}); });
$("#domain-deny-table").on("click-cell.bs.table", function (field, value, row, $element) { $("#domain-deny-table").on("click-cell.bs.table", function (field, value, row, $element) {
if (value === 2) { if (value === 2) {
confirmDialog("btndeletedomain", $element.id, domainHandle); confirmDialog("btndeletedomain", "GeneralDeleteModal", $element.id, domainHandle);
} }
}); });
$("#restrictModal").on("hidden.bs.modal", function () { $("#restrictModal").on("hidden.bs.modal", function (e) {
// Destroy table and remove hooks for buttons // Destroy table and remove hooks for buttons
$("#restrict-elements-table").unbind(); $("#restrict-elements-table").unbind();
$("#restrict-elements-table").bootstrapTable("destroy"); $("#restrict-elements-table").bootstrapTable("destroy");
@ -258,8 +264,54 @@ $(function() {
$("#h2").addClass("hidden"); $("#h2").addClass("hidden");
$("#h3").addClass("hidden"); $("#h3").addClass("hidden");
$("#h4").addClass("hidden"); $("#h4").addClass("hidden");
$("#add_element").val("");
}); });
function startTable(type, userId) {
function startTable(target, userId) {
var type = 0;
switch(target) {
case "get_column_values":
type = 1;
$("#h2").removeClass("hidden");
break;
case "get_tags":
type = 0;
$("#h1").removeClass("hidden");
break;
case "get_user_column_values":
type = 3;
$("#h4").removeClass("hidden");
break;
case "get_user_tags":
type = 2;
$("#h3").removeClass("hidden");
break;
case "denied_tags":
type = 2;
$("#h2").removeClass("hidden");
$("#submit_allow").addClass("hidden");
$("#submit_restrict").removeClass("hidden");
break;
case "allowed_tags":
type = 2;
$("#h2").removeClass("hidden");
$("#submit_restrict").addClass("hidden");
$("#submit_allow").removeClass("hidden");
break;
case "allowed_column_value":
type = 3;
$("#h2").removeClass("hidden");
$("#submit_restrict").addClass("hidden");
$("#submit_allow").removeClass("hidden");
break;
case "denied_column_value":
type = 3;
$("#h2").removeClass("hidden");
$("#submit_allow").addClass("hidden");
$("#submit_restrict").removeClass("hidden");
break;
}
$("#restrict-elements-table").bootstrapTable({ $("#restrict-elements-table").bootstrapTable({
formatNoMatches: function () { formatNoMatches: function () {
return ""; return "";
@ -273,6 +325,10 @@ $(function() {
return {classes: "bg-dark-danger"}; return {classes: "bg-dark-danger"};
} }
}, },
onLoadSuccess: function () {
$(".no-records-found").addClass("hidden");
$(".fixed-table-loading").addClass("hidden");
},
onClickCell: function (field, value, row) { onClickCell: function (field, value, row) {
if (field === 3) { if (field === 3) {
$.ajax ({ $.ajax ({
@ -326,28 +382,192 @@ $(function() {
return; return;
}); });
} }
$("#get_column_values").on("click", function() {
startTable(1, 0); $("#restrictModal").on("show.bs.modal", function(e) {
$("#h2").removeClass("hidden"); 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 {
dataId = $(e.relatedTarget).data("id");
}
startTable(target, dataId);
}); });
$("#get_tags").on("click", function() { // User table handling
startTable(0, 0); var user_column = [];
$("#h1").removeClass("hidden"); $("#user-table > thead > tr > th").each(function() {
var element = {};
if ($(this).attr("data-edit")) {
element = {
editable: {
mode: "inline",
emptytext: "<span class='glyphicon glyphicon-plus'></span>",
error: function(response) {
return response.responseText;
}
}
};
}
var validateText = $(this).attr("data-edit-validate");
if (validateText) {
element.editable.validate = function (value) {
if ($.trim(value) === "") return validateText;
};
}
user_column.push(element);
});
$("#user-table").bootstrapTable({
sidePagination: "server",
pagination: true,
paginationLoop: false,
paginationDetailHAlign: " hidden",
paginationHAlign: "left",
idField: "id",
uniqueId: "id",
search: true,
showColumns: true,
searchAlign: "left",
showSearchButton : true,
searchOnEnterKey: true,
checkboxHeader: true,
maintainMetaData: true,
responseHandler: responseHandler,
columns: user_column,
formatNoMatches: function () {
return "";
},
onPostBody () {
// Remove all checkboxes from Headers for showing the texts in the column selector
$('.columns [data-field]').each(function(){
var elText = $(this).next().text();
$(this).next().empty();
var index = elText.lastIndexOf('\n', elText.length - 2);
if ( index > -1) {
elText = elText.substr(index);
}
$(this).next().text(elText);
});
},
onLoadSuccess: function () {
var guest = $(".editable[data-name='name'][data-value='Guest']");
guest.editable("disable");
$(".editable[data-name='locale'][data-pk='"+guest.data("pk")+"']").editable("disable");
$("input[data-name='admin_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$("input[data-name='passwd_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$("input[data-name='edit_shelf_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
// ToDo: Disable delete
},
// 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) {
var visible = $("#user-table").bootstrapTable("getVisibleColumns");
var hidden = $("#user-table").bootstrapTable("getHiddenColumns");
var st = "";
visible.forEach(function(item) {
st += "\"" + item.name + "\":\"" + "true" + "\",";
});
hidden.forEach(function(item) {
st += "\"" + item.name + "\":\"" + "false" + "\",";
});
st = st.slice(0, -1);
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/user_table_settings",
data: "{" + st + "}",
});
},
}); });
$("#get_user_column_values").on("click", function() {
startTable(3, $(this).data("id")); $("#user_delete_selection").click(function() {
$("#h4").removeClass("hidden"); $("#user-table").bootstrapTable("uncheckAll");
}); });
$("#get_user_tags").on("click", function() { function user_handle (userId) {
startTable(2, $(this).data("id")); $.ajax({
$(this)[0].blur(); method:"post",
$("#h3").removeClass("hidden"); 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);
}
}); });
$("#user-table").on("check.bs.table check-all.bs.table uncheck.bs.table uncheck-all.bs.table",
function (e, rowsAfter, rowsBefore) {
var rows = rowsAfter;
if (e.type === "uncheck-all") {
rows = rowsBefore;
}
var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) {
return row.id;
});
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");
}
});
}); });
/* Function for deleting domain restrictions */ /* Function for deleting domain restrictions */
function TableActions (value, row) { function TableActions (value, row) {
return [ return [
@ -358,7 +578,10 @@ function TableActions (value, row) {
].join(""); ].join("");
} }
function editEntry(param)
{
console.log(param);
}
/* Function for deleting domain restrictions */ /* Function for deleting domain restrictions */
function RestrictionActions (value, row) { function RestrictionActions (value, row) {
return [ return [
@ -377,6 +600,15 @@ function EbookActions (value, row) {
].join(""); ].join("");
} }
/* Function for deleting books */
function UserActions (value, row) {
return [
"<div class=\"user-remove\" data-target=\"#GeneralDeleteModal\" title=\"Remove\">",
"<i class=\"glyphicon glyphicon-trash\"></i>",
"</div>"
].join("");
}
/* Function for keeping checked rows */ /* Function for keeping checked rows */
function responseHandler(res) { function responseHandler(res) {
$.each(res.rows, function (i, row) { $.each(res.rows, function (i, row) {
@ -384,3 +616,122 @@ function responseHandler(res) {
}); });
return res; return res;
} }
function singleUserFormatter(value, row) {
return '<a class="btn btn-default" href="/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 + ')">';
else
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.name + '" onchange="checkboxChange(this, ' + row.id + ', \'' + this.field + '\', ' + 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>*/
});
$.ajax({
method:"get",
url: window.location.pathname + "/../../ajax/listusers",
async: true,
timeout: 900,
success:function(data) {
$("#user-table").bootstrapTable("load", data);
}
});
}
function deactivateHeaderButtons(e) {
$("#user_delete_selection").addClass("disabled");
$("#user_delete_selection").attr("aria-disabled", true);
$(".check_head").attr("aria-disabled", true);
$(".check_head").attr("disabled", true);
$(".check_head").prop('checked', false);
$(".button_head").attr("aria-disabled", true);
$(".button_head").addClass("disabled");
$(".header_select").attr("disabled", true);
}
function selectHeader(element, field) {
if (element.value !== "None") {
confirmDialog(element.id, "GeneralChangeModal", 0, function () {
var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id);
$.ajax({
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();
}
});
}
});
});
}
}
function checkboxHeader(CheckboxState, field, field_index) {
confirmDialog(field, "GeneralChangeModal", 0, function() {
var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id);
$.ajax({
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);
}
});
}
});
});
}
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");
}

@ -75,8 +75,8 @@ class TaskConvert(CalibreTask):
self.settings['body'], self.settings['body'],
internal=True) internal=True)
) )
except Exception as e: except Exception as ex:
return self._handleError(str(e)) return self._handleError(str(ex))
def _convert_ebook_format(self): def _convert_ebook_format(self):
error_message = None error_message = None

@ -4,6 +4,8 @@ import os
import smtplib import smtplib
import threading import threading
import socket import socket
import mimetypes
import base64
try: try:
from StringIO import StringIO from StringIO import StringIO
@ -16,11 +18,14 @@ except ImportError:
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email import encoders from email import encoders
from email.utils import formatdate, make_msgid from email.utils import formatdate, make_msgid
from email.generator import Generator from email.generator import Generator
from cps.services.worker import CalibreTask from cps.services.worker import CalibreTask
from cps.services import gmail
from cps import logger, config from cps import logger, config
from cps import gdriveutils from cps import gdriveutils
@ -107,68 +112,38 @@ class TaskEmail(CalibreTask):
self.recipent = recipient self.recipent = recipient
self.text = text self.text = text
self.asyncSMTP = None self.asyncSMTP = None
self.results = dict() self.results = dict()
def run(self, worker_thread): def prepare_message(self):
# create MIME message message = MIMEMultipart()
msg = MIMEMultipart() message['to'] = self.recipent
msg['Subject'] = self.subject message['from'] = self.settings["mail_from"]
msg['Message-Id'] = make_msgid('calibre-web') message['subject'] = self.subject
msg['Date'] = formatdate(localtime=True) message['Message-Id'] = make_msgid('calibre-web')
message['Date'] = formatdate(localtime=True)
text = self.text text = self.text
msg.attach(MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8')) msg = MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8')
message.attach(msg)
if self.attachment: if self.attachment:
result = self._get_attachment(self.filepath, self.attachment) result = self._get_attachment(self.filepath, self.attachment)
if result: if result:
msg.attach(result) message.attach(result)
else: else:
self._handleError(u"Attachment not found") self._handleError(u"Attachment not found")
return return
return message
msg['From'] = self.settings["mail_from"] def run(self, worker_thread):
msg['To'] = self.recipent # create MIME message
msg = self.prepare_message()
use_ssl = int(self.settings.get('mail_use_ssl', 0))
try: try:
# convert MIME message to string if self.settings['mail_server_type'] == 0:
fp = StringIO() self.send_standard_email(msg)
gen = Generator(fp, mangle_from_=False)
gen.flatten(msg)
msg = fp.getvalue()
# send email
timeout = 600 # set timeout to 5mins
# redirect output to logfile on python2 pn python3 debugoutput is caught with overwritten
# _print_debug function
if sys.version_info < (3, 0):
org_smtpstderr = smtplib.stderr
smtplib.stderr = logger.StderrLogger('worker.smtp')
if use_ssl == 2:
self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"],
timeout=timeout)
else: else:
self.asyncSMTP = Email(self.settings["mail_server"], self.settings["mail_port"], timeout=timeout) self.send_gmail_email(msg)
except MemoryError as e:
# link to logginglevel
if logger.is_debug_enabled():
self.asyncSMTP.set_debuglevel(1)
if use_ssl == 1:
self.asyncSMTP.starttls()
if self.settings["mail_password"]:
self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password"]))
self.asyncSMTP.sendmail(self.settings["mail_from"], self.recipent, msg)
self.asyncSMTP.quit()
self._handleSuccess()
if sys.version_info < (3, 0):
smtplib.stderr = org_smtpstderr
except (MemoryError) as e:
log.debug_or_exception(e) log.debug_or_exception(e)
self._handleError(u'MemoryError sending email: ' + str(e)) self._handleError(u'MemoryError sending email: {}'.format(str(e)))
except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e: except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
log.debug_or_exception(e) log.debug_or_exception(e)
if hasattr(e, "smtp_error"): if hasattr(e, "smtp_error"):
@ -179,12 +154,55 @@ class TaskEmail(CalibreTask):
text = '\n'.join(e.args) text = '\n'.join(e.args)
else: else:
text = '' text = ''
self._handleError(u'Smtplib Error sending email: ' + text) self._handleError(u'Smtplib Error sending email: {}'.format(text))
except (socket.error) as e: except socket.error as e:
log.debug_or_exception(e) log.debug_or_exception(e)
self._handleError(u'Socket Error sending email: ' + e.strerror) self._handleError(u'Socket Error sending email: {}'.format(e.strerror))
except Exception as ex:
log.debug_or_exception(ex)
self._handleError(u'Error sending email: {}'.format(ex))
def send_standard_email(self, msg):
use_ssl = int(self.settings.get('mail_use_ssl', 0))
timeout = 600 # set timeout to 5mins
# redirect output to logfile on python2 on python3 debugoutput is caught with overwritten
# _print_debug function
if sys.version_info < (3, 0):
org_smtpstderr = smtplib.stderr
smtplib.stderr = logger.StderrLogger('worker.smtp')
if use_ssl == 2:
self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"],
timeout=timeout)
else:
self.asyncSMTP = Email(self.settings["mail_server"], self.settings["mail_port"], timeout=timeout)
# link to logginglevel
if logger.is_debug_enabled():
self.asyncSMTP.set_debuglevel(1)
if use_ssl == 1:
self.asyncSMTP.starttls()
if self.settings["mail_password"]:
self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password"]))
# Convert message to something to send
fp = StringIO()
gen = Generator(fp, mangle_from_=False)
gen.flatten(msg)
self.asyncSMTP.sendmail(self.settings["mail_from"], self.recipent, fp.getvalue())
self.asyncSMTP.quit()
self._handleSuccess()
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)
@property @property
def progress(self): def progress(self):
if self.asyncSMTP is not None: if self.asyncSMTP is not None:
@ -203,13 +221,13 @@ class TaskEmail(CalibreTask):
@classmethod @classmethod
def _get_attachment(cls, bookpath, filename): def _get_attachment(cls, bookpath, filename):
"""Get file as MIMEBase message""" """Get file as MIMEBase message"""
calibrepath = config.config_calibre_dir calibre_path = config.config_calibre_dir
if config.config_use_google_drive: if config.config_use_google_drive:
df = gdriveutils.getFileFromEbooksFolder(bookpath, filename) df = gdriveutils.getFileFromEbooksFolder(bookpath, filename)
if df: if df:
datafile = os.path.join(calibrepath, bookpath, filename) datafile = os.path.join(calibre_path, bookpath, filename)
if not os.path.exists(os.path.join(calibrepath, bookpath)): if not os.path.exists(os.path.join(calibre_path, bookpath)):
os.makedirs(os.path.join(calibrepath, bookpath)) os.makedirs(os.path.join(calibre_path, bookpath))
df.GetContentFile(datafile) df.GetContentFile(datafile)
else: else:
return None return None
@ -219,19 +237,22 @@ class TaskEmail(CalibreTask):
os.remove(datafile) os.remove(datafile)
else: else:
try: try:
file_ = open(os.path.join(calibrepath, bookpath, filename), 'rb') file_ = open(os.path.join(calibre_path, bookpath, filename), 'rb')
data = file_.read() data = file_.read()
file_.close() file_.close()
except IOError as e: except IOError as e:
log.debug_or_exception(e) log.debug_or_exception(e)
log.error(u'The requested file could not be read. Maybe wrong permissions?') log.error(u'The requested file could not be read. Maybe wrong permissions?')
return None return None
# Set mimetype
attachment = MIMEBase('application', 'octet-stream') content_type, encoding = mimetypes.guess_type(filename)
if content_type is None or encoding is not None:
content_type = 'application/octet-stream'
main_type, sub_type = content_type.split('/', 1)
attachment = MIMEBase(main_type, sub_type)
attachment.set_payload(data) attachment.set_payload(data)
encoders.encode_base64(attachment) encoders.encode_base64(attachment)
attachment.add_header('Content-Disposition', 'attachment', attachment.add_header('Content-Disposition', 'attachment', filename=filename)
filename=filename)
return attachment return attachment
@property @property

@ -7,6 +7,7 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h2>{{_('Users')}}</h2> <h2>{{_('Users')}}</h2>
{% if allUser.__len__() < 10 %}
<table class="table table-striped" id="table_user"> <table class="table table-striped" id="table_user">
<tr> <tr>
<th>{{_('Username')}}</th> <th>{{_('Username')}}</th>
@ -25,7 +26,7 @@
{% for user in allUser %} {% for user in allUser %}
{% if not user.role_anonymous() or config.config_anonbrowse %} {% if not user.role_anonymous() or config.config_anonbrowse %}
<tr> <tr>
<td><a href="{{url_for('admin.edit_user', user_id=user.id)}}">{{user.nickname}}</a></td> <td><a href="{{url_for('admin.edit_user', user_id=user.id)}}">{{user.name}}</a></td>
<td>{{user.email}}</td> <td>{{user.email}}</td>
<td>{{user.kindle_mail}}</td> <td>{{user.kindle_mail}}</td>
<td>{{user.downloads.count()}}</td> <td>{{user.downloads.count()}}</td>
@ -41,6 +42,8 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</table> </table>
{% endif %}
<a class="btn btn-default" id="admin_user_table" href="{{url_for('admin.edit_user_table')}}">{{_('Edit Users')}}</a>
<a class="btn btn-default" id="admin_new_user" href="{{url_for('admin.new_user')}}">{{_('Add New User')}}</a> <a class="btn btn-default" id="admin_new_user" href="{{url_for('admin.new_user')}}">{{_('Add New User')}}</a>
{% if (config.config_login_type == 1) %} {% if (config.config_login_type == 1) %}
<div class="btn btn-default" id="import_ldap_users" data-toggle="modal" data-target="#StatusDialog">{{_('Import LDAP Users')}}</div> <div class="btn btn-default" id="import_ldap_users" data-toggle="modal" data-target="#StatusDialog">{{_('Import LDAP Users')}}</div>
@ -52,29 +55,42 @@
<div class="col"> <div class="col">
<h2>{{_('E-mail Server Settings')}}</h2> <h2>{{_('E-mail Server Settings')}}</h2>
{% if config.get_mail_server_configured() %} {% if config.get_mail_server_configured() %}
<div class="col-xs-12 col-sm-12"> {% if email.mail_server_type == 0 %}
<div class="row"> <div class="col-xs-12 col-sm-12">
<div class="row">
<div class="col-xs-6 col-sm-3">{{_('SMTP Hostname')}}</div> <div class="col-xs-6 col-sm-3">{{_('SMTP Hostname')}}</div>
<div class="col-xs-6 col-sm-3">{{email.mail_server}}</div> <div class="col-xs-6 col-sm-3">{{email.mail_server}}</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-sm-3">{{_('SMTP Port')}}</div> <div class="col-xs-6 col-sm-3">{{_('SMTP Port')}}</div>
<div class="col-xs-6 col-sm-3">{{email.mail_port}}</div> <div class="col-xs-6 col-sm-3">{{email.mail_port}}</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-sm-3">{{_('Encryption')}}</div> <div class="col-xs-6 col-sm-3">{{_('Encryption')}}</div>
<div class="col-xs-6 col-sm-3">{{ display_bool_setting(email.mail_use_ssl) }}</div> <div class="col-xs-6 col-sm-3">{{ display_bool_setting(email.mail_use_ssl) }}</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-3">{{_('SMTP Login')}}</div>
<div class="col-xs-6 col-sm-3">{{email.mail_login}}</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-3">{{_('From E-mail')}}</div>
<div class="col-xs-6 col-sm-3">{{email.mail_from}}</div>
</div>
</div> </div>
<div class="row"> {% else %}
<div class="col-xs-6 col-sm-3">{{_('SMTP Login')}}</div> <div class="col-xs-12 col-sm-12">
<div class="col-xs-6 col-sm-3">{{email.mail_login}}</div> <div class="row">
</div> <div class="col-xs-6 col-sm-3">{{_('E-Mail Service')}}</div>
<div class="row"> <div class="col-xs-6 col-sm-3">{{_('Gmail via Oauth2')}}</div>
<div class="col-xs-6 col-sm-3">{{_('From E-mail')}}</div> </div>
<div class="col-xs-6 col-sm-3">{{email.mail_from}}</div> <div class="row">
<div class="col-xs-6 col-sm-3">{{_('From E-mail')}}</div>
<div class="col-xs-6 col-sm-3">{{email.mail_gmail_token['email']}}</div>
</div>
</div> </div>
</div>
{% endif %} {% endif %}
{% endif %}
<a class="btn btn-default emailconfig" id="admin_edit_email" href="{{url_for('admin.edit_mailsettings')}}">{{_('Edit E-mail Server Settings')}}</a> <a class="btn btn-default emailconfig" id="admin_edit_email" href="{{url_for('admin.edit_mailsettings')}}">{{_('Edit E-mail Server Settings')}}</a>
</div> </div>
</div> </div>

@ -30,8 +30,8 @@
<label for="autoupdate_titlesort">{{_('Update Title Sort automatically')}}</label> <label for="autoupdate_titlesort">{{_('Update Title Sort automatically')}}</label>
</div> </div>
<div class="row"> <div class="row">
<input type="checkbox" id="autoupdate_autorsort" name="autoupdate_autorsort" checked> <input type="checkbox" id="autoupdate_authorsort" name="autoupdate_authorsort" checked>
<label for="autoupdate_autorsort">{{_('Update Author Sort automatically')}}</label> <label for="autoupdate_authorsort">{{_('Update Author Sort automatically')}}</label>
</div> </div>
</div> </div>
@ -53,7 +53,7 @@
{{ text_table_row('languages', _('Enter Languages'),_('Languages'), false) }} {{ text_table_row('languages', _('Enter Languages'),_('Languages'), false) }}
<!--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--> <!--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) }}
{% if g.user.role_edit() %} {% if g.user.role_delete_books() and g.user.role_edit()%}
<th data-align="right" data-formatter="EbookActions" data-switchable="false">{{_('Delete')}}</th> <th data-align="right" data-formatter="EbookActions" data-switchable="false">{{_('Delete')}}</th>
{% endif %} {% endif %}
</tr> </tr>
@ -94,6 +94,4 @@
<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-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-table/bootstrap-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/table.js') }}"></script> <script src="{{ url_for('static', filename='js/table.js') }}"></script>
<script>
</script>
{% endblock %} {% endblock %}

@ -141,8 +141,8 @@
<input type="checkbox" name="Show_detail_random" id="Show_detail_random" {% if conf.show_detail_random() %}checked{% endif %}> <input type="checkbox" name="Show_detail_random" id="Show_detail_random" {% if conf.show_detail_random() %}checked{% endif %}>
<label for="Show_detail_random">{{_('Show Random Books in Detail View')}}</label> <label for="Show_detail_random">{{_('Show Random Books in Detail View')}}</label>
</div> </div>
<a href="#" id="get_tags" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add Allowed/Denied Tags')}}</a> <a href="#" id="get_tags" data-id="0" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add Allowed/Denied Tags')}}</a>
<a href="#" id="get_column_values" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add Allowed/Denied custom column values')}}</a> <a href="#" id="get_column_values" data-id="0" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add Allowed/Denied custom column values')}}</a>
</div> </div>
</div> </div>
</div> </div>

@ -7,44 +7,66 @@
<div class="discover"> <div class="discover">
<h1>{{title}}</h1> <h1>{{title}}</h1>
<form role="form" class="col-md-10 col-lg-6" method="POST"> <form role="form" class="col-md-10 col-lg-6" method="POST">
{% if feature_support['gmail'] %}
<div class="form-group"> <div class="form-group">
<label for="mail_server">{{_('SMTP Hostname')}}</label> <label for="mail_server_type">{{_('Choose Server Type')}}</label>
<input type="text" class="form-control" name="mail_server" id="mail_server" value="{{content.mail_server}}" required> <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>
</select>
</div> </div>
<div class="form-group"> <div data-related="email-settings-1">
<label for="mail_port">{{_('SMTP Port')}}</label> <div class="form-group">
<input type="number" min="1" max="65535" step="1" class="form-control" name="mail_port" id="mail_port" value="{% if content.mail_port != None %}{{ content.mail_port }}{% endif %}" autocomplete="off" required> {% if content.mail_gmail_token == {} %}
</div> <button type="submit" id="gmail_server" name="gmail" value="submit" class="btn btn-default">{{_('Setup Gmail Account as E-Mail Server')}}</button>
<div class="form-group"> {% else %}
<label for="mail_use_ssl">{{_('Encryption')}}</label> <button type="submit" id="invalidate_server" name="invalidate" value="submit" class="btn btn-danger">{{_('Revoke G-Mail Access')}}</button>
<select name="mail_use_ssl" id="mail_use_ssl" class="form-control"> {% endif %}
<option value="0" {% if content.mail_use_ssl == 0 %}selected{% endif %}>{{ _('None') }}</option> </div>
<option value="1" {% if content.mail_use_ssl == 1 %}selected{% endif %}>{{ _('STARTTLS') }}</option>
<option value="2" {% if content.mail_use_ssl == 2 %}selected{% endif %}>{{ _('SSL/TLS') }}</option>
</select>
</div>
<div class="form-group">
<label for="mail_login">{{_('SMTP Login')}}</label>
<input type="text" class="form-control" name="mail_login" id="mail_login" value="{{content.mail_login}}">
</div>
<div class="form-group">
<label for="mail_password">{{_('SMTP Password')}}</label>
<input type="password" class="form-control" name="mail_password" id="mail_password" value="{{content.mail_password}}">
</div>
<div class="form-group">
<label for="mail_from">{{_('From E-mail')}}</label>
<input type="text" class="form-control" name="mail_from" id="mail_from" value="{{content.mail_from}}" required>
</div> </div>
<label for="mail_size">{{_('Attachment Size Limit')}}</label> <div data-related="email-settings-0">
<div class="form-group input-group"> {% endif %}
<input type="number" min="1" max="600" step="1" class="form-control" name="mail_size" id="mail_size" value="{% if content.mail_size != None %}{{ (content.mail_size / 1024 / 1024)|int }}{% endif %}" required> <div class="form-group">
<span class="input-group-btn"> <label for="mail_server">{{_('SMTP Hostname')}}</label>
<button type="button" id="attachement_size" class="btn btn-default" disabled>MB</button> <input type="text" class="form-control" name="mail_server" id="mail_server" value="{{content.mail_server}}" required>
</span> </div>
<div class="form-group">
<label for="mail_port">{{_('SMTP Port')}}</label>
<input type="number" min="1" max="65535" step="1" class="form-control" name="mail_port" id="mail_port" value="{% if content.mail_port != None %}{{ content.mail_port }}{% endif %}" autocomplete="off" required>
</div>
<div class="form-group">
<label for="mail_use_ssl">{{_('Encryption')}}</label>
<select name="mail_use_ssl" id="mail_use_ssl" class="form-control">
<option value="0" {% if content.mail_use_ssl == 0 %}selected{% endif %}>{{ _('None') }}</option>
<option value="1" {% if content.mail_use_ssl == 1 %}selected{% endif %}>{{ _('STARTTLS') }}</option>
<option value="2" {% if content.mail_use_ssl == 2 %}selected{% endif %}>{{ _('SSL/TLS') }}</option>
</select>
</div>
<div class="form-group">
<label for="mail_login">{{_('SMTP Login')}}</label>
<input type="text" class="form-control" name="mail_login" id="mail_login" value="{{content.mail_login}}">
</div>
<div class="form-group">
<label for="mail_password">{{_('SMTP Password')}}</label>
<input type="password" class="form-control" name="mail_password" id="mail_password" value="{{content.mail_password}}">
</div>
<div class="form-group">
<label for="mail_from">{{_('From E-mail')}}</label>
<input type="text" class="form-control" name="mail_from" id="mail_from" value="{{content.mail_from}}" required>
</div>
<label for="mail_size">{{_('Attachment Size Limit')}}</label>
<div class="form-group input-group">
<input type="number" min="1" max="600" step="1" class="form-control" name="mail_size" id="mail_size" value="{% if content.mail_size != None %}{{ (content.mail_size / 1024 / 1024)|int }}{% endif %}" required>
<span class="input-group-btn">
<button type="button" id="attachement_size" class="btn btn-default" disabled>MB</button>
</span>
</div>
<button type="submit" name="submit" value="submit" class="btn btn-default">{{_('Save')}}</button>
<button type="submit" name="test" value="test" class="btn btn-default">{{_('Save and Send Test E-mail')}}</button>
{% if feature_support['gmail'] %}
</div> </div>
<button type="submit" name="submit" value="submit" class="btn btn-default">{{_('Save')}}</button> {% endif %}
<button type="submit" name="test" value="test" class="btn btn-default">{{_('Save and Send Test E-mail')}}</button> <a href="{{ url_for('admin.admin') }}" id="back" class="btn btn-default">{{_('Back')}}</a>
<a href="{{ url_for('admin.admin') }}" id="back" class="btn btn-default">{{_('Cancel')}}</a>
</form> </form>
{% if g.allow_registration %} {% if g.allow_registration %}
<div class="col-md-10 col-lg-6"> <div class="col-md-10 col-lg-6">

@ -84,4 +84,11 @@
<link rel="subsection" type="application/atom+xml;profile=opds-catalog" href="{{url_for(folder, book_id=entry.id)}}"/> <link rel="subsection" type="application/atom+xml;profile=opds-catalog" href="{{url_for(folder, book_id=entry.id)}}"/>
</entry> </entry>
{% endfor %} {% endfor %}
{% for entry in letterelements %}
<entry>
<title>{{entry['name']}}</title>
<id>{{ url_for(folder, book_id=entry['id']) }}</id>
<link rel="subsection" type="application/atom+xml;profile=opds-catalog" href="{{url_for(folder, book_id=entry['id'])}}"/>
</entry>
{% endfor %}
</feed> </feed>

@ -8,8 +8,8 @@
<button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button> <button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button>
{% endif %} {% endif %}
{% endif %} {% endif %}
<button id="desc" data-id="series" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button> <button id="asc" data-id="series" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button>
<button id="asc" data-id="series" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button> <button id="desc" data-id="series" data-order="{{ order }}" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button>
{% if charlist|length %} {% if charlist|length %}
<button id="all" class="btn btn-primary">{{_('All')}}</button> <button id="all" class="btn btn-primary">{{_('All')}}</button>
{% endif %} {% endif %}

@ -14,6 +14,13 @@
<name>{{instance}}</name> <name>{{instance}}</name>
<uri>https://github.com/janeczku/calibre-web</uri> <uri>https://github.com/janeczku/calibre-web</uri>
</author> </author>
<entry>
<title>{{_('Alphabetical Books')}}</title>
<link href="{{url_for('opds.feed_booksindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('opds.feed_booksindex')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books sorted alphabetically')}}</content>
</entry>
<entry> <entry>
<title>{{_('Hot Books')}}</title> <title>{{_('Hot Books')}}</title>
<link href="{{url_for('opds.feed_hot')}}" type="application/atom+xml;profile=opds-catalog"/> <link href="{{url_for('opds.feed_hot')}}" type="application/atom+xml;profile=opds-catalog"/>

@ -1,4 +1,4 @@
{% from 'modal_dialogs.html' import restrict_modal, delete_book, filechooser_modal, delete_confirm_modal %} {% from 'modal_dialogs.html' import restrict_modal, delete_book, filechooser_modal, delete_confirm_modal, change_confirm_modal %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ g.user.locale }}"> <html lang="{{ g.user.locale }}">
<head> <head>
@ -76,7 +76,7 @@
{% if g.user.role_admin() %} {% if g.user.role_admin() %}
<li><a id="top_admin" data-text="{{_('Settings')}}" href="{{url_for('admin.admin')}}"><span class="glyphicon glyphicon-dashboard"></span> <span class="hidden-sm">{{_('Admin')}}</span></a></li> <li><a id="top_admin" data-text="{{_('Settings')}}" href="{{url_for('admin.admin')}}"><span class="glyphicon glyphicon-dashboard"></span> <span class="hidden-sm">{{_('Admin')}}</span></a></li>
{% endif %} {% endif %}
<li><a id="top_user" data-text="{{_('Account')}}" href="{{url_for('web.profile')}}"><span class="glyphicon glyphicon-user"></span> <span class="hidden-sm">{{g.user.nickname}}</span></a></li> <li><a id="top_user" data-text="{{_('Account')}}" href="{{url_for('web.profile')}}"><span class="glyphicon glyphicon-user"></span> <span class="hidden-sm">{{g.user.name}}</span></a></li>
{% if not g.user.is_anonymous %} {% if not g.user.is_anonymous %}
<li><a id="logout" href="{{url_for('web.logout')}}"><span class="glyphicon glyphicon-log-out"></span> <span class="hidden-sm">{{_('Logout')}}</span></a></li> <li><a id="logout" href="{{url_for('web.logout')}}"><span class="glyphicon glyphicon-log-out"></span> <span class="hidden-sm">{{_('Logout')}}</span></a></li>
{% endif %} {% endif %}

@ -8,8 +8,8 @@
<button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button> <button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button>
{% endif %} {% endif %}
{% endif %} {% endif %}
<button id="desc" data-id="{{ data }}" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button> <button id="asc" data-order="{{ order }}" data-id="{{ data }}" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button>
<button id="asc" data-id="{{ data }}" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button> <button id="desc" data-id="{{ data }}" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button>
{% if charlist|length %} {% if charlist|length %}
<button id="all" class="btn btn-primary">{{_('All')}}</button> <button id="all" class="btn btn-primary">{{_('All')}}</button>
{% endif %} {% endif %}

@ -22,7 +22,7 @@
<form id="add_restriction" action="" method="POST"> <form id="add_restriction" action="" method="POST">
<div class="form-group required"> <div class="form-group required">
<label for="add_element">{{_('Add View Restriction')}}</label> <label for="add_element">{{_('Add View Restriction')}}</label>
<input type="text" class="form-control" name="add_element" id="add_element" > <input type="text" class="form-control" name="add_element" id="add_element">
</div> </div>
<div class="form-group required"> <div class="form-group required">
<input type="button" class="btn btn-default" value="{{_('Allow')}}" name="submit_allow" id="submit_allow" data-dismiss="static"> <input type="button" class="btn btn-default" value="{{_('Allow')}}" name="submit_allow" id="submit_allow" data-dismiss="static">
@ -108,13 +108,31 @@
<div class="modal-dialog modal-sm"> <div class="modal-dialog modal-sm">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header bg-danger text-center"> <div class="modal-header bg-danger text-center">
<span id="header"></span> <span id="header-GeneralDeleteModal"></span>
</div> </div>
<div class="modal-body text-center"> <div class="modal-body text-center">
<span id="text"></span> <span id="text-GeneralDeleteModal"></span>
<p></p> <p></p>
<button id="btnConfirmYes" type="button" class="btn btn btn-danger">{{_('Delete')}}</button> <button id="btnConfirmYes-GeneralDeleteModal" type="button" class="btn btn btn-danger">{{_('Delete')}}</button>
<button id="btnConfirmNo" type="button" class="btn btn-default">{{_('Cancel')}}</button> <button id="btnConfirmNo-GeneralDeleteModal" type="button" class="btn btn-default">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
{% endmacro %}
{% macro change_confirm_modal() %}
<div id="GeneralChangeModal" class="modal fade" role="Dialog">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header bg-info text-center">
<span id="header-GeneralChangeModal"></span>
</div>
<div class="modal-body text-center">
<span id="text-GeneralChangeModal"></span>
<p></p>
<button id="btnConfirmYes-GeneralChangeModal" type="button" class="btn btn btn-default">{{_('Ok')}}</button>
<button id="btnConfirmNo-GeneralChangeModal" type="button" class="btn btn-default">{{_('Cancel')}}</button>
</div> </div>
</div> </div>
</div> </div>

@ -5,8 +5,8 @@
<form method="POST" role="form"> <form method="POST" role="form">
{% if not config.config_register_email %} {% if not config.config_register_email %}
<div class="form-group required"> <div class="form-group required">
<label for="nickname">{{_('Username')}}</label> <label for="name">{{_('Username')}}</label>
<input type="text" class="form-control" id="nickname" name="nickname" placeholder="{{_('Choose a username')}}" required> <input type="text" class="form-control" id="name" name="name" placeholder="{{_('Choose a username')}}" required>
</div> </div>
{% endif %} {% endif %}
<div class="form-group required"> <div class="form-group required">

@ -4,10 +4,10 @@
<h1>{{title}}</h1> <h1>{{title}}</h1>
<form role="form" method="POST" autocomplete="off"> <form role="form" method="POST" autocomplete="off">
<div class="col-md-10 col-lg-8"> <div class="col-md-10 col-lg-8">
{% if new_user or ( g.user and content.nickname != "Guest" and g.user.role_admin() ) %} {% if new_user or ( g.user and content.name != "Guest" and g.user.role_admin() ) %}
<div class="form-group required"> <div class="form-group required">
<label for="nickname">{{_('Username')}}</label> <label for="name">{{_('Username')}}</label>
<input type="text" class="form-control" name="nickname" id="nickname" value="{{ content.nickname if content.nickname != None }}" autocomplete="off"> <input type="text" class="form-control" name="name" id="name" value="{{ content.name if content.name != None }}" autocomplete="off">
</div> </div>
{% endif %} {% endif %}
<div class="form-group"> <div class="form-group">

@ -0,0 +1,152 @@
{% extends "layout.html" %}
{% macro user_table_row(parameter, edit_text, show_text, validate, button=False, id=0) -%}
<th data-field="{{ parameter }}" id="{{ parameter }}"
data-name="{{ parameter }}"
data-visible="{{visiblility.get(parameter)}}"
data-editable-type="text"
data-editable-url="{{ url_for('admin.edit_list_user', param=parameter)}}"
data-editable-title="{{ edit_text }}"
data-edit="true"
{% if not button %}
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>
{% endif %}
{{ show_text }}
</th>
{%- endmacro %}
{% macro user_checkbox_row(parameter, array_field, show_text, element, value) -%}
<th data-name="{{array_field}}" data-field="{{parameter}}"
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>
<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>
{%- endmacro %}
{% macro user_select_languages(parameter, url, show_text, validate) -%}
<th data-field="{{ parameter }}" id="{{ parameter }}"
data-name="{{ parameter }}"
data-visible="{{visiblility.get(parameter)}}"
data-editable-type="select"
data-edit="true"
data-sortable="true"
data-editable-url="{{ url_for('admin.edit_list_user', param=parameter)}}"
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="">
<option value="all">{{ _('Show All') }}</option>
{% for language in languages %}
<option value="{{language.lang_code}}">{{language.name}}</option>
{% endfor %}
</select>
</div><br>
{{ show_text }}
</th>
{%- endmacro %}
{% macro user_select_translations(parameter, url, show_text, validate) -%}
<th data-field="{{ parameter }}" id="{{ parameter }}"
data-name="{{ parameter }}"
data-visible="{{visiblility.get(parameter)}}"
data-editable-type="select"
data-edit="true"
data-sortable="true"
data-editable-url="{{ url_for('admin.edit_list_user', param=parameter)}}"
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="">
<option value="None">{{_('Select...')}}</option>
{% for translation in translations %}
<option value="{{translation}}">{{translation.display_name|capitalize}}</option>
{% endfor %}
</select>
</div><br>
{{ show_text }}
</th>
{%- endmacro %}
{% 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">
{% endblock %}
{% block body %}
<h2 class="{{page}}">{{_(title)}}</h2>
<div class="col-xs-12 col-sm-12">
<div class="row">
<div class="btn btn-default disabled" id="user_delete_selection" aria-disabled="true">{{_('Remove Selections')}}</div>
</div>
</div>
<table id="user-table" class="table table-no-bordered table-striped"
data-url="{{url_for('admin.list_users')}}">
<thead>
<tr>
<th data-name="edit" data-buttontext="{{_('Edit User')}}" data-visible="{{visiblility.get('edit')}}" data-formatter="singleUserFormatter">{{_('Edit')}}</th>
<th data-name="state" data-field="state" data-checkbox="true" data-visible="{{visiblility.get('state')}}" data-sortable="true"></th>
<th data-name="id" data-field="id" id="id" data-visible="false" data-switchable="false"></th>
{{ user_table_row('name', _('Enter Username'), _('Username'), true) }}
{{ user_table_row('email', _('Enter E-mail Address'), _('E-mail Address'), true) }}
{{ 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_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", "viewer_role", _('View'), 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_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)}}
{{ user_checkbox_row("sidebar_view", "sidebar_author", _('Show author selection'), visiblility, sidebar_settings)}}
{{ user_checkbox_row("sidebar_view", "sidebar_best_rated", _('Show Top Rated Books'), visiblility, sidebar_settings)}}
{{ user_checkbox_row("sidebar_view", "sidebar_read_and_unread", _('Show random books'), visiblility, sidebar_settings)}}
{{ user_checkbox_row("sidebar_view", "sidebar_publisher", _('Show publisher selection'), visiblility, sidebar_settings)}}
{{ user_checkbox_row("sidebar_view", "sidebar_rating", _('Show ratings selection'), visiblility, sidebar_settings)}}
{{ user_checkbox_row("sidebar_view", "sidebar_format", _('Show file formats selection'), visiblility, sidebar_settings)}}
{{ user_checkbox_row("sidebar_view", "sidebar_archived", _('Show archived books'), visiblility, sidebar_settings)}}
{{ user_checkbox_row("sidebar_view", "sidebar_download", _('Show Downloaded Books'), visiblility, sidebar_settings)}}
{{ user_checkbox_row("sidebar_view", "sidebar_list", _('Show Books List'), visiblility, sidebar_settings)}}
<th data-align="right" data-formatter="UserActions" data-switchable="false"><div><div class="btn btn-default button_head disabled" aria-disabled="true">{{_('Delete User')}}</div></div><br>{{_('Delete User')}}</th>
</tr>
</thead>
</table>
{% endblock %}
{% block modal %}
{{ delete_confirm_modal() }}
{{ change_confirm_modal() }}
{{ restrict_modal() }}
{% endblock %}
{% block js %}
<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/table.js') }}"></script>
<script>
</script>
{% endblock %}

@ -1693,7 +1693,7 @@ msgstr "Speicherort der Calibre-Datenbank"
#: cps/templates/config_edit.html:29 #: cps/templates/config_edit.html:29
msgid "To activate serverside filepicker start Calibre-Web with -f option" msgid "To activate serverside filepicker start Calibre-Web with -f option"
msgstr "Calibre-Web mit -f Option starten um die serverseitige Dateiauswahl zu aktivieren" msgstr "Calibre-Web mit -f Option starten, um die serverseitige Dateiauswahl zu aktivieren"
#: cps/templates/config_edit.html:35 #: cps/templates/config_edit.html:35
msgid "Use Google Drive?" msgid "Use Google Drive?"

@ -31,19 +31,23 @@ from flask_login import AnonymousUserMixin, current_user
try: try:
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
oauth_support = True oauth_support = True
except ImportError: except ImportError as e:
# fails on flask-dance >1.3, due to renaming # fails on flask-dance >1.3, due to renaming
try: try:
from flask_dance.consumer.storage.sqla import OAuthConsumerMixin from flask_dance.consumer.storage.sqla import OAuthConsumerMixin
oauth_support = True oauth_support = True
except ImportError: except ImportError as e:
oauth_support = False oauth_support = False
from sqlalchemy import create_engine, exc, exists, event from sqlalchemy import create_engine, exc, exists, event, text
from sqlalchemy import Column, ForeignKey from sqlalchemy import Column, ForeignKey
from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float, JSON from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float, JSON
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
try:
# Compability with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import backref, relationship, sessionmaker, Session, scoped_session from sqlalchemy.orm import backref, relationship, sessionmaker, Session, scoped_session
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
@ -158,7 +162,7 @@ class UserBase:
# ToDo: Error message # ToDo: Error message
def __repr__(self): def __repr__(self):
return '<User %r>' % self.nickname return '<User %r>' % self.name
# Baseclass for Users in Calibre-Web, settings which are depending on certain users are stored here. It is derived from # Baseclass for Users in Calibre-Web, settings which are depending on certain users are stored here. It is derived from
@ -168,7 +172,7 @@ class User(UserBase, Base):
__table_args__ = {'sqlite_autoincrement': True} __table_args__ = {'sqlite_autoincrement': True}
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
nickname = Column(String(64), unique=True) name = Column(String(64), unique=True)
email = Column(String(120), unique=True, default="") email = Column(String(120), unique=True, default="")
role = Column(SmallInteger, default=constants.ROLE_USER) role = Column(SmallInteger, default=constants.ROLE_USER)
password = Column(String) password = Column(String)
@ -178,7 +182,6 @@ class User(UserBase, Base):
locale = Column(String(2), default="en") locale = Column(String(2), default="en")
sidebar_view = Column(Integer, default=1) sidebar_view = Column(Integer, default=1)
default_language = Column(String(3), default="all") default_language = Column(String(3), default="all")
mature_content = Column(Boolean, default=True)
denied_tags = Column(String, default="") denied_tags = Column(String, default="")
allowed_tags = Column(String, default="") allowed_tags = Column(String, default="")
denied_column_value = Column(String, default="") denied_column_value = Column(String, default="")
@ -214,13 +217,12 @@ class Anonymous(AnonymousUserMixin, UserBase):
def loadSettings(self): def loadSettings(self):
data = session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS)\ data = session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS)\
.first() # type: User .first() # type: User
self.nickname = data.nickname self.name = data.name
self.role = data.role self.role = data.role
self.id=data.id self.id=data.id
self.sidebar_view = data.sidebar_view self.sidebar_view = data.sidebar_view
self.default_language = data.default_language self.default_language = data.default_language
self.locale = data.locale self.locale = data.locale
# self.mature_content = data.mature_content
self.kindle_mail = data.kindle_mail self.kindle_mail = data.kindle_mail
self.denied_tags = data.denied_tags self.denied_tags = data.denied_tags
self.allowed_tags = data.allowed_tags self.allowed_tags = data.allowed_tags
@ -484,7 +486,7 @@ def migrate_registration_table(engine, session):
def migrate_guest_password(engine, session): def migrate_guest_password(engine, session):
try: try:
with engine.connect() as conn: with engine.connect() as conn:
conn.execute("UPDATE user SET password='' where nickname = 'Guest' and password !=''") conn.execute(text("UPDATE user SET password='' where name = 'Guest' and password !=''"))
session.commit() session.commit()
except exc.OperationalError: except exc.OperationalError:
print('Settings database is not writeable. Exiting...') print('Settings database is not writeable. Exiting...')
@ -590,37 +592,42 @@ def migrate_Database(session):
with engine.connect() as conn: with engine.connect() as conn:
conn.execute("ALTER TABLE user ADD column `view_settings` VARCHAR(10) DEFAULT '{}'") conn.execute("ALTER TABLE user ADD column `view_settings` VARCHAR(10) DEFAULT '{}'")
session.commit() session.commit()
if session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() \
is None:
create_anonymous_user(session)
try: try:
# check if one table with autoincrement is existing (should be user table) # check if name is in User table instead of nickname
with engine.connect() as conn: session.query(exists().where(User.name)).scalar()
conn.execute("SELECT COUNT(*) FROM sqlite_sequence WHERE name='user'")
except exc.OperationalError: except exc.OperationalError:
# Create new table user_id and copy contents of table user into it # Create new table user_id and copy contents of table user into it
with engine.connect() as conn: with engine.connect() as conn:
conn.execute("CREATE TABLE user_id (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," conn.execute(text("CREATE TABLE user_id (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
"nickname VARCHAR(64)," "name VARCHAR(64),"
"email VARCHAR(120)," "email VARCHAR(120),"
"role SMALLINT," "role SMALLINT,"
"password VARCHAR," "password VARCHAR,"
"kindle_mail VARCHAR(120)," "kindle_mail VARCHAR(120),"
"locale VARCHAR(2)," "locale VARCHAR(2),"
"sidebar_view INTEGER," "sidebar_view INTEGER,"
"default_language VARCHAR(3)," "default_language VARCHAR(3),"
"view_settings VARCHAR," "denied_tags VARCHAR,"
"UNIQUE (nickname)," "allowed_tags VARCHAR,"
"UNIQUE (email))") "denied_column_value VARCHAR,"
conn.execute("INSERT INTO user_id(id, nickname, email, role, password, kindle_mail,locale," "allowed_column_value VARCHAR,"
"sidebar_view, default_language, view_settings) " "view_settings JSON,"
"UNIQUE (name),"
"UNIQUE (email))"))
conn.execute(text("INSERT INTO user_id(id, name, email, role, password, kindle_mail,locale,"
"sidebar_view, default_language, denied_tags, allowed_tags, denied_column_value, "
"allowed_column_value, view_settings)"
"SELECT id, nickname, email, role, password, kindle_mail, locale," "SELECT id, nickname, email, role, password, kindle_mail, locale,"
"sidebar_view, default_language FROM user") "sidebar_view, default_language, denied_tags, allowed_tags, denied_column_value, "
"allowed_column_value, view_settings FROM user"))
# delete old user table and rename new user_id table to user: # delete old user table and rename new user_id table to user:
conn.execute("DROP TABLE user") conn.execute(text("DROP TABLE user"))
conn.execute("ALTER TABLE user_id RENAME TO user") conn.execute(text("ALTER TABLE user_id RENAME TO user"))
session.commit() session.commit()
if session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() \
is None:
create_anonymous_user(session)
migrate_guest_password(engine, session) migrate_guest_password(engine, session)
@ -656,7 +663,7 @@ def delete_download(book_id):
# Generate user Guest (translated text), as anonymous user, no rights # Generate user Guest (translated text), as anonymous user, no rights
def create_anonymous_user(session): def create_anonymous_user(session):
user = User() user = User()
user.nickname = "Guest" user.name = "Guest"
user.email = 'no@email' user.email = 'no@email'
user.role = constants.ROLE_ANONYMOUS user.role = constants.ROLE_ANONYMOUS
user.password = '' user.password = ''
@ -671,7 +678,7 @@ def create_anonymous_user(session):
# Generate User admin with admin123 password, and access to everything # Generate User admin with admin123 password, and access to everything
def create_admin_user(session): def create_admin_user(session):
user = User() user = User()
user.nickname = "admin" user.name = "admin"
user.role = constants.ADMIN_USER_ROLES user.role = constants.ADMIN_USER_ROLES
user.sidebar_view = constants.ADMIN_USER_SIDEBAR user.sidebar_view = constants.ADMIN_USER_SIDEBAR
@ -707,7 +714,7 @@ def init_db(app_db_path):
if cli.user_credentials: if cli.user_credentials:
username, password = cli.user_credentials.split(':') username, password = cli.user_credentials.split(':')
user = session.query(User).filter(func.lower(User.nickname) == username.lower()).first() user = session.query(User).filter(func.lower(User.name) == username.lower()).first()
if user: if user:
user.password = generate_password_hash(password) user.password = generate_password_hash(password)
if session_commit() == "": if session_commit() == "":

@ -227,11 +227,17 @@ class Updater(threading.Thread):
os.sep + 'vendor', os.sep + 'calibre-web.log', os.sep + '.git', os.sep + 'client_secrets.json', os.sep + 'vendor', os.sep + 'calibre-web.log', os.sep + '.git', os.sep + 'client_secrets.json',
os.sep + 'gdrive_credentials', os.sep + 'settings.yaml', os.sep + 'venv', os.sep + 'virtualenv', os.sep + 'gdrive_credentials', os.sep + 'settings.yaml', os.sep + 'venv', os.sep + 'virtualenv',
os.sep + 'access.log', os.sep + 'access.log1', os.sep + 'access.log2', os.sep + 'access.log', os.sep + 'access.log1', os.sep + 'access.log2',
os.sep + '.calibre-web.log.swp', os.sep + '_sqlite3.so' os.sep + '.calibre-web.log.swp', os.sep + '_sqlite3.so', os.sep + 'cps' + os.sep + '.HOMEDIR',
os.sep + 'gmail.json'
) )
additional_path = self.is_venv() additional_path = self.is_venv()
if additional_path: if additional_path:
exclude = exclude + (additional_path,) exclude = exclude + (additional_path,)
# check if we are in a package, rename cps.py to __init__.py
if constants.HOME_CONFIG:
shutil.move(os.path.join(source, 'cps.py'), os.path.join(source, '__init__.py'))
for root, dirs, files in os.walk(destination, topdown=True): for root, dirs, files in os.walk(destination, topdown=True):
for name in files: for name in files:
old_list.append(os.path.join(root, name).replace(destination, '')) old_list.append(os.path.join(root, name).replace(destination, ''))
@ -398,6 +404,52 @@ class Updater(threading.Thread):
return json.dumps(status) return json.dumps(status)
return '' return ''
def _stable_updater_set_status(self, i, newer, status, parents, commit):
if i == -1 and newer == False:
status.update({
'update': True,
'success': True,
'message': _(
u'Click on the button below to update to the latest stable version.'),
'history': parents
})
self.updateFile = commit[0]['zipball_url']
elif i == -1 and newer == True:
status.update({
'update': True,
'success': True,
'message': _(u'A new update is available. Click on the button below to '
u'update to version: %(version)s', version=commit[0]['tag_name']),
'history': parents
})
self.updateFile = commit[0]['zipball_url']
return status
def _stable_updater_parse_major_version(self, commit, i, parents, current_version, status):
if int(commit[i + 1]['tag_name'].split('.')[1]) == current_version[1]:
parents.append([commit[i]['tag_name'],
commit[i]['body'].replace('\r\n', '<p>').replace('\n', '<p>')])
status.update({
'update': True,
'success': True,
'message': _(u'A new update is available. Click on the button below to '
u'update to version: %(version)s', version=commit[i]['tag_name']),
'history': parents
})
self.updateFile = commit[i]['zipball_url']
else:
parents.append([commit[i + 1]['tag_name'],
commit[i + 1]['body'].replace('\r\n', '<p>').replace('\n', '<p>')])
status.update({
'update': True,
'success': True,
'message': _(u'A new update is available. Click on the button below to '
u'update to version: %(version)s', version=commit[i + 1]['tag_name']),
'history': parents
})
self.updateFile = commit[i + 1]['zipball_url']
return status, parents
def _stable_available_updates(self, request_method): def _stable_available_updates(self, request_method):
if request_method == "GET": if request_method == "GET":
parents = [] parents = []
@ -459,48 +511,14 @@ class Updater(threading.Thread):
# before major update # before major update
if i == (len(commit) - 1): if i == (len(commit) - 1):
i -= 1 i -= 1
if int(commit[i+1]['tag_name'].split('.')[1]) == current_version[1]: status, parents = self._stable_updater_parse_major_version(commit,
parents.append([commit[i]['tag_name'], i,
commit[i]['body'].replace('\r\n', '<p>').replace('\n', '<p>')]) parents,
status.update({ current_version,
'update': True, status)
'success': True,
'message': _(u'A new update is available. Click on the button below to '
u'update to version: %(version)s', version=commit[i]['tag_name']),
'history': parents
})
self.updateFile = commit[i]['zipball_url']
else:
parents.append([commit[i+1]['tag_name'],
commit[i+1]['body'].replace('\r\n', '<p>').replace('\n', '<p>')])
status.update({
'update': True,
'success': True,
'message': _(u'A new update is available. Click on the button below to '
u'update to version: %(version)s', version=commit[i+1]['tag_name']),
'history': parents
})
self.updateFile = commit[i+1]['zipball_url']
break break
if i == -1 and newer == False:
status.update({
'update': True,
'success': True,
'message': _(
u'Click on the button below to update to the latest stable version.'),
'history': parents
})
self.updateFile = commit[0]['zipball_url']
elif i == -1 and newer == True:
status.update({
'update': True,
'success': True,
'message': _(u'A new update is available. Click on the button below to '
u'update to version: %(version)s', version=commit[0]['tag_name']),
'history': parents
})
self.updateFile = commit[0]['zipball_url']
status = self._stable_updater_set_status(i, newer, status, parents, commit)
return json.dumps(status) return json.dumps(status)
def _get_request_path(self): def _get_request_path(self):

@ -117,8 +117,8 @@ def parse_xmp(pdf_file):
""" """
try: try:
xmp_info = pdf_file.getXmpMetadata() xmp_info = pdf_file.getXmpMetadata()
except Exception as e: except Exception as ex:
log.debug('Can not read XMP metadata %e', e) log.debug('Can not read XMP metadata {}'.format(ex))
return None return None
if xmp_info: if xmp_info:
@ -162,8 +162,8 @@ def parse_xmp(pdf_file):
""" """
try: try:
xmp_info = pdf_file.getXmpMetadata() xmp_info = pdf_file.getXmpMetadata()
except Exception as e: except Exception as ex:
log.debug('Can not read XMP metadata', e) log.debug('Can not read XMP metadata {}'.format(ex))
return None return None
if xmp_info: if xmp_info:

@ -41,7 +41,7 @@ def login_required_if_no_ano(func):
def _fetch_user_by_name(username): def _fetch_user_by_name(username):
return ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first() return ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).first()
@lm.user_loader @lm.user_loader

@ -24,7 +24,6 @@ from __future__ import division, print_function, unicode_literals
import os import os
from datetime import datetime from datetime import datetime
import json import json
import re
import mimetypes import mimetypes
import chardet # dependency of requests import chardet # dependency of requests
@ -50,9 +49,9 @@ from . import constants, logger, isoLanguages, services
from . import babel, db, ub, config, get_locale, app from . import babel, db, ub, config, get_locale, app
from . import calibre_db from . import calibre_db
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
from .helper import check_valid_domain, render_task_status, \ from .helper import check_valid_domain, render_task_status, check_email, check_username, \
get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \ get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \
send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password, valid_email
from .pagination import Pagination from .pagination import Pagination
from .redirect import redirect_back from .redirect import redirect_back
from .usermanagement import login_required_if_no_ano from .usermanagement import login_required_if_no_ano
@ -215,8 +214,8 @@ def update_view():
for element in to_save: for element in to_save:
for param in to_save[element]: for param in to_save[element]:
current_user.set_view_property(element, param, to_save[element][param]) current_user.set_view_property(element, param, to_save[element][param])
except Exception as e: except Exception as ex:
log.error("Could not save view_settings: %r %r: %e", request, to_save, e) log.error("Could not save view_settings: %r %r: %e", request, to_save, ex)
return "Invalid request", 400 return "Invalid request", 400
return "1", 200 return "1", 200
@ -371,7 +370,6 @@ def get_sort_function(sort, data):
def render_books_list(data, sort, book_id, page): def render_books_list(data, sort, book_id, page):
order = get_sort_function(sort, data) order = get_sort_function(sort, data)
if data == "rated": if data == "rated":
return render_rated_books(page, book_id, order=order) return render_rated_books(page, book_id, order=order)
elif data == "discover": elif data == "discover":
@ -383,7 +381,7 @@ def render_books_list(data, sort, book_id, page):
elif data == "hot": elif data == "hot":
return render_hot_books(page) return render_hot_books(page)
elif data == "download": elif data == "download":
return render_downloaded_books(page, order) return render_downloaded_books(page, order, book_id)
elif data == "author": elif data == "author":
return render_author_books(page, book_id, order) return render_author_books(page, book_id, order)
elif data == "publisher": elif data == "publisher":
@ -463,7 +461,11 @@ def render_hot_books(page):
abort(404) abort(404)
def render_downloaded_books(page, order): def render_downloaded_books(page, order, user_id):
if current_user.role_admin():
user_id = int(user_id)
else:
user_id = current_user.id
if current_user.check_visibility(constants.SIDEBAR_DOWNLOAD): if current_user.check_visibility(constants.SIDEBAR_DOWNLOAD):
if current_user.show_detail_random(): if current_user.show_detail_random():
random = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \ random = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \
@ -474,19 +476,20 @@ def render_downloaded_books(page, order):
entries, __, pagination = calibre_db.fill_indexpage(page, entries, __, pagination = calibre_db.fill_indexpage(page,
0, 0,
db.Books, db.Books,
ub.Downloads.user_id == int(current_user.id), ub.Downloads.user_id == user_id,
order, order,
ub.Downloads, db.Books.id == ub.Downloads.book_id) ub.Downloads, db.Books.id == ub.Downloads.book_id)
for book in entries: for book in entries:
if not calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \ if not calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \
.filter(db.Books.id == book.id).first(): .filter(db.Books.id == book.id).first():
ub.delete_download(book.id) ub.delete_download(book.id)
user = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
return render_title_template('index.html', return render_title_template('index.html',
random=random, random=random,
entries=entries, entries=entries,
pagination=pagination, pagination=pagination,
title=_(u"Downloaded books by %(user)s",user=current_user.nickname), id=user_id,
title=_(u"Downloaded books by %(user)s",user=user.name),
page="download") page="download")
else: else:
abort(404) abort(404)
@ -498,6 +501,7 @@ def render_author_books(page, author_id, order):
db.Books.authors.any(db.Authors.id == author_id), db.Books.authors.any(db.Authors.id == author_id),
[order[0], db.Series.name, db.Books.series_index], [order[0], db.Series.name, db.Books.series_index],
db.books_series_link, db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series) db.Series)
if entries is None or not len(entries): if entries is None or not len(entries):
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
@ -526,6 +530,7 @@ def render_publisher_books(page, book_id, order):
db.Books.publishers.any(db.Publishers.id == book_id), db.Books.publishers.any(db.Publishers.id == book_id),
[db.Series.name, order[0], db.Books.series_index], [db.Series.name, order[0], db.Books.series_index],
db.books_series_link, db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series) db.Series)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id, return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
title=_(u"Publisher: %(name)s", name=publisher.name), page="publisher") title=_(u"Publisher: %(name)s", name=publisher.name), page="publisher")
@ -579,7 +584,9 @@ def render_category_books(page, book_id, order):
db.Books, db.Books,
db.Books.tags.any(db.Tags.id == book_id), db.Books.tags.any(db.Tags.id == book_id),
[order[0], db.Series.name, db.Books.series_index], [order[0], db.Series.name, db.Books.series_index],
db.books_series_link, db.Series) db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id, return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
title=_(u"Category: %(name)s", name=name.name), page="category") title=_(u"Category: %(name)s", name=name.name), page="category")
else: else:
@ -799,8 +806,10 @@ def author_list():
if current_user.check_visibility(constants.SIDEBAR_AUTHOR): if current_user.check_visibility(constants.SIDEBAR_AUTHOR):
if current_user.get_view_property('author', 'dir') == 'desc': if current_user.get_view_property('author', 'dir') == 'desc':
order = db.Authors.sort.desc() order = db.Authors.sort.desc()
order_no = 0
else: else:
order = db.Authors.sort.asc() order = db.Authors.sort.asc()
order_no = 1
entries = calibre_db.session.query(db.Authors, func.count('books_authors_link.book').label('count')) \ entries = calibre_db.session.query(db.Authors, func.count('books_authors_link.book').label('count')) \
.join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters()) \ .join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_authors_link.author')).order_by(order).all() .group_by(text('books_authors_link.author')).order_by(order).all()
@ -810,7 +819,27 @@ def author_list():
for entry in entries: for entry in entries:
entry.Authors.name = entry.Authors.name.replace('|', ',') 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=entries, folder='web.books_list', charlist=charlist,
title=u"Authors", page="authorlist", data='author') title=u"Authors", page="authorlist", data='author', order=order_no)
else:
abort(404)
@web.route("/downloadlist")
@login_required_if_no_ano
def download_list():
if current_user.get_view_property('download', 'dir') == 'desc':
order = ub.User.name.desc()
order_no = 0
else:
order = ub.User.name.asc()
order_no = 1
if current_user.check_visibility(constants.SIDEBAR_DOWNLOAD) and current_user.role_admin():
entries = ub.session.query(ub.User, func.count(ub.Downloads.book_id).label('count'))\
.join(ub.Downloads).group_by(ub.Downloads.user_id).order_by(order).all()
charlist = ub.session.query(func.upper(func.substr(ub.User.name, 1, 1)).label('char')) \
.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS) \
.group_by(func.upper(func.substr(ub.User.name, 1, 1))).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
title=_(u"Downloads"), page="downloadlist", data="download", order=order_no)
else: else:
abort(404) abort(404)
@ -820,8 +849,10 @@ def author_list():
def publisher_list(): def publisher_list():
if current_user.get_view_property('publisher', 'dir') == 'desc': if current_user.get_view_property('publisher', 'dir') == 'desc':
order = db.Publishers.name.desc() order = db.Publishers.name.desc()
order_no = 0
else: else:
order = db.Publishers.name.asc() order = db.Publishers.name.asc()
order_no = 1
if current_user.check_visibility(constants.SIDEBAR_PUBLISHER): if current_user.check_visibility(constants.SIDEBAR_PUBLISHER):
entries = calibre_db.session.query(db.Publishers, func.count('books_publishers_link.book').label('count')) \ entries = calibre_db.session.query(db.Publishers, func.count('books_publishers_link.book').label('count')) \
.join(db.books_publishers_link).join(db.Books).filter(calibre_db.common_filters()) \ .join(db.books_publishers_link).join(db.Books).filter(calibre_db.common_filters()) \
@ -830,7 +861,7 @@ def publisher_list():
.join(db.books_publishers_link).join(db.Books).filter(calibre_db.common_filters()) \ .join(db.books_publishers_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(func.upper(func.substr(db.Publishers.name, 1, 1))).all() .group_by(func.upper(func.substr(db.Publishers.name, 1, 1))).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist, return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
title=_(u"Publishers"), page="publisherlist", data="publisher") title=_(u"Publishers"), page="publisherlist", data="publisher", order=order_no)
else: else:
abort(404) abort(404)
@ -841,8 +872,10 @@ def series_list():
if current_user.check_visibility(constants.SIDEBAR_SERIES): if current_user.check_visibility(constants.SIDEBAR_SERIES):
if current_user.get_view_property('series', 'dir') == 'desc': if current_user.get_view_property('series', 'dir') == 'desc':
order = db.Series.sort.desc() order = db.Series.sort.desc()
order_no = 0
else: else:
order = db.Series.sort.asc() order = db.Series.sort.asc()
order_no = 1
if current_user.get_view_property('series', 'series_view') == 'list': if current_user.get_view_property('series', 'series_view') == 'list':
entries = calibre_db.session.query(db.Series, func.count('books_series_link.book').label('count')) \ entries = calibre_db.session.query(db.Series, func.count('books_series_link.book').label('count')) \
.join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \ .join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \
@ -861,7 +894,8 @@ def series_list():
.group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all() .group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=charlist, return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=charlist,
title=_(u"Series"), page="serieslist", data="series", bodyClass="grid-view") title=_(u"Series"), page="serieslist", data="series", bodyClass="grid-view",
order=order_no)
else: else:
abort(404) abort(404)
@ -872,14 +906,16 @@ def ratings_list():
if current_user.check_visibility(constants.SIDEBAR_RATING): if current_user.check_visibility(constants.SIDEBAR_RATING):
if current_user.get_view_property('ratings', 'dir') == 'desc': if current_user.get_view_property('ratings', 'dir') == 'desc':
order = db.Ratings.rating.desc() order = db.Ratings.rating.desc()
order_no = 0
else: else:
order = db.Ratings.rating.asc() order = db.Ratings.rating.asc()
order_no = 1
entries = calibre_db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'), entries = calibre_db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'),
(db.Ratings.rating / 2).label('name')) \ (db.Ratings.rating / 2).label('name')) \
.join(db.books_ratings_link).join(db.Books).filter(calibre_db.common_filters()) \ .join(db.books_ratings_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_ratings_link.rating')).order_by(order).all() .group_by(text('books_ratings_link.rating')).order_by(order).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(), return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
title=_(u"Ratings list"), page="ratingslist", data="ratings") title=_(u"Ratings list"), page="ratingslist", data="ratings", order=order_no)
else: else:
abort(404) abort(404)
@ -890,15 +926,17 @@ def formats_list():
if current_user.check_visibility(constants.SIDEBAR_FORMAT): if current_user.check_visibility(constants.SIDEBAR_FORMAT):
if current_user.get_view_property('ratings', 'dir') == 'desc': if current_user.get_view_property('ratings', 'dir') == 'desc':
order = db.Data.format.desc() order = db.Data.format.desc()
order_no = 0
else: else:
order = db.Data.format.asc() order = db.Data.format.asc()
order_no = 1
entries = calibre_db.session.query(db.Data, entries = calibre_db.session.query(db.Data,
func.count('data.book').label('count'), func.count('data.book').label('count'),
db.Data.format.label('format')) \ db.Data.format.label('format')) \
.join(db.Books).filter(calibre_db.common_filters()) \ .join(db.Books).filter(calibre_db.common_filters()) \
.group_by(db.Data.format).order_by(order).all() .group_by(db.Data.format).order_by(order).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(), return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
title=_(u"File formats list"), page="formatslist", data="formats") title=_(u"File formats list"), page="formatslist", data="formats", order=order_no)
else: else:
abort(404) abort(404)
@ -938,8 +976,10 @@ def category_list():
if current_user.check_visibility(constants.SIDEBAR_CATEGORY): if current_user.check_visibility(constants.SIDEBAR_CATEGORY):
if current_user.get_view_property('category', 'dir') == 'desc': if current_user.get_view_property('category', 'dir') == 'desc':
order = db.Tags.name.desc() order = db.Tags.name.desc()
order_no = 0
else: else:
order = db.Tags.name.asc() order = db.Tags.name.asc()
order_no = 1
entries = calibre_db.session.query(db.Tags, func.count('books_tags_link.book').label('count')) \ entries = calibre_db.session.query(db.Tags, func.count('books_tags_link.book').label('count')) \
.join(db.books_tags_link).join(db.Books).order_by(order).filter(calibre_db.common_filters()) \ .join(db.books_tags_link).join(db.Books).order_by(order).filter(calibre_db.common_filters()) \
.group_by(text('books_tags_link.tag')).all() .group_by(text('books_tags_link.tag')).all()
@ -947,7 +987,7 @@ def category_list():
.join(db.books_tags_link).join(db.Books).filter(calibre_db.common_filters()) \ .join(db.books_tags_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(func.upper(func.substr(db.Tags.name, 1, 1))).all() .group_by(func.upper(func.substr(db.Tags.name, 1, 1))).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist, return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
title=_(u"Categories"), page="catlist", data="category") title=_(u"Categories"), page="catlist", data="category", order=order_no)
else: else:
abort(404) abort(404)
@ -1123,14 +1163,6 @@ def extend_search_term(searchterm,
searchterm.extend(tag.name for tag in tag_names) searchterm.extend(tag.name for tag in tag_names)
tag_names = calibre_db.session.query(db_element).filter(db.Tags.id.in_(tags['exclude_' + key])).all() tag_names = calibre_db.session.query(db_element).filter(db.Tags.id.in_(tags['exclude_' + key])).all()
searchterm.extend(tag.name for tag in tag_names) searchterm.extend(tag.name for tag in tag_names)
#serie_names = calibre_db.session.query(db.Series).filter(db.Series.id.in_(tags['include_serie'])).all()
#searchterm.extend(serie.name for serie in serie_names)
#serie_names = calibre_db.session.query(db.Series).filter(db.Series.id.in_(tags['include_serie'])).all()
#searchterm.extend(serie.name for serie in serie_names)
#shelf_names = ub.session.query(ub.Shelf).filter(ub.Shelf.id.in_(tags['include_shelf'])).all()
#searchterm.extend(shelf.name for shelf in shelf_names)
#shelf_names = ub.session.query(ub.Shelf).filter(ub.Shelf.id.in_(tags['include_shelf'])).all()
#searchterm.extend(shelf.name for shelf in shelf_names)
language_names = calibre_db.session.query(db.Languages). \ language_names = calibre_db.session.query(db.Languages). \
filter(db.Languages.id.in_(tags['include_language'])).all() filter(db.Languages.id.in_(tags['include_language'])).all()
if language_names: if language_names:
@ -1304,11 +1336,7 @@ def serve_book(book_id, book_format, anyname):
@login_required_if_no_ano @login_required_if_no_ano
@download_required @download_required
def download_link(book_id, book_format, anyname): def download_link(book_id, book_format, anyname):
if "Kobo" in request.headers.get('User-Agent'): client = "kobo" if "Kobo" in request.headers.get('User-Agent') else ""
client = "kobo"
else:
client=""
return get_download_link(book_id, book_format, client) return get_download_link(book_id, book_format, client)
@ -1320,7 +1348,7 @@ def send_to_kindle(book_id, book_format, convert):
flash(_(u"Please configure the SMTP mail settings first..."), category="error") flash(_(u"Please configure the SMTP mail settings first..."), category="error")
elif current_user.kindle_mail: elif current_user.kindle_mail:
result = send_mail(book_id, book_format, convert, current_user.kindle_mail, config.config_calibre_dir, result = send_mail(book_id, book_format, convert, current_user.kindle_mail, config.config_calibre_dir,
current_user.nickname) current_user.name)
if result is None: if result is None:
flash(_(u"Book successfully queued for sending to %(kindlemail)s", kindlemail=current_user.kindle_mail), flash(_(u"Book successfully queued for sending to %(kindlemail)s", kindlemail=current_user.kindle_mail),
category="success") category="success")
@ -1350,52 +1378,41 @@ def register():
if request.method == "POST": if request.method == "POST":
to_save = request.form.to_dict() to_save = request.form.to_dict()
if config.config_register_email: nickname = to_save["email"].strip() if config.config_register_email else to_save.get('name')
nickname = to_save["email"] if not nickname or not to_save.get("email"):
else:
nickname = to_save.get('nickname', None)
if not nickname or not to_save.get("email", None):
flash(_(u"Please fill out all fields!"), category="error") flash(_(u"Please fill out all fields!"), category="error")
return render_title_template('register.html', title=_(u"register"), page="register") return render_title_template('register.html', title=_(u"register"), page="register")
#if to_save["email"].count("@") != 1 or not \ try:
# Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation nickname = check_username(nickname)
if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$", email = check_email(to_save["email"])
to_save["email"]): except Exception as ex:
flash(_(u"Invalid e-mail address format"), category="error") flash(str(ex), category="error")
log.warning('Registering failed for user "%s" e-mail address: %s', nickname, to_save["email"])
return render_title_template('register.html', title=_(u"register"), page="register") return render_title_template('register.html', title=_(u"register"), page="register")
existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == nickname content = ub.User()
.lower()).first() if check_valid_domain(email):
existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()).first() content.name = nickname
if not existing_user and not existing_email: content.email = email
content = ub.User() password = generate_random_password()
if check_valid_domain(to_save["email"]): content.password = generate_password_hash(password)
content.nickname = nickname content.role = config.config_default_role
content.email = to_save["email"] content.sidebar_view = config.config_default_show
password = generate_random_password() try:
content.password = generate_password_hash(password) ub.session.add(content)
content.role = config.config_default_role ub.session.commit()
content.sidebar_view = config.config_default_show if feature_support['oauth']:
try: register_user_with_oauth(content)
ub.session.add(content) send_registration_mail(to_save["email"].strip(), nickname, password)
ub.session.commit() except Exception:
if feature_support['oauth']: ub.session.rollback()
register_user_with_oauth(content) flash(_(u"An unknown error occurred. Please try again later."), category="error")
send_registration_mail(to_save["email"], nickname, password)
except Exception:
ub.session.rollback()
flash(_(u"An unknown error occurred. Please try again later."), category="error")
return render_title_template('register.html', title=_(u"register"), page="register")
else:
flash(_(u"Your e-mail is not allowed to register"), category="error")
log.warning('Registering failed for user "%s" e-mail address: %s', nickname, to_save["email"])
return render_title_template('register.html', title=_(u"register"), page="register") return render_title_template('register.html', title=_(u"register"), page="register")
flash(_(u"Confirmation e-mail was send to your e-mail account."), category="success")
return redirect(url_for('web.login'))
else: else:
flash(_(u"This username or e-mail address is already in use."), category="error") flash(_(u"Your e-mail is not allowed to register"), category="error")
log.warning('Registering failed for user "%s" e-mail address: %s', nickname, to_save["email"])
return render_title_template('register.html', title=_(u"register"), page="register") return render_title_template('register.html', title=_(u"register"), page="register")
flash(_(u"Confirmation e-mail was send to your e-mail account."), category="success")
return redirect(url_for('web.login'))
if feature_support['oauth']: if feature_support['oauth']:
register_user_with_oauth() register_user_with_oauth()
@ -1414,22 +1431,22 @@ def login():
flash(_(u"Cannot activate LDAP authentication"), category="error") flash(_(u"Cannot activate LDAP authentication"), category="error")
if request.method == "POST": if request.method == "POST":
form = request.form.to_dict() form = request.form.to_dict()
user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == form['username'].strip().lower()) \ user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == form['username'].strip().lower()) \
.first() .first()
if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user and form['password'] != "": if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user and form['password'] != "":
login_result, error = services.ldap.bind_user(form['username'], form['password']) login_result, error = services.ldap.bind_user(form['username'], form['password'])
if login_result: if login_result:
login_user(user, remember=bool(form.get('remember_me'))) login_user(user, remember=bool(form.get('remember_me')))
log.debug(u"You are now logged in as: '%s'", user.nickname) log.debug(u"You are now logged in as: '%s'", user.name)
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name),
category="success") category="success")
return redirect_back(url_for("web.index")) return redirect_back(url_for("web.index"))
elif login_result is None and user and check_password_hash(str(user.password), form['password']) \ elif login_result is None and user and check_password_hash(str(user.password), form['password']) \
and user.nickname != "Guest": and user.name != "Guest":
login_user(user, remember=bool(form.get('remember_me'))) login_user(user, remember=bool(form.get('remember_me')))
log.info("Local Fallback Login as: '%s'", user.nickname) log.info("Local Fallback Login as: '%s'", user.name)
flash(_(u"Fallback Login as: '%(nickname)s', LDAP Server not reachable, or user not known", flash(_(u"Fallback Login as: '%(nickname)s', LDAP Server not reachable, or user not known",
nickname=user.nickname), nickname=user.name),
category="warning") category="warning")
return redirect_back(url_for("web.index")) return redirect_back(url_for("web.index"))
elif login_result is None: elif login_result is None:
@ -1442,7 +1459,7 @@ def login():
else: else:
ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr) ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr)
if 'forgot' in form and form['forgot'] == 'forgot': if 'forgot' in form and form['forgot'] == 'forgot':
if user != None and user.nickname != "Guest": if user != None and user.name != "Guest":
ret, __ = reset_password(user.id) ret, __ = reset_password(user.id)
if ret == 1: if ret == 1:
flash(_(u"New Password was send to your email address"), category="info") flash(_(u"New Password was send to your email address"), category="info")
@ -1454,10 +1471,10 @@ def login():
flash(_(u"Please enter valid username to reset password"), category="error") 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', ipAdress)
else: else:
if user and check_password_hash(str(user.password), form['password']) and user.nickname != "Guest": if user and check_password_hash(str(user.password), form['password']) and user.name != "Guest":
login_user(user, remember=bool(form.get('remember_me'))) login_user(user, remember=bool(form.get('remember_me')))
log.debug(u"You are now logged in as: '%s'", user.nickname) log.debug(u"You are now logged in as: '%s'", user.name)
flash(_(u"You are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success") flash(_(u"You are now logged in as: '%(nickname)s'", nickname=user.name), category="success")
config.config_is_initial = False config.config_is_initial = False
return redirect_back(url_for("web.index")) return redirect_back(url_for("web.index"))
else: else:
@ -1486,63 +1503,41 @@ def logout():
return redirect(url_for('web.login')) return redirect(url_for('web.login'))
# ################################### Users own configuration ######################################################### # ################################### Users own configuration #########################################################
def change_profile_email(to_save, kobo_support, local_oauth_check, oauth_status):
if "email" in to_save and to_save["email"] != current_user.email:
if config.config_public_reg and not check_valid_domain(to_save["email"]):
flash(_(u"E-mail is not from valid domain"), category="error")
return render_title_template("user_edit.html", content=current_user,
title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
kobo_support=kobo_support,
registered_oauth=local_oauth_check, oauth_status=oauth_status)
current_user.email = to_save["email"]
def change_profile_nickname(to_save, kobo_support, local_oauth_check, translations, languages):
if "nickname" in to_save and to_save["nickname"] != current_user.nickname:
# Query User nickname, if not existing, change
if not ub.session.query(ub.User).filter(ub.User.nickname == to_save["nickname"]).scalar():
current_user.nickname = to_save["nickname"]
else:
flash(_(u"This username is already taken"), category="error")
return render_title_template("user_edit.html",
translations=translations,
languages=languages,
kobo_support=kobo_support,
new_user=0, content=current_user,
registered_oauth=local_oauth_check,
title=_(u"Edit User %(nick)s",
nick=current_user.nickname),
page="edituser")
def change_profile(kobo_support, local_oauth_check, oauth_status, translations, languages): def change_profile(kobo_support, local_oauth_check, oauth_status, translations, languages):
to_save = request.form.to_dict() to_save = request.form.to_dict()
current_user.random_books = 0 current_user.random_books = 0
if current_user.role_passwd() or current_user.role_admin(): if current_user.role_passwd() or current_user.role_admin():
if "password" in to_save and to_save["password"]: if to_save.get("password"):
current_user.password = generate_password_hash(to_save["password"]) current_user.password = generate_password_hash(to_save["password"])
if "kindle_mail" in to_save and to_save["kindle_mail"] != current_user.kindle_mail: try:
current_user.kindle_mail = to_save["kindle_mail"] if to_save.get("allowed_tags", current_user.allowed_tags) != current_user.allowed_tags:
if "allowed_tags" in to_save and to_save["allowed_tags"] != current_user.allowed_tags: current_user.allowed_tags = to_save["allowed_tags"].strip()
current_user.allowed_tags = to_save["allowed_tags"].strip() if to_save.get("kindle_mail", current_user.kindle_mail) != current_user.kindle_mail:
change_profile_email(to_save, kobo_support, local_oauth_check, oauth_status) current_user.kindle_mail = valid_email(to_save["kindle_mail"])
change_profile_nickname(to_save, kobo_support, local_oauth_check, translations, languages) if to_save.get("email", current_user.email) != current_user.email:
if "show_random" in to_save and to_save["show_random"] == "on": current_user.email = check_email(to_save["email"])
current_user.random_books = 1 if to_save.get("name", current_user.name) != current_user.name:
if "default_language" in to_save: # Query User name, if not existing, change
current_user.default_language = to_save["default_language"] current_user.name = check_username(to_save["name"])
if "locale" in to_save: current_user.random_books = 1 if to_save.get("show_random") == "on" else 0
current_user.locale = to_save["locale"] if to_save.get("default_language"):
current_user.default_language = to_save["default_language"]
if to_save.get("locale"):
current_user.locale = to_save["locale"]
except Exception as ex:
flash(str(ex), category="error")
return render_title_template("user_edit.html", content=current_user,
title=_(u"%(name)s's profile", name=current_user.name), page="me",
kobo_support=kobo_support,
registered_oauth=local_oauth_check, oauth_status=oauth_status)
val = 0 val = 0
for key, __ in to_save.items(): for key, __ in to_save.items():
if key.startswith('show'): if key.startswith('show'):
val += int(key[5:]) val += int(key[5:])
current_user.sidebar_view = val current_user.sidebar_view = val
if "Show_detail_random" in to_save: if to_save.get("Show_detail_random"):
current_user.sidebar_view += constants.DETAIL_RANDOM current_user.sidebar_view += constants.DETAIL_RANDOM
try: try:
@ -1580,7 +1575,7 @@ def profile():
languages=languages, languages=languages,
content=current_user, content=current_user,
kobo_support=kobo_support, kobo_support=kobo_support,
title=_(u"%(name)s's profile", name=current_user.nickname), title=_(u"%(name)s's profile", name=current_user.name),
page="me", page="me",
registered_oauth=local_oauth_check, registered_oauth=local_oauth_check,
oauth_status=oauth_status) oauth_status=oauth_status)

@ -1,5 +1,4 @@
# GDrive Integration # GDrive Integration
google-api-python-client>=1.7.11,<1.13.0
gevent>20.6.0,<21.2.0 gevent>20.6.0,<21.2.0
greenlet>=0.4.17,<1.1.0 greenlet>=0.4.17,<1.1.0
httplib2>=0.9.2,<0.18.0 httplib2>=0.9.2,<0.18.0
@ -12,6 +11,12 @@ PyYAML>=3.12
rsa>=3.4.2,<4.1.0 rsa>=3.4.2,<4.1.0
six>=1.10.0,<1.15.0 six>=1.10.0,<1.15.0
# Gdrive and Gmail integration
google-api-python-client>=1.7.11,<2.1.0
# Gmail
google-auth-oauthlib>=0.4.3,<0.5.0
# goodreads # goodreads
goodreads>=0.3.2,<0.4.0 goodreads>=0.3.2,<0.4.0
python-Levenshtein>=0.12.0,<0.13.0 python-Levenshtein>=0.12.0,<0.13.0

@ -9,7 +9,7 @@ iso-639>=0.4.5,<0.5.0
PyPDF3>=1.0.0,<1.0.4 PyPDF3>=1.0.0,<1.0.4
pytz>=2016.10 pytz>=2016.10
requests>=2.11.1,<2.25.0 requests>=2.11.1,<2.25.0
SQLAlchemy>=1.3.0,<1.4.0 SQLAlchemy>=1.3.0,<1.4.0 # oauth fails on 1.4+ due to import problems in flask_dance
tornado>=4.1,<6.2 tornado>=4.1,<6.2
Wand>=0.4.4,<0.7.0 Wand>=0.4.4,<0.7.0
unidecode>=0.04.19,<1.2.0 unidecode>=0.04.19,<1.2.0

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save