Merge remote-tracking branch 'upstream/master' into master

pull/1759/head
mmonkey 3 years ago
commit bc8bdfe385

@ -41,6 +41,8 @@ from cps.shelf import shelf
from cps.admin import admi
from cps.gdrive import gdrive
from cps.editbooks import editbook
from cps.remotelogin import remotelogin
from cps.error_handler import init_errorhandler
try:
from cps.kobo import kobo, get_kobo_activated
@ -58,14 +60,18 @@ except ImportError:
def main():
app = create_app()
init_errorhandler()
app.register_blueprint(web)
app.register_blueprint(opds)
app.register_blueprint(jinjia)
app.register_blueprint(about)
app.register_blueprint(shelf)
app.register_blueprint(admi)
if config.config_use_google_drive:
app.register_blueprint(gdrive)
app.register_blueprint(remotelogin)
# if config.config_use_google_drive:
app.register_blueprint(gdrive)
app.register_blueprint(editbook)
if kobo_available:
app.register_blueprint(kobo)

@ -94,7 +94,8 @@ def create_app():
app.root_path = app.root_path.decode('utf-8')
app.instance_path = app.instance_path.decode('utf-8')
cache_buster.init_cache_busting(app)
if os.environ.get('FLASK_DEBUG'):
cache_buster.init_cache_busting(app)
log.info('Starting Calibre Web...')
Principal(app)

@ -31,7 +31,7 @@ import werkzeug, flask, flask_login, flask_principal, jinja2
from flask_babel import gettext as _
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants
from .web import render_title_template
from .render_template import render_title_template
try:
from flask_login import __version__ as flask_loginVersion
except ImportError:

@ -5,7 +5,7 @@
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
# apetresc, nanu-c, mutschler
# apetresc, nanu-c, mutschler, GammaC0de, vuolter
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -26,24 +26,31 @@ import re
import base64
import json
import time
import operator
from datetime import datetime, timedelta
from babel import Locale as LC
from babel.dates import format_datetime
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory
from flask_login import login_required, current_user, logout_user
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g
from flask_login import login_required, current_user, logout_user, confirm_login
from flask_babel import gettext as _
from sqlalchemy import and_
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
from sqlalchemy.sql.expression import func
from sqlalchemy.sql.expression import func, or_
from . import constants, logger, helper, services
from .cli import filepicker
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 .gdriveutils import is_gdrive_ready, gdrive_support
from .web import admin_required, render_title_template, before_request, unconfigured
from .render_template import render_title_template, get_sidebar_config
from . import debug_info
try:
from functools import wraps
except ImportError:
pass # We're not using Python 3
log = logger.create()
feature_support = {
@ -72,6 +79,49 @@ feature_support['gdrive'] = gdrive_support
admi = Blueprint('admin', __name__)
def admin_required(f):
"""
Checks if current_user.role == 1
"""
@wraps(f)
def inner(*args, **kwargs):
if current_user.role_admin():
return f(*args, **kwargs)
abort(403)
return inner
def unconfigured(f):
"""
Checks if calibre-web instance is not configured
"""
@wraps(f)
def inner(*args, **kwargs):
if not config.db_configured:
return f(*args, **kwargs)
abort(403)
return inner
@admi.before_app_request
def before_request():
if current_user.is_authenticated:
confirm_login()
g.constants = constants
g.user = current_user
g.allow_registration = config.config_public_reg
g.allow_anonymous = config.config_anonbrowse
g.allow_upload = config.config_uploading
g.current_theme = config.config_theme
g.config_authors_max = config.config_authors_max
g.shelves_access = ub.session.query(ub.Shelf).filter(
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
if not config.db_configured and request.endpoint not in (
'admin.basic_configuration', 'login', 'admin.config_pathchooser') and '/static/' not in request.path:
return redirect(url_for('admin.basic_configuration'))
@admi.route("/admin")
@login_required
@ -143,7 +193,7 @@ def admin():
@admin_required
def configuration():
if request.method == "POST":
return _configuration_update_helper()
return _configuration_update_helper(True)
return _configuration_result()
@ -195,6 +245,21 @@ def update_view_configuration():
return view_configuration()
@admi.route("/ajax/loaddialogtexts/<element_id>")
@login_required
def load_dialogtexts(element_id):
texts = { "header": "", "main": "" }
if element_id == "config_delete_kobo_token":
texts["main"] = _('Do you really want to delete the Kobo Token?')
elif element_id == "btndeletedomain":
texts["main"] = _('Do you really want to delete this domain?')
elif element_id == "btndeluser":
texts["main"] = _('Do you really want to delete this user?')
elif element_id == "delete_shelf":
texts["main"] = _('Are you sure you want to delete this shelf?')
return json.dumps(texts)
@admi.route("/ajax/editdomain/<int:allow>", methods=['POST'])
@login_required
@admin_required
@ -206,7 +271,10 @@ def edit_domain(allow):
vals = request.form.to_dict()
answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first()
answer.domain = vals['value'].replace('*', '%').replace('?', '_').lower()
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return ""
@ -220,7 +288,10 @@ def add_domain(allow):
if not check:
new_domain = ub.Registration(domain=domain_name, allow=allow)
ub.session.add(new_domain)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return ""
@ -228,14 +299,23 @@ def add_domain(allow):
@login_required
@admin_required
def delete_domain():
domain_id = request.form.to_dict()['domainid'].replace('*', '%').replace('?', '_').lower()
ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete()
ub.session.commit()
# If last domain was deleted, add all domains by default
if not ub.session.query(ub.Registration).filter(ub.Registration.allow==1).count():
new_domain = ub.Registration(domain="%.%",allow=1)
ub.session.add(new_domain)
ub.session.commit()
try:
domain_id = request.form.to_dict()['domainid'].replace('*', '%').replace('?', '_').lower()
ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
# If last domain was deleted, add all domains by default
if not ub.session.query(ub.Registration).filter(ub.Registration.allow==1).count():
new_domain = ub.Registration(domain="%.%",allow=1)
ub.session.add(new_domain)
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
except KeyError:
pass
return ""
@ -250,10 +330,11 @@ def list_domain(allow):
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@admi.route("/ajax/editrestriction/<int:res_type>", methods=['POST'])
@admi.route("/ajax/editrestriction/<int:res_type>", defaults={"user_id":0}, methods=['POST'])
@admi.route("/ajax/editrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
@login_required
@admin_required
def edit_restriction(res_type):
def edit_restriction(res_type, user_id):
element = request.form.to_dict()
if element['id'].startswith('a'):
if res_type == 0: # Tags as template
@ -267,25 +348,29 @@ def edit_restriction(res_type):
config.config_allowed_column_value = ','.join(elementlist)
config.save()
if res_type == 2: # Tags per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
if isinstance(user_id, int):
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
else:
usr = current_user
elementlist = usr.list_allowed_tags()
elementlist[int(element['id'][1:])]=element['Element']
usr.allowed_tags = ','.join(elementlist)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
if res_type == 3: # CColumn per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
if isinstance(user_id, int):
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
else:
usr = current_user
elementlist = usr.list_allowed_column_values()
elementlist[int(element['id'][1:])]=element['Element']
usr.allowed_column_value = ','.join(elementlist)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
if element['id'].startswith('d'):
if res_type == 0: # Tags as template
elementlist = config.list_denied_tags()
@ -298,25 +383,29 @@ def edit_restriction(res_type):
config.config_denied_column_value = ','.join(elementlist)
config.save()
if res_type == 2: # Tags per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
if isinstance(user_id, int):
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
else:
usr = current_user
elementlist = usr.list_denied_tags()
elementlist[int(element['id'][1:])]=element['Element']
usr.denied_tags = ','.join(elementlist)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
if res_type == 3: # CColumn per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
if isinstance(user_id, int):
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
else:
usr = current_user
elementlist = usr.list_denied_column_values()
elementlist[int(element['id'][1:])]=element['Element']
usr.denied_column_value = ','.join(elementlist)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return ""
def restriction_addition(element, list_func):
@ -335,10 +424,11 @@ def restriction_deletion(element, list_func):
return ','.join(elementlist)
@admi.route("/ajax/addrestriction/<int:res_type>", methods=['POST'])
@admi.route("/ajax/addrestriction/<int:res_type>", defaults={"user_id":0}, methods=['POST'])
@admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
@login_required
@admin_required
def add_restriction(res_type):
def add_restriction(res_type, user_id):
element = request.form.to_dict()
if res_type == 0: # Tags as template
if 'submit_allow' in element:
@ -355,35 +445,46 @@ def add_restriction(res_type):
config.config_denied_column_value = restriction_addition(element, config.list_allowed_column_values)
config.save()
if res_type == 2: # Tags per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
if isinstance(user_id, int):
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
else:
usr = current_user
if 'submit_allow' in element:
usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
elif 'submit_deny' in element:
usr.denied_tags = restriction_addition(element, usr.list_denied_tags)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
if res_type == 3: # CustomC per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
if isinstance(user_id, int):
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
else:
usr = current_user
if 'submit_allow' in element:
usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
elif 'submit_deny' in element:
usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return ""
@admi.route("/ajax/deleterestriction/<int:res_type>", methods=['POST'])
@admi.route("/ajax/deleterestriction/<int:res_type>", defaults={"user_id":0}, methods=['POST'])
@admi.route("/ajax/deleterestriction/<int:res_type>/<int:user_id>", methods=['POST'])
@login_required
@admin_required
def delete_restriction(res_type):
def delete_restriction(res_type, user_id):
element = request.form.to_dict()
if res_type == 0: # Tags as template
if element['id'].startswith('a'):
@ -400,36 +501,46 @@ def delete_restriction(res_type):
config.config_denied_column_value = restriction_deletion(element, config.list_denied_column_values)
config.save()
elif res_type == 2: # Tags per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
if isinstance(user_id, int):
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
else:
usr = current_user
if element['id'].startswith('a'):
usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
elif element['id'].startswith('d'):
usr.denied_tags = restriction_deletion(element, usr.list_denied_tags)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
elif res_type == 3: # Columns per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True: # select current user if admins are editing their own rights
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
if isinstance(user_id, int):
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
else:
usr = current_user
if element['id'].startswith('a'):
usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
elif element['id'].startswith('d'):
usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return ""
@admi.route("/ajax/listrestriction/<int:res_type>")
@admi.route("/ajax/listrestriction/<int:res_type>", defaults={"user_id":0})
@admi.route("/ajax/listrestriction/<int:res_type>/<int:user_id>")
@login_required
@admin_required
def list_restriction(res_type):
def list_restriction(res_type, user_id):
if res_type == 0: # Tags as template
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
for i,x in enumerate(config.list_denied_tags()) if x != '' ]
@ -443,9 +554,8 @@ def list_restriction(res_type):
for i,x in enumerate(config.list_allowed_column_values()) if x != '']
json_dumps = restrict + allow
elif res_type == 2: # Tags per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id == usr_id).first()
if isinstance(user_id, int):
usr = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
else:
usr = current_user
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
@ -454,9 +564,8 @@ def list_restriction(res_type):
for i,x in enumerate(usr.list_allowed_tags()) if x != '']
json_dumps = restrict + allow
elif res_type == 3: # CustomC per user
usr_id = os.path.split(request.referrer)[-1]
if usr_id.isdigit() == True:
usr = ub.session.query(ub.User).filter(ub.User.id==usr_id).first()
if isinstance(user_id, int):
usr = ub.session.query(ub.User).filter(ub.User.id==user_id).first()
else:
usr = current_user
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
@ -471,14 +580,108 @@ def list_restriction(res_type):
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@admi.route("/basicconfig/pathchooser/")
@unconfigured
def config_pathchooser():
if filepicker:
return pathchooser()
abort(403)
@admi.route("/ajax/pathchooser/")
@login_required
@admin_required
def ajax_pathchooser():
return pathchooser()
def pathchooser():
browse_for = "folder"
folder_only = request.args.get('folder', False) == "true"
file_filter = request.args.get('filter', "")
path = os.path.normpath(request.args.get('path', ""))
if os.path.isfile(path):
oldfile = path
path = os.path.dirname(path)
else:
oldfile = ""
abs = False
if os.path.isdir(path):
#if os.path.isabs(path):
cwd = os.path.realpath(path)
abs = True
#else:
# cwd = os.path.relpath(path)
else:
cwd = os.getcwd()
cwd = os.path.normpath(os.path.realpath(cwd))
parentdir = os.path.dirname(cwd)
if not abs:
if os.path.realpath(cwd) == os.path.realpath("/"):
cwd = os.path.relpath(cwd)
else:
cwd = os.path.relpath(cwd) + os.path.sep
parentdir = os.path.relpath(parentdir) + os.path.sep
@admi.route("/config", methods=["GET", "POST"])
if os.path.realpath(cwd) == os.path.realpath("/"):
parentdir = ""
try:
folders = os.listdir(cwd)
except Exception:
folders = []
files = []
# locale = get_locale()
for f in folders:
try:
data = {"name": f, "fullpath": os.path.join(cwd, f)}
data["sort"] = data["fullpath"].lower()
except Exception:
continue
if os.path.isfile(os.path.join(cwd, f)):
if folder_only:
continue
if file_filter != "" and file_filter != f:
continue
data["type"] = "file"
data["size"] = os.path.getsize(os.path.join(cwd, f))
power = 0
while (data["size"] >> 10) > 0.3:
power += 1
data["size"] >>= 10
units = ("", "K", "M", "G", "T")
data["size"] = str(data["size"]) + " " + units[power] + "Byte"
else:
data["type"] = "dir"
data["size"] = ""
files.append(data)
files = sorted(files, key=operator.itemgetter("type", "sort"))
context = {
"cwd": cwd,
"files": files,
"parentdir": parentdir,
"type": browse_for,
"oldfile": oldfile,
"absolute": abs,
}
return json.dumps(context)
@admi.route("/basicconfig", methods=["GET", "POST"])
@unconfigured
def basic_configuration():
logout_user()
if request.method == "POST":
return _configuration_update_helper()
return _configuration_result()
return _configuration_update_helper(configured=filepicker)
return _configuration_result(configured=filepicker)
def _config_int(to_save, x, func=int):
@ -633,13 +836,13 @@ def _configuration_ldap_helper(to_save, gdriveError):
return reboot_required, None
def _configuration_update_helper():
def _configuration_update_helper(configured):
reboot_required = False
db_change = False
to_save = request.form.to_dict()
gdriveError = None
to_save['config_calibre_dir'] = re.sub('[\\/]metadata\.db$',
to_save['config_calibre_dir'] = re.sub(r'[\\/]metadata\.db$',
'',
to_save['config_calibre_dir'],
flags=re.IGNORECASE)
@ -653,11 +856,15 @@ def _configuration_update_helper():
reboot_required |= _config_string(to_save, "config_keyfile")
if config.config_keyfile and not os.path.isfile(config.config_keyfile):
return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'), gdriveError)
return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'),
gdriveError,
configured)
reboot_required |= _config_string(to_save, "config_certfile")
if config.config_certfile and not os.path.isfile(config.config_certfile):
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'), gdriveError)
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'),
gdriveError,
configured)
_config_checkbox_int(to_save, "config_uploading")
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
@ -722,10 +929,10 @@ def _configuration_update_helper():
if "config_rarfile_location" in to_save:
unrar_status = helper.check_unrar(config.config_rarfile_location)
if unrar_status:
return _configuration_result(unrar_status, gdriveError)
return _configuration_result(unrar_status, gdriveError, configured)
except (OperationalError, InvalidRequestError):
ub.session.rollback()
_configuration_result(_(u"Settings DB is not Writeable"), gdriveError)
_configuration_result(_(u"Settings DB is not Writeable"), gdriveError, configured)
try:
metadata_db = os.path.join(config.config_calibre_dir, "metadata.db")
@ -733,11 +940,13 @@ def _configuration_update_helper():
gdriveutils.downloadFile(None, "metadata.db", metadata_db)
db_change = True
except Exception as e:
return _configuration_result('%s' % e, gdriveError)
return _configuration_result('%s' % e, gdriveError, configured)
if db_change:
if not calibre_db.setup_db(config, ub.app_DB_path):
return _configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdriveError)
return _configuration_result(_('DB Location is not Valid, Please Enter Correct Path'),
gdriveError,
configured)
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
flash(_(u"DB is not Writeable"), category="warning")
@ -746,10 +955,10 @@ def _configuration_update_helper():
if reboot_required:
web_server.stop(True)
return _configuration_result(None, gdriveError)
return _configuration_result(None, gdriveError, configured)
def _configuration_result(error_flash=None, gdriveError=None):
def _configuration_result(error_flash=None, gdriveError=None, configured=True):
gdrive_authenticate = not is_gdrive_ready()
gdrivefolders = []
if gdriveError is None:
@ -770,7 +979,7 @@ def _configuration_result(error_flash=None, gdriveError=None):
return render_title_template("config_edit.html", config=config, provider=oauthblueprints,
show_back_button=show_back_button, show_login_button=show_login_button,
show_authenticate_google_drive=gdrive_authenticate,
show_authenticate_google_drive=gdrive_authenticate, filepicker=configured,
gdriveError=gdriveError, gdrivefolders=gdrivefolders, feature_support=feature_support,
title=_(u"Basic Configuration"), page="config")
@ -816,7 +1025,10 @@ def _handle_new_user(to_save, content,languages, translations, kobo_support):
content.allowed_column_value = config.config_allowed_column_value
content.denied_column_value = config.config_denied_column_value
ub.session.add(content)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
flash(_(u"User '%(user)s' created", user=content.nickname), category="success")
return redirect(url_for('admin.admin'))
except IntegrityError:
@ -832,7 +1044,10 @@ def _handle_edit_user(to_save, content,languages, translations, kobo_support):
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()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success")
return redirect(url_for('admin.admin'))
else:
@ -855,7 +1070,7 @@ def _handle_edit_user(to_save, content,languages, translations, kobo_support):
content.role &= ~constants.ROLE_ANONYMOUS
val = [int(k[5:]) for k in to_save if k.startswith('show_')]
sidebar = ub.get_sidebar_config()
sidebar = get_sidebar_config()
for element in sidebar:
value = element['visibility']
if value in val and not content.check_visibility(value):
@ -907,7 +1122,10 @@ def _handle_edit_user(to_save, content,languages, translations, kobo_support):
if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail:
content.kindle_mail = to_save["kindle_mail"]
try:
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success")
except IntegrityError:
ub.session.rollback()
@ -1119,3 +1337,110 @@ def get_updater_status():
except Exception:
status['status'] = 11
return json.dumps(status)
@admi.route('/import_ldap_users')
@login_required
@admin_required
def import_ldap_users():
showtext = {}
try:
new_users = services.ldap.get_group_members(config.config_ldap_group_name)
except (services.ldap.LDAPException, TypeError, AttributeError, KeyError) as e:
log.debug_or_exception(e)
showtext['text'] = _(u'Error: %(ldaperror)s', ldaperror=e)
return json.dumps(showtext)
if not new_users:
log.debug('LDAP empty response')
showtext['text'] = _(u'Error: No user returned in response of LDAP server')
return json.dumps(showtext)
imported = 0
for username in new_users:
user = username.decode('utf-8')
if '=' in user:
# if member object field is empty take user object as filter
if config.config_ldap_member_user_object:
query_filter = config.config_ldap_member_user_object
else:
query_filter = config.config_ldap_user_object
try:
user_identifier = extract_user_identifier(user, query_filter)
except Exception as e:
log.warning(e)
continue
else:
user_identifier = user
query_filter = None
try:
user_data = services.ldap.get_object_details(user=user_identifier, query_filter=query_filter)
except AttributeError as e:
log.debug_or_exception(e)
continue
if user_data:
user_login_field = extract_dynamic_field_from_filter(user, config.config_ldap_user_object)
username = user_data[user_login_field][0].decode('utf-8')
# 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(ub.User.nickname == username).first():
log.warning("LDAP User %s Already in Database", user_data)
continue
kindlemail = ''
if 'mail' in user_data:
useremail = user_data['mail'][0].decode('utf-8')
if (len(user_data['mail']) > 1):
kindlemail = user_data['mail'][1].decode('utf-8')
else:
log.debug('No Mail Field Found in LDAP Response')
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)
continue
content = ub.User()
content.nickname = username
content.password = '' # dummy password which will be replaced by ldap one
content.email = useremail
content.kindle_mail = kindlemail
content.role = config.config_default_role
content.sidebar_view = config.config_default_show
content.allowed_tags = config.config_allowed_tags
content.denied_tags = config.config_denied_tags
content.allowed_column_value = config.config_allowed_column_value
content.denied_column_value = config.config_denied_column_value
ub.session.add(content)
try:
ub.session.commit()
imported +=1
except Exception as e:
log.warning("Failed to create LDAP user: %s - %s", user, e)
ub.session.rollback()
showtext['text'] = _(u'Failed to Create at Least One LDAP User')
else:
log.warning("LDAP User: %s Not Found", user)
showtext['text'] = _(u'At Least One LDAP User Not Found in Database')
if not showtext:
showtext['text'] = _(u'{} User Successfully Imported'.format(imported))
return json.dumps(showtext)
def extract_user_data_from_field(user, field):
match = re.search(field + "=([\d\s\w-]+)", user, re.IGNORECASE | re.UNICODE)
if match:
return match.group(1)
else:
raise Exception("Could Not Parse LDAP User: {}".format(user))
def extract_dynamic_field_from_filter(user, filter):
match = re.search("([a-zA-Z0-9-]+)=%s", filter, re.IGNORECASE | re.UNICODE)
if match:
return match.group(1)
else:
raise Exception("Could Not Parse LDAP Userfield: {}", user)
def extract_user_identifier(user, filter):
dynamic_field = extract_dynamic_field_from_filter(user, filter)
return extract_user_data_from_field(user, dynamic_field)

@ -45,6 +45,7 @@ parser.add_argument('-v', '--version', action='version', help='Shows version num
version=version_info())
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password')
parser.add_argument('-f', action='store_true', help='Enables filepicker in unconfigured mode')
args = parser.parse_args()
if sys.version_info < (3, 0):
@ -110,3 +111,6 @@ if ipadress:
# handle and check user password argument
user_password = args.s or None
# Handles enableing of filepicker
filepicker = args.f or None

@ -18,21 +18,21 @@
from __future__ import division, print_function, unicode_literals
import os
import io
from . import logger, isoLanguages
from .constants import BookMeta
try:
from PIL import Image as PILImage
use_PIL = True
except ImportError as e:
use_PIL = False
log = logger.create()
try:
from wand.image import Image
use_IM = True
except (ImportError, RuntimeError) as e:
use_IM = False
try:
from comicapi.comicarchive import ComicArchive, MetaDataStyle
use_comic_meta = True
@ -52,20 +52,23 @@ except (ImportError, LookupError) as e:
use_rarfile = False
use_comic_meta = False
NO_JPEG_EXTENSIONS = ['.png', '.webp', '.bmp']
COVER_EXTENSIONS = ['.png', '.webp', '.bmp', '.jpg', '.jpeg']
def _cover_processing(tmp_file_name, img, extension):
if use_PIL:
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg')
if use_IM:
# convert to jpg because calibre only supports jpg
if extension in ('.png', '.webp'):
imgc = PILImage.open(io.BytesIO(img))
im = imgc.convert('RGB')
tmp_bytesio = io.BytesIO()
im.save(tmp_bytesio, format='JPEG')
img = tmp_bytesio.getvalue()
if extension in NO_JPEG_EXTENSIONS:
with Image(filename=tmp_file_name) as imgc:
imgc.format = 'jpeg'
imgc.transform_colorspace('rgb')
imgc.save(tmp_cover_name)
return tmp_cover_name
if not img:
return None
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg')
with open(tmp_cover_name, 'wb') as f:
f.write(img)
return tmp_cover_name
@ -80,7 +83,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
if extension in COVER_EXTENSIONS:
cover_data = archive.getPage(index)
break
else:
@ -90,7 +93,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
if extension in COVER_EXTENSIONS:
cover_data = cf.read(name)
break
elif original_file_extension.upper() == '.CBT':
@ -99,7 +102,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
if extension in COVER_EXTENSIONS:
cover_data = cf.extractfile(name).read()
break
elif original_file_extension.upper() == '.CBR' and use_rarfile:
@ -110,7 +113,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
if extension in COVER_EXTENSIONS:
cover_data = cf.read(name)
break
except Exception as e:

@ -22,6 +22,7 @@ import os
import sys
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
from sqlalchemy.exc import OperationalError
from sqlalchemy.ext.declarative import declarative_base
from . import constants, cli, logger, ub
@ -271,6 +272,14 @@ class _ConfigSQL(object):
setattr(self, field, new_value)
return True
def toDict(self):
storage = {}
for k, v in self.__dict__.items():
if k[0] != '_' or k.endswith("password"):
storage[k] = v
return storage
def load(self):
'''Load all configuration values from the underlying storage.'''
s = self._read_from_storage() # type: _Settings
@ -295,7 +304,11 @@ class _ConfigSQL(object):
log.warning("Log path %s not valid, falling back to default", self.config_logfile)
self.config_logfile = logfile
self._session.merge(s)
self._session.commit()
try:
self._session.commit()
except OperationalError as e:
log.error('Database error: %s', e)
self._session.rollback()
def save(self):
'''Apply all configuration values to the underlying storage.'''
@ -309,7 +322,11 @@ class _ConfigSQL(object):
log.debug("_ConfigSQL updating storage")
self._session.merge(s)
self._session.commit()
try:
self._session.commit()
except OperationalError as e:
log.error('Database error: %s', e)
self._session.rollback()
self.load()
def invalidate(self, error=None):
@ -350,7 +367,10 @@ def _migrate_table(session, orm_class):
changed = True
if changed:
session.commit()
try:
session.commit()
except OperationalError:
session.rollback()
def autodetect_calibre_binary():

@ -32,9 +32,9 @@ from sqlalchemy.orm import relationship, sessionmaker, scoped_session
from sqlalchemy.orm.collections import InstrumentedList
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
from sqlalchemy.pool import StaticPool
from flask_login import current_user
from sqlalchemy.sql.expression import and_, true, false, text, func, or_
from sqlalchemy.ext.associationproxy import association_proxy
from flask_login import current_user
from babel import Locale as LC
from babel.core import UnknownLocaleError
from flask_babel import gettext as _
@ -425,18 +425,19 @@ class CalibreDB():
# instances alive once they reach the end of their respective scopes
instances = WeakSet()
def __init__(self):
def __init__(self, expire_on_commit=True):
""" Initialize a new CalibreDB session
"""
self.session = None
if self._init:
self.initSession()
self.initSession(expire_on_commit)
self.instances.add(self)
def initSession(self):
def initSession(self, expire_on_commit=True):
self.session = self.session_factory()
self.session.expire_on_commit = expire_on_commit
self.update_title_sort(self.config)
@classmethod
@ -444,6 +445,8 @@ class CalibreDB():
cls.config = config
cls.dispose()
# toDo: if db changed -> delete shelfs, delete download books, delete read boks, kobo sync??
if not config.config_calibre_dir:
config.invalidate()
return False
@ -764,5 +767,5 @@ def lcase(s):
return unidecode.unidecode(s.lower())
except Exception as e:
log = logger.create()
log.exception(e)
log.debug_or_exception(e)
return s.lower()

@ -21,7 +21,12 @@ import shutil
import glob
import zipfile
import json
import io
from io import BytesIO
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
import os
from flask import send_file
@ -32,11 +37,12 @@ from .about import collect_stats
log = logger.create()
def assemble_logfiles(file_name):
log_list = glob.glob(file_name + '*')
wfd = io.StringIO()
log_list = sorted(glob.glob(file_name + '*'), reverse=True)
wfd = StringIO()
for f in log_list:
with open(f, 'r') as fd:
shutil.copyfileobj(fd, wfd)
wfd.seek(0)
return send_file(wfd,
as_attachment=True,
attachment_filename=os.path.basename(file_name))
@ -44,8 +50,12 @@ def assemble_logfiles(file_name):
def send_debug():
file_list = glob.glob(logger.get_logfile(config.config_logfile) + '*')
file_list.extend(glob.glob(logger.get_accesslogfile(config.config_access_logfile) + '*'))
memory_zip = io.BytesIO()
for element in [logger.LOG_TO_STDOUT, logger.LOG_TO_STDERR]:
if element in file_list:
file_list.remove(element)
memory_zip = BytesIO()
with zipfile.ZipFile(memory_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr('settings.txt', json.dumps(config.toDict()))
zf.writestr('libs.txt', json.dumps(collect_stats()))
for fp in file_list:
zf.write(fp, os.path.basename(fp))

@ -37,13 +37,38 @@ from . import config, get_locale, ub, db
from . import calibre_db
from .services.worker import WorkerThread
from .tasks.upload import TaskUpload
from .web import login_required_if_no_ano, render_title_template, edit_required, upload_required
from .render_template import render_title_template
from .usermanagement import login_required_if_no_ano
try:
from functools import wraps
except ImportError:
pass # We're not using Python 3
editbook = Blueprint('editbook', __name__)
log = logger.create()
def upload_required(f):
@wraps(f)
def inner(*args, **kwargs):
if current_user.role_upload() or current_user.role_admin():
return f(*args, **kwargs)
abort(403)
return inner
def edit_required(f):
@wraps(f)
def inner(*args, **kwargs):
if current_user.role_edit() or current_user.role_admin():
return f(*args, **kwargs)
abort(403)
return inner
# 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):
@ -259,7 +284,7 @@ def delete_book(book_id, book_format, jsonResponse):
filter(db.Data.format == book_format).delete()
calibre_db.session.commit()
except Exception as e:
log.exception(e)
log.debug_or_exception(e)
calibre_db.session.rollback()
else:
# book not found
@ -287,7 +312,7 @@ def delete_book(book_id, book_format, jsonResponse):
def render_edit_book(book_id):
calibre_db.update_title_sort(config)
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
book = calibre_db.get_filtered_book(book_id)
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
if not book:
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
return redirect(url_for("web.index"))
@ -716,7 +741,7 @@ def edit_book(book_id):
flash(error, category="error")
return render_edit_book(book_id)
except Exception as e:
log.exception(e)
log.debug_or_exception(e)
calibre_db.session.rollback()
flash(_("Error editing book, please check logfile for details"), category="error")
return redirect(url_for('web.show_book', book_id=book.id))

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2020 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import traceback
from flask import render_template
from werkzeug.exceptions import default_exceptions
try:
from werkzeug.exceptions import FailedDependency
except ImportError:
from werkzeug.exceptions import UnprocessableEntity as FailedDependency
from . import config, app, logger, services
log = logger.create()
# custom error page
def error_http(error):
return render_template('http_error.html',
error_code="Error {0}".format(error.code),
error_name=error.name,
issue=False,
instance=config.config_calibre_web_title
), error.code
def internal_error(error):
return render_template('http_error.html',
error_code="Internal Server Error",
error_name=str(error),
issue=True,
error_stack=traceback.format_exc().split("\n"),
instance=config.config_calibre_web_title
), 500
def init_errorhandler():
# http error handling
for ex in default_exceptions:
if ex < 500:
app.register_error_handler(ex, error_http)
elif ex == 500:
app.register_error_handler(ex, internal_error)
if services.ldap:
# Only way of catching the LDAPException upon logging in with LDAP server down
@app.errorhandler(services.ldap.LDAPException)
def handle_exception(e):
log.debug('LDAP server not accessible while trying to login to opds feed')
return error_http(FailedDependency())
# @app.errorhandler(InvalidRequestError)
#@app.errorhandler(OperationalError)
#def handle_db_exception(e):
# db.session.rollback()
# log.error('Database request error: %s',e)
# return internal_error(InternalServerError(e))

@ -35,9 +35,9 @@ from flask_babel import gettext as _
from flask_login import login_required
from . import logger, gdriveutils, config, ub, calibre_db
from .web import admin_required
from .admin import admin_required
gdrive = Blueprint('gdrive', __name__)
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
log = logger.create()
try:
@ -50,7 +50,7 @@ current_milli_time = lambda: int(round(time() * 1000))
gdrive_watch_callback_token = 'target=calibreweb-watch_files'
@gdrive.route("/gdrive/authenticate")
@gdrive.route("/authenticate")
@login_required
@admin_required
def authenticate_google_drive():
@ -63,7 +63,7 @@ def authenticate_google_drive():
return redirect(authUrl)
@gdrive.route("/gdrive/callback")
@gdrive.route("/callback")
def google_drive_callback():
auth_code = request.args.get('code')
if not auth_code:
@ -77,18 +77,14 @@ def google_drive_callback():
return redirect(url_for('admin.configuration'))
@gdrive.route("/gdrive/watch/subscribe")
@gdrive.route("/watch/subscribe")
@login_required
@admin_required
def watch_gdrive():
if not config.config_google_drive_watch_changes_response:
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
filedata = json.load(settings)
if filedata['web']['redirect_uris'][0].endswith('/'):
filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-((len('/gdrive/callback')+1))]
else:
filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-(len('/gdrive/callback'))]
address = '%s/gdrive/watch/callback' % filedata['web']['redirect_uris'][0]
address = filedata['web']['redirect_uris'][0].rstrip('/').replace('/gdrive/callback', '/gdrive/watch/callback')
notification_id = str(uuid4())
try:
result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id,
@ -98,14 +94,15 @@ def watch_gdrive():
except HttpError as e:
reason=json.loads(e.content)['error']['errors'][0]
if reason['reason'] == u'push.webhookUrlUnauthorized':
flash(_(u'Callback domain is not verified, please follow steps to verify domain in google developer console'), category="error")
flash(_(u'Callback domain is not verified, '
u'please follow steps to verify domain in google developer console'), category="error")
else:
flash(reason['message'], category="error")
return redirect(url_for('admin.configuration'))
@gdrive.route("/gdrive/watch/revoke")
@gdrive.route("/watch/revoke")
@login_required
@admin_required
def revoke_watch_gdrive():
@ -121,14 +118,14 @@ def revoke_watch_gdrive():
return redirect(url_for('admin.configuration'))
@gdrive.route("/gdrive/watch/callback", methods=['GET', 'POST'])
@gdrive.route("/watch/callback", methods=['GET', 'POST'])
def on_received_watch_confirmation():
if not config.config_google_drive_watch_changes_response:
return ''
if request.headers.get('X-Goog-Channel-Token') != gdrive_watch_callback_token \
or request.headers.get('X-Goog-Resource-State') != 'change' \
or not request.data:
return '' # redirect(url_for('admin.configuration'))
return ''
log.debug('%r', request.headers)
log.debug('%r', request.data)
@ -146,15 +143,18 @@ def on_received_watch_confirmation():
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
and response['file']['md5Checksum'] != hashlib.md5(dbpath):
tmpDir = tempfile.gettempdir()
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
if not os.path.isdir(tmp_dir):
os.mkdir(tmp_dir)
log.info('Database file updated')
copyfile(dbpath, os.path.join(tmpDir, "metadata.db_" + str(current_milli_time())))
copyfile(dbpath, os.path.join(tmp_dir, "metadata.db_" + str(current_milli_time())))
log.info('Backing up existing and downloading updated metadata.db')
gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmpDir, "tmp_metadata.db"))
gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmp_dir, "tmp_metadata.db"))
log.info('Setting up new DB')
# prevent error on windows, as os.rename does on exisiting files
move(os.path.join(tmpDir, "tmp_metadata.db"), dbpath)
# 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)
calibre_db.reconnect_db(config, ub.app_DB_path)
except Exception as e:
log.exception(e)
log.debug_or_exception(e)
return ''

@ -32,16 +32,25 @@ from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.exc import OperationalError, InvalidRequestError
try:
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from pydrive.auth import RefreshError
from apiclient import errors
from httplib2 import ServerNotFoundError
gdrive_support = True
importError = None
except ImportError as err:
importError = err
gdrive_support = True
except ImportError as e:
importError = e
gdrive_support = False
try:
from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive
from pydrive2.auth import RefreshError
except ImportError as err:
try:
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from pydrive.auth import RefreshError
except ImportError as err:
importError = err
gdrive_support = False
from . import logger, cli, config
from .constants import CONFIG_DIR as _CONFIG_DIR
@ -91,7 +100,7 @@ class Singleton:
except AttributeError:
self._instance = self._decorated()
return self._instance
except ImportError as e:
except (ImportError, NameError) as e:
log.debug(e)
return None
@ -190,7 +199,7 @@ def getDrive(drive=None, gauth=None):
except RefreshError as e:
log.error("Google Drive error: %s", e)
except Exception as e:
log.exception(e)
log.debug_or_exception(e)
else:
# Initialize the saved creds
gauth.Authorize()
@ -208,7 +217,7 @@ def listRootFolders():
drive = getDrive(Gdrive.Instance().drive)
folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false"
fileList = drive.ListFile({'q': folder}).GetList()
except ServerNotFoundError as e:
except (ServerNotFoundError, ssl.SSLError) as e:
log.info("GDrive Error %s" % e)
fileList = []
return fileList
@ -547,21 +556,24 @@ def partial(total_byte_len, part_size_limit):
return s
# downloads files in chunks from gdrive
def do_gdrive_download(df, headers):
def do_gdrive_download(df, headers, convert_encoding=False):
total_size = int(df.metadata.get('fileSize'))
download_url = df.metadata.get('downloadUrl')
s = partial(total_size, 1024 * 1024) # I'm downloading BIG files, so 100M chunk size is fine for me
def stream():
def stream(convert_encoding):
for byte in s:
headers = {"Range": 'bytes=%s-%s' % (byte[0], byte[1])}
resp, content = df.auth.Get_Http_Object().request(download_url, headers=headers)
if resp.status == 206:
if convert_encoding:
result = chardet.detect(content)
content = content.decode(result['encoding']).encode('utf-8')
yield content
else:
log.warning('An error occurred: %s', resp)
return
return Response(stream_with_context(stream()), headers=headers)
return Response(stream_with_context(stream(convert_encoding)), headers=headers)
_SETTINGS_YAML_TEMPLATE = """

@ -24,10 +24,7 @@ import io
import mimetypes
import re
import shutil
import glob
import time
import zipfile
import json
import unicodedata
from datetime import datetime, timedelta
from tempfile import gettempdir
@ -53,13 +50,6 @@ try:
except ImportError:
use_unidecode = False
try:
from PIL import Image as PILImage
from PIL import UnidentifiedImageError
use_PIL = True
except ImportError:
use_PIL = False
from . import calibre_db
from .tasks.convert import TaskConvert
from . import logger, config, get_locale, db, ub
@ -69,9 +59,16 @@ from .subproc_wrapper import process_wait
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS
from .tasks.mail import TaskEmail
log = logger.create()
try:
from wand.image import Image
from wand.exceptions import MissingDelegateError
use_IM = True
except (ImportError, RuntimeError) as e:
log.debug('Cannot import Image, generating covers from non jpg files will not work: %s', e)
use_IM = False
# Convert existing book entry to new format
def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, user_id, kindle_mail=None):
@ -112,21 +109,21 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
def send_test_mail(kindle_mail, user_name):
WorkerThread.add(user_name, TaskEmail(_(u'Calibre-Web test e-mail'), None, None,
config.get_mail_settings(), kindle_mail, _(u"Test e-mail"),
_(u'This e-mail has been sent via Calibre-Web.')))
_(u'This e-mail has been sent via Calibre-Web.')))
return
# Send registration email or password reset email, depending on parameter resend (False means welcome email)
def send_registration_mail(e_mail, user_name, default_password, resend=False):
text = "Hello %s!\r\n" % user_name
txt = "Hello %s!\r\n" % user_name
if not resend:
text += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n"
text += "Please log in to your account using the following informations:\r\n"
text += "User name: %s\r\n" % user_name
text += "Password: %s\r\n" % default_password
text += "Don't forget to change your password after first login.\r\n"
text += "Sincerely\r\n\r\n"
text += "Your Calibre-Web team"
txt += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n"
txt += "Please log in to your account using the following informations:\r\n"
txt += "User name: %s\r\n" % user_name
txt += "Password: %s\r\n" % default_password
txt += "Don't forget to change your password after first login.\r\n"
txt += "Sincerely\r\n\r\n"
txt += "Your Calibre-Web team"
WorkerThread.add(None, TaskEmail(
subject=_(u'Get Started with Calibre-Web'),
filepath=None,
@ -134,7 +131,7 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
settings=config.get_mail_settings(),
recipient=e_mail,
taskMessage=_(u"Registration e-mail for user: %(name)s", name=user_name),
text=text
text=txt
))
return
@ -180,7 +177,7 @@ def check_send_to_kindle(entry):
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Pdf')})
if config.config_converterpath:
if 'EPUB' in formats and not 'MOBI' in formats:
if 'EPUB' in formats and 'MOBI' not in formats:
bookformats.append({'format': 'Mobi',
'convert':1,
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
@ -565,8 +562,7 @@ def get_book_cover_internal(book, use_generic_cover_on_failure):
log.error('%s/cover.jpg not found on Google Drive', book.path)
return get_cover_on_failure(use_generic_cover_on_failure)
except Exception as e:
log.exception(e)
# traceback.print_exc()
log.debug_or_exception(e)
return get_cover_on_failure(use_generic_cover_on_failure)
else:
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
@ -589,16 +585,15 @@ def save_cover_from_url(url, book_path):
requests.exceptions.Timeout) as ex:
log.info(u'Cover Download Error %s', ex)
return False, _("Error Downloading Cover")
except UnidentifiedImageError as ex:
except MissingDelegateError as ex:
log.info(u'File Format Error %s', ex)
return False, _("Cover Format Error")
def save_cover_from_filestorage(filepath, saved_filename, img):
if hasattr(img, '_content'):
f = open(os.path.join(filepath, saved_filename), "wb")
f.write(img._content)
f.close()
if hasattr(img,"metadata"):
img.save(filename=os.path.join(filepath, saved_filename))
img.close()
else:
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file
if not os.path.exists(filepath):
@ -619,31 +614,33 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
def save_cover(img, book_path):
content_type = img.headers.get('content-type')
if use_PIL:
if content_type not in ('image/jpeg', 'image/png', 'image/webp'):
log.error("Only jpg/jpeg/png/webp files are supported as coverfile")
return False, _("Only jpg/jpeg/png/webp files are supported as coverfile")
if use_IM:
if content_type not in ('image/jpeg', 'image/png', 'image/webp', 'image/bmp'):
log.error("Only jpg/jpeg/png/webp/bmp files are supported as coverfile")
return False, _("Only jpg/jpeg/png/webp/bmp files are supported as coverfile")
# convert to jpg because calibre only supports jpg
if content_type in ('image/png', 'image/webp'):
if content_type != 'image/jpg':
if hasattr(img, 'stream'):
imgc = PILImage.open(img.stream)
imgc = Image(blob=img.stream)
else:
imgc = PILImage.open(io.BytesIO(img.content))
im = imgc.convert('RGB')
tmp_bytesio = io.BytesIO()
im.save(tmp_bytesio, format='JPEG')
img._content = tmp_bytesio.getvalue()
imgc = Image(blob=io.BytesIO(img.content))
imgc.format = 'jpeg'
imgc.transform_colorspace("rgb")
img = imgc
else:
if content_type not in 'image/jpeg':
log.error("Only jpg/jpeg files are supported as coverfile")
return False, _("Only jpg/jpeg files are supported as coverfile")
if config.config_use_google_drive:
tmpDir = gettempdir()
ret, message = save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img)
tmp_dir = os.path.join(gettempdir(), 'calibre_web')
if not os.path.isdir(tmp_dir):
os.mkdir(tmp_dir)
ret, message = save_cover_from_filestorage(tmp_dir, "uploaded_cover.jpg", img)
if ret is True:
gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'),
os.path.join(tmpDir, "uploaded_cover.jpg"))
gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg').replace("\\","/"),
os.path.join(tmp_dir, "uploaded_cover.jpg"))
log.info("Cover is saved on Google Drive")
return True, None
else:
@ -697,7 +694,7 @@ def check_unrar(unrarLocation):
log.debug("unrar version %s", version)
break
except (OSError, UnicodeDecodeError) as err:
log.exception(err)
log.debug_or_exception(err)
return _('Error excecuting UnRar')
@ -827,4 +824,3 @@ def get_download_link(book_id, book_format, client):
return do_download_file(book, book_format, client, data1, headers)
else:
abort(404)

@ -43,6 +43,8 @@ from flask_login import current_user
from werkzeug.datastructures import Headers
from sqlalchemy import func
from sqlalchemy.sql.expression import and_, or_
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm import load_only
from sqlalchemy.exc import StatementError
import requests
@ -56,6 +58,8 @@ KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]}
KOBO_STOREAPI_URL = "https://storeapi.kobo.com"
KOBO_IMAGEHOST_URL = "https://kbimages1-a.akamaihd.net"
SYNC_ITEM_LIMIT = 5
kobo = Blueprint("kobo", __name__, url_prefix="/kobo/<auth_token>")
kobo_auth.disable_failed_auth_redirect_for_blueprint(kobo)
kobo_auth.register_url_value_preprocessor(kobo)
@ -142,68 +146,80 @@ def HandleSyncRequest():
new_books_last_modified = sync_token.books_last_modified
new_books_last_created = sync_token.books_last_created
new_reading_state_last_modified = sync_token.reading_state_last_modified
new_archived_last_modified = datetime.datetime.min
sync_results = []
# We reload the book database so that the user get's a fresh view of the library
# in case of external changes (e.g: adding a book through Calibre).
calibre_db.reconnect_db(config, ub.app_DB_path)
archived_books = (
ub.session.query(ub.ArchivedBook)
.filter(ub.ArchivedBook.user_id == int(current_user.id))
.all()
)
# We join-in books that have had their Archived bit recently modified in order to either:
# * Restore them to the user's device.
# * Delete them from the user's device.
# (Ideally we would use a join for this logic, however cross-database joins don't look trivial in SqlAlchemy.)
recently_restored_or_archived_books = []
archived_book_ids = {}
new_archived_last_modified = datetime.datetime.min
for archived_book in archived_books:
if archived_book.last_modified > sync_token.archive_last_modified:
recently_restored_or_archived_books.append(archived_book.book_id)
if archived_book.is_archived:
archived_book_ids[archived_book.book_id] = True
new_archived_last_modified = max(
new_archived_last_modified, archived_book.last_modified)
# sqlite gives unexpected results when performing the last_modified comparison without the datetime cast.
# It looks like it's treating the db.Books.last_modified field as a string and may fail
# the comparison because of the +00:00 suffix.
changed_entries = (
calibre_db.session.query(db.Books)
.join(db.Data)
.filter(or_(func.datetime(db.Books.last_modified) > sync_token.books_last_modified,
db.Books.id.in_(recently_restored_or_archived_books)))
.filter(db.Data.format.in_(KOBO_FORMATS))
.all()
)
if sync_token.books_last_id > -1:
changed_entries = (
calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
.filter(db.Books.last_modified >= sync_token.books_last_modified)
.filter(db.Books.id>sync_token.books_last_id)
.filter(db.Data.format.in_(KOBO_FORMATS))
.order_by(db.Books.last_modified)
.order_by(db.Books.id)
.limit(SYNC_ITEM_LIMIT)
)
else:
changed_entries = (
calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
.filter(db.Books.last_modified > sync_token.books_last_modified)
.filter(db.Data.format.in_(KOBO_FORMATS))
.order_by(db.Books.last_modified)
.order_by(db.Books.id)
.limit(SYNC_ITEM_LIMIT)
)
reading_states_in_new_entitlements = []
for book in changed_entries:
kobo_reading_state = get_or_create_reading_state(book.id)
kobo_reading_state = get_or_create_reading_state(book.Books.id)
entitlement = {
"BookEntitlement": create_book_entitlement(book, archived=(book.id in archived_book_ids)),
"BookMetadata": get_metadata(book),
"BookEntitlement": create_book_entitlement(book.Books, archived=(book.is_archived == True)),
"BookMetadata": get_metadata(book.Books),
}
if kobo_reading_state.last_modified > sync_token.reading_state_last_modified:
entitlement["ReadingState"] = get_kobo_reading_state_response(book, kobo_reading_state)
entitlement["ReadingState"] = get_kobo_reading_state_response(book.Books, kobo_reading_state)
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
reading_states_in_new_entitlements.append(book.id)
reading_states_in_new_entitlements.append(book.Books.id)
if book.timestamp > sync_token.books_last_created:
if book.Books.timestamp > sync_token.books_last_created:
sync_results.append({"NewEntitlement": entitlement})
else:
sync_results.append({"ChangedEntitlement": entitlement})
new_books_last_modified = max(
book.last_modified, new_books_last_modified
book.Books.last_modified, new_books_last_modified
)
new_books_last_created = max(book.timestamp, new_books_last_created)
new_books_last_created = max(book.Books.timestamp, new_books_last_created)
max_change = (changed_entries
.from_self()
.filter(ub.ArchivedBook.is_archived)
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc())
.first()
)
if max_change:
max_change = max_change.last_modified
else:
max_change = new_archived_last_modified
new_archived_last_modified = max(new_archived_last_modified, max_change)
# no. of books returned
book_count = changed_entries.count()
# last entry:
if book_count:
books_last_id = changed_entries.all()[-1].Books.id or -1
else:
books_last_id = -1
# generate reading state data
changed_reading_states = (
ub.session.query(ub.KoboReadingState)
.filter(and_(func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified,
@ -225,11 +241,12 @@ def HandleSyncRequest():
sync_token.books_last_modified = new_books_last_modified
sync_token.archive_last_modified = new_archived_last_modified
sync_token.reading_state_last_modified = new_reading_state_last_modified
sync_token.books_last_id = books_last_id
return generate_sync_response(sync_token, sync_results)
return generate_sync_response(sync_token, sync_results, book_count)
def generate_sync_response(sync_token, sync_results):
def generate_sync_response(sync_token, sync_results, set_cont=False):
extra_headers = {}
if config.config_kobo_proxy:
# Merge in sync results from the official Kobo store.
@ -245,6 +262,8 @@ def generate_sync_response(sync_token, sync_results):
except Exception as e:
log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e))
if set_cont:
extra_headers["x-kobo-sync"] = "continue"
sync_token.to_headers(extra_headers)
response = make_response(jsonify(sync_results), extra_headers)
@ -443,8 +462,10 @@ def HandleTagCreate():
items_unknown_to_calibre = add_items_to_shelf(items, shelf)
if items_unknown_to_calibre:
log.debug("Received request to add unknown books to a collection. Silently ignoring items.")
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return make_response(jsonify(str(shelf.uuid)), 201)
@ -476,7 +497,10 @@ def HandleTagUpdate(tag_id):
shelf.name = name
ub.session.merge(shelf)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return make_response(' ', 200)
@ -528,7 +552,10 @@ def HandleTagAddItem(tag_id):
log.debug("Received request to add an unknown book to a collection. Silently ignoring item.")
ub.session.merge(shelf)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return make_response('', 201)
@ -569,7 +596,10 @@ def HandleTagRemoveItem(tag_id):
shelf.books.filter(ub.BookShelf.book_id == book.id).delete()
except KeyError:
items_unknown_to_calibre.append(item)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
if items_unknown_to_calibre:
log.debug("Received request to remove an unknown book to a collecition. Silently ignoring item.")
@ -615,7 +645,10 @@ def sync_shelves(sync_token, sync_results):
"ChangedTag": tag
})
sync_token.tags_last_modified = new_tags_last_modified
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
# Creates a Kobo "Tag" object from a ub.Shelf object
@ -696,7 +729,10 @@ def HandleStateRequest(book_uuid):
abort(400, description="Malformed request data is missing 'ReadingStates' key")
ub.session.merge(kobo_reading_state)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return jsonify({
"RequestResult": "Success",
"UpdateResults": [update_results_response],
@ -734,7 +770,10 @@ def get_or_create_reading_state(book_id):
kobo_reading_state.statistics = ub.KoboStatistics()
book_read.kobo_reading_state = kobo_reading_state
ub.session.add(book_read)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return book_read.kobo_reading_state
@ -837,7 +876,10 @@ def HandleBookDeletionRequest(book_uuid):
archived_book.last_modified = datetime.datetime.utcnow()
ub.session.merge(archived_book)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return ("", 204)

@ -66,9 +66,10 @@ from os import urandom
from flask import g, Blueprint, url_for, abort, request
from flask_login import login_user, login_required
from flask_babel import gettext as _
from sqlalchemy.exc import OperationalError
from . import logger, ub, lm
from .web import render_title_template
from .render_template import render_title_template
try:
from functools import wraps
@ -147,7 +148,10 @@ def generate_auth_token(user_id):
auth_token.token_type = 1
ub.session.add(auth_token)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return render_title_template(
"generate_kobo_auth_url.html",
title=_(u"Kobo Setup"),
@ -164,5 +168,8 @@ def delete_auth_token(user_id):
# Invalidate any prevously generated Kobo Auth token for this user.
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
.filter(ub.RemoteAuthToken.token_type==1).delete()
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return ""

@ -41,10 +41,18 @@ logging.addLevelName(logging.WARNING, "WARN")
logging.addLevelName(logging.CRITICAL, "CRIT")
class _Logger(logging.Logger):
def debug_or_exception(self, message, *args, **kwargs):
if is_debug_enabled():
self.exception(message, stacklevel=2, *args, **kwargs)
else:
self.error(message, stacklevel=2, *args, **kwargs)
def get(name=None):
return logging.getLogger(name)
def create():
parent_frame = inspect.stack(0)[1]
if hasattr(parent_frame, 'frame'):
@ -54,7 +62,6 @@ def create():
parent_module = inspect.getmodule(parent_frame)
return get(parent_module.__name__)
def is_debug_enabled():
return logging.root.level <= logging.DEBUG
@ -99,6 +106,7 @@ def setup(log_file, log_level=None):
May be called multiple times.
'''
log_level = log_level or DEFAULT_LOG_LEVEL
logging.setLoggerClass(_Logger)
logging.getLogger(__package__).setLevel(log_level)
r = logging.root

@ -30,11 +30,12 @@ from flask_babel import gettext as _
from flask_dance.consumer import oauth_authorized, oauth_error
from flask_dance.contrib.github import make_github_blueprint, github
from flask_dance.contrib.google import make_google_blueprint, google
from flask_login import login_user, current_user
from flask_login import login_user, current_user, login_required
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.exc import OperationalError
from . import constants, logger, config, app, ub
from .web import login_required
from .oauth import OAuthBackend, backend_resultcode
@ -87,7 +88,7 @@ def register_user_with_oauth(user=None):
try:
ub.session.commit()
except Exception as e:
log.exception(e)
log.debug_or_exception(e)
ub.session.rollback()
@ -109,7 +110,10 @@ if ub.oauth_support:
oauthProvider.provider_name = "google"
oauthProvider.active = False
ub.session.add(oauthProvider)
ub.session.commit()
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
oauth_ids = ub.session.query(ub.OAuthProvider).all()
ele1 = dict(provider_name='github',
@ -203,7 +207,7 @@ if ub.oauth_support:
ub.session.add(oauth_entry)
ub.session.commit()
except Exception as e:
log.exception(e)
log.debug_or_exception(e)
ub.session.rollback()
# Disable Flask-Dance's default behavior for saving the OAuth token
@ -235,7 +239,7 @@ if ub.oauth_support:
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
return redirect(url_for('web.profile'))
except Exception as e:
log.exception(e)
log.debug_or_exception(e)
ub.session.rollback()
else:
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
@ -282,7 +286,7 @@ if ub.oauth_support:
logout_oauth_user()
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
except Exception as e:
log.exception(e)
log.debug_or_exception(e)
ub.session.rollback()
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
except NoResultFound:

@ -33,7 +33,8 @@ from werkzeug.security import check_password_hash
from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
from .helper import get_download_link, get_book_cover
from .pagination import Pagination
from .web import render_read_books, download_required, load_user_from_request
from .web import render_read_books
from .usermanagement import load_user_from_request
from flask_babel import gettext as _
from babel import Locale as LC
from babel.core import UnknownLocaleError

@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11,
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
# apetresc, nanu-c, mutschler
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
from datetime import datetime
from flask import Blueprint, request, make_response, abort, url_for, flash, redirect
from flask_login import login_required, current_user, login_user
from flask_babel import gettext as _
from sqlalchemy.sql.expression import true
from . import config, logger, ub
from .render_template import render_title_template
try:
from functools import wraps
except ImportError:
pass # We're not using Python 3
remotelogin = Blueprint('remotelogin', __name__)
log = logger.create()
def remote_login_required(f):
@wraps(f)
def inner(*args, **kwargs):
if config.config_remote_login:
return f(*args, **kwargs)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
data = {'status': 'error', 'message': 'Forbidden'}
response = make_response(json.dumps(data, ensure_ascii=False))
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response, 403
abort(403)
return inner
@remotelogin.route('/remote/login')
@remote_login_required
def remote_login():
auth_token = ub.RemoteAuthToken()
ub.session.add(auth_token)
ub.session.commit()
verify_url = url_for('remotelogin.verify_token', token=auth_token.auth_token, _external=true)
log.debug(u"Remot Login request with token: %s", auth_token.auth_token)
return render_title_template('remote_login.html', title=_(u"login"), token=auth_token.auth_token,
verify_url=verify_url, page="remotelogin")
@remotelogin.route('/verify/<token>')
@remote_login_required
@login_required
def verify_token(token):
auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first()
# Token not found
if auth_token is None:
flash(_(u"Token not found"), category="error")
log.error(u"Remote Login token not found")
return redirect(url_for('web.index'))
# Token expired
if datetime.now() > auth_token.expiration:
ub.session.delete(auth_token)
ub.session.commit()
flash(_(u"Token has expired"), category="error")
log.error(u"Remote Login token expired")
return redirect(url_for('web.index'))
# Update token with user information
auth_token.user_id = current_user.id
auth_token.verified = True
ub.session.commit()
flash(_(u"Success! Please return to your device"), category="success")
log.debug(u"Remote Login token for userid %s verified", auth_token.user_id)
return redirect(url_for('web.index'))
@remotelogin.route('/ajax/verify_token', methods=['POST'])
@remote_login_required
def token_verified():
token = request.form['token']
auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first()
data = {}
# Token not found
if auth_token is None:
data['status'] = 'error'
data['message'] = _(u"Token not found")
# Token expired
elif datetime.now() > auth_token.expiration:
ub.session.delete(auth_token)
ub.session.commit()
data['status'] = 'error'
data['message'] = _(u"Token has expired")
elif not auth_token.verified:
data['status'] = 'not_verified'
else:
user = ub.session.query(ub.User).filter(ub.User.id == auth_token.user_id).first()
login_user(user)
ub.session.delete(auth_token)
ub.session.commit()
data['status'] = 'success'
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")
response = make_response(json.dumps(data, ensure_ascii=False))
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response

@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2020 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import render_template
from flask_babel import gettext as _
from flask import g
from werkzeug.local import LocalProxy
from flask_login import current_user
from . import config, constants, ub, logger, db, calibre_db
from .ub import User
log = logger.create()
def get_sidebar_config(kwargs=None):
kwargs = kwargs or []
if 'content' in kwargs:
content = kwargs['content']
content = isinstance(content, (User, LocalProxy)) and not content.role_anonymous()
else:
content = 'conf' in kwargs
sidebar = list()
sidebar.append({"glyph": "glyphicon-book", "text": _('Recently Added'), "link": 'web.index', "id": "new",
"visibility": constants.SIDEBAR_RECENT, 'public': True, "page": "root",
"show_text": _('Show recent books'), "config_show":False})
sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot",
"visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot",
"show_text": _('Show Hot Books'), "config_show": True})
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(
{"glyph": "glyphicon-star", "text": _('Top Rated Books'), "link": 'web.books_list', "id": "rated",
"visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated",
"show_text": _('Show Top Rated Books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read",
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous),
"page": "read", "show_text": _('Show read and unread'), "config_show": content})
sidebar.append(
{"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread",
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread",
"show_text": _('Show unread'), "config_show": False})
sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand",
"visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover",
"show_text": _('Show random books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category",
"show_text": _('Show category selection'), "config_show": True})
sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie",
"visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series",
"show_text": _('Show series selection'), "config_show": True})
sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author",
"visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author",
"show_text": _('Show author selection'), "config_show": True})
sidebar.append(
{"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher",
"visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher",
"show_text": _('Show publisher selection'), "config_show":True})
sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang",
"visibility": constants.SIDEBAR_LANGUAGE, 'public': (g.user.filter_language() == 'all'),
"page": "language",
"show_text": _('Show language selection'), "config_show": True})
sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate",
"visibility": constants.SIDEBAR_RATING, 'public': True,
"page": "rating", "show_text": _('Show ratings selection'), "config_show": True})
sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format",
"visibility": constants.SIDEBAR_FORMAT, 'public': True,
"page": "format", "show_text": _('Show file formats selection'), "config_show": True})
sidebar.append(
{"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived",
"show_text": _('Show archived books'), "config_show": content})
sidebar.append(
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
"visibility": constants.SIDEBAR_LIST, 'public': (not g.user.is_anonymous), "page": "list",
"show_text": _('Show Books List'), "config_show": content})
return sidebar
def get_readbooks_ids():
if not config.config_read_column:
readBooks = ub.session.query(ub.ReadBook).filter(ub.ReadBook.user_id == int(current_user.id))\
.filter(ub.ReadBook.read_status == ub.ReadBook.STATUS_FINISHED).all()
return frozenset([x.book_id for x in readBooks])
else:
try:
readBooks = calibre_db.session.query(db.cc_classes[config.config_read_column])\
.filter(db.cc_classes[config.config_read_column].value == True).all()
return frozenset([x.book for x in readBooks])
except (KeyError, AttributeError):
log.error("Custom Column No.%d is not existing in calibre database", config.config_read_column)
return []
# Returns the template for rendering and includes the instance name
def render_title_template(*args, **kwargs):
sidebar = get_sidebar_config(kwargs)
return render_template(instance=config.config_calibre_web_title, sidebar=sidebar,
accept=constants.EXTENSIONS_UPLOAD, read_book_ids=get_readbooks_ids(),
*args, **kwargs)

@ -85,6 +85,7 @@ class SyncToken:
"archive_last_modified": {"type": "string"},
"reading_state_last_modified": {"type": "string"},
"tags_last_modified": {"type": "string"},
"books_last_id": {"type": "integer", "optional": True}
},
}
@ -96,6 +97,7 @@ class SyncToken:
archive_last_modified=datetime.min,
reading_state_last_modified=datetime.min,
tags_last_modified=datetime.min,
books_last_id=-1
):
self.raw_kobo_store_token = raw_kobo_store_token
self.books_last_created = books_last_created
@ -103,6 +105,7 @@ class SyncToken:
self.archive_last_modified = archive_last_modified
self.reading_state_last_modified = reading_state_last_modified
self.tags_last_modified = tags_last_modified
self.books_last_id = books_last_id
@staticmethod
def from_headers(headers):
@ -137,9 +140,12 @@ class SyncToken:
archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified")
reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified")
tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified")
books_last_id = data_json["books_last_id"]
except TypeError:
log.error("SyncToken timestamps don't parse to a datetime.")
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
except KeyError:
books_last_id = -1
return SyncToken(
raw_kobo_store_token=raw_kobo_store_token,
@ -147,7 +153,8 @@ class SyncToken:
books_last_modified=books_last_modified,
archive_last_modified=archive_last_modified,
reading_state_last_modified=reading_state_last_modified,
tags_last_modified=tags_last_modified
tags_last_modified=tags_last_modified,
books_last_id=books_last_id
)
def set_kobo_store_header(self, store_headers):
@ -170,7 +177,8 @@ class SyncToken:
"books_last_created": to_epoch_timestamp(self.books_last_created),
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified),
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified)
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified),
"books_last_id":self.books_last_id
},
}
return b64encode_json(token)

@ -110,7 +110,7 @@ class WorkerThread(threading.Thread):
# We don't use a daemon here because we don't want the tasks to just be abruptly halted, leading to
# possible file / database corruption
item = self.queue.get(timeout=1)
except queue.Empty as ex:
except queue.Empty:
time.sleep(1)
continue
@ -161,7 +161,7 @@ class CalibreTask:
self.run(*args)
except Exception as e:
self._handleError(str(e))
log.exception(e)
log.debug_or_exception(e)
self.end_time = datetime.now()
@ -210,7 +210,6 @@ class CalibreTask:
self._progress = x
def _handleError(self, error_message):
log.exception(error_message)
self.stat = STAT_FAIL
self.progress = 1
self.error = error_message

@ -22,6 +22,7 @@
from __future__ import division, print_function, unicode_literals
from datetime import datetime
import sys
from flask import Blueprint, request, flash, redirect, url_for
from flask_babel import gettext as _
@ -29,8 +30,9 @@ from flask_login import login_required, current_user
from sqlalchemy.sql.expression import func
from sqlalchemy.exc import OperationalError, InvalidRequestError
from . import logger, ub, calibre_db
from .web import login_required_if_no_ano, render_title_template
from . import logger, ub, calibre_db, db
from .render_template import render_title_template
from .usermanagement import login_required_if_no_ano
shelf = Blueprint('shelf', __name__)
@ -138,18 +140,14 @@ def search_to_shelf(shelf_id):
books_for_shelf = ub.searched_ids[current_user.id]
if not books_for_shelf:
log.error("Books are already part of %s", shelf)
log.error("Books are already part of %s", shelf.name)
flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
return redirect(url_for('web.index'))
maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()
if maxOrder[0] is None:
maxOrder = 0
else:
maxOrder = maxOrder[0]
maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0
for book in books_for_shelf:
maxOrder = maxOrder + 1
maxOrder += 1
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder))
shelf.last_modified = datetime.utcnow()
try:
@ -322,8 +320,11 @@ def delete_shelf_helper(cur_shelf):
ub.session.delete(cur_shelf)
ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete()
ub.session.add(ub.ShelfArchive(uuid=cur_shelf.uuid, user_id=cur_shelf.user_id))
ub.session.commit()
log.info("successfully deleted %s", cur_shelf)
try:
ub.session.commit()
log.info("successfully deleted %s", cur_shelf)
except OperationalError:
ub.session.rollback()
@ -333,44 +334,22 @@ def delete_shelf(shelf_id):
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
try:
delete_shelf_helper(cur_shelf)
except (OperationalError, InvalidRequestError):
except InvalidRequestError:
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
return redirect(url_for('web.index'))
@shelf.route("/shelf/<int:shelf_id>", defaults={'shelf_type': 1})
@shelf.route("/shelf/<int:shelf_id>/<int:shelf_type>")
@shelf.route("/simpleshelf/<int:shelf_id>")
@login_required_if_no_ano
def show_shelf(shelf_type, shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
result = list()
# user is allowed to access shelf
if shelf and check_shelf_view_permissions(shelf):
page = "shelf.html" if shelf_type == 1 else 'shelfdown.html'
def show_simpleshelf(shelf_id):
return render_show_shelf(2, shelf_id, 1, None)
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\
.order_by(ub.BookShelf.order.asc()).all()
for book in books_in_shelf:
cur_book = calibre_db.get_filtered_book(book.book_id)
if cur_book:
result.append(cur_book)
else:
cur_book = calibre_db.get_book(book.book_id)
if not cur_book:
log.info('Not existing book %s in %s deleted', book.book_id, shelf)
try:
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete()
ub.session.commit()
except (OperationalError, InvalidRequestError):
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
return render_title_template(page, entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name),
shelf=shelf, page="shelf")
else:
flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error")
return redirect(url_for("web.index"))
@shelf.route("/shelf/<int:shelf_id>", defaults={"sort_param": "order", 'page': 1})
@shelf.route("/shelf/<int:shelf_id>/<sort_param>", defaults={'page': 1})
@shelf.route("/shelf/<int:shelf_id>/<sort_param>/<int:page>")
@login_required_if_no_ano
def show_shelf(shelf_id, sort_param, page):
return render_show_shelf(1, shelf_id, page, sort_param)
@shelf.route("/shelf/order/<int:shelf_id>", methods=["GET", "POST"])
@ -394,22 +373,80 @@ def order_shelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
result = list()
if shelf and check_shelf_view_permissions(shelf):
books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
.order_by(ub.BookShelf.order.asc()).all()
for book in books_in_shelf2:
cur_book = calibre_db.get_filtered_book(book.book_id)
if cur_book:
result.append({'title': cur_book.title,
'id': cur_book.id,
'author': cur_book.authors,
'series': cur_book.series,
'series_index': cur_book.series_index})
else:
cur_book = calibre_db.get_book(book.book_id)
result.append({'title': _('Hidden Book'),
'id': cur_book.id,
'author': [],
'series': []})
result = calibre_db.session.query(db.Books)\
.join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id , isouter=True) \
.add_columns(calibre_db.common_filters().label("visible")) \
.filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all()
return render_title_template('shelf_order.html', entries=result,
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
shelf=shelf, page="shelforder")
def change_shelf_order(shelf_id, order):
result = calibre_db.session.query(db.Books).join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id)\
.filter(ub.BookShelf.shelf == shelf_id).order_by(*order).all()
for index, entry in enumerate(result):
book = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
.filter(ub.BookShelf.book_id == entry.id).first()
book.order = index
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
# check user is allowed to access shelf
if shelf and check_shelf_view_permissions(shelf):
if shelf_type == 1:
# order = [ub.BookShelf.order.asc()]
if sort_param == 'pubnew':
change_shelf_order(shelf_id, [db.Books.pubdate.desc()])
if sort_param == 'pubold':
change_shelf_order(shelf_id, [db.Books.pubdate])
if sort_param == 'abc':
change_shelf_order(shelf_id, [db.Books.sort])
if sort_param == 'zyx':
change_shelf_order(shelf_id, [db.Books.sort.desc()])
if sort_param == 'new':
change_shelf_order(shelf_id, [db.Books.timestamp.desc()])
if sort_param == 'old':
change_shelf_order(shelf_id, [db.Books.timestamp])
if sort_param == 'authaz':
change_shelf_order(shelf_id, [db.Books.author_sort.asc()])
if sort_param == 'authza':
change_shelf_order(shelf_id, [db.Books.author_sort.desc()])
page = "shelf.html"
pagesize = 0
else:
pagesize = sys.maxsize
page = 'shelfdown.html'
result, __, pagination = calibre_db.fill_indexpage(page_no, pagesize,
db.Books,
ub.BookShelf.shelf == shelf_id,
[ub.BookShelf.order.asc()],
ub.BookShelf,ub.BookShelf.book_id == db.Books.id)
# delete chelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web
wrong_entries = calibre_db.session.query(ub.BookShelf)\
.join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True)\
.filter(db.Books.id == None).all()
for entry in wrong_entries:
log.info('Not existing book {} in {} deleted'.format(entry.book_id, shelf))
try:
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == entry.book_id).delete()
ub.session.commit()
except (OperationalError, InvalidRequestError):
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
return render_title_template(page,
entries=result,
pagination=pagination,
title=_(u"Shelf: '%(name)s'", name=shelf.name),
shelf=shelf,
page="shelf")
else:
flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error")
return redirect(url_for("web.index"))

@ -240,7 +240,7 @@ body.blur .row-fluid .col-sm-10 {
.col-sm-10 .book-meta > div.btn-toolbar:after {
content: '';
direction: block;
direction: ltr;
position: fixed;
top: 120px;
right: 0;
@ -398,20 +398,17 @@ body.blur .row-fluid .col-sm-10 {
.shelforder #sortTrue > div:hover {
background-color: hsla(0, 0%, 100%, .06) !important;
cursor: move;
cursor: grab;
cursor: -webkit-grab;
color: #eee
}
.shelforder #sortTrue > div:active {
cursor: grabbing;
cursor: -webkit-grabbing
}
.shelforder #sortTrue > div:before {
content: "\EA53";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
margin-right: 30px;
margin-left: 15px;
vertical-align: bottom;
@ -446,7 +443,7 @@ body.blur .row-fluid .col-sm-10 {
body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before {
content: "\e155";
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
font-style: normal;
font-weight: 400;
line-height: 1;
@ -494,7 +491,7 @@ body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before {
}
#have_read_cb + label:before, #have_read_cb:checked + label:before {
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
font-size: 16px;
height: 40px;
width: 60px;
@ -550,13 +547,12 @@ body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before {
height: 60px;
width: 50px;
cursor: pointer;
margin: 0;
display: inline-block;
margin-top: -4px;
margin: -4px 0 0;
}
#archived_cb + label:before, #archived_cb:checked + label:before {
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
font-size: 16px;
height: 40px;
width: 60px;
@ -618,7 +614,7 @@ div[aria-label="Edit/Delete book"] > .btn > span {
div[aria-label="Edit/Delete book"] > .btn > span:before {
content: "\EA5d";
font-family: plex-icons;
font-family: plex-icons, serif;
font-size: 20px;
padding: 16px 15px;
display: inline-block;
@ -641,7 +637,7 @@ div[aria-label="Edit/Delete book"] > .btn > span:hover {
width: 225px;
max-width: 225px;
position: relative !important;
left: auto !important;
left: auto !important;
top: auto !important;
-webkit-transform: none !important;
-ms-transform: none !important;
@ -760,7 +756,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a
.home-btn {
color: hsla(0, 0%, 100%, .7);
line-height: 34.29px;
line-height: 34px;
margin: 0;
padding: 0;
position: absolute;
@ -770,7 +766,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a
.home-btn > a {
color: rgba(255, 255, 255, .7);
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
line-height: 60px;
position: relative;
text-align: center;
@ -800,7 +796,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.home-btn > a:hove
.glyphicon-search:before {
content: "\EA4F";
font-family: plex-icons
font-family: plex-icons, serif
}
#nav_about:after, .profileDrop > span:after, .profileDrop > span:before {
@ -833,7 +829,7 @@ body:not(.read-frame) {
overflow: hidden;
margin: 0;
/* scroll bar fix for firefox */
scrollbar-color: hsla(0, 0%, 100%, .2) transparent;
scrollbar-color: hsla(0, 0%, 100%, .2) transparent;
scrollbar-width: thin;
}
@ -966,7 +962,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d
#form-upload .form-group .btn:before {
content: "\e043";
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
font-style: normal;
font-weight: 400;
line-height: 1;
@ -991,7 +987,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d
#form-upload .form-group .btn:after {
content: "\EA13";
position: absolute;
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
font-size: 8px;
background: #3c444a;
color: hsla(0, 0%, 100%, .7);
@ -1019,7 +1015,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d
text-transform: none;
font-weight: 400;
font-style: normal;
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1;
@ -1075,7 +1071,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d
body > div.navbar.navbar-default.navbar-static-top > div > form > div > span > button:before {
content: "\EA32";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
color: #eee;
background: #555;
font-size: 10px;
@ -1097,7 +1093,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form > div > span > b
body > div.navbar.navbar-default.navbar-static-top > div > form:before {
content: "\EA4F";
display: block;
font-family: plex-icons;
font-family: plex-icons, serif;
position: absolute;
color: #eee;
font-weight: 400;
@ -1120,7 +1116,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form:before {
body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > div > span.input-group-btn:before {
content: "\EA4F";
display: block;
font-family: plex-icons;
font-family: plex-icons, serif;
position: absolute;
left: -298px;
top: 8px;
@ -1193,7 +1189,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.c
body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul > li > #top_admin > .glyphicon-dashboard::before {
content: "\EA31";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
font-size: 20px
}
@ -1272,7 +1268,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover {
user-select: none
}
.navigation li, .navigation li:not(ul>li) {
.navigation li, .navigation li:not(ul > li) {
border-radius: 0 4px 4px 0
}
@ -1352,32 +1348,32 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover {
#nav_hot .glyphicon-fire::before {
content: "\1F525";
font-family: glyphicons regular
font-family: glyphicons regular, serif
}
.glyphicon-star:before {
content: "\EA10";
font-family: plex-icons-new
font-family: plex-icons-new, serif
}
#nav_rand .glyphicon-random::before {
content: "\EA44";
font-family: plex-icons-new
font-family: plex-icons-new, serif
}
.glyphicon-list::before {
content: "\EA4D";
font-family: plex-icons-new
font-family: plex-icons-new, serif
}
#nav_about .glyphicon-info-sign::before {
content: "\EA26";
font-family: plex-icons-new
font-family: plex-icons-new, serif
}
#nav_cat .glyphicon-inbox::before, .glyphicon-tags::before {
content: "\E067";
font-family: Glyphicons Regular;
font-family: Glyphicons Regular, serif;
margin-left: 2px
}
@ -1423,7 +1419,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover {
.navigation .create-shelf a:before {
content: "\EA13";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
font-size: 100%;
padding-right: 10px;
vertical-align: middle
@ -1473,7 +1469,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover {
#books > .cover > a:before, #books_rand > .cover > a:before, .book.isotope-item > .cover > a:before, body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 > div.col-sm-12 > div.col-sm-2 > a:before {
content: "\e352";
font-family: Glyphicons Regular;
font-family: Glyphicons Regular, serif;
background: var(--color-secondary);
border-radius: 50%;
font-weight: 400;
@ -1521,8 +1517,8 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form
top: 0;
left: 0;
opacity: 0;
background: -webkit-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
background: -o-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
background: -webkit-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
background: -o-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
z-index: -9
}
@ -1562,8 +1558,8 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form
top: 0;
left: 0;
opacity: 0;
background: -webkit-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
background: -o-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
background: -webkit-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
background: -o-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%)
}
@ -1739,7 +1735,7 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 {
body.me > div.container-fluid > div.row-fluid > div.col-sm-10:before {
content: '';
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
font-style: normal;
font-weight: 400;
font-size: 6vw;
@ -1947,7 +1943,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.pagination .page-next > a
body > div.container-fluid > div > div.col-sm-10 > div.pagination .page-previous > a
{
top: 0;
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
font-weight: 100;
-webkit-font-smoothing: antialiased;
line-height: 60px;
@ -2026,7 +2022,7 @@ body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container > di
body.serieslist > div.container-fluid > div > div.col-sm-10:before {
content: "\e044";
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
font-style: normal;
font-weight: 400;
line-height: 1;
@ -2130,7 +2126,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container
body.catlist > div.container-fluid > div.row-fluid > div.col-sm-10:before {
content: "\E067";
font-family: Glyphicons Regular;
font-family: Glyphicons Regular, serif;
font-style: normal;
font-weight: 400;
line-height: 1;
@ -2150,7 +2146,7 @@ body.catlist > div.container-fluid > div.row-fluid > div.col-sm-10:before {
body.authorlist > div.container-fluid > div.row-fluid > div.col-sm-10:before, body.langlist > div.container-fluid > div > div.col-sm-10:before {
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
font-style: normal;
font-weight: 400;
line-height: 1;
@ -2249,7 +2245,7 @@ body.langlist > div.container-fluid > div > div.col-sm-10 > div.container:before
body.advsearch > div.container-fluid > div > div.col-sm-10 > div.col-md-10.col-lg-6 {
padding: 15px 10px 15px 40px;
}
}
@media screen and (max-width: 992px) {
body.advsearch > div.container-fluid > div > div.col-sm-10 > div.col-md-10.col-lg-6 {
@ -2491,7 +2487,6 @@ body > div.container-fluid > div > div.col-sm-10 > div.col-sm-8 > form > .btn.bt
}
textarea {
resize: none;
resize: vertical
}
@ -2837,7 +2832,7 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form
body.advanced_search > div.container-fluid > div > div.col-sm-10 > div.col-sm-8:before {
content: "\EA4F";
font-family: plex-icons;
font-family: plex-icons, serif;
font-style: normal;
font-weight: 400;
line-height: 1;
@ -3159,7 +3154,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div.
#add-to-shelf > span.glyphicon.glyphicon-list:before {
content: "\EA59";
font-family: plex-icons;
font-family: plex-icons, serif;
font-size: 18px
}
@ -3171,7 +3166,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div.
#read-in-browser > span.glyphicon-eye-open:before, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.glyphicon-eye-open:before {
content: "\e352";
font-family: Glyphicons Regular;
font-family: Glyphicons Regular, serif;
font-size: 18px;
padding-right: 5px
}
@ -3183,7 +3178,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div.
#btnGroupDrop1 > span.glyphicon-download:before {
font-size: 20px;
content: "\ea66";
font-family: plex-icons
font-family: plex-icons, serif
}
.col-sm-10 .book-meta > div.btn-toolbar {
@ -3287,7 +3282,6 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd
-webkit-box-shadow: 0 4px 10px rgba(0, 0, 0, .35);
box-shadow: 0 4px 10px rgba(0, 0, 0, .35);
-o-transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4);
transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4);
transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4), -webkit-transform .15s cubic-bezier(.6, .4, .2, 1.4);
-webkit-transform-origin: center top;
-ms-transform-origin: center top;
@ -3416,7 +3410,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover > .btn-primary:l
.book-meta > div.more-stuff > .btn-toolbar > .btn-group[aria-label="Remove from shelves"] > a > .glyphicon-remove:before {
content: "\ea64";
font-family: plex-icons
font-family: plex-icons, serif
}
body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > .col-sm-6 {
@ -3530,7 +3524,7 @@ body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-b
body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [data-target="#DeleteShelfDialog"]:before {
content: "\EA6D";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
position: absolute;
color: hsla(0, 0%, 100%, .7);
font-size: 20px;
@ -3560,7 +3554,7 @@ body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-b
body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [href*=edit]:before {
content: "\EA5d";
font-family: plex-icons;
font-family: plex-icons, serif;
position: absolute;
color: hsla(0, 0%, 100%, .7);
font-size: 20px;
@ -3590,7 +3584,7 @@ body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-b
body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [href*=order]:before {
content: "\E409";
font-family: Glyphicons Regular;
font-family: Glyphicons Regular, serif;
position: absolute;
color: hsla(0, 0%, 100%, .7);
font-size: 20px;
@ -3727,7 +3721,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a
.plexBack > a {
color: rgba(255, 255, 255, .7);
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
-webkit-font-variant-ligatures: normal;
font-variant-ligatures: normal;
line-height: 60px;
@ -3839,11 +3833,9 @@ body.login > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.l
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
border-style: solid;
vertical-align: middle;
-webkit-transition: border .2s, -webkit-transform .4s;
-o-transition: border .2s, transform .4s;
transition: border .2s, transform .4s;
transition: border .2s, transform .4s, -webkit-transform .4s;
margin: 9px 6px
}
@ -3862,11 +3854,9 @@ body.login > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.l
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
border-style: solid;
vertical-align: middle;
-webkit-transition: border .2s, -webkit-transform .4s;
-o-transition: border .2s, transform .4s;
transition: border .2s, transform .4s;
transition: border .2s, transform .4s, -webkit-transform .4s;
margin: 12px 6px
}
@ -3946,7 +3936,7 @@ body.author img.bg-blur[src=undefined] {
body.author:not(.authorlist) .undefined-img:before {
content: "\e008";
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
font-style: normal;
font-weight: 400;
line-height: 1;
@ -4095,7 +4085,7 @@ body.shelf.modal-open > .container-fluid {
font-size: 18px;
color: #999;
display: inline-block;
font-family: Glyphicons Regular;
font-family: Glyphicons Regular, serif;
font-style: normal;
font-weight: 400
}
@ -4196,7 +4186,7 @@ body.shelf.modal-open > .container-fluid {
#remove-from-shelves > .btn > span:before {
content: "\EA52";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
color: transparent;
padding-left: 5px
}
@ -4208,7 +4198,7 @@ body.shelf.modal-open > .container-fluid {
#remove-from-shelves > a:first-of-type:before {
content: "\EA4D";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
position: absolute;
color: hsla(0, 0%, 100%, .45);
font-style: normal;
@ -4248,7 +4238,7 @@ body.shelf.modal-open > .container-fluid {
content: "\E208";
padding-right: 10px;
display: block;
font-family: Glyphicons Regular;
font-family: Glyphicons Regular, serif;
font-style: normal;
font-weight: 400;
position: absolute;
@ -4259,7 +4249,6 @@ body.shelf.modal-open > .container-fluid {
opacity: .5;
-webkit-transition: -webkit-transform .3s ease-out;
-o-transition: transform .3s ease-out;
transition: transform .3s ease-out;
transition: transform .3s ease-out, -webkit-transform .3s ease-out;
-webkit-transform: translate(0, -60px);
-ms-transform: translate(0, -60px);
@ -4319,7 +4308,7 @@ body.advanced_search > div.container-fluid > div > div.col-sm-10 > div.col-sm-8
.glyphicon-remove:before {
content: "\EA52";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
font-weight: 400
}
@ -4405,7 +4394,7 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div
body:not(.blur) #nav_new:before {
content: "\EA4F";
font-family: plex-icons;
font-family: plex-icons, serif;
font-style: normal;
font-weight: 400;
line-height: 1;
@ -4431,7 +4420,7 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div
color: hsla(0, 0%, 100%, .7);
cursor: pointer;
display: block;
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
font-size: 20px;
font-stretch: 100%;
font-style: normal;
@ -4527,12 +4516,12 @@ body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > d
}
body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row > div.col .table > tbody > tr > td, body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row > div.col .table > tbody > tr > th, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > .table > tbody > tr > td, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > .table > tbody > tr > th {
border: collapse
border: collapse;
}
body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.newuser.admin > div.container-fluid > div.row-fluid > div.col-sm-10::before {
content: '';
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
font-style: normal;
font-weight: 400;
font-size: 6vw;
@ -4636,7 +4625,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
content: "\e352";
display: inline-block;
position: absolute;
font-family: Glyphicons Regular;
font-family: Glyphicons Regular, serif;
background: var(--color-secondary);
color: #fff;
border-radius: 50%;
@ -4674,8 +4663,8 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
top: 0;
left: 0;
opacity: 0;
background: -webkit-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
background: -o-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
background: -webkit-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
background: -o-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%)
}
@ -4727,7 +4716,7 @@ body.admin td > a:hover {
.glyphicon-ok::before {
content: "\EA55";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
font-weight: 400
}
@ -4796,7 +4785,7 @@ body:not(.blur):not(.login):not(.me):not(.author):not(.editbook):not(.upload):no
background-position: center center, center center, center center !important;
background-size: auto, auto, cover !important;
-webkit-background-size: auto, auto, cover !important;
-moz-background-size: autom, auto, cover !important;
-moz-background-size: auto, auto, cover !important;
-o-background-size: auto, auto, cover !important;
width: 100%;
height: 60px;
@ -4862,7 +4851,6 @@ body.read:not(.blur) a[href*=readbooks] {
.tooltip.in {
opacity: 1;
-o-transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4);
transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4);
transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4), -webkit-transform .15s cubic-bezier(.6, .4, .2, 1.4);
-webkit-transform: translate(0) scale(1);
-ms-transform: translate(0) scale(1);
@ -4962,7 +4950,7 @@ body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div
body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-3 > div.text-center > #delete:before, body.upload > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-3 > div.text-center > #delete:before {
content: "\EA6D";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
font-size: 18px;
color: hsla(0, 0%, 100%, .7)
}
@ -5047,7 +5035,7 @@ body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.boot
body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.bootstrap-table > div.fixed-table-container > div.fixed-table-body > #table > thead > tr > th > .th-inner.asc:after {
content: "\EA58";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
font-weight: 400;
right: 20px;
position: absolute
@ -5055,7 +5043,7 @@ body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.boot
body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.bootstrap-table > div.fixed-table-container > div.fixed-table-body > #table > thead > tr > th > .th-inner.desc:after {
content: "\EA57";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
font-weight: 400;
right: 20px;
position: absolute
@ -5118,7 +5106,7 @@ body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.boot
.epub-back:before {
content: "\EA1C";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
font-weight: 400;
color: #4f4f4f;
position: absolute;
@ -5281,7 +5269,7 @@ body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm
body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-3 > div.text-center > #delete:before, body.upload > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-3 > div.text-center > #delete:before {
content: "\EA6D";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
font-size: 18px;
color: hsla(0, 0%, 100%, .7);
vertical-align: super
@ -5441,7 +5429,7 @@ body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm
#main-nav + #scnd-nav .create-shelf a:before {
content: "\EA13";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
font-size: 100%;
padding-right: 10px;
vertical-align: middle
@ -5486,7 +5474,7 @@ body.admin.modal-open .navbar {
content: "\E208";
padding-right: 10px;
display: block;
font-family: Glyphicons Regular;
font-family: Glyphicons Regular, serif;
font-style: normal;
font-weight: 400;
position: absolute;
@ -5497,7 +5485,6 @@ body.admin.modal-open .navbar {
opacity: .5;
-webkit-transition: -webkit-transform .3s ease-out;
-o-transition: transform .3s ease-out;
transition: transform .3s ease-out;
transition: transform .3s ease-out, -webkit-transform .3s ease-out;
-webkit-transform: translate(0, -60px);
-ms-transform: translate(0, -60px);
@ -5551,22 +5538,22 @@ body.admin.modal-open .navbar {
#RestartDialog > .modal-dialog > .modal-content > .modal-header:before {
content: "\EA4F";
font-family: plex-icons-new
font-family: plex-icons-new, serif
}
#ShutdownDialog > .modal-dialog > .modal-content > .modal-header:before {
content: "\E064";
font-family: glyphicons regular
font-family: glyphicons regular, serif
}
#StatusDialog > .modal-dialog > .modal-content > .modal-header:before {
content: "\EA15";
font-family: plex-icons-new
font-family: plex-icons-new, serif
}
#deleteModal > .modal-dialog > .modal-content > .modal-header:before {
content: "\EA6D";
font-family: plex-icons-new
font-family: plex-icons-new, serif
}
#RestartDialog > .modal-dialog > .modal-content > .modal-header:after {
@ -5957,7 +5944,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
.home-btn {
height: 48px;
line-height: 28.29px;
line-height: 28px;
right: 10px;
left: auto
}
@ -5969,7 +5956,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
.plexBack {
height: 48px;
line-height: 28.29px;
line-height: 28px;
left: 48px;
display: none
}
@ -6048,7 +6035,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > div > span.input-group-btn:before {
content: "\EA33";
display: block;
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
position: fixed;
left: 0;
top: 0;
@ -6200,7 +6187,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
#form-upload .form-group .btn:before {
content: "\e043";
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
line-height: 1;
-webkit-font-smoothing: antialiased;
color: #fff;
@ -6218,7 +6205,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
#form-upload .form-group .btn:after {
content: "\EA13";
position: absolute;
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
font-size: 8px;
background: #3c444a;
color: #fff;
@ -6271,7 +6258,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
}
#top_admin, #top_tasks {
padding: 11.5px 15px;
padding: 12px 15px;
font-size: 13px;
line-height: 1.71428571;
overflow: hidden
@ -6280,7 +6267,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
#top_admin > .glyphicon, #top_tasks > .glyphicon-tasks {
position: relative;
top: 0;
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
line-height: 1;
border-radius: 0;
background: 0 0;
@ -6299,7 +6286,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
#top_tasks > .glyphicon-tasks::before, body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul > li > #top_admin > .glyphicon-dashboard::before {
text-transform: none;
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
@ -6624,7 +6611,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
.author > .container-fluid > .row-fluid > .col-sm-10 > h2:after {
content: "\e008";
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
font-style: normal;
font-weight: 400;
line-height: 1;
@ -6829,7 +6816,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
color: hsla(0, 0%, 100%, .7);
cursor: pointer;
display: block;
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
font-size: 20px;
font-stretch: 100%;
font-style: normal;
@ -6996,11 +6983,9 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
border-style: solid;
vertical-align: middle;
-webkit-transition: border .2s, -webkit-transform .4s;
-o-transition: border .2s, transform .4s;
transition: border .2s, transform .4s;
transition: border .2s, transform .4s, -webkit-transform .4s;
margin: 12px 6px
}
@ -7019,18 +7004,16 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
border-style: solid;
vertical-align: middle;
-webkit-transition: border .2s, -webkit-transform .4s;
-o-transition: border .2s, transform .4s;
transition: border .2s, transform .4s;
transition: border .2s, transform .4s, -webkit-transform .4s;
margin: 9px 6px
}
body.author:not(.authorlist) .blur-wrapper:before, body.author > .container-fluid > .row-fluid > .col-sm-10 > h2:after {
content: "\e008";
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
font-weight: 400;
z-index: 9;
line-height: 1;
@ -7361,7 +7344,6 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
transform: translate3d(0, 0, 0);
-webkit-transition: -webkit-transform .5s;
-o-transition: transform .5s;
transition: transform .5s;
transition: transform .5s, -webkit-transform .5s;
z-index: 99
}
@ -7376,7 +7358,6 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
transform: translate3d(-240px, 0, 0);
-webkit-transition: -webkit-transform .5s;
-o-transition: transform .5s;
transition: transform .5s;
transition: transform .5s, -webkit-transform .5s;
top: 0;
margin: 0;
@ -7415,7 +7396,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
text-align: center;
min-width: 40px;
pointer-events: none;
color: #
// color: #
}
.col-xs-12 > .row > .col-xs-10 {
@ -7526,7 +7507,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
body.publisherlist > div.container-fluid > div > div.col-sm-10:before {
content: "\e241";
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
font-style: normal;
font-weight: 400;
line-height: 1;
@ -7546,7 +7527,7 @@ body.publisherlist > div.container-fluid > div > div.col-sm-10:before {
body.ratingslist > div.container-fluid > div > div.col-sm-10:before {
content: "\e007";
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
font-style: normal;
font-weight: 400;
line-height: 1;
@ -7572,7 +7553,7 @@ body.ratingslist > div.container-fluid > div > div.col-sm-10:before {
body.formatslist > div.container-fluid > div > div.col-sm-10:before {
content: "\e022";
font-family: 'Glyphicons Halflings';
font-family: 'Glyphicons Halflings', serif;
font-style: normal;
font-weight: 400;
line-height: 1;
@ -7747,7 +7728,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .editabl
body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphicon-trash:before {
content: "\EA6D";
font-family: plex-icons-new
font-family: plex-icons-new, serif
}
#DeleteDomain {
@ -7770,7 +7751,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic
content: "\E208";
padding-right: 10px;
display: block;
font-family: Glyphicons Regular;
font-family: Glyphicons Regular, serif;
font-style: normal;
font-weight: 400;
position: absolute;
@ -7781,7 +7762,6 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic
opacity: .5;
-webkit-transition: -webkit-transform .3s ease-out;
-o-transition: transform .3s ease-out;
transition: transform .3s ease-out;
transition: transform .3s ease-out, -webkit-transform .3s ease-out;
-webkit-transform: translate(0, -60px);
-ms-transform: translate(0, -60px);
@ -7820,7 +7800,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic
#DeleteDomain > .modal-dialog > .modal-content > .modal-header:before {
content: "\EA6D";
font-family: plex-icons-new;
font-family: plex-icons-new, serif;
padding-right: 10px;
font-size: 18px;
color: #999;

@ -1,11 +1,11 @@
body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{
display: none;
}
.cover .badge{
position: absolute;
top: 0;
left: 0;
color: #fff;
background-color: #cc7b19;
border-radius: 0;
padding: 0 8px;
@ -15,3 +15,8 @@ body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{
.cover{
box-shadow: 0 0 4px rgba(0,0,0,.6);
}
.cover .read{
padding: 0 0px;
line-height: 15px;
}

File diff suppressed because one or more lines are too long

@ -25,10 +25,9 @@ body {
overflow: hidden;
-webkit-transition: -webkit-transform 0.4s, width 0.2s;
-moz-transition: -webkit-transform 0.4s, width 0.2s;
-ms-transition: -webkit-transform 0.4s, width 0.2s;
transition: -webkit-transform 0.4s, width 0.2s;
-moz-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
-webkit-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
-ms-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
}
@ -45,7 +44,7 @@ body {
text-align: center;
-webkit-transition: opacity 0.5s;
-moz-transition: opacity 0.5s;
-ms-transition: opacity 0.5s;
transition: opacity 0.5s;
z-index: 10;
}
@ -79,7 +78,6 @@ body {
color: rgba(0, 0, 0, 0.6);
-moz-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
-webkit-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
-ms-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
}
@ -121,7 +119,6 @@ body {
font-weight: bold;
cursor: pointer;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
@ -147,7 +144,7 @@ body {
height: 100%;
-webkit-transition: -webkit-transform 0.5s;
-moz-transition: -moz-transform 0.5s;
-ms-transition: -moz-transform 0.5s;
transition: -moz-transform 0.5s;
overflow: hidden;
}
@ -183,7 +180,6 @@ body {
height: 14px;
-moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
-ms-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
}
@ -232,7 +228,6 @@ body {
input::-webkit-input-placeholder { color: #454545; }
input:-moz-placeholder { color: #454545; }
input:-ms-placeholder { color: #454545; }
#divider {
position: absolute;
@ -268,18 +263,18 @@ input:-ms-placeholder { color: #454545; }
width: 25%;
height: 100%;
visibility: hidden;
-webkit-transition: visibility 0 ease 0.5s;
-moz-transition: visibility 0 ease 0.5s;
-ms-transition: visibility 0 ease 0.5s;
-webkit-transition: visibility 0s ease 0.5s;
-moz-transition: visibility 0s ease 0.5s;
transition: visibility 0s ease 0.5s;
}
#sidebar.open #tocView,
#sidebar.open #bookmarksView {
overflow-y: auto;
visibility: visible;
-webkit-transition: visibility 0 ease 0;
-moz-transition: visibility 0 ease 0;
-ms-transition: visibility 0 ease 0;
-webkit-transition: visibility 0s ease 0s;
-moz-transition: visibility 0s ease 0s;
transition: visibility 0s ease 0s;
}
#sidebar.open #tocView {
@ -495,9 +490,8 @@ input:-ms-placeholder { color: #454545; }
position: fixed;
top: 50%;
left: 50%;
width: 50%;
// width: 50%;
width: 630px;
height: auto;
z-index: 2000;
visibility: hidden;
@ -518,7 +512,6 @@ input:-ms-placeholder { color: #454545; }
background: rgba(255, 255, 255, 0.8);
-webkit-transition: all 0.3s;
-moz-transition: all 0.3s;
-ms-transition: all 0.3s;
transition: all 0.3s;
}
@ -588,7 +581,6 @@ input:-ms-placeholder { color: #454545; }
opacity: 0;
-webkit-transition: all 0.3s;
-moz-transition: all 0.3s;
-ms-transition: all 0.3s;
transition: all 0.3s;
}
@ -601,7 +593,7 @@ input:-ms-placeholder { color: #454545; }
}
.md-content > .closer {
font-size: 18px;
//font-size: 18px;
position: absolute;
right: 0;
top: 0;
@ -663,7 +655,7 @@ input:-ms-placeholder { color: #454545; }
-ms-transform: translate(0, 0);
-webkit-transition: -webkit-transform .3s;
-moz-transition: -moz-transform .3s;
-ms-transition: -moz-transform .3s;
transition: -moz-transform .3s;
}
#main.closed {
@ -778,7 +770,7 @@ and (orientation : landscape)
}
[class^="icon-"]:before, [class*=" icon-"]:before {
font-family: "fontello";
font-family: "fontello", serif;
font-style: normal;
font-weight: normal;
speak: none;

@ -116,6 +116,7 @@ a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d
display: block;
max-width: 100%;
height: auto;
max-height: 100%;
}
.container-fluid .discover{ margin-bottom: 50px; }
@ -132,12 +133,19 @@ a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d
position: relative;
}
.container-fluid .book .cover img {
border: 1px solid #fff;
box-sizing: border-box;
height: 100%;
.container-fluid .book .cover span.img {
bottom: 0;
height: 100%;
position: absolute;
}
.container-fluid .book .cover span img {
position: relative;
top: 0;
left: 0;
height: 100%;
border: 1px solid #fff;
box-sizing: border-box;
-webkit-box-shadow: 0 5px 8px -6px #777;
-moz-box-shadow: 0 5px 8px -6px #777;
box-shadow: 0 5px 8px -6px #777;
@ -206,11 +214,22 @@ span.glyphicon.glyphicon-tags {
.navbar-default .navbar-toggle .icon-bar {background-color: #000; }
.navbar-default .navbar-toggle {border-color: #000; }
.cover { margin-bottom: 10px; }
.cover .badge{
position: absolute;
top: 2px;
left: 2px;
background-color: #777;
color: #000;
border-radius: 10px;
background-color: #fff;
}
.cover .read{
left: auto;
right: 2px;
width: 17px;
height: 17px;
display: inline-block;
padding: 2px;
}
.cover-height { max-height: 100px;}
@ -241,7 +260,7 @@ span.glyphicon.glyphicon-tags {
.button-link {color: #fff; }
.btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open .dropdown-toggle.btn-primary{ background-color: #1C5484; }
.btn-primary.disabled, .btn-primary[disabled], fieldset[disabled] .btn-primary, .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled:active, .btn-primary[disabled]:active, fieldset[disabled] .btn-primary:active, .btn-primary.disabled.active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary.active { background-color: #89B9E2; }
.btn-toolbar>.btn+.btn, .btn-toolbar>.btn-group+.btn, .btn-toolbar>.btn+.btn-group, .btn-toolbar>.btn-group+.btn-group { margin-left: 0px; }
.btn-toolbar>.btn+.btn, .btn-toolbar>.btn-group+.btn, .btn-toolbar>.btn+.btn-group, .btn-toolbar>.btn-group+.btn-group { margin-left: 0; }
.panel-body {background-color: #f5f5f5; }
.spinner {margin: 0 41%; }
.spinner2 {margin: 0 41%; }
@ -311,11 +330,11 @@ input.pill:not(:checked) + label .glyphicon { display: none; }
.editable-input { display:inline-block; }
.editable-cancel {
margin-bottom: 0px !important;
margin-bottom: 0 !important;
margin-left: 7px !important;
}
.editable-submit { margin-bottom: 0px !important; }
.editable-submit { margin-bottom: 0 !important; }
.filterheader { margin-bottom: 20px; }
.errorlink { margin-top: 20px; }
.emailconfig { margin-top: 10px; }
@ -326,7 +345,7 @@ input.pill:not(:checked) + label .glyphicon { display: none; }
}
div.log {
font-family: Courier New;
font-family: Courier New, serif;
font-size: 12px;
box-sizing: border-box;
height: 700px;

@ -411,19 +411,6 @@ bitjs.archive = bitjs.archive || {};
return "unrar.js";
};
/**
* Unrarrer5
* @extends {bitjs.archive.Unarchiver}
* @constructor
*/
bitjs.archive.Unrarrer5 = function(arrayBuffer, optPathToBitJS) {
bitjs.base(this, arrayBuffer, optPathToBitJS);
};
bitjs.inherits(bitjs.archive.Unrarrer5, bitjs.archive.Unarchiver);
bitjs.archive.Unrarrer5.prototype.getScriptFileName = function() {
return "unrar5.js";
};
/**
* Untarrer
* @extends {bitjs.archive.Unarchiver}

@ -14,10 +14,10 @@
/* global VM_FIXEDGLOBALSIZE, VM_GLOBALMEMSIZE, MAXWINMASK, VM_GLOBALMEMADDR, MAXWINSIZE */
// This file expects to be invoked as a Worker (see onmessage below).
/*importScripts("../io/bitstream.js");
importScripts("../io/bitstream.js");
importScripts("../io/bytebuffer.js");
importScripts("archive.js");
importScripts("rarvm.js");*/
importScripts("rarvm.js");
// Progress variables.
var currentFilename = "";
@ -29,21 +29,19 @@ var totalFilesInArchive = 0;
// Helper functions.
var info = function(str) {
console.log(str);
// postMessage(new bitjs.archive.UnarchiveInfoEvent(str));
postMessage(new bitjs.archive.UnarchiveInfoEvent(str));
};
var err = function(str) {
console.log(str);
// postMessage(new bitjs.archive.UnarchiveErrorEvent(str));
postMessage(new bitjs.archive.UnarchiveErrorEvent(str));
};
var postProgress = function() {
/*postMessage(new bitjs.archive.UnarchiveProgressEvent(
postMessage(new bitjs.archive.UnarchiveProgressEvent(
currentFilename,
currentFileNumber,
currentBytesUnarchivedInFile,
currentBytesUnarchived,
totalUncompressedBytesInArchive,
totalFilesInArchive));*/
totalFilesInArchive));
};
// shows a byte value as its hex representation
@ -1300,7 +1298,7 @@ var unrar = function(arrayBuffer) {
totalUncompressedBytesInArchive = 0;
totalFilesInArchive = 0;
//postMessage(new bitjs.archive.UnarchiveStartEvent());
postMessage(new bitjs.archive.UnarchiveStartEvent());
var bstream = new bitjs.io.BitStream(arrayBuffer, false /* rtl */);
var header = new RarVolumeHeader(bstream);
@ -1350,7 +1348,7 @@ var unrar = function(arrayBuffer) {
localfile.unrar();
if (localfile.isValid) {
// postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile));
postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile));
postProgress();
}
}
@ -1360,7 +1358,7 @@ var unrar = function(arrayBuffer) {
} else {
err("Invalid RAR file");
}
// postMessage(new bitjs.archive.UnarchiveFinishEvent());
postMessage(new bitjs.archive.UnarchiveFinishEvent());
};
// event.data.file has the ArrayBuffer.

@ -249,18 +249,26 @@ promisePublishers.done(function() {
);
});
$("#search").on("change input.typeahead:selected", function() {
$("#search").on("change input.typeahead:selected", function(event) {
if (event.target.type == "search" && event.target.tagName == "INPUT") {
return;
}
var form = $("form").serialize();
$.getJSON( getPath() + "/get_matching_tags", form, function( data ) {
$(".tags_click").each(function() {
if ($.inArray(parseInt($(this).children("input").first().val(), 10), data.tags) === -1 ) {
if (!($(this).hasClass("active"))) {
$(this).addClass("disabled");
if ($.inArray(parseInt($(this).val(), 10), data.tags) === -1) {
if(!$(this).prop("selected")) {
$(this).prop("disabled", true);
}
} else {
$(this).removeClass("disabled");
$(this).prop("disabled", false);
}
});
$("#include_tag option:selected").each(function () {
$("#exclude_tag").find("[value="+$(this).val()+"]").prop("disabled", true);
});
$('#include_tag').selectpicker("refresh");
$('#exclude_tag').selectpicker("refresh");
});
});

@ -19,16 +19,9 @@ var direction = 0; // Descending order
var sort = 0; // Show sorted entries
$("#sort_name").click(function() {
var class_name = $("h1").attr('Class') + "_sort_name";
var className = $("h1").attr("Class") + "_sort_name";
var obj = {};
obj[class_name] = sort;
/*$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/view",
data: JSON.stringify({obj}),
});*/
obj[className] = sort;
var count = 0;
var index = 0;

@ -162,15 +162,10 @@ function initProgressClick() {
function loadFromArrayBuffer(ab) {
var start = (new Date).getTime();
var h = new Uint8Array(ab, 0, 10);
unrar5(ab);
var pathToBitJS = "../../static/js/archive/";
var lastCompletion = 0;
/*if (h[0] === 0x52 && h[1] === 0x61 && h[2] === 0x72 && h[3] === 0x21) { //Rar!
if (h[7] === 0x01) {
unarchiver = new bitjs.archive.Unrarrer(ab, pathToBitJS);
} else {
unarchiver = new bitjs.archive.Unrarrer5(ab, pathToBitJS);
}
if (h[0] === 0x52 && h[1] === 0x61 && h[2] === 0x72 && h[3] === 0x21) { //Rar!
unarchiver = new bitjs.archive.Unrarrer(ab, pathToBitJS);
} else if (h[0] === 80 && h[1] === 75) { //PK (Zip)
unarchiver = new bitjs.archive.Unzipper(ab, pathToBitJS);
} else if (h[0] === 255 && h[1] === 216) { // JPEG
@ -234,7 +229,7 @@ function loadFromArrayBuffer(ab) {
unarchiver.start();
} else {
alert("Some error");
}*/
}
}
function scrollTocToActive() {

File diff suppressed because one or more lines are too long

@ -0,0 +1,8 @@
/*!
* Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2020 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
!function(e,n){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return n(e)}):"object"==typeof module&&module.exports?module.exports=n(require("jquery")):n(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"Vyberte ze seznamu",noneResultsText:"Pro hled\xe1n\xed {0} nebyly nalezeny \u017e\xe1dn\xe9 v\xfdsledky",countSelectedText:"Vybran\xe9 {0} z {1}",maxOptionsText:["Limit p\u0159ekro\u010den ({n} {var} max)","Limit skupiny p\u0159ekro\u010den ({n} {var} max)",["polo\u017eek","polo\u017eka"]],multipleSeparator:", ",selectAllText:"Vybrat v\u0161e",deselectAllText:"Zru\u0161it v\xfdb\u011br"}});

@ -0,0 +1,8 @@
/*!
* Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2020 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"Bitte w\xe4hlen...",noneResultsText:"Keine Ergebnisse f\xfcr {0}",countSelectedText:function(e,t){return 1==e?"{0} Element ausgew\xe4hlt":"{0} Elemente ausgew\xe4hlt"},maxOptionsText:function(e,t){return[1==e?"Limit erreicht ({n} Element max.)":"Limit erreicht ({n} Elemente max.)",1==t?"Gruppen-Limit erreicht ({n} Element max.)":"Gruppen-Limit erreicht ({n} Elemente max.)"]},selectAllText:"Alles ausw\xe4hlen",deselectAllText:"Nichts ausw\xe4hlen",multipleSeparator:", "}});

@ -0,0 +1,8 @@
/*!
* Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2020 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
!function(e,o){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return o(e)}):"object"==typeof module&&module.exports?module.exports=o(require("jquery")):o(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"No hay selecci\xf3n",noneResultsText:"No hay resultados {0}",countSelectedText:"Seleccionados {0} de {1}",maxOptionsText:["L\xedmite alcanzado ({n} {var} max)","L\xedmite del grupo alcanzado({n} {var} max)",["elementos","element"]],multipleSeparator:", ",selectAllText:"Seleccionar Todos",deselectAllText:"Desmarcar Todos"}});

@ -0,0 +1,8 @@
/*!
* Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2020 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"Ei valintoja",noneResultsText:"Ei hakutuloksia {0}",countSelectedText:function(e,t){return 1==e?"{0} valittu":"{0} valitut"},maxOptionsText:function(e,t){return["Valintojen maksimim\xe4\xe4r\xe4 ({n} saavutettu)","Ryhm\xe4n maksimim\xe4\xe4r\xe4 ({n} saavutettu)"]},selectAllText:"Valitse kaikki",deselectAllText:"Poista kaikki",multipleSeparator:", "}});

@ -0,0 +1,8 @@
/*!
* Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2020 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"Aucune s\xe9lection",noneResultsText:"Aucun r\xe9sultat pour {0}",countSelectedText:function(e,t){return 1<e?"{0} \xe9l\xe9ments s\xe9lectionn\xe9s":"{0} \xe9l\xe9ment s\xe9lectionn\xe9"},maxOptionsText:function(e,t){return[1<e?"Limite atteinte ({n} \xe9l\xe9ments max)":"Limite atteinte ({n} \xe9l\xe9ment max)",1<t?"Limite du groupe atteinte ({n} \xe9l\xe9ments max)":"Limite du groupe atteinte ({n} \xe9l\xe9ment max)"]},multipleSeparator:", ",selectAllText:"Tout s\xe9lectionner",deselectAllText:"Tout d\xe9s\xe9lectionner"}});

@ -0,0 +1,8 @@
/*!
* Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2020 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"V\xe1lasszon!",noneResultsText:"Nincs tal\xe1lat {0}",countSelectedText:function(e,t){return"{0} elem kiv\xe1lasztva"},maxOptionsText:function(e,t){return["Legfeljebb {n} elem v\xe1laszthat\xf3","A csoportban legfeljebb {n} elem v\xe1laszthat\xf3"]},selectAllText:"Mind",deselectAllText:"Egyik sem",multipleSeparator:", "}});

@ -0,0 +1,8 @@
/*!
* Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2020 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"Nessuna selezione",noneResultsText:"Nessun risultato per {0}",countSelectedText:function(e,t){return 1==e?"Selezionato {0} di {1}":"Selezionati {0} di {1}"},maxOptionsText:["Limite raggiunto ({n} {var} max)","Limite del gruppo raggiunto ({n} {var} max)",["elementi","elemento"]],multipleSeparator:", ",selectAllText:"Seleziona Tutto",deselectAllText:"Deseleziona Tutto"}});

@ -0,0 +1,8 @@
/*!
* Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2020 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"\u9078\u629e\u3055\u308c\u3066\u3044\u307e\u305b\u3093",noneResultsText:"'{0}'\u306f\u898b\u3064\u304b\u308a\u307e\u305b\u3093",countSelectedText:"{0}/{1} \u9078\u629e\u4e2d",maxOptionsText:["\u9078\u629e\u4e0a\u9650\u6570\u3092\u8d85\u3048\u3066\u3044\u307e\u3059(\u6700\u5927{n}{var})","\u30b0\u30eb\u30fc\u30d7\u306e\u9078\u629e\u4e0a\u9650\u6570\u3092\u8d85\u3048\u3066\u3044\u307e\u3059(\u6700\u5927{n}{var})",["\u30a2\u30a4\u30c6\u30e0","\u30a2\u30a4\u30c6\u30e0"]],selectAllText:"\u5168\u3066\u9078\u629e",deselectAllText:"\u9078\u629e\u3092\u30af\u30ea\u30a2",multipleSeparator:", "}});

@ -0,0 +1,8 @@
/*!
* Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2020 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"\u1798\u17b7\u1793\u1798\u17b6\u1793\u17a2\u17d2\u179c\u17b8\u1794\u17b6\u1793\u1787\u17d2\u179a\u17be\u179f\u179a\u17be\u179f",noneResultsText:"\u1798\u17b7\u1793\u1798\u17b6\u1793\u179b\u1791\u17d2\u1792\u1795\u179b {0}",countSelectedText:function(e,t){return"{0} \u1792\u17b6\u178f\u17bb\u178a\u17c2\u179b\u1794\u17b6\u1793\u1787\u17d2\u179a\u17be\u179f"},maxOptionsText:function(e,t){return[1==e?"\u1788\u17b6\u1793\u178a\u179b\u17cb\u178a\u17c2\u1793\u1780\u17c6\u178e\u178f\u17cb ( {n} \u1792\u17b6\u178f\u17bb\u17a2\u178f\u17b7\u1794\u179a\u1798\u17b6)":"\u17a2\u178f\u17b7\u1794\u179a\u1798\u17b6\u1788\u17b6\u1793\u178a\u179b\u17cb\u178a\u17c2\u1793\u1780\u17c6\u178e\u178f\u17cb ( {n} \u1792\u17b6\u178f\u17bb)",1==t?"\u178a\u17c2\u1793\u1780\u17c6\u178e\u178f\u17cb\u1780\u17d2\u179a\u17bb\u1798\u1788\u17b6\u1793\u178a\u179b\u17cb ( {n} \u17a2\u178f\u17b7\u1794\u179a\u1798\u17b6\u1792\u17b6\u178f\u17bb)":"\u17a2\u178f\u17b7\u1794\u179a\u1798\u17b6\u1780\u17d2\u179a\u17bb\u1798\u1788\u17b6\u1793\u178a\u179b\u17cb\u178a\u17c2\u1793\u1780\u17c6\u178e\u178f\u17cb ( {n} \u1792\u17b6\u178f\u17bb)"]},selectAllText:"\u1787\u17d2\u179a\u17be\u179f\u200b\u1799\u1780\u200b\u1791\u17b6\u17c6\u1784\u17a2\u179f\u17cb",deselectAllText:"\u1798\u17b7\u1793\u1787\u17d2\u179a\u17be\u179f\u200b\u1799\u1780\u200b\u1791\u17b6\u17c6\u1784\u17a2\u179f",multipleSeparator:", "}});

@ -0,0 +1,8 @@
/*!
* Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2020 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"Niets geselecteerd",noneResultsText:"Geen resultaten gevonden voor {0}",countSelectedText:"{0} van {1} geselecteerd",maxOptionsText:["Limiet bereikt ({n} {var} max)","Groep limiet bereikt ({n} {var} max)",["items","item"]],selectAllText:"Alles selecteren",deselectAllText:"Alles deselecteren",multipleSeparator:", "}});

@ -0,0 +1,8 @@
/*!
* Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2020 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
!function(e,n){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return n(e)}):"object"==typeof module&&module.exports?module.exports=n(require("jquery")):n(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"Nic nie zaznaczono",noneResultsText:"Brak wynik\xf3w wyszukiwania {0}",countSelectedText:"Zaznaczono {0} z {1}",maxOptionsText:["Osi\u0105gni\u0119to limit ({n} {var} max)","Limit grupy osi\u0105gni\u0119ty ({n} {var} max)",["elementy","element"]],selectAllText:"Zaznacz wszystkie",deselectAllText:"Odznacz wszystkie",multipleSeparator:", "}});

@ -0,0 +1,8 @@
/*!
* Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2020 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"\u041d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u043e",noneResultsText:"\u0421\u043e\u0432\u043f\u0430\u0434\u0435\u043d\u0438\u0439 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e {0}",countSelectedText:"\u0412\u044b\u0431\u0440\u0430\u043d\u043e {0} \u0438\u0437 {1}",maxOptionsText:["\u0414\u043e\u0441\u0442\u0438\u0433\u043d\u0443\u0442 \u043f\u0440\u0435\u0434\u0435\u043b ({n} {var} \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c)","\u0414\u043e\u0441\u0442\u0438\u0433\u043d\u0443\u0442 \u043f\u0440\u0435\u0434\u0435\u043b \u0432 \u0433\u0440\u0443\u043f\u043f\u0435 ({n} {var} \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c)",["\u0448\u0442.","\u0448\u0442."]],doneButtonText:"\u0417\u0430\u043a\u0440\u044b\u0442\u044c",selectAllText:"\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0432\u0441\u0435",deselectAllText:"\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0432\u0441\u0435",multipleSeparator:", "}});

@ -0,0 +1,8 @@
/*!
* Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2020 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"Inget valt",noneResultsText:"Inget s\xf6kresultat matchar {0}",countSelectedText:function(e,t){return 1===e?"{0} alternativ valt":"{0} alternativ valda"},maxOptionsText:function(e,t){return["Gr\xe4ns uppn\xe5d (max {n} alternativ)","Gr\xe4ns uppn\xe5d (max {n} gruppalternativ)"]},selectAllText:"Markera alla",deselectAllText:"Avmarkera alla",multipleSeparator:", "}});

@ -0,0 +1,8 @@
/*!
* Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2020 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
!function(e,i){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return i(e)}):"object"==typeof module&&module.exports?module.exports=i(require("jquery")):i(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"Hi\xe7biri se\xe7ilmedi",noneResultsText:"Hi\xe7bir sonu\xe7 bulunamad\u0131 {0}",countSelectedText:function(e,i){return"{0} \xf6\u011fe se\xe7ildi"},maxOptionsText:function(e,i){return[1==e?"Limit a\u015f\u0131ld\u0131 (maksimum {n} say\u0131da \xf6\u011fe )":"Limit a\u015f\u0131ld\u0131 (maksimum {n} say\u0131da \xf6\u011fe)","Grup limiti a\u015f\u0131ld\u0131 (maksimum {n} say\u0131da \xf6\u011fe)"]},selectAllText:"T\xfcm\xfcn\xfc Se\xe7",deselectAllText:"Se\xe7iniz",multipleSeparator:", "}});

@ -0,0 +1,8 @@
/*!
* Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2020 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){e.fn.selectpicker.defaults={noneSelectedText:"\u6ca1\u6709\u9009\u4e2d\u4efb\u4f55\u9879",noneResultsText:"\u6ca1\u6709\u627e\u5230\u5339\u914d\u9879",countSelectedText:"\u9009\u4e2d{1}\u4e2d\u7684{0}\u9879",maxOptionsText:["\u8d85\u51fa\u9650\u5236 (\u6700\u591a\u9009\u62e9{n}\u9879)","\u7ec4\u9009\u62e9\u8d85\u51fa\u9650\u5236(\u6700\u591a\u9009\u62e9{n}\u7ec4)"],multipleSeparator:", ",selectAllText:"\u5168\u9009",deselectAllText:"\u53d6\u6d88\u5168\u9009"}});

@ -110,6 +110,34 @@ $(document).ready(function() {
}
});
function ConfirmDialog(id, dataValue, yesFn, noFn) {
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
var path = src.substring(0, src.lastIndexOf("/"));
var $confirm = $("#GeneralDeleteModal");
// var dataValue= e.data('value'); // target.data('value');
$confirm.modal('show');
$.ajax({
method:"get",
dataType: "json",
url: path + "/../../ajax/loaddialogtexts/" + id,
success: function success(data) {
$("#header").html(data.header);
$("#text").html(data.main);
}
});
$("#btnConfirmYes").off('click').click(function () {
yesFn(dataValue);
$confirm.modal("hide");
});
$("#btnConfirmNo").off('click').click(function () {
if (typeof noFn !== 'undefined') {
noFn(dataValue);
}
$confirm.modal("hide");
});
}
$("#delete_confirm").click(function() {
//get data-id attribute of the clicked element
@ -213,6 +241,56 @@ $(function() {
});
}
function fillFileTable(path, type, folder, filt) {
if (window.location.pathname.endsWith("/basicconfig")) {
var request_path = "/../basicconfig/pathchooser/";
} else {
var request_path = "/../../ajax/pathchooser/";
}
$.ajax({
dataType: "json",
data: {
path: path,
folder: folder,
filter: filt
},
url: window.location.pathname + request_path,
success: function success(data) {
if ($("#element_selected").text() ==="") {
$("#element_selected").text(data.cwd);
}
$("#file_table > tbody > tr").each(function () {
if ($(this).attr("id") !== "parent") {
$(this).closest("tr").remove();
} else {
if(data.absolute && data.parentdir !== "") {
$(this)[0].attributes['data-path'].value = data.parentdir;
} else {
$(this)[0].attributes['data-path'].value = "..";
}
}
});
if (data.parentdir !== "") {
$("#parent").removeClass('hidden')
} else {
$("#parent").addClass('hidden')
}
// console.log(data);
data.files.forEach(function(entry) {
if(entry.type === "dir") {
var type = "<span class=\"glyphicon glyphicon-folder-close\"></span>";
} else {
var type = "";
}
$("<tr class=\"tr-clickable\" data-type=\"" + entry.type + "\" data-path=\"" +
entry.fullpath + "\"><td>" + type + "</td><td>" + entry.name + "</td><td>" +
entry.size + "</td></tr>").appendTo($("#file_table"));
});
},
timeout: 2000
});
}
$(".discover .row").isotope({
// options
itemSelector : ".book",
@ -402,20 +480,100 @@ $(function() {
$("#config_delete_kobo_token").show();
});
$("#btndeletetoken").click(function() {
//get data-id attribute of the clicked element
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
var path = src.substring(0, src.lastIndexOf("/"));
// var domainId = $(this).value("domainId");
$.ajax({
method:"get",
url: path + "/../../kobo_auth/deleteauthtoken/" + this.value,
});
$("#modalDeleteToken").modal("hide");
$("#config_delete_kobo_token").hide();
$("#config_delete_kobo_token").click(function() {
ConfirmDialog(
$(this).attr('id'),
$(this).data('value'),
function (value) {
var pathname = document.getElementsByTagName("script");
var src = pathname[pathname.length - 1].src;
var path = src.substring(0, src.lastIndexOf("/"));
$.ajax({
method: "get",
url: path + "/../../kobo_auth/deleteauthtoken/" + value,
});
$("#config_delete_kobo_token").hide();
}
);
});
$("#toggle_order_shelf").click(function() {
$("#new").toggleClass("disabled");
$("#old").toggleClass("disabled");
$("#asc").toggleClass("disabled");
$("#desc").toggleClass("disabled");
$("#auth_az").toggleClass("disabled");
$("#auth_za").toggleClass("disabled");
$("#pub_new").toggleClass("disabled");
$("#pub_old").toggleClass("disabled");
var alternative_text = $("#toggle_order_shelf").data('alt-text');
$("#toggle_order_shelf")[0].attributes['data-alt-text'].value = $("#toggle_order_shelf").html();
$("#toggle_order_shelf").html(alternative_text);
});
$("#btndeluser").click(function() {
ConfirmDialog(
$(this).attr('id'),
$(this).data('value'),
function(value){
var subform = $('#user_submit').closest("form");
subform.submit(function(eventObj) {
$(this).append('<input type="hidden" name="delete" value="True" />');
return true;
});
subform.submit();
}
);
});
$("#user_submit").click(function() {
this.closest("form").submit();
});
$("#delete_shelf").click(function() {
ConfirmDialog(
$(this).attr('id'),
$(this).data('value'),
function(value){
window.location.href = window.location.pathname + "/../../shelf/delete/" + value
}
);
});
$("#fileModal").on("show.bs.modal", function(e) {
var target = $(e.relatedTarget);
var path = $("#" + target.data("link"))[0].value;
var folder = target.data("folderonly");
var filter = target.data("filefilter");
$("#element_selected").text(path);
$("#file_confirm")[0].attributes["data-link"].value = target.data("link");
$("#file_confirm")[0].attributes["data-folderonly"].value = (typeof folder === 'undefined') ? false : true;
$("#file_confirm")[0].attributes["data-filefilter"].value = (typeof filter === 'undefined') ? "" : filter;
$("#file_confirm")[0].attributes["data-newfile"].value = target.data("newfile");
fillFileTable(path,"dir", folder, filter);
});
$("#file_confirm").click(function() {
$("#" + $(this).data("link"))[0].value = $("#element_selected").text()
});
$(document).on("click", ".tr-clickable", function() {
var path = this.attributes["data-path"].value;
var type = this.attributes["data-type"].value;
var folder = $(file_confirm).data("folderonly");
var filter = $(file_confirm).data("filefilter");
var newfile = $(file_confirm).data("newfile");
if (newfile !== 'undefined') {
$("#element_selected").text(path + $("#new_file".text()));
} else {
$("#element_selected").text(path);
}
if(type === "dir") {
fillFileTable(path, type, folder, filter);
}
});
$(window).resize(function() {
$(".discover .row").isotope("layout");
});

@ -45,14 +45,13 @@ $(function() {
if (selections.length < 1) {
$("#delete_selection").addClass("disabled");
$("#delete_selection").attr("aria-disabled", true);
}
else{
} else {
$("#delete_selection").removeClass("disabled");
$("#delete_selection").attr("aria-disabled", false);
}
});
$("#delete_selection").click(function() {
$("#books-table").bootstrapTable('uncheckAll');
$("#books-table").bootstrapTable("uncheckAll");
});
$("#merge_confirm").click(function() {
@ -63,8 +62,8 @@ $(function() {
url: window.location.pathname + "/../../ajax/mergebooks",
data: JSON.stringify({"Merge_books":selections}),
success: function success() {
$('#books-table').bootstrapTable('refresh');
$("#books-table").bootstrapTable('uncheckAll');
$("#books-table").bootstrapTable("refresh");
$("#books-table").bootstrapTable("uncheckAll");
}
});
});
@ -76,11 +75,11 @@ $(function() {
dataType: "json",
url: window.location.pathname + "/../../ajax/simulatemerge",
data: JSON.stringify({"Merge_books":selections}),
success: function success(book_titles) {
$.each(book_titles.from, function(i, item) {
success: function success(booTitles) {
$.each(booTitles.from, function(i, item) {
$("<span>- " + item + "</span>").appendTo("#merge_from");
});
$('#merge_to').text("- " + book_titles.to);
$("#merge_to").text("- " + booTitles.to);
}
});
@ -126,34 +125,35 @@ $(function() {
formatNoMatches: function () {
return "";
},
// 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);
}
});
}
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 = $("#books-table").bootstrapTable('getVisibleColumns');
var hidden = $("#books-table").bootstrapTable('getHiddenColumns');
var visibility =[]
var st = ""
var visible = $("#books-table").bootstrapTable("getVisibleColumns");
var hidden = $("#books-table").bootstrapTable("getHiddenColumns");
var st = "";
visible.forEach(function(item) {
st += "\""+ item.field + "\":\"" +"true"+ "\","
st += "\"" + item.field + "\":\"" + "true" + "\",";
});
hidden.forEach(function(item) {
st += "\""+ item.field + "\":\"" +"false"+ "\","
st += "\"" + item.field + "\":\"" + "false" + "\",";
});
st = st.slice(0, -1);
$.ajax({
@ -208,15 +208,13 @@ $(function() {
},
striped: false
});
$("#btndeletedomain").click(function() {
//get data-id attribute of the clicked element
var domainId = $(this).data("domainId");
function domain_handle(domainId) {
$.ajax({
method:"post",
url: window.location.pathname + "/../../ajax/deletedomain",
data: {"domainid":domainId}
});
$("#DeleteDomain").modal("hide");
$.ajax({
method:"get",
url: window.location.pathname + "/../../ajax/domainlist/1",
@ -235,12 +233,16 @@ $(function() {
$("#domain-deny-table").bootstrapTable("load", data);
}
});
}
$("#domain-allow-table").on("click-cell.bs.table", function (field, value, row, $element) {
if (value === 2) {
ConfirmDialog("btndeletedomain", $element.id, domain_handle);
}
});
//triggered when modal is about to be shown
$("#DeleteDomain").on("show.bs.modal", function(e) {
//get data-id attribute of the clicked element and store in button
var domainId = $(e.relatedTarget).data("domain-id");
$(e.currentTarget).find("#btndeletedomain").data("domainId", domainId);
$("#domain-deny-table").on("click-cell.bs.table", function (field, value, row, $element) {
if (value === 2) {
ConfirmDialog("btndeletedomain", $element.id, domain_handle);
}
});
$("#restrictModal").on("hidden.bs.modal", function () {
@ -253,14 +255,14 @@ $(function() {
$("#h3").addClass("hidden");
$("#h4").addClass("hidden");
});
function startTable(type) {
function startTable(type, user_id) {
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
var path = src.substring(0, src.lastIndexOf("/"));
$("#restrict-elements-table").bootstrapTable({
formatNoMatches: function () {
return "";
},
url: path + "/../../ajax/listrestriction/" + type,
url: path + "/../../ajax/listrestriction/" + type + "/" + user_id,
rowStyle: function(row) {
// console.log('Reihe :' + row + " Index :" + index);
if (row.id.charAt(0) === "a") {
@ -274,13 +276,13 @@ $(function() {
$.ajax ({
type: "Post",
data: "id=" + row.id + "&type=" + row.type + "&Element=" + encodeURIComponent(row.Element),
url: path + "/../../ajax/deleterestriction/" + type,
url: path + "/../../ajax/deleterestriction/" + type + "/" + user_id,
async: true,
timeout: 900,
success:function() {
$.ajax({
method:"get",
url: path + "/../../ajax/listrestriction/" + type,
url: path + "/../../ajax/listrestriction/" + type + "/" + user_id,
async: true,
timeout: 900,
success:function(data) {
@ -296,7 +298,7 @@ $(function() {
$("#restrict-elements-table").removeClass("table-hover");
$("#restrict-elements-table").on("editable-save.bs.table", function (e, field, row) {
$.ajax({
url: path + "/../../ajax/editrestriction/" + type,
url: path + "/../../ajax/editrestriction/" + type + "/" + user_id,
type: "Post",
data: row
});
@ -304,13 +306,13 @@ $(function() {
$("[id^=submit_]").click(function() {
$(this)[0].blur();
$.ajax({
url: path + "/../../ajax/addrestriction/" + type,
url: path + "/../../ajax/addrestriction/" + type + "/" + user_id,
type: "Post",
data: $(this).closest("form").serialize() + "&" + $(this)[0].name + "=",
success: function () {
$.ajax ({
method:"get",
url: path + "/../../ajax/listrestriction/" + type,
url: path + "/../../ajax/listrestriction/" + type + "/" + user_id,
async: true,
timeout: 900,
success:function(data) {
@ -323,21 +325,21 @@ $(function() {
});
}
$("#get_column_values").on("click", function() {
startTable(1);
startTable(1, 0);
$("#h2").removeClass("hidden");
});
$("#get_tags").on("click", function() {
startTable(0);
startTable(0, 0);
$("#h1").removeClass("hidden");
});
$("#get_user_column_values").on("click", function() {
startTable(3);
startTable(3, $(this).data('id'));
$("#h4").removeClass("hidden");
});
$("#get_user_tags").on("click", function() {
startTable(2);
startTable(2, $(this).data('id'));
$(this)[0].blur();
$("#h3").removeClass("hidden");
});
@ -347,7 +349,7 @@ $(function() {
/* Function for deleting domain restrictions */
function TableActions (value, row) {
return [
"<a class=\"danger remove\" data-toggle=\"modal\" data-target=\"#DeleteDomain\" data-domain-id=\"" + row.id
"<a class=\"danger remove\" data-value=\"" + row.id
+ "\" title=\"Remove\">",
"<i class=\"glyphicon glyphicon-trash\"></i>",
"</a>"

@ -9,7 +9,7 @@ from shutil import copyfile
from sqlalchemy.exc import SQLAlchemyError
from cps.services.worker import CalibreTask
from cps import calibre_db, db
from cps import db
from cps import logger, config
from cps.subproc_wrapper import process_open
from flask_babel import gettext as _
@ -33,8 +33,9 @@ class TaskConvert(CalibreTask):
def run(self, worker_thread):
self.worker_thread = worker_thread
if config.config_use_google_drive:
cur_book = calibre_db.get_book(self.bookid)
data = calibre_db.get_book_format(self.bookid, self.settings['old_book_format'])
worker_db = db.CalibreDB(expire_on_commit=False)
cur_book = worker_db.get_book(self.bookid)
data = worker_db.get_book_format(self.bookid, self.settings['old_book_format'])
df = gdriveutils.getFileFromEbooksFolder(cur_book.path,
data.name + "." + self.settings['old_book_format'].lower())
if df:
@ -44,10 +45,12 @@ class TaskConvert(CalibreTask):
if not os.path.exists(os.path.join(config.config_calibre_dir, cur_book.path)):
os.makedirs(os.path.join(config.config_calibre_dir, cur_book.path))
df.GetContentFile(datafile)
worker_db.session.close()
else:
error_message = _(u"%(format)s not found on Google Drive: %(fn)s",
format=self.settings['old_book_format'],
fn=data.name + "." + self.settings['old_book_format'].lower())
worker_db.session.close()
return error_message
filename = self._convert_ebook_format()
@ -71,21 +74,23 @@ class TaskConvert(CalibreTask):
def _convert_ebook_format(self):
error_message = None
local_session = db.CalibreDB().session
local_db = db.CalibreDB(expire_on_commit=False)
file_path = self.file_path
book_id = self.bookid
format_old_ext = u'.' + self.settings['old_book_format'].lower()
format_new_ext = u'.' + self.settings['new_book_format'].lower()
# check to see if destination format already exists -
# check to see if destination format already exists - or if book is in database
# if it does - mark the conversion task as complete and return a success
# this will allow send to kindle workflow to continue to work
if os.path.isfile(file_path + format_new_ext):
if os.path.isfile(file_path + format_new_ext) or\
local_db.get_book_format(self.bookid, self.settings['new_book_format']):
log.info("Book id %d already converted to %s", book_id, format_new_ext)
cur_book = calibre_db.get_book(book_id)
cur_book = local_db.get_book(book_id)
self.results['path'] = file_path
self.results['title'] = cur_book.title
self._handleSuccess()
local_db.session.close()
return os.path.basename(file_path + format_new_ext)
else:
log.info("Book id %d - target format of %s does not exist. Moving forward with convert.",
@ -105,18 +110,18 @@ class TaskConvert(CalibreTask):
check, error_message = self._convert_calibre(file_path, format_old_ext, format_new_ext)
if check == 0:
cur_book = calibre_db.get_book(book_id)
cur_book = local_db.get_book(book_id)
if os.path.isfile(file_path + format_new_ext):
# self.db_queue.join()
new_format = db.Data(name=cur_book.data[0].name,
book_format=self.settings['new_book_format'].upper(),
book=book_id, uncompressed_size=os.path.getsize(file_path + format_new_ext))
try:
local_session.merge(new_format)
local_session.commit()
local_db.session.merge(new_format)
local_db.session.commit()
except SQLAlchemyError as e:
local_session.rollback()
local_db.session.rollback()
log.error("Database error: %s", e)
local_db.session.close()
return
self.results['path'] = cur_book.path
self.results['title'] = cur_book.title
@ -125,6 +130,7 @@ class TaskConvert(CalibreTask):
return os.path.basename(file_path + format_new_ext)
else:
error_message = _('%(format)s format not found on disk', format=format_new_ext.upper())
local_db.session.close()
log.info("ebook converter failed with error while converting book")
if not error_message:
error_message = _('Ebook converter failed with unknown error')

@ -167,7 +167,7 @@ class TaskEmail(CalibreTask):
smtplib.stderr = org_smtpstderr
except (MemoryError) as e:
log.exception(e)
log.debug_or_exception(e)
self._handleError(u'MemoryError sending email: ' + str(e))
# return None
except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
@ -178,7 +178,7 @@ class TaskEmail(CalibreTask):
elif hasattr(e, "args"):
text = '\n'.join(e.args)
else:
log.exception(e)
log.debug_or_exception(e)
text = ''
self._handleError(u'Smtplib Error sending email: ' + text)
# return None
@ -225,7 +225,7 @@ class TaskEmail(CalibreTask):
data = file_.read()
file_.close()
except IOError as e:
log.exception(e)
log.debug_or_exception(e)
log.error(u'The requested file could not be read. Maybe wrong permissions?')
return None

@ -36,7 +36,10 @@
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" />
<span class="img">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" />
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>
</div>
<div class="meta">

@ -197,7 +197,8 @@
{% endblock %}
{% block modal %}
{{ delete_book(book.id) }}
{{ delete_book() }}
{{ delete_confirm_modal() }}
<div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel">
<div class="modal-dialog modal-lg" role="document">

@ -61,7 +61,7 @@
</table>
{% endblock %}
{% block modal %}
{{ delete_book(0) }}
{{ delete_book() }}
{% if g.user.role_edit() %}
<div class="modal fade" id="mergeModal" role="dialog" aria-labelledby="metaMergeLabel">
<div class="modal-dialog">

@ -16,12 +16,19 @@
<div id="collapseOne" class="panel-collapse collapse in">
<div class="panel-body">
<label for="config_calibre_dir">{{_('Location of Calibre Database')}}</label>
<div class="form-group required input-group">
<div class="form-group required{% if filepicker %} input-group{% endif %}">
<input type="text" class="form-control" id="config_calibre_dir" name="config_calibre_dir" value="{% if config.config_calibre_dir != None %}{{ config.config_calibre_dir }}{% endif %}" autocomplete="off">
{% if filepicker %}
<span class="input-group-btn">
<button type="button" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
<button type="button" data-toggle="modal" id="calibre_modal_path" data-link="config_calibre_dir" data-filefilter="metadata.db" data-target="#fileModal" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
{% endif %}
</div>
{% if not filepicker %}
<div class="form-group">
<label id="filepicker-hint">{{_('To activate serverside filepicker start Calibre-Web with -f optionn')}}</label>
</div>
{% endif %}
{% if feature_support['gdrive'] %}
<div class="form-group required">
<input type="checkbox" id="config_use_google_drive" name="config_use_google_drive" data-control="gdrive_settings" {% if config.config_use_google_drive %}checked{% endif %} >
@ -94,14 +101,14 @@
<div class="form-group input-group">
<input type="text" class="form-control" id="config_certfile" name="config_certfile" value="{% if config.config_certfile != None %}{{ config.config_certfile }}{% endif %}" autocomplete="off">
<span class="input-group-btn">
<button type="button" id="certfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
<button type="button" data-toggle="modal" data-link="config_certfile" data-target="#fileModal" id="certfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
</div>
<label for="config_calibre_dir" >{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label>
<div class="form-group input-group">
<input type="text" class="form-control" id="config_keyfile" name="config_keyfile" value="{% if config.config_keyfile != None %}{{ config.config_keyfile }}{% endif %}" autocomplete="off">
<span class="input-group-btn">
<button type="button" id="keyfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
<button type="button" id="keyfile_path" data-toggle="modal" data-link="config_keyfile" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
</div>
<div class="form-group">
@ -268,21 +275,21 @@
<div class="form-group input-group">
<input type="text" class="form-control" id="config_ldap_cacert_path" name="config_ldap_cacert_path" value="{% if config.config_ldap_cacert_path != None %}{{ config.config_ldap_cacert_path }}{% endif %}" autocomplete="off">
<span class="input-group-btn">
<button type="button" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
<button type="button" id="library_path" data-toggle="modal" data-link="config_ldap_cacert_path" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
</div>
<label for="config_ldap_cert_path">{{_('LDAP Certificate Path (Only needed for Client Certificate Authentication)')}}</label>
<div class="form-group input-group">
<input type="text" class="form-control" id="config_ldap_cert_path" name="config_ldap_cert_path" value="{% if config.config_ldap_cert_path != None %}{{ config.config_ldap_cert_path }}{% endif %}" autocomplete="off">
<span class="input-group-btn">
<button type="button" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
<button type="button" id="library_path" data-toggle="modal" data-link="config_ldap_cert_path" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
</div>
<label for="config_ldap_key_path">{{_('LDAP Keyfile Path (Only needed for Client Certificate Authentication)')}}</label>
<div class="form-group input-group">
<input type="text" class="form-control" id="config_ldap_key_path" name="config_ldap_key_path" value="{% if config.config_ldap_key_path != None %}{{ config.config_ldap_key_path }}{% endif %}" autocomplete="off">
<span class="input-group-btn">
<button type="button" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
<button type="button" id="library_path" data-toggle="modal" data-link="config_ldap_key_path" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
</div>
</div>
@ -384,7 +391,7 @@
<div class="form-group input-group">
<input type="text" class="form-control" id="config_converterpath" name="config_converterpath" value="{% if config.config_converterpath != None %}{{ config.config_converterpath }}{% endif %}" autocomplete="off">
<span class="input-group-btn">
<button type="button" id="converter_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
<button type="button" data-toggle="modal" id="converter_modal_path" data-link="config_converterpath" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
</div>
<div class="form-group">
@ -395,7 +402,7 @@
<div class="form-group input-group">
<input type="text" class="form-control" id="config_kepubifypath" name="config_kepubifypath" value="{% if config.config_kepubifypath != None %}{{ config.config_kepubifypath }}{% endif %}" autocomplete="off">
<span class="input-group-btn">
<button type="button" id="kepubify_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
<button type="button" id="kepubify_path" data-toggle="modal" data-link="config_kepubifypath" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
</div>
{% if feature_support['rar'] %}
@ -403,7 +410,7 @@
<div class="form-group input-group">
<input type="text" class="form-control" id="config_rarfile_location" name="config_rarfile_location" value="{% if config.config_rarfile_location != None %}{{ config.config_rarfile_location }}{% endif %}" autocomplete="off">
<span class="input-group-btn">
<button type="button" id="unrar_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
<button type="button" id="unrar_path" data-toggle="modal" data-link="config_rarfile_location" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
</div>
{% endif %}
@ -412,8 +419,6 @@
</div>
{% endif %}
</div>
<div class="col-sm-12">
{% if not show_login_button %}
<button type="submit" name="submit" class="btn btn-default">{{_('Save')}}</button>
@ -428,6 +433,9 @@
</form>
</div>
{% endblock %}
{% block modal %}
{{ filechooser_modal() }}
{% endblock %}
{% block js %}
<script type="text/javascript">
$(document).on('change', '#config_use_google_drive', function() {

@ -8,7 +8,10 @@
<div class="cover">
{% if entry.has_cover is defined %}
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
<span class="img">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>
{% endif %}
</div>

@ -89,20 +89,7 @@
{% endblock %}
{% block modal %}
{% if g.allow_registration %}
<div id="DeleteDomain" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header bg-danger">
</div>
<div class="modal-body text-center">
<p>{{_('Are you sure you want to delete this domain?')}}</p>
<button type="button" class="btn btn-danger" id="btndeletedomain" >{{_('Delete')}}</button>
<button type="button" class="btn btn-default" id="btncancel" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
{{ delete_confirm_modal() }}
{% endif %}
{% endblock %}
{% block js %}

@ -28,8 +28,10 @@
<div class="col-sm-3 col-lg-2 col-xs-6 book sortable" {% if entry[0].sort %}data-name="{{entry[0].series[0].name}}"{% endif %} data-id="{% if entry[0].series[0].name %}{{entry[0].series[0].name}}{% endif %}">
<div class="cover">
<a href="{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].series[0].id )}}">
<img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/>
<span class="badge">{{entry.count}}</span>
<span class="img">
<img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/>
<span class="badge">{{entry.count}}</span>
</span>
</a>
</div>
<div class="meta">

@ -8,7 +8,10 @@
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books_rand">
<div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
<span class="img">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>
</div>
<div class="meta">
@ -82,7 +85,10 @@
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books">
<div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<span class="img">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}"/>
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>
</div>
<div class="meta">

@ -1,4 +1,4 @@
{% from 'modal_dialogs.html' import restrict_modal, delete_book %}
{% from 'modal_dialogs.html' import restrict_modal, delete_book, filechooser_modal, delete_confirm_modal %}
<!DOCTYPE html>
<html lang="{{ g.user.locale }}">
<head>
@ -189,8 +189,6 @@
</div>
</div>
{% block modal %}{% endblock %}
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
@ -200,14 +198,7 @@
<script src="{{ url_for('static', filename='js/libs/context.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/plugins.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/jquery.form.js') }}"></script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<script src="{{ url_for('static', filename='js/uploadprogress.js') }}"> </script>
{% if g.current_theme == 1 %}
<script src="{{ url_for('static', filename='js/libs/jquery.visible.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/compromise.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/readmore.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/caliBlur.js') }}"></script>
{% endif %}
<script type="text/javascript">
$(function() {
$("#form-upload").uploadprogress({
@ -219,6 +210,13 @@
});
});
</script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% if g.current_theme == 1 %}
<script src="{{ url_for('static', filename='js/libs/jquery.visible.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/compromise.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/readmore.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/caliBlur.js') }}"></script>
{% endif %}
{% block js %}{% endblock %}
</body>
</html>

@ -22,7 +22,7 @@
<button type="submit" name="forgot" value="forgot" class="btn btn-default">{{_('Forgot Password?')}}</button>
{% endif %}
{% if config.config_remote_login %}
<a href="{{url_for('web.remote_login')}}" class="pull-right">{{_('Log in with Magic Link')}}</a>
<a href="{{url_for('remotelogin.remote_login')}}" id="remote_login" class="pull-right">{{_('Log in with Magic Link')}}</a>
{% endif %}
{% if config.config_login_type == 2 %}
{% if 1 in oauth_check %}

@ -37,7 +37,7 @@
</div>
</div>
{% endmacro %}
{% macro delete_book(bookid) %}
{% macro delete_book() %}
{% if g.user.role_delete_books() %}
<div class="modal fade" id="deleteModal" role="dialog" aria-labelledby="metaDeleteLabel">
<div class="modal-dialog">
@ -68,3 +68,56 @@
</div>
{% endif %}
{% endmacro %}
{% macro filechooser_modal() %}
<div class="modal fade" id="fileModal" role="dialog" aria-labelledby="metafileLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-info text-center">
<span>{{_('Choose File Location')}}</span>
</div>
<div class="modal-body">
<table id="file_table" class="table table-striped">
<thead>
<tr>
<th>{{_('type')}}</th>
<th>{{_('name')}}</th>
<th>{{_('size')}}</th>
</tr>
</thead>
<tbody id="tbody">
<tr class="tr-clickable hidden" id="parent" data-type="dir" data-path="..">
<td><span class="glyphicon glyphicon-folder-close"></span></td>
<td title="{{_('Parent Directory')}}"><span class="parentdir">..</span></td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<div class="text-left" id="element_selected"></div>
<input type="button" class="btn btn-primary" data-path="" data-link="" data-folderonly="" data-filefilter="" data-newfile="" value="{{_('Select')}}" name="file_confirm" id="file_confirm" data-dismiss="modal">
<button type="button" id="file_abort" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
{% endmacro %}
{% macro delete_confirm_modal() %}
<div id="GeneralDeleteModal" class="modal fade" role="Dialog">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
<span id="header"></span>
</div>
<div class="modal-body text-center">
<span id="text"></span>
<p></p>
<button id="btnConfirmYes" type="button" class="btn btn btn-danger">{{_('Delete')}}</button>
<button id="btnConfirmNo" type="button" class="btn btn-default">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
{% endmacro %}

@ -4,7 +4,7 @@
<h2 style="margin-top: 0">{{_('Magic Link - Authorise New Device')}}</h2>
<p>
{{_('On another device, login and visit:')}}
<h4><a href="{{verify_url}}">{{verify_url}}</a></b>
<h4><a id="verify_url" href="{{verify_url}}">{{verify_url}}</a></b>
</h4>
<p>
{{_('Once verified, you will automatically be logged in on this device.')}}
@ -20,7 +20,7 @@
(function () {
// Poll the server to check if the user has authenticated
var t = setInterval(function () {
$.post('{{url_for("web.token_verified")}}', { token: '{{token}}' })
$.post('{{url_for("remotelogin.token_verified")}}', { token: '{{token}}' })
.done(function(response) {
if (response.status === 'success') {
// Wait a tick so cookies are updated

@ -43,7 +43,10 @@
<div class="cover">
{% if entry.has_cover is defined %}
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
<span class="img">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>
{% endif %}
</div>

@ -31,87 +31,87 @@
</div>
</div>
</div>
<label for="include_tag">{{_('Tags')}}</label>
<div class="form-group" id="tag">
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
{% for tag in tags %}
<label id="tag_{{tag.id}}" class="btn btn-primary tags_click">
<input type="checkbox" autocomplete="off" name="include_tag" id="include_tag" value="{{tag.id}}">{{tag.name}}</input>
</label>
{% endfor %}
</div>
</div>
<label for="exclude_tag">{{_('Exclude Tags')}}</label>
<div class="form-group">
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
{% for tag in tags %}
<label id="exclude_tag_{{tag.id}}" class="btn btn-danger tags_click">
<input type="checkbox" autocomplete="off" name="exclude_tag" id="exclude_tag" value="{{tag.id}}">{{tag.name}}</input>
</label>
{% endfor %}
</div>
<label for="read_status">{{_('Read Status')}}</label>
<select name="read_status" id="read_status" class="form-control">
<option value="" selected></option>
<option value="True" >{{_('Yes')}}</option>
<option value="False" >{{_('No')}}</option>
</select>
</div>
<label for="include_serie">{{_('Series')}}</label>
<div class="form-group">
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
{% for serie in series %}
<label id="serie_{{serie.id}}" class="btn btn-primary serie_click">
<input type="checkbox" autocomplete="off" name="include_serie" id="include_serie" value="{{serie.id}}">{{serie.name}}</input>
</label>
{% endfor %}
<div class="row">
<div class="form-group col-sm-6" id="tag">
<div><label for="include_tag">{{_('Tags')}}</label></div>
<select class="selectpicker" name="include_tag" id="include_tag" data-live-search="true" data-style="btn-primary" multiple>
{% for tag in tags %}
<option class="tags_click" value="{{tag.id}}">{{tag.name}}</option>
{% endfor %}
</select>
</div>
<div class="form-group col-sm-6">
<div><label for="exclude_tag">{{_('Exclude Tags')}}</label></div>
<select class="selectpicker" name="exclude_tag" id="exclude_tag" data-live-search="true" data-style="btn-danger" multiple>
{% for tag in tags %}
<option class="tags_click" value="{{tag.id}}">{{tag.name}}</option>
{% endfor %}
</select>
</div>
</div>
<label for="exclude_serie">{{_('Exclude Series')}}</label>
<div class="form-group">
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
{% for serie in series %}
<label id="exclude_serie_{{serie.id}}" class="btn btn-danger serie_click">
<input type="checkbox" autocomplete="off" name="exclude_serie" id="exclude_serie" value="{{serie.id}}">{{serie.name}}</input>
</label>
{% endfor %}
<div class="row">
<div class="form-group col-sm-6">
<div><label for="include_serie">{{_('Series')}}</label></div>
<select class="selectpicker" name="include_serie" id="include_serie" data-live-search="true" data-style="btn-primary" multiple>
{% for serie in series %}
<option value="{{serie.id}}">{{serie.name}}</option>
{% endfor %}
</select>
</div>
<div class="form-group col-sm-6">
<div><label for="exclude_serie">{{_('Exclude Series')}}</label></div>
<select class="selectpicker" name="exclude_serie" id="exclude_serie" data-live-search="true" data-style="btn-danger" multiple>
{% for serie in series %}
<option value="{{serie.id}}">{{serie.name}}</option>
{% endfor %}
</select>
</div>
</div>
{% if languages %}
<label for="include_language">{{_('Languages')}}</label>
<div class="form-group">
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
<div class="row">
<div class="form-group col-sm-6">
<div><label for="include_language">{{_('Languages')}}</label></div>
<select class="selectpicker" name="include_language" id="include_language" data-live-search="true" data-style="btn-primary" multiple>
{% for language in languages %}
<label id="language_{{language.id}}" class="btn btn-primary serie_click">
<input type="checkbox" autocomplete="off" name="include_language" id="include_language" value="{{language.id}}">{{language.name}}</input>
</label>
<option value="{{language.id}}">{{language.name}}</option>
{% endfor %}
</div>
</select>
</div>
<label for="exclude_language">{{_('Exclude Languages')}}</label>
<div class="form-group">
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
<div class="form-group col-sm-6">
<div><label for="exclude_language">{{_('Exclude Languages')}}</label></div>
<select class="selectpicker" name="exclude_language" id="exclude_language" data-live-search="true" data-style="btn-danger" multiple>
{% for language in languages %}
<label id="exclude_language_{{language.id}}" class="btn btn-danger language_click">
<input type="checkbox" autocomplete="off" name="exclude_language" id="exclude_language" value="{{language.id}}">{{language.name}}</input>
</label>
<option value="{{language.id}}">{{language.name}}</option>
{% endfor %}
</div>
</select>
</div>
</div>
{% endif%}
<label for="include_extension">{{_('Extensions')}}</label>
<div class="form-group" id="extension">
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
<div class="row">
<div class="form-group col-sm-6">
<div><label for="include_extension">{{_('Extensions')}}</label></div>
<select id="include_extension" class="selectpicker" name="include_extension" id="include_extension" data-live-search="true" data-style="btn-primary" multiple>
{% for extension in extensions %}
<label id="extension_{{extension.format}}" class="btn btn-primary extension_click">
<input type="checkbox" autocomplete="off" name="include_extension" id="include_extension" value="{{extension.format}}">{{extension.format}}</input>
</label>
{% endfor %}
</div>
</div>
<label for="exclude_extension">{{_('Exclude Extensions')}}</label>
<div class="form-group">
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
<option value="{{extension.format}}">{{extension.format}}</option>
{% endfor %}
</select>
</div>
<div class="form-group col-sm-6">
<div><label for="exclude_extension">{{_('Exclude Extensions')}}</label></div>
<select id="exclude_extension" class="selectpicker" name="exclude_extension" id="exclude_extension" data-live-search="true" data-style="btn-danger" multiple>
{% for extension in extensions %}
<label id="exclude_extension_{{extension.format}}" class="btn btn-danger extension_click">
<input type="checkbox" autocomplete="off" name="exclude_extension" id="exclude_extension" value="{{extension.format}}">{{extension.format}}</input>
</label>
{% endfor %}
</div>
<option value="{{extension.format}}">{{extension.format}}</option>
{% endfor %}
</select>
</div>
</div>
<div class="row">
<div class="form-group col-sm-6">
@ -189,10 +189,13 @@
<script src="{{ url_for('static', filename='js/libs/bootstrap-rating-input.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/typeahead.bundle.js') }}"></script>
<script src="{{ url_for('static', filename='js/edit_books.js') }}"></script>
<script>
</script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-select.min.js')}}"></script>
{% if not g.user.locale == 'en' %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-select/defaults-' + g.user.locale + '.min.js') }}" charset="UTF-8"></script>
{% endif %}
{% endblock %}
{% block header %}
<link href="{{ url_for('static', filename='css/libs/typeahead.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/libs/bootstrap-datepicker3.min.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/libs/bootstrap-select.min.css') }}" rel="stylesheet" >
{% endblock %}

@ -2,23 +2,38 @@
{% block body %}
<div class="discover">
<h2>{{title}}</h2>
{% if g.user.role_download() %}
<a id="shelf_down" href="{{ url_for('shelf.show_shelf', shelf_type=2, shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a>
{% if g.user.role_download() %}
<a id="shelf_down" href="{{ url_for('shelf.show_simpleshelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a>
{% endif %}
{% if g.user.is_authenticated %}
{% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
<div id="delete_shelf" data-toggle="modal" data-target="#DeleteShelfDialog" class="btn btn-danger">{{ _('Delete this Shelf') }} </div>
<a id="edit_shelf" href="{{ url_for('shelf.edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf') }} </a>
<a id="order_shelf" href="{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a>
<div class="btn btn-danger" id="delete_shelf" data-value="{{ shelf.id }}">{{ _('Delete this Shelf') }}</div>
<a id="edit_shelf" href="{{ url_for('shelf.edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf Properties') }} </a>
{% if entries.__len__() %}
<a id="order_shelf" href="{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Arrange books manually') }} </a>
<button id="toggle_order_shelf" type="button" data-alt-text="{{ _('Disable Change order') }}" class="btn btn-primary">{{ _('Enable Change order') }}</button>
<div class="filterheader hidden-xs hidden-sm">
<a data-toggle="tooltip" title="{{_('Sort according to book date, newest first')}}" id="new" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a data-toggle="tooltip" title="{{_('Sort according to book date, oldest first')}}" id="old" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
<a data-toggle="tooltip" title="{{_('Sort title in alphabetical order')}}" id="asc" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
<a data-toggle="tooltip" title="{{_('Sort title in reverse alphabetical order')}}" id="desc" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
<a data-toggle="tooltip" title="{{_('Sort authors in alphabetical order')}}" id="auth_az" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='authaz')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
<a data-toggle="tooltip" title="{{_('Sort authors in reverse alphabetical order')}}" id="auth_za" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='authza')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
<a data-toggle="tooltip" title="{{_('Sort according to publishing date, newest first')}}" id="pub_new" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a data-toggle="tooltip" title="{{_('Sort according to publishing date, oldest first')}}" id="pub_old" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
</div>
{% endif %}
{% endif %}
{% endif %}
<div class="row display-flex">
{% for entry in entries %}
<div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
<span class="img">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span>
</a>
</div>
<div class="meta">
@ -68,7 +83,7 @@
{% endfor %}
</div>
</div>
<div id="DeleteShelfDialog" class="modal fade" role="dialog">
<!--div id="DeleteShelfDialog" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
@ -82,6 +97,9 @@
</div>
</div>
</div>
</div>
</div-->
{% endblock %}
{% block modal %}
{{ delete_confirm_modal() }}
{% endblock %}

@ -5,30 +5,39 @@
<div>{{_('Drag to Rearrange Order')}}</div>
<div id="sortTrue" class="list-group">
{% for entry in entries %}
<div id="{{entry['id']}}" class="list-group-item">
<div id="{{entry['Books']['id']}}" class="list-group-item">
<div class="row">
<div class="col-lg-2 col-sm-4 hidden-xs">
<img class="cover-height" src="{{ url_for('web.get_cover', book_id=entry['id']) }}">
{% if entry['visible'] %}
<img class="cover-height" src="{{ url_for('web.get_cover', book_id=entry['Books']['id']) }}">
{% else %}
<img class="cover-height" src="{{ url_for('static', filename='generic_cover.jpg') }}">
{% endif %}
</div>
<div class="col-lg-10 col-sm-8 col-xs-12">
{{entry['title']}}
{% if entry['series']|length > 0 %}
{% if entry['visible'] %}
{{entry['Books']['title']}}
{% if entry['Books']['series']|length > 0 %}
<br>
{{entry['series_index']}} - {{entry['series'][0].name}}
{{entry['Books']['series_index']}} - {{entry['Books']['series'][0].name}}
{% endif %}
<br>
{% for author in entry['author'] %}
{% for author in entry['Books']['author'] %}
{{author.name.replace('|',',')}}
{% if not loop.last %}
&amp;
{% endif %}
{% endfor %}
{% else %}
{{_('Hidden Book')}}
<br>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
<button onclick="sendData('{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}')" class="btn btn-default" id="ChangeOrder">{{_('Change order')}}</button>
<button onclick="sendData('{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}')" class="btn btn-default" id="ChangeOrder">{{_('Save')}}</button>
<a href="{{ url_for('shelf.show_shelf', shelf_id=shelf.id) }}" id="shelf_back" class="btn btn-default">{{_('Back')}}</a>
</div>
{% endblock %}

@ -55,27 +55,14 @@
</div>
<div class="btn-group" role="group" aria-label="Download, send to Kindle, reading">
{% if g.user.role_download() %}
{% if g.user.role_download() %}
{% if entry.data|length %}
<div class="btn-group" role="group">
{% if entry.data|length < 2 %}
{% for format in entry.data %}
<a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}" id="btnGroupDrop1{{format.format|lower}}" class="btn btn-primary" role="button">
<span class="glyphicon glyphicon-download"></span>{{format.format}} ({{ format.uncompressed_size|filesizeformat }})
</a>
{% endfor %}
{% else %}
<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-download"></span> {{_('Download')}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
{% for format in entry.data %}
<li><a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}">{{format.format}} ({{ format.uncompressed_size|filesizeformat }})</a></li>
{% endfor %}
</ul>
{% endif %}
{% for format in entry.data %}
<a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}" id="btnGroupDrop{{entry.id}}{{format.format|lower}}" class="btn btn-primary" role="button">
<span class="glyphicon glyphicon-download"></span>{{format.format}} ({{ format.uncompressed_size|filesizeformat }})
</a>
{% endfor %}
</div>
{% endif %}
{% endif %}

@ -63,7 +63,7 @@
<label>{{ _('Kobo Sync Token')}}</label>
<div class="form-group col">
<a class="btn btn-default" id="config_create_kobo_token" data-toggle="modal" data-target="#modal_kobo_token" data-remote="false" href="{{ url_for('kobo_auth.generate_auth_token', user_id=content.id) }}">{{_('Create/View')}}</a>
<div class="btn btn-danger" id="config_delete_kobo_token" data-toggle="modal" data-target="#modalDeleteToken" data-remote="false" {% if not content.remote_auth_token.first() %} style="display: none;" {% endif %}>{{_('Delete')}}</div>
<div class="btn btn-danger" id="config_delete_kobo_token" data-value="{{ content.id }}" data-remote="false" {% if not content.remote_auth_token.first() %} style="display: none;" {% endif %}>{{_('Delete')}}</div>
</div>
{% endif %}
</div>
@ -82,8 +82,8 @@
<label for="Show_detail_random">{{_('Show Random Books in Detail View')}}</label>
</div>
{% if ( g.user and g.user.role_admin() and not new_user ) %}
<a href="#" id="get_user_tags" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add Allowed/Denied Tags')}}</a>
<a href="#" id="get_user_column_values" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add allowed/Denied Custom Column Values')}}</a>
<a href="#" id="get_user_tags" class="btn btn-default" data-id="{{content.id}}" data-toggle="modal" data-target="#restrictModal">{{_('Add Allowed/Denied Tags')}}</a>
<a href="#" id="get_user_column_values" data-id="{{content.id}}" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add allowed/Denied Custom Column Values')}}</a>
{% endif %}
</div>
<div class="col-sm-6">
@ -125,19 +125,15 @@
</div>
{% endif %}
{% endif %}
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
<div class="checkbox">
<label>
<input type="checkbox" id="delete" name="delete"> {{_('Delete User')}}
</label>
</div>
{% endif %}
</div>
<div class="col-sm-12">
<button type="submit" id="submit" class="btn btn-default">{{_('Save')}}</button>
<div id="user_submit" class="btn btn-default">{{_('Save')}}</div>
{% if not profile %}
<a href="{{ url_for('admin.admin') }}" id="back" class="btn btn-default">{{_('Cancel')}}</a>
{% endif %}
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
<div class="btn btn-danger" id="btndeluser" data-value="{{ content.id }}" data-remote="false" >{{_('Delete User')}}</div>
{% endif %}
</div>
</form>
</div>
@ -157,23 +153,10 @@
</div>
</div>
<div id="modalDeleteToken" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header bg-danger">
</div>
<div class="modal-body text-center">
<p>{{_('Do you really want to delete the Kobo Token?')}}</p>
<button type="button" class="btn btn-danger" id="btndeletetoken" value="{{content.id}}">{{_('Delete')}}</button>
<button type="button" class="btn btn-default" id="btncancel" data-dismiss="modal">{{_('Back')}}</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block modal %}
{{ restrict_modal() }}
{{ delete_confirm_modal() }}
{% endblock %}
{% block js %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save