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

Merge branch 'Develop' into master

This commit is contained in:
OzzieIsaacs 2021-01-02 14:54:43 +01:00
commit e269bab186
97 changed files with 6945 additions and 7414 deletions

10
cps.py
View File

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

View File

@ -94,7 +94,8 @@ def create_app():
app.root_path = app.root_path.decode('utf-8') app.root_path = app.root_path.decode('utf-8')
app.instance_path = app.instance_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...') log.info('Starting Calibre Web...')
Principal(app) Principal(app)

View File

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

View File

@ -5,7 +5,7 @@
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, # andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, # falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
# ruben-herold, marblepebble, JackED42, SiphonSquirrel, # 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 # 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 # it under the terms of the GNU General Public License as published by
@ -26,24 +26,31 @@ import re
import base64 import base64
import json import json
import time import time
import operator
from datetime import datetime, timedelta from datetime import datetime, timedelta
from babel import Locale as LC from babel import Locale as LC
from babel.dates import format_datetime from babel.dates import format_datetime
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory 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 from flask_login import login_required, current_user, logout_user, confirm_login
from flask_babel import gettext as _ from flask_babel import gettext as _
from sqlalchemy import and_ from sqlalchemy import and_
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError 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 . 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 . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash
from .gdriveutils import is_gdrive_ready, gdrive_support 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 from . import debug_info
try:
from functools import wraps
except ImportError:
pass # We're not using Python 3
log = logger.create() log = logger.create()
feature_support = { feature_support = {
@ -72,6 +79,49 @@ feature_support['gdrive'] = gdrive_support
admi = Blueprint('admin', __name__) 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") @admi.route("/admin")
@login_required @login_required
@ -143,7 +193,7 @@ def admin():
@admin_required @admin_required
def configuration(): def configuration():
if request.method == "POST": if request.method == "POST":
return _configuration_update_helper() return _configuration_update_helper(True)
return _configuration_result() return _configuration_result()
@ -195,6 +245,21 @@ def update_view_configuration():
return 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']) @admi.route("/ajax/editdomain/<int:allow>", methods=['POST'])
@login_required @login_required
@admin_required @admin_required
@ -206,7 +271,10 @@ def edit_domain(allow):
vals = request.form.to_dict() vals = request.form.to_dict()
answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first() answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first()
answer.domain = vals['value'].replace('*', '%').replace('?', '_').lower() answer.domain = vals['value'].replace('*', '%').replace('?', '_').lower()
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return "" return ""
@ -220,7 +288,10 @@ def add_domain(allow):
if not check: if not check:
new_domain = ub.Registration(domain=domain_name, allow=allow) new_domain = ub.Registration(domain=domain_name, allow=allow)
ub.session.add(new_domain) ub.session.add(new_domain)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return "" return ""
@ -228,14 +299,23 @@ def add_domain(allow):
@login_required @login_required
@admin_required @admin_required
def delete_domain(): def delete_domain():
domain_id = request.form.to_dict()['domainid'].replace('*', '%').replace('?', '_').lower() try:
ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete() domain_id = request.form.to_dict()['domainid'].replace('*', '%').replace('?', '_').lower()
ub.session.commit() ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete()
# If last domain was deleted, add all domains by default try:
if not ub.session.query(ub.Registration).filter(ub.Registration.allow==1).count(): ub.session.commit()
new_domain = ub.Registration(domain="%.%",allow=1) except OperationalError:
ub.session.add(new_domain) ub.session.rollback()
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)
try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
except KeyError:
pass
return "" return ""
@ -250,10 +330,11 @@ def list_domain(allow):
response.headers["Content-Type"] = "application/json; charset=utf-8" response.headers["Content-Type"] = "application/json; charset=utf-8"
return response 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 @login_required
@admin_required @admin_required
def edit_restriction(res_type): def edit_restriction(res_type, user_id):
element = request.form.to_dict() element = request.form.to_dict()
if element['id'].startswith('a'): if element['id'].startswith('a'):
if res_type == 0: # Tags as template if res_type == 0: # Tags as template
@ -267,25 +348,29 @@ def edit_restriction(res_type):
config.config_allowed_column_value = ','.join(elementlist) config.config_allowed_column_value = ','.join(elementlist)
config.save() config.save()
if res_type == 2: # Tags per user if res_type == 2: # Tags per user
usr_id = os.path.split(request.referrer)[-1] if isinstance(user_id, int):
if usr_id.isdigit() == True: usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else: else:
usr = current_user usr = current_user
elementlist = usr.list_allowed_tags() elementlist = usr.list_allowed_tags()
elementlist[int(element['id'][1:])]=element['Element'] elementlist[int(element['id'][1:])]=element['Element']
usr.allowed_tags = ','.join(elementlist) usr.allowed_tags = ','.join(elementlist)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
if res_type == 3: # CColumn per user if res_type == 3: # CColumn per user
usr_id = os.path.split(request.referrer)[-1] if isinstance(user_id, int):
if usr_id.isdigit() == True: usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else: else:
usr = current_user usr = current_user
elementlist = usr.list_allowed_column_values() elementlist = usr.list_allowed_column_values()
elementlist[int(element['id'][1:])]=element['Element'] elementlist[int(element['id'][1:])]=element['Element']
usr.allowed_column_value = ','.join(elementlist) usr.allowed_column_value = ','.join(elementlist)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
if element['id'].startswith('d'): if element['id'].startswith('d'):
if res_type == 0: # Tags as template if res_type == 0: # Tags as template
elementlist = config.list_denied_tags() elementlist = config.list_denied_tags()
@ -298,25 +383,29 @@ def edit_restriction(res_type):
config.config_denied_column_value = ','.join(elementlist) config.config_denied_column_value = ','.join(elementlist)
config.save() config.save()
if res_type == 2: # Tags per user if res_type == 2: # Tags per user
usr_id = os.path.split(request.referrer)[-1] if isinstance(user_id, int):
if usr_id.isdigit() == True: usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else: else:
usr = current_user usr = current_user
elementlist = usr.list_denied_tags() elementlist = usr.list_denied_tags()
elementlist[int(element['id'][1:])]=element['Element'] elementlist[int(element['id'][1:])]=element['Element']
usr.denied_tags = ','.join(elementlist) usr.denied_tags = ','.join(elementlist)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
if res_type == 3: # CColumn per user if res_type == 3: # CColumn per user
usr_id = os.path.split(request.referrer)[-1] if isinstance(user_id, int):
if usr_id.isdigit() == True: usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else: else:
usr = current_user usr = current_user
elementlist = usr.list_denied_column_values() elementlist = usr.list_denied_column_values()
elementlist[int(element['id'][1:])]=element['Element'] elementlist[int(element['id'][1:])]=element['Element']
usr.denied_column_value = ','.join(elementlist) usr.denied_column_value = ','.join(elementlist)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return "" return ""
def restriction_addition(element, list_func): def restriction_addition(element, list_func):
@ -335,10 +424,11 @@ def restriction_deletion(element, list_func):
return ','.join(elementlist) 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 @login_required
@admin_required @admin_required
def add_restriction(res_type): def add_restriction(res_type, user_id):
element = request.form.to_dict() element = request.form.to_dict()
if res_type == 0: # Tags as template if res_type == 0: # Tags as template
if 'submit_allow' in element: 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.config_denied_column_value = restriction_addition(element, config.list_allowed_column_values)
config.save() config.save()
if res_type == 2: # Tags per user if res_type == 2: # Tags per user
usr_id = os.path.split(request.referrer)[-1] if isinstance(user_id, int):
if usr_id.isdigit() == True: usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else: else:
usr = current_user usr = current_user
if 'submit_allow' in element: if 'submit_allow' in element:
usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags) usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
elif 'submit_deny' in element: elif 'submit_deny' in element:
usr.denied_tags = restriction_addition(element, usr.list_denied_tags) usr.denied_tags = restriction_addition(element, usr.list_denied_tags)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
if res_type == 3: # CustomC per user if res_type == 3: # CustomC per user
usr_id = os.path.split(request.referrer)[-1] if isinstance(user_id, int):
if usr_id.isdigit() == True: usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else: else:
usr = current_user usr = current_user
if 'submit_allow' in element: if 'submit_allow' in element:
usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values) usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
elif 'submit_deny' in element: elif 'submit_deny' in element:
usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values) usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return "" 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 @login_required
@admin_required @admin_required
def delete_restriction(res_type): def delete_restriction(res_type, user_id):
element = request.form.to_dict() element = request.form.to_dict()
if res_type == 0: # Tags as template if res_type == 0: # Tags as template
if element['id'].startswith('a'): 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.config_denied_column_value = restriction_deletion(element, config.list_denied_column_values)
config.save() config.save()
elif res_type == 2: # Tags per user elif res_type == 2: # Tags per user
usr_id = os.path.split(request.referrer)[-1] if isinstance(user_id, int):
if usr_id.isdigit() == True: usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else: else:
usr = current_user usr = current_user
if element['id'].startswith('a'): if element['id'].startswith('a'):
usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags) usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
elif element['id'].startswith('d'): elif element['id'].startswith('d'):
usr.denied_tags = restriction_deletion(element, usr.list_denied_tags) usr.denied_tags = restriction_deletion(element, usr.list_denied_tags)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
elif res_type == 3: # Columns per user elif res_type == 3: # Columns per user
usr_id = os.path.split(request.referrer)[-1] if isinstance(user_id, int):
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(user_id)).first()
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
else: else:
usr = current_user usr = current_user
if element['id'].startswith('a'): if element['id'].startswith('a'):
usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values) usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
elif element['id'].startswith('d'): elif element['id'].startswith('d'):
usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values) usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return "" return ""
@admi.route("/ajax/listrestriction/<int:res_type>", defaults={"user_id":0})
@admi.route("/ajax/listrestriction/<int:res_type>") @admi.route("/ajax/listrestriction/<int:res_type>/<int:user_id>")
@login_required @login_required
@admin_required @admin_required
def list_restriction(res_type): def list_restriction(res_type, user_id):
if res_type == 0: # Tags as template if res_type == 0: # Tags as template
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) } restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
for i,x in enumerate(config.list_denied_tags()) if x != '' ] 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 != ''] for i,x in enumerate(config.list_allowed_column_values()) if x != '']
json_dumps = restrict + allow json_dumps = restrict + allow
elif res_type == 2: # Tags per user elif res_type == 2: # Tags per user
usr_id = os.path.split(request.referrer)[-1] if isinstance(user_id, int):
if usr_id.isdigit() == True: usr = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
usr = ub.session.query(ub.User).filter(ub.User.id == usr_id).first()
else: else:
usr = current_user usr = current_user
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) } 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 != ''] for i,x in enumerate(usr.list_allowed_tags()) if x != '']
json_dumps = restrict + allow json_dumps = restrict + allow
elif res_type == 3: # CustomC per user elif res_type == 3: # CustomC per user
usr_id = os.path.split(request.referrer)[-1] if isinstance(user_id, int):
if usr_id.isdigit() == True: usr = ub.session.query(ub.User).filter(ub.User.id==user_id).first()
usr = ub.session.query(ub.User).filter(ub.User.id==usr_id).first()
else: else:
usr = current_user usr = current_user
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) } 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" response.headers["Content-Type"] = "application/json; charset=utf-8"
return response return response
@admi.route("/basicconfig/pathchooser/")
@unconfigured
def config_pathchooser():
if filepicker:
return pathchooser()
abort(403)
@admi.route("/config", methods=["GET", "POST"]) @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
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 @unconfigured
def basic_configuration(): def basic_configuration():
logout_user() logout_user()
if request.method == "POST": if request.method == "POST":
return _configuration_update_helper() return _configuration_update_helper(configured=filepicker)
return _configuration_result() return _configuration_result(configured=filepicker)
def _config_int(to_save, x, func=int): def _config_int(to_save, x, func=int):
@ -633,13 +836,13 @@ def _configuration_ldap_helper(to_save, gdriveError):
return reboot_required, None return reboot_required, None
def _configuration_update_helper(): def _configuration_update_helper(configured):
reboot_required = False reboot_required = False
db_change = False db_change = False
to_save = request.form.to_dict() to_save = request.form.to_dict()
gdriveError = None 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'], to_save['config_calibre_dir'],
flags=re.IGNORECASE) flags=re.IGNORECASE)
@ -653,11 +856,15 @@ def _configuration_update_helper():
reboot_required |= _config_string(to_save, "config_keyfile") reboot_required |= _config_string(to_save, "config_keyfile")
if config.config_keyfile and not os.path.isfile(config.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") reboot_required |= _config_string(to_save, "config_certfile")
if config.config_certfile and not os.path.isfile(config.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") _config_checkbox_int(to_save, "config_uploading")
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case # 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: if "config_rarfile_location" in to_save:
unrar_status = helper.check_unrar(config.config_rarfile_location) unrar_status = helper.check_unrar(config.config_rarfile_location)
if unrar_status: if unrar_status:
return _configuration_result(unrar_status, gdriveError) return _configuration_result(unrar_status, gdriveError, configured)
except (OperationalError, InvalidRequestError): except (OperationalError, InvalidRequestError):
ub.session.rollback() ub.session.rollback()
_configuration_result(_(u"Settings DB is not Writeable"), gdriveError) _configuration_result(_(u"Settings DB is not Writeable"), gdriveError, configured)
try: try:
metadata_db = os.path.join(config.config_calibre_dir, "metadata.db") 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) gdriveutils.downloadFile(None, "metadata.db", metadata_db)
db_change = True db_change = True
except Exception as e: except Exception as e:
return _configuration_result('%s' % e, gdriveError) return _configuration_result('%s' % e, gdriveError, configured)
if db_change: if db_change:
if not calibre_db.setup_db(config, ub.app_DB_path): if not calibre_db.setup_db(config, ub.app_DB_path):
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): if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
flash(_(u"DB is not Writeable"), category="warning") flash(_(u"DB is not Writeable"), category="warning")
@ -746,10 +955,10 @@ def _configuration_update_helper():
if reboot_required: if reboot_required:
web_server.stop(True) 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() gdrive_authenticate = not is_gdrive_ready()
gdrivefolders = [] gdrivefolders = []
if gdriveError is None: 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, return render_title_template("config_edit.html", config=config, provider=oauthblueprints,
show_back_button=show_back_button, show_login_button=show_login_button, 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, gdriveError=gdriveError, gdrivefolders=gdrivefolders, feature_support=feature_support,
title=_(u"Basic Configuration"), page="config") 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.allowed_column_value = config.config_allowed_column_value
content.denied_column_value = config.config_denied_column_value content.denied_column_value = config.config_denied_column_value
ub.session.add(content) ub.session.add(content)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
flash(_(u"User '%(user)s' created", user=content.nickname), category="success") flash(_(u"User '%(user)s' created", user=content.nickname), category="success")
return redirect(url_for('admin.admin')) return redirect(url_for('admin.admin'))
except IntegrityError: 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, if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
ub.User.id != content.id).count(): ub.User.id != content.id).count():
ub.session.query(ub.User).filter(ub.User.id == content.id).delete() 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") flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success")
return redirect(url_for('admin.admin')) return redirect(url_for('admin.admin'))
else: else:
@ -855,7 +1070,7 @@ def _handle_edit_user(to_save, content,languages, translations, kobo_support):
content.role &= ~constants.ROLE_ANONYMOUS content.role &= ~constants.ROLE_ANONYMOUS
val = [int(k[5:]) for k in to_save if k.startswith('show_')] 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: for element in sidebar:
value = element['visibility'] value = element['visibility']
if value in val and not content.check_visibility(value): 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: if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail:
content.kindle_mail = to_save["kindle_mail"] content.kindle_mail = to_save["kindle_mail"]
try: try:
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success") flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success")
except IntegrityError: except IntegrityError:
ub.session.rollback() ub.session.rollback()
@ -1119,3 +1337,110 @@ def get_updater_status():
except Exception: except Exception:
status['status'] = 11 status['status'] = 11
return json.dumps(status) 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)

View File

@ -45,6 +45,7 @@ parser.add_argument('-v', '--version', action='version', help='Shows version num
version=version_info()) version=version_info())
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen') 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('-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() args = parser.parse_args()
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
@ -110,3 +111,6 @@ if ipadress:
# handle and check user password argument # handle and check user password argument
user_password = args.s or None user_password = args.s or None
# Handles enableing of filepicker
filepicker = args.f or None

View File

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

View File

@ -22,6 +22,7 @@ import os
import sys import sys
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
from sqlalchemy.exc import OperationalError
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from . import constants, cli, logger, ub from . import constants, cli, logger, ub
@ -271,6 +272,14 @@ class _ConfigSQL(object):
setattr(self, field, new_value) setattr(self, field, new_value)
return True 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): def load(self):
'''Load all configuration values from the underlying storage.''' '''Load all configuration values from the underlying storage.'''
s = self._read_from_storage() # type: _Settings 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) log.warning("Log path %s not valid, falling back to default", self.config_logfile)
self.config_logfile = logfile self.config_logfile = logfile
self._session.merge(s) 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): def save(self):
'''Apply all configuration values to the underlying storage.''' '''Apply all configuration values to the underlying storage.'''
@ -309,7 +322,11 @@ class _ConfigSQL(object):
log.debug("_ConfigSQL updating storage") log.debug("_ConfigSQL updating storage")
self._session.merge(s) 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() self.load()
def invalidate(self, error=None): def invalidate(self, error=None):
@ -350,7 +367,10 @@ def _migrate_table(session, orm_class):
changed = True changed = True
if changed: if changed:
session.commit() try:
session.commit()
except OperationalError:
session.rollback()
def autodetect_calibre_binary(): def autodetect_calibre_binary():

View File

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

View File

@ -44,8 +44,12 @@ def assemble_logfiles(file_name):
def send_debug(): def send_debug():
file_list = glob.glob(logger.get_logfile(config.config_logfile) + '*') file_list = glob.glob(logger.get_logfile(config.config_logfile) + '*')
file_list.extend(glob.glob(logger.get_accesslogfile(config.config_access_logfile) + '*')) file_list.extend(glob.glob(logger.get_accesslogfile(config.config_access_logfile) + '*'))
for element in [logger.LOG_TO_STDOUT, logger.LOG_TO_STDERR]:
if element in file_list:
file_list.remove(element)
memory_zip = io.BytesIO() memory_zip = io.BytesIO()
with zipfile.ZipFile(memory_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf: 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())) zf.writestr('libs.txt', json.dumps(collect_stats()))
for fp in file_list: for fp in file_list:
zf.write(fp, os.path.basename(fp)) zf.write(fp, os.path.basename(fp))

View File

@ -37,13 +37,38 @@ from . import config, get_locale, ub, db
from . import calibre_db from . import calibre_db
from .services.worker import WorkerThread from .services.worker import WorkerThread
from .tasks.upload import TaskUpload 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__) editbook = Blueprint('editbook', __name__)
log = logger.create() 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 # 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 # 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): 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() filter(db.Data.format == book_format).delete()
calibre_db.session.commit() calibre_db.session.commit()
except Exception as e: except Exception as e:
log.exception(e) log.debug_or_exception(e)
calibre_db.session.rollback() calibre_db.session.rollback()
else: else:
# book not found # book not found
@ -287,7 +312,7 @@ def delete_book(book_id, book_format, jsonResponse):
def render_edit_book(book_id): def render_edit_book(book_id):
calibre_db.update_title_sort(config) calibre_db.update_title_sort(config)
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() 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: if not book:
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error") flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
return redirect(url_for("web.index")) return redirect(url_for("web.index"))
@ -716,7 +741,7 @@ def edit_book(book_id):
flash(error, category="error") flash(error, category="error")
return render_edit_book(book_id) return render_edit_book(book_id)
except Exception as e: except Exception as e:
log.exception(e) log.debug_or_exception(e)
calibre_db.session.rollback() calibre_db.session.rollback()
flash(_("Error editing book, please check logfile for details"), category="error") flash(_("Error editing book, please check logfile for details"), category="error")
return redirect(url_for('web.show_book', book_id=book.id)) return redirect(url_for('web.show_book', book_id=book.id))

73
cps/error_handler.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -43,6 +43,8 @@ from flask_login import current_user
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.sql.expression import and_, or_ from sqlalchemy.sql.expression import and_, or_
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm import load_only
from sqlalchemy.exc import StatementError from sqlalchemy.exc import StatementError
import requests import requests
@ -56,6 +58,8 @@ KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]}
KOBO_STOREAPI_URL = "https://storeapi.kobo.com" KOBO_STOREAPI_URL = "https://storeapi.kobo.com"
KOBO_IMAGEHOST_URL = "https://kbimages1-a.akamaihd.net" KOBO_IMAGEHOST_URL = "https://kbimages1-a.akamaihd.net"
SYNC_ITEM_LIMIT = 5
kobo = Blueprint("kobo", __name__, url_prefix="/kobo/<auth_token>") kobo = Blueprint("kobo", __name__, url_prefix="/kobo/<auth_token>")
kobo_auth.disable_failed_auth_redirect_for_blueprint(kobo) kobo_auth.disable_failed_auth_redirect_for_blueprint(kobo)
kobo_auth.register_url_value_preprocessor(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_modified = sync_token.books_last_modified
new_books_last_created = sync_token.books_last_created new_books_last_created = sync_token.books_last_created
new_reading_state_last_modified = sync_token.reading_state_last_modified new_reading_state_last_modified = sync_token.reading_state_last_modified
new_archived_last_modified = datetime.datetime.min
sync_results = [] sync_results = []
# We reload the book database so that the user get's a fresh view of the library # 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). # in case of external changes (e.g: adding a book through Calibre).
calibre_db.reconnect_db(config, ub.app_DB_path) calibre_db.reconnect_db(config, ub.app_DB_path)
archived_books = ( if sync_token.books_last_id > -1:
ub.session.query(ub.ArchivedBook) changed_entries = (
.filter(ub.ArchivedBook.user_id == int(current_user.id)) calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
.all() .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)
# We join-in books that have had their Archived bit recently modified in order to either: .filter(db.Data.format.in_(KOBO_FORMATS))
# * Restore them to the user's device. .order_by(db.Books.last_modified)
# * Delete them from the user's device. .order_by(db.Books.id)
# (Ideally we would use a join for this logic, however cross-database joins don't look trivial in SqlAlchemy.) .limit(SYNC_ITEM_LIMIT)
recently_restored_or_archived_books = [] )
archived_book_ids = {} else:
new_archived_last_modified = datetime.datetime.min changed_entries = (
for archived_book in archived_books: calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
if archived_book.last_modified > sync_token.archive_last_modified: .join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
recently_restored_or_archived_books.append(archived_book.book_id) .filter(db.Books.last_modified > sync_token.books_last_modified)
if archived_book.is_archived: .filter(db.Data.format.in_(KOBO_FORMATS))
archived_book_ids[archived_book.book_id] = True .order_by(db.Books.last_modified)
new_archived_last_modified = max( .order_by(db.Books.id)
new_archived_last_modified, archived_book.last_modified) .limit(SYNC_ITEM_LIMIT)
)
# 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()
)
reading_states_in_new_entitlements = [] reading_states_in_new_entitlements = []
for book in changed_entries: 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 = { entitlement = {
"BookEntitlement": create_book_entitlement(book, archived=(book.id in archived_book_ids)), "BookEntitlement": create_book_entitlement(book.Books, archived=(book.is_archived == True)),
"BookMetadata": get_metadata(book), "BookMetadata": get_metadata(book.Books),
} }
if kobo_reading_state.last_modified > sync_token.reading_state_last_modified: 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) 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}) sync_results.append({"NewEntitlement": entitlement})
else: else:
sync_results.append({"ChangedEntitlement": entitlement}) sync_results.append({"ChangedEntitlement": entitlement})
new_books_last_modified = max( 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 = ( changed_reading_states = (
ub.session.query(ub.KoboReadingState) ub.session.query(ub.KoboReadingState)
.filter(and_(func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified, .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.books_last_modified = new_books_last_modified
sync_token.archive_last_modified = new_archived_last_modified sync_token.archive_last_modified = new_archived_last_modified
sync_token.reading_state_last_modified = new_reading_state_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 = {} extra_headers = {}
if config.config_kobo_proxy: if config.config_kobo_proxy:
# Merge in sync results from the official Kobo store. # 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: except Exception as e:
log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e)) log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e))
if set_cont:
extra_headers["x-kobo-sync"] = "continue"
sync_token.to_headers(extra_headers) sync_token.to_headers(extra_headers)
response = make_response(jsonify(sync_results), 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) items_unknown_to_calibre = add_items_to_shelf(items, shelf)
if items_unknown_to_calibre: if items_unknown_to_calibre:
log.debug("Received request to add unknown books to a collection. Silently ignoring items.") 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) return make_response(jsonify(str(shelf.uuid)), 201)
@ -476,7 +497,10 @@ def HandleTagUpdate(tag_id):
shelf.name = name shelf.name = name
ub.session.merge(shelf) ub.session.merge(shelf)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return make_response(' ', 200) 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.") log.debug("Received request to add an unknown book to a collection. Silently ignoring item.")
ub.session.merge(shelf) ub.session.merge(shelf)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return make_response('', 201) return make_response('', 201)
@ -569,7 +596,10 @@ def HandleTagRemoveItem(tag_id):
shelf.books.filter(ub.BookShelf.book_id == book.id).delete() shelf.books.filter(ub.BookShelf.book_id == book.id).delete()
except KeyError: except KeyError:
items_unknown_to_calibre.append(item) items_unknown_to_calibre.append(item)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
if items_unknown_to_calibre: if items_unknown_to_calibre:
log.debug("Received request to remove an unknown book to a collecition. Silently ignoring item.") 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 "ChangedTag": tag
}) })
sync_token.tags_last_modified = new_tags_last_modified 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 # 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") abort(400, description="Malformed request data is missing 'ReadingStates' key")
ub.session.merge(kobo_reading_state) ub.session.merge(kobo_reading_state)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return jsonify({ return jsonify({
"RequestResult": "Success", "RequestResult": "Success",
"UpdateResults": [update_results_response], "UpdateResults": [update_results_response],
@ -734,7 +770,10 @@ def get_or_create_reading_state(book_id):
kobo_reading_state.statistics = ub.KoboStatistics() kobo_reading_state.statistics = ub.KoboStatistics()
book_read.kobo_reading_state = kobo_reading_state book_read.kobo_reading_state = kobo_reading_state
ub.session.add(book_read) ub.session.add(book_read)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return book_read.kobo_reading_state return book_read.kobo_reading_state
@ -837,7 +876,10 @@ def HandleBookDeletionRequest(book_uuid):
archived_book.last_modified = datetime.datetime.utcnow() archived_book.last_modified = datetime.datetime.utcnow()
ub.session.merge(archived_book) ub.session.merge(archived_book)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return ("", 204) return ("", 204)

View File

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

View File

@ -41,10 +41,18 @@ logging.addLevelName(logging.WARNING, "WARN")
logging.addLevelName(logging.CRITICAL, "CRIT") 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): def get(name=None):
return logging.getLogger(name) return logging.getLogger(name)
def create(): def create():
parent_frame = inspect.stack(0)[1] parent_frame = inspect.stack(0)[1]
if hasattr(parent_frame, 'frame'): if hasattr(parent_frame, 'frame'):
@ -54,7 +62,6 @@ def create():
parent_module = inspect.getmodule(parent_frame) parent_module = inspect.getmodule(parent_frame)
return get(parent_module.__name__) return get(parent_module.__name__)
def is_debug_enabled(): def is_debug_enabled():
return logging.root.level <= logging.DEBUG return logging.root.level <= logging.DEBUG
@ -99,6 +106,7 @@ def setup(log_file, log_level=None):
May be called multiple times. May be called multiple times.
''' '''
log_level = log_level or DEFAULT_LOG_LEVEL log_level = log_level or DEFAULT_LOG_LEVEL
logging.setLoggerClass(_Logger)
logging.getLogger(__package__).setLevel(log_level) logging.getLogger(__package__).setLevel(log_level)
r = logging.root r = logging.root

View File

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

View File

@ -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 . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
from .helper import get_download_link, get_book_cover from .helper import get_download_link, get_book_cover
from .pagination import Pagination 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 flask_babel import gettext as _
from babel import Locale as LC from babel import Locale as LC
from babel.core import UnknownLocaleError from babel.core import UnknownLocaleError

139
cps/remotelogin.py Normal file
View File

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

116
cps/render_template.py Normal file
View File

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

View File

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

View File

@ -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 # 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 # possible file / database corruption
item = self.queue.get(timeout=1) item = self.queue.get(timeout=1)
except queue.Empty as ex: except queue.Empty:
time.sleep(1) time.sleep(1)
continue continue
@ -161,7 +161,7 @@ class CalibreTask:
self.run(*args) self.run(*args)
except Exception as e: except Exception as e:
self._handleError(str(e)) self._handleError(str(e))
log.exception(e) log.debug_or_exception(e)
self.end_time = datetime.now() self.end_time = datetime.now()
@ -210,7 +210,6 @@ class CalibreTask:
self._progress = x self._progress = x
def _handleError(self, error_message): def _handleError(self, error_message):
log.exception(error_message)
self.stat = STAT_FAIL self.stat = STAT_FAIL
self.progress = 1 self.progress = 1
self.error = error_message self.error = error_message

View File

@ -22,6 +22,7 @@
from __future__ import division, print_function, unicode_literals from __future__ import division, print_function, unicode_literals
from datetime import datetime from datetime import datetime
import sys
from flask import Blueprint, request, flash, redirect, url_for from flask import Blueprint, request, flash, redirect, url_for
from flask_babel import gettext as _ 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.sql.expression import func
from sqlalchemy.exc import OperationalError, InvalidRequestError from sqlalchemy.exc import OperationalError, InvalidRequestError
from . import logger, ub, calibre_db from . import logger, ub, calibre_db, db
from .web import login_required_if_no_ano, render_title_template from .render_template import render_title_template
from .usermanagement import login_required_if_no_ano
shelf = Blueprint('shelf', __name__) shelf = Blueprint('shelf', __name__)
@ -138,18 +140,14 @@ def search_to_shelf(shelf_id):
books_for_shelf = ub.searched_ids[current_user.id] books_for_shelf = ub.searched_ids[current_user.id]
if not books_for_shelf: 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") flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first() maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0
if maxOrder[0] is None:
maxOrder = 0
else:
maxOrder = maxOrder[0]
for book in books_for_shelf: 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.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder))
shelf.last_modified = datetime.utcnow() shelf.last_modified = datetime.utcnow()
try: try:
@ -322,8 +320,11 @@ def delete_shelf_helper(cur_shelf):
ub.session.delete(cur_shelf) ub.session.delete(cur_shelf)
ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete() 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.add(ub.ShelfArchive(uuid=cur_shelf.uuid, user_id=cur_shelf.user_id))
ub.session.commit() try:
log.info("successfully deleted %s", cur_shelf) 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() cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
try: try:
delete_shelf_helper(cur_shelf) delete_shelf_helper(cur_shelf)
except (OperationalError, InvalidRequestError): except InvalidRequestError:
ub.session.rollback() ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error") flash(_(u"Settings DB is not Writeable"), category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
@shelf.route("/simpleshelf/<int:shelf_id>")
@shelf.route("/shelf/<int:shelf_id>", defaults={'shelf_type': 1})
@shelf.route("/shelf/<int:shelf_id>/<int:shelf_type>")
@login_required_if_no_ano @login_required_if_no_ano
def show_shelf(shelf_type, shelf_id): def show_simpleshelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() return render_show_shelf(2, shelf_id, 1, None)
result = list() @shelf.route("/shelf/<int:shelf_id>", defaults={"sort_param": "order", 'page': 1})
# user is allowed to access shelf @shelf.route("/shelf/<int:shelf_id>/<sort_param>", defaults={'page': 1})
if shelf and check_shelf_view_permissions(shelf): @shelf.route("/shelf/<int:shelf_id>/<sort_param>/<int:page>")
page = "shelf.html" if shelf_type == 1 else 'shelfdown.html' @login_required_if_no_ano
def show_shelf(shelf_id, sort_param, page):
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\ return render_show_shelf(1, shelf_id, page, sort_param)
.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/order/<int:shelf_id>", methods=["GET", "POST"]) @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() shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
result = list() result = list()
if shelf and check_shelf_view_permissions(shelf): if shelf and check_shelf_view_permissions(shelf):
books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \ result = calibre_db.session.query(db.Books)\
.order_by(ub.BookShelf.order.asc()).all() .join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id , isouter=True) \
for book in books_in_shelf2: .add_columns(calibre_db.common_filters().label("visible")) \
cur_book = calibre_db.get_filtered_book(book.book_id) .filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all()
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': []})
return render_title_template('shelf_order.html', entries=result, return render_title_template('shelf_order.html', entries=result,
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name), title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
shelf=shelf, page="shelforder") 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"))

View File

@ -240,7 +240,7 @@ body.blur .row-fluid .col-sm-10 {
.col-sm-10 .book-meta > div.btn-toolbar:after { .col-sm-10 .book-meta > div.btn-toolbar:after {
content: ''; content: '';
direction: block; direction: ltr;
position: fixed; position: fixed;
top: 120px; top: 120px;
right: 0; right: 0;
@ -398,20 +398,17 @@ body.blur .row-fluid .col-sm-10 {
.shelforder #sortTrue > div:hover { .shelforder #sortTrue > div:hover {
background-color: hsla(0, 0%, 100%, .06) !important; background-color: hsla(0, 0%, 100%, .06) !important;
cursor: move;
cursor: grab; cursor: grab;
cursor: -webkit-grab;
color: #eee color: #eee
} }
.shelforder #sortTrue > div:active { .shelforder #sortTrue > div:active {
cursor: grabbing; cursor: grabbing;
cursor: -webkit-grabbing
} }
.shelforder #sortTrue > div:before { .shelforder #sortTrue > div:before {
content: "\EA53"; content: "\EA53";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
margin-right: 30px; margin-right: 30px;
margin-left: 15px; margin-left: 15px;
vertical-align: bottom; 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 { body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before {
content: "\e155"; content: "\e155";
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings', serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 1; 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 { #have_read_cb + label:before, #have_read_cb:checked + label:before {
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings', serif;
font-size: 16px; font-size: 16px;
height: 40px; height: 40px;
width: 60px; width: 60px;
@ -550,13 +547,12 @@ body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before {
height: 60px; height: 60px;
width: 50px; width: 50px;
cursor: pointer; cursor: pointer;
margin: 0;
display: inline-block; display: inline-block;
margin-top: -4px; margin: -4px 0 0;
} }
#archived_cb + label:before, #archived_cb:checked + label:before { #archived_cb + label:before, #archived_cb:checked + label:before {
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings', serif;
font-size: 16px; font-size: 16px;
height: 40px; height: 40px;
width: 60px; width: 60px;
@ -618,7 +614,7 @@ div[aria-label="Edit/Delete book"] > .btn > span {
div[aria-label="Edit/Delete book"] > .btn > span:before { div[aria-label="Edit/Delete book"] > .btn > span:before {
content: "\EA5d"; content: "\EA5d";
font-family: plex-icons; font-family: plex-icons, serif;
font-size: 20px; font-size: 20px;
padding: 16px 15px; padding: 16px 15px;
display: inline-block; display: inline-block;
@ -760,7 +756,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a
.home-btn { .home-btn {
color: hsla(0, 0%, 100%, .7); color: hsla(0, 0%, 100%, .7);
line-height: 34.29px; line-height: 34px;
margin: 0; margin: 0;
padding: 0; padding: 0;
position: absolute; position: absolute;
@ -770,7 +766,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a
.home-btn > a { .home-btn > a {
color: rgba(255, 255, 255, .7); color: rgba(255, 255, 255, .7);
font-family: plex-icons-new; font-family: plex-icons-new, serif;
line-height: 60px; line-height: 60px;
position: relative; position: relative;
text-align: center; text-align: center;
@ -800,7 +796,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.home-btn > a:hove
.glyphicon-search:before { .glyphicon-search:before {
content: "\EA4F"; content: "\EA4F";
font-family: plex-icons font-family: plex-icons, serif
} }
#nav_about:after, .profileDrop > span:after, .profileDrop > span:before { #nav_about:after, .profileDrop > span:after, .profileDrop > span:before {
@ -966,7 +962,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d
#form-upload .form-group .btn:before { #form-upload .form-group .btn:before {
content: "\e043"; content: "\e043";
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings', serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 1; 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 { #form-upload .form-group .btn:after {
content: "\EA13"; content: "\EA13";
position: absolute; position: absolute;
font-family: plex-icons-new; font-family: plex-icons-new, serif;
font-size: 8px; font-size: 8px;
background: #3c444a; background: #3c444a;
color: hsla(0, 0%, 100%, .7); 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; text-transform: none;
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
font-family: plex-icons-new; font-family: plex-icons-new, serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
line-height: 1; 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 { body > div.navbar.navbar-default.navbar-static-top > div > form > div > span > button:before {
content: "\EA32"; content: "\EA32";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
color: #eee; color: #eee;
background: #555; background: #555;
font-size: 10px; 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 { body > div.navbar.navbar-default.navbar-static-top > div > form:before {
content: "\EA4F"; content: "\EA4F";
display: block; display: block;
font-family: plex-icons; font-family: plex-icons, serif;
position: absolute; position: absolute;
color: #eee; color: #eee;
font-weight: 400; 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 { body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > div > span.input-group-btn:before {
content: "\EA4F"; content: "\EA4F";
display: block; display: block;
font-family: plex-icons; font-family: plex-icons, serif;
position: absolute; position: absolute;
left: -298px; left: -298px;
top: 8px; 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 { body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul > li > #top_admin > .glyphicon-dashboard::before {
content: "\EA31"; content: "\EA31";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
font-size: 20px font-size: 20px
} }
@ -1272,7 +1268,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover {
user-select: none user-select: none
} }
.navigation li, .navigation li:not(ul>li) { .navigation li, .navigation li:not(ul > li) {
border-radius: 0 4px 4px 0 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 { #nav_hot .glyphicon-fire::before {
content: "\1F525"; content: "\1F525";
font-family: glyphicons regular font-family: glyphicons regular, serif
} }
.glyphicon-star:before { .glyphicon-star:before {
content: "\EA10"; content: "\EA10";
font-family: plex-icons-new font-family: plex-icons-new, serif
} }
#nav_rand .glyphicon-random::before { #nav_rand .glyphicon-random::before {
content: "\EA44"; content: "\EA44";
font-family: plex-icons-new font-family: plex-icons-new, serif
} }
.glyphicon-list::before { .glyphicon-list::before {
content: "\EA4D"; content: "\EA4D";
font-family: plex-icons-new font-family: plex-icons-new, serif
} }
#nav_about .glyphicon-info-sign::before { #nav_about .glyphicon-info-sign::before {
content: "\EA26"; content: "\EA26";
font-family: plex-icons-new font-family: plex-icons-new, serif
} }
#nav_cat .glyphicon-inbox::before, .glyphicon-tags::before { #nav_cat .glyphicon-inbox::before, .glyphicon-tags::before {
content: "\E067"; content: "\E067";
font-family: Glyphicons Regular; font-family: Glyphicons Regular, serif;
margin-left: 2px 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 { .navigation .create-shelf a:before {
content: "\EA13"; content: "\EA13";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
font-size: 100%; font-size: 100%;
padding-right: 10px; padding-right: 10px;
vertical-align: middle 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 { #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"; content: "\e352";
font-family: Glyphicons Regular; font-family: Glyphicons Regular, serif;
background: var(--color-secondary); background: var(--color-secondary);
border-radius: 50%; border-radius: 50%;
font-weight: 400; font-weight: 400;
@ -1521,8 +1517,8 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form
top: 0; top: 0;
left: 0; left: 0;
opacity: 0; opacity: 0;
background: -webkit-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(50% 50%, farthest-corner, 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%); background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
z-index: -9 z-index: -9
} }
@ -1562,8 +1558,8 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form
top: 0; top: 0;
left: 0; left: 0;
opacity: 0; opacity: 0;
background: -webkit-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(50% 50%, farthest-corner, 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%) 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 { body.me > div.container-fluid > div.row-fluid > div.col-sm-10:before {
content: ''; content: '';
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings', serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-size: 6vw; 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 body > div.container-fluid > div > div.col-sm-10 > div.pagination .page-previous > a
{ {
top: 0; top: 0;
font-family: plex-icons-new; font-family: plex-icons-new, serif;
font-weight: 100; font-weight: 100;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
line-height: 60px; 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 { body.serieslist > div.container-fluid > div > div.col-sm-10:before {
content: "\e044"; content: "\e044";
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings', serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
@ -2131,7 +2127,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 { body.catlist > div.container-fluid > div.row-fluid > div.col-sm-10:before {
content: "\E067"; content: "\E067";
font-family: Glyphicons Regular; font-family: Glyphicons Regular, serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
@ -2151,7 +2147,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 { 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-style: normal;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
@ -2492,7 +2488,6 @@ body > div.container-fluid > div > div.col-sm-10 > div.col-sm-8 > form > .btn.bt
} }
textarea { textarea {
resize: none;
resize: vertical resize: vertical
} }
@ -2838,7 +2833,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 { body.advanced_search > div.container-fluid > div > div.col-sm-10 > div.col-sm-8:before {
content: "\EA4F"; content: "\EA4F";
font-family: plex-icons; font-family: plex-icons, serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
@ -3195,7 +3190,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div.
#add-to-shelf > span.glyphicon.glyphicon-list:before { #add-to-shelf > span.glyphicon.glyphicon-list:before {
content: "\EA59"; content: "\EA59";
font-family: plex-icons; font-family: plex-icons, serif;
font-size: 18px font-size: 18px
} }
@ -3207,7 +3202,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 { #read-in-browser > span.glyphicon-eye-open:before, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.glyphicon-eye-open:before {
content: "\e352"; content: "\e352";
font-family: Glyphicons Regular; font-family: Glyphicons Regular, serif;
font-size: 18px; font-size: 18px;
padding-right: 5px padding-right: 5px
} }
@ -3219,7 +3214,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div.
#btnGroupDrop1 > span.glyphicon-download:before { #btnGroupDrop1 > span.glyphicon-download:before {
font-size: 20px; font-size: 20px;
content: "\ea66"; content: "\ea66";
font-family: plex-icons font-family: plex-icons, serif
} }
.col-sm-10 .book-meta > div.btn-toolbar { .col-sm-10 .book-meta > div.btn-toolbar {
@ -3323,7 +3318,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); -webkit-box-shadow: 0 4px 10px rgba(0, 0, 0, .35);
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); -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); 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; -webkit-transform-origin: center top;
-ms-transform-origin: center top; -ms-transform-origin: center top;
@ -3441,7 +3435,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 { .book-meta > div.more-stuff > .btn-toolbar > .btn-group[aria-label="Remove from shelves"] > a > .glyphicon-remove:before {
content: "\ea64"; 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 { body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > .col-sm-6 {
@ -3555,7 +3549,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 { body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [data-target="#DeleteShelfDialog"]:before {
content: "\EA6D"; content: "\EA6D";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
position: absolute; position: absolute;
color: hsla(0, 0%, 100%, .7); color: hsla(0, 0%, 100%, .7);
font-size: 20px; font-size: 20px;
@ -3585,7 +3579,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 { body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [href*=edit]:before {
content: "\EA5d"; content: "\EA5d";
font-family: plex-icons; font-family: plex-icons, serif;
position: absolute; position: absolute;
color: hsla(0, 0%, 100%, .7); color: hsla(0, 0%, 100%, .7);
font-size: 20px; font-size: 20px;
@ -3615,7 +3609,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 { body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [href*=order]:before {
content: "\E409"; content: "\E409";
font-family: Glyphicons Regular; font-family: Glyphicons Regular, serif;
position: absolute; position: absolute;
color: hsla(0, 0%, 100%, .7); color: hsla(0, 0%, 100%, .7);
font-size: 20px; font-size: 20px;
@ -3752,7 +3746,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a
.plexBack > a { .plexBack > a {
color: rgba(255, 255, 255, .7); color: rgba(255, 255, 255, .7);
font-family: plex-icons-new; font-family: plex-icons-new, serif;
-webkit-font-variant-ligatures: normal; -webkit-font-variant-ligatures: normal;
font-variant-ligatures: normal; font-variant-ligatures: normal;
line-height: 60px; line-height: 60px;
@ -3864,11 +3858,9 @@ body.login > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.l
-webkit-transform: translateY(-50%); -webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%); -ms-transform: translateY(-50%);
transform: translateY(-50%); transform: translateY(-50%);
border-style: solid;
vertical-align: middle; vertical-align: middle;
-webkit-transition: border .2s, -webkit-transform .4s; -webkit-transition: border .2s, -webkit-transform .4s;
-o-transition: border .2s, transform .4s; -o-transition: border .2s, transform .4s;
transition: border .2s, transform .4s;
transition: border .2s, transform .4s, -webkit-transform .4s; transition: border .2s, transform .4s, -webkit-transform .4s;
margin: 9px 6px margin: 9px 6px
} }
@ -3887,11 +3879,9 @@ body.login > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.l
-webkit-transform: translateY(-50%); -webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%); -ms-transform: translateY(-50%);
transform: translateY(-50%); transform: translateY(-50%);
border-style: solid;
vertical-align: middle; vertical-align: middle;
-webkit-transition: border .2s, -webkit-transform .4s; -webkit-transition: border .2s, -webkit-transform .4s;
-o-transition: border .2s, transform .4s; -o-transition: border .2s, transform .4s;
transition: border .2s, transform .4s;
transition: border .2s, transform .4s, -webkit-transform .4s; transition: border .2s, transform .4s, -webkit-transform .4s;
margin: 12px 6px margin: 12px 6px
} }
@ -3971,7 +3961,7 @@ body.author img.bg-blur[src=undefined] {
body.author:not(.authorlist) .undefined-img:before { body.author:not(.authorlist) .undefined-img:before {
content: "\e008"; content: "\e008";
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings', serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
@ -4120,7 +4110,7 @@ body.shelf.modal-open > .container-fluid {
font-size: 18px; font-size: 18px;
color: #999; color: #999;
display: inline-block; display: inline-block;
font-family: Glyphicons Regular; font-family: Glyphicons Regular, serif;
font-style: normal; font-style: normal;
font-weight: 400 font-weight: 400
} }
@ -4221,7 +4211,7 @@ body.shelf.modal-open > .container-fluid {
#remove-from-shelves > .btn > span:before { #remove-from-shelves > .btn > span:before {
content: "\EA52"; content: "\EA52";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
color: transparent; color: transparent;
padding-left: 5px padding-left: 5px
} }
@ -4233,7 +4223,7 @@ body.shelf.modal-open > .container-fluid {
#remove-from-shelves > a:first-of-type:before { #remove-from-shelves > a:first-of-type:before {
content: "\EA4D"; content: "\EA4D";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
position: absolute; position: absolute;
color: hsla(0, 0%, 100%, .45); color: hsla(0, 0%, 100%, .45);
font-style: normal; font-style: normal;
@ -4273,7 +4263,7 @@ body.shelf.modal-open > .container-fluid {
content: "\E208"; content: "\E208";
padding-right: 10px; padding-right: 10px;
display: block; display: block;
font-family: Glyphicons Regular; font-family: Glyphicons Regular, serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
position: absolute; position: absolute;
@ -4284,7 +4274,6 @@ body.shelf.modal-open > .container-fluid {
opacity: .5; opacity: .5;
-webkit-transition: -webkit-transform .3s ease-out; -webkit-transition: -webkit-transform .3s ease-out;
-o-transition: 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; transition: transform .3s ease-out, -webkit-transform .3s ease-out;
-webkit-transform: translate(0, -60px); -webkit-transform: translate(0, -60px);
-ms-transform: translate(0, -60px); -ms-transform: translate(0, -60px);
@ -4344,7 +4333,7 @@ body.advanced_search > div.container-fluid > div > div.col-sm-10 > div.col-sm-8
.glyphicon-remove:before { .glyphicon-remove:before {
content: "\EA52"; content: "\EA52";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
font-weight: 400 font-weight: 400
} }
@ -4430,7 +4419,7 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div
body:not(.blur) #nav_new:before { body:not(.blur) #nav_new:before {
content: "\EA4F"; content: "\EA4F";
font-family: plex-icons; font-family: plex-icons, serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
@ -4456,7 +4445,7 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div
color: hsla(0, 0%, 100%, .7); color: hsla(0, 0%, 100%, .7);
cursor: pointer; cursor: pointer;
display: block; display: block;
font-family: plex-icons-new; font-family: plex-icons-new, serif;
font-size: 20px; font-size: 20px;
font-stretch: 100%; font-stretch: 100%;
font-style: normal; font-style: normal;
@ -4552,12 +4541,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 { 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 { 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: ''; content: '';
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings', serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-size: 6vw; font-size: 6vw;
@ -4661,7 +4650,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
content: "\e352"; content: "\e352";
display: inline-block; display: inline-block;
position: absolute; position: absolute;
font-family: Glyphicons Regular; font-family: Glyphicons Regular, serif;
background: var(--color-secondary); background: var(--color-secondary);
color: #fff; color: #fff;
border-radius: 50%; border-radius: 50%;
@ -4699,8 +4688,8 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
top: 0; top: 0;
left: 0; left: 0;
opacity: 0; opacity: 0;
background: -webkit-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(50% 50%, farthest-corner, 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%) background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%)
} }
@ -4752,7 +4741,7 @@ body.admin td > a:hover {
.glyphicon-ok::before { .glyphicon-ok::before {
content: "\EA55"; content: "\EA55";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
font-weight: 400 font-weight: 400
} }
@ -4821,7 +4810,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-position: center center, center center, center center !important;
background-size: auto, auto, cover !important; background-size: auto, auto, cover !important;
-webkit-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; -o-background-size: auto, auto, cover !important;
width: 100%; width: 100%;
height: 60px; height: 60px;
@ -4887,7 +4876,6 @@ body.read:not(.blur) a[href*=readbooks] {
.tooltip.in { .tooltip.in {
opacity: 1; opacity: 1;
-o-transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4); -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); 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); -webkit-transform: translate(0) scale(1);
-ms-transform: translate(0) scale(1); -ms-transform: translate(0) scale(1);
@ -4987,7 +4975,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 { 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"; content: "\EA6D";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
font-size: 18px; font-size: 18px;
color: hsla(0, 0%, 100%, .7) color: hsla(0, 0%, 100%, .7)
} }
@ -5072,7 +5060,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 { 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"; content: "\EA58";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
font-weight: 400; font-weight: 400;
right: 20px; right: 20px;
position: absolute position: absolute
@ -5080,7 +5068,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 { 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"; content: "\EA57";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
font-weight: 400; font-weight: 400;
right: 20px; right: 20px;
position: absolute position: absolute
@ -5143,7 +5131,7 @@ body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.boot
.epub-back:before { .epub-back:before {
content: "\EA1C"; content: "\EA1C";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
font-weight: 400; font-weight: 400;
color: #4f4f4f; color: #4f4f4f;
position: absolute; position: absolute;
@ -5306,7 +5294,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 { 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"; content: "\EA6D";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
font-size: 18px; font-size: 18px;
color: hsla(0, 0%, 100%, .7); color: hsla(0, 0%, 100%, .7);
vertical-align: super vertical-align: super
@ -5466,7 +5454,7 @@ body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm
#main-nav + #scnd-nav .create-shelf a:before { #main-nav + #scnd-nav .create-shelf a:before {
content: "\EA13"; content: "\EA13";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
font-size: 100%; font-size: 100%;
padding-right: 10px; padding-right: 10px;
vertical-align: middle vertical-align: middle
@ -5511,7 +5499,7 @@ body.admin.modal-open .navbar {
content: "\E208"; content: "\E208";
padding-right: 10px; padding-right: 10px;
display: block; display: block;
font-family: Glyphicons Regular; font-family: Glyphicons Regular, serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
position: absolute; position: absolute;
@ -5522,7 +5510,6 @@ body.admin.modal-open .navbar {
opacity: .5; opacity: .5;
-webkit-transition: -webkit-transform .3s ease-out; -webkit-transition: -webkit-transform .3s ease-out;
-o-transition: 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; transition: transform .3s ease-out, -webkit-transform .3s ease-out;
-webkit-transform: translate(0, -60px); -webkit-transform: translate(0, -60px);
-ms-transform: translate(0, -60px); -ms-transform: translate(0, -60px);
@ -5576,22 +5563,22 @@ body.admin.modal-open .navbar {
#RestartDialog > .modal-dialog > .modal-content > .modal-header:before { #RestartDialog > .modal-dialog > .modal-content > .modal-header:before {
content: "\EA4F"; content: "\EA4F";
font-family: plex-icons-new font-family: plex-icons-new, serif
} }
#ShutdownDialog > .modal-dialog > .modal-content > .modal-header:before { #ShutdownDialog > .modal-dialog > .modal-content > .modal-header:before {
content: "\E064"; content: "\E064";
font-family: glyphicons regular font-family: glyphicons regular, serif
} }
#StatusDialog > .modal-dialog > .modal-content > .modal-header:before { #StatusDialog > .modal-dialog > .modal-content > .modal-header:before {
content: "\EA15"; content: "\EA15";
font-family: plex-icons-new font-family: plex-icons-new, serif
} }
#deleteModal > .modal-dialog > .modal-content > .modal-header:before { #deleteModal > .modal-dialog > .modal-content > .modal-header:before {
content: "\EA6D"; content: "\EA6D";
font-family: plex-icons-new font-family: plex-icons-new, serif
} }
#RestartDialog > .modal-dialog > .modal-content > .modal-header:after { #RestartDialog > .modal-dialog > .modal-content > .modal-header:after {
@ -5982,7 +5969,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
.home-btn { .home-btn {
height: 48px; height: 48px;
line-height: 28.29px; line-height: 28px;
right: 10px; right: 10px;
left: auto left: auto
} }
@ -5994,7 +5981,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
.plexBack { .plexBack {
height: 48px; height: 48px;
line-height: 28.29px; line-height: 28px;
left: 48px; left: 48px;
display: none display: none
} }
@ -6073,7 +6060,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 { body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > div > span.input-group-btn:before {
content: "\EA33"; content: "\EA33";
display: block; display: block;
font-family: plex-icons-new; font-family: plex-icons-new, serif;
position: fixed; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
@ -6225,7 +6212,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
#form-upload .form-group .btn:before { #form-upload .form-group .btn:before {
content: "\e043"; content: "\e043";
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings', serif;
line-height: 1; line-height: 1;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
color: #fff; color: #fff;
@ -6243,7 +6230,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
#form-upload .form-group .btn:after { #form-upload .form-group .btn:after {
content: "\EA13"; content: "\EA13";
position: absolute; position: absolute;
font-family: plex-icons-new; font-family: plex-icons-new, serif;
font-size: 8px; font-size: 8px;
background: #3c444a; background: #3c444a;
color: #fff; color: #fff;
@ -6296,7 +6283,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
} }
#top_admin, #top_tasks { #top_admin, #top_tasks {
padding: 11.5px 15px; padding: 12px 15px;
font-size: 13px; font-size: 13px;
line-height: 1.71428571; line-height: 1.71428571;
overflow: hidden overflow: hidden
@ -6305,7 +6292,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
#top_admin > .glyphicon, #top_tasks > .glyphicon-tasks { #top_admin > .glyphicon, #top_tasks > .glyphicon-tasks {
position: relative; position: relative;
top: 0; top: 0;
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings', serif;
line-height: 1; line-height: 1;
border-radius: 0; border-radius: 0;
background: 0 0; background: 0 0;
@ -6324,7 +6311,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 { #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; text-transform: none;
font-family: plex-icons-new; font-family: plex-icons-new, serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
@ -6649,7 +6636,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 { .author > .container-fluid > .row-fluid > .col-sm-10 > h2:after {
content: "\e008"; content: "\e008";
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings', serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
@ -6854,7 +6841,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
color: hsla(0, 0%, 100%, .7); color: hsla(0, 0%, 100%, .7);
cursor: pointer; cursor: pointer;
display: block; display: block;
font-family: plex-icons-new; font-family: plex-icons-new, serif;
font-size: 20px; font-size: 20px;
font-stretch: 100%; font-stretch: 100%;
font-style: normal; font-style: normal;
@ -7025,11 +7012,9 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
-webkit-transform: translateY(-50%); -webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%); -ms-transform: translateY(-50%);
transform: translateY(-50%); transform: translateY(-50%);
border-style: solid;
vertical-align: middle; vertical-align: middle;
-webkit-transition: border .2s, -webkit-transform .4s; -webkit-transition: border .2s, -webkit-transform .4s;
-o-transition: border .2s, transform .4s; -o-transition: border .2s, transform .4s;
transition: border .2s, transform .4s;
transition: border .2s, transform .4s, -webkit-transform .4s; transition: border .2s, transform .4s, -webkit-transform .4s;
margin: 12px 6px margin: 12px 6px
} }
@ -7048,18 +7033,16 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
-webkit-transform: translateY(-50%); -webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%); -ms-transform: translateY(-50%);
transform: translateY(-50%); transform: translateY(-50%);
border-style: solid;
vertical-align: middle; vertical-align: middle;
-webkit-transition: border .2s, -webkit-transform .4s; -webkit-transition: border .2s, -webkit-transform .4s;
-o-transition: border .2s, transform .4s; -o-transition: border .2s, transform .4s;
transition: border .2s, transform .4s;
transition: border .2s, transform .4s, -webkit-transform .4s; transition: border .2s, transform .4s, -webkit-transform .4s;
margin: 9px 6px margin: 9px 6px
} }
body.author:not(.authorlist) .blur-wrapper:before, body.author > .container-fluid > .row-fluid > .col-sm-10 > h2:after { body.author:not(.authorlist) .blur-wrapper:before, body.author > .container-fluid > .row-fluid > .col-sm-10 > h2:after {
content: "\e008"; content: "\e008";
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings', serif;
font-weight: 400; font-weight: 400;
z-index: 9; z-index: 9;
line-height: 1; line-height: 1;
@ -7390,7 +7373,6 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
-webkit-transition: -webkit-transform .5s; -webkit-transition: -webkit-transform .5s;
-o-transition: transform .5s; -o-transition: transform .5s;
transition: transform .5s;
transition: transform .5s, -webkit-transform .5s; transition: transform .5s, -webkit-transform .5s;
z-index: 99 z-index: 99
} }
@ -7405,7 +7387,6 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
transform: translate3d(-240px, 0, 0); transform: translate3d(-240px, 0, 0);
-webkit-transition: -webkit-transform .5s; -webkit-transition: -webkit-transform .5s;
-o-transition: transform .5s; -o-transition: transform .5s;
transition: transform .5s;
transition: transform .5s, -webkit-transform .5s; transition: transform .5s, -webkit-transform .5s;
top: 0; top: 0;
margin: 0; margin: 0;
@ -7444,7 +7425,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
text-align: center; text-align: center;
min-width: 40px; min-width: 40px;
pointer-events: none; pointer-events: none;
color: # // color: #
} }
.col-xs-12 > .row > .col-xs-10 { .col-xs-12 > .row > .col-xs-10 {
@ -7555,7 +7536,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 { body.publisherlist > div.container-fluid > div > div.col-sm-10:before {
content: "\e241"; content: "\e241";
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings', serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
@ -7575,7 +7556,7 @@ body.publisherlist > div.container-fluid > div > div.col-sm-10:before {
body.ratingslist > div.container-fluid > div > div.col-sm-10:before { body.ratingslist > div.container-fluid > div > div.col-sm-10:before {
content: "\e007"; content: "\e007";
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings', serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
@ -7601,7 +7582,7 @@ body.ratingslist > div.container-fluid > div > div.col-sm-10:before {
body.formatslist > div.container-fluid > div > div.col-sm-10:before { body.formatslist > div.container-fluid > div > div.col-sm-10:before {
content: "\e022"; content: "\e022";
font-family: 'Glyphicons Halflings'; font-family: 'Glyphicons Halflings', serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
@ -7776,7 +7757,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 { body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphicon-trash:before {
content: "\EA6D"; content: "\EA6D";
font-family: plex-icons-new font-family: plex-icons-new, serif
} }
#DeleteDomain { #DeleteDomain {
@ -7799,7 +7780,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic
content: "\E208"; content: "\E208";
padding-right: 10px; padding-right: 10px;
display: block; display: block;
font-family: Glyphicons Regular; font-family: Glyphicons Regular, serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
position: absolute; position: absolute;
@ -7810,7 +7791,6 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic
opacity: .5; opacity: .5;
-webkit-transition: -webkit-transform .3s ease-out; -webkit-transition: -webkit-transform .3s ease-out;
-o-transition: 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; transition: transform .3s ease-out, -webkit-transform .3s ease-out;
-webkit-transform: translate(0, -60px); -webkit-transform: translate(0, -60px);
-ms-transform: translate(0, -60px); -ms-transform: translate(0, -60px);
@ -7849,7 +7829,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic
#DeleteDomain > .modal-dialog > .modal-content > .modal-header:before { #DeleteDomain > .modal-dialog > .modal-content > .modal-header:before {
content: "\EA6D"; content: "\EA6D";
font-family: plex-icons-new; font-family: plex-icons-new, serif;
padding-right: 10px; padding-right: 10px;
font-size: 18px; font-size: 18px;
color: #999; color: #999;

View File

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

View File

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

View File

@ -116,6 +116,7 @@ a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d
display: block; display: block;
max-width: 100%; max-width: 100%;
height: auto; height: auto;
max-height: 100%;
} }
.container-fluid .discover{ margin-bottom: 50px; } .container-fluid .discover{ margin-bottom: 50px; }
@ -132,12 +133,19 @@ a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d
position: relative; position: relative;
} }
.container-fluid .book .cover img { .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; border: 1px solid #fff;
box-sizing: border-box; box-sizing: border-box;
height: 100%;
bottom: 0;
position: absolute;
-webkit-box-shadow: 0 5px 8px -6px #777; -webkit-box-shadow: 0 5px 8px -6px #777;
-moz-box-shadow: 0 5px 8px -6px #777; -moz-box-shadow: 0 5px 8px -6px #777;
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 .icon-bar {background-color: #000; }
.navbar-default .navbar-toggle {border-color: #000; } .navbar-default .navbar-toggle {border-color: #000; }
.cover { margin-bottom: 10px; } .cover { margin-bottom: 10px; }
.cover .badge{ .cover .badge{
position: absolute; position: absolute;
top: 2px; top: 2px;
left: 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;} .cover-height { max-height: 100px;}
@ -241,7 +260,7 @@ span.glyphicon.glyphicon-tags {
.button-link {color: #fff; } .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: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-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; } .panel-body {background-color: #f5f5f5; }
.spinner {margin: 0 41%; } .spinner {margin: 0 41%; }
.spinner2 {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-input { display:inline-block; }
.editable-cancel { .editable-cancel {
margin-bottom: 0px !important; margin-bottom: 0 !important;
margin-left: 7px !important; margin-left: 7px !important;
} }
.editable-submit { margin-bottom: 0px !important; } .editable-submit { margin-bottom: 0 !important; }
.filterheader { margin-bottom: 20px; } .filterheader { margin-bottom: 20px; }
.errorlink { margin-top: 20px; } .errorlink { margin-top: 20px; }
.emailconfig { margin-top: 10px; } .emailconfig { margin-top: 10px; }
@ -326,7 +345,7 @@ input.pill:not(:checked) + label .glyphicon { display: none; }
} }
div.log { div.log {
font-family: Courier New; font-family: Courier New, serif;
font-size: 12px; font-size: 12px;
box-sizing: border-box; box-sizing: border-box;
height: 700px; height: 700px;

View File

@ -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(); var form = $("form").serialize();
$.getJSON( getPath() + "/get_matching_tags", form, function( data ) { $.getJSON( getPath() + "/get_matching_tags", form, function( data ) {
$(".tags_click").each(function() { $(".tags_click").each(function() {
if ($.inArray(parseInt($(this).children("input").first().val(), 10), data.tags) === -1 ) { if ($.inArray(parseInt($(this).val(), 10), data.tags) === -1) {
if (!($(this).hasClass("active"))) { if(!$(this).prop("selected")) {
$(this).addClass("disabled"); $(this).prop("disabled", true);
} }
} else { } 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");
}); });
}); });

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() { $("#delete_confirm").click(function() {
//get data-id attribute of the clicked element //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({ $(".discover .row").isotope({
// options // options
itemSelector : ".book", itemSelector : ".book",
@ -402,18 +480,98 @@ $(function() {
$("#config_delete_kobo_token").show(); $("#config_delete_kobo_token").show();
}); });
$("#btndeletetoken").click(function() { $("#config_delete_kobo_token").click(function() {
//get data-id attribute of the clicked element ConfirmDialog(
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src; $(this).attr('id'),
var path = src.substring(0, src.lastIndexOf("/")); $(this).data('value'),
// var domainId = $(this).value("domainId"); function (value) {
$.ajax({ var pathname = document.getElementsByTagName("script");
method:"get", var src = pathname[pathname.length - 1].src;
url: path + "/../../kobo_auth/deleteauthtoken/" + this.value, var path = src.substring(0, src.lastIndexOf("/"));
}); $.ajax({
$("#modalDeleteToken").modal("hide"); method: "get",
$("#config_delete_kobo_token").hide(); 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() { $(window).resize(function() {

View File

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

View File

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

View File

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

View File

@ -36,7 +36,10 @@
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book"> <div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover"> <div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}"> <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> </a>
</div> </div>
<div class="meta"> <div class="meta">

View File

@ -197,7 +197,8 @@
{% endblock %} {% endblock %}
{% block modal %} {% 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 fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">

View File

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

View File

@ -16,12 +16,19 @@
<div id="collapseOne" class="panel-collapse collapse in"> <div id="collapseOne" class="panel-collapse collapse in">
<div class="panel-body"> <div class="panel-body">
<label for="config_calibre_dir">{{_('Location of Calibre Database')}}</label> <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"> <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"> <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> </span>
{% endif %}
</div> </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'] %} {% if feature_support['gdrive'] %}
<div class="form-group required"> <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 %} > <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"> <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"> <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"> <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> </span>
</div> </div>
<label for="config_calibre_dir" >{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label> <label for="config_calibre_dir" >{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label>
<div class="form-group input-group"> <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"> <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"> <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> </span>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -268,21 +275,21 @@
<div class="form-group input-group"> <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"> <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"> <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> </span>
</div> </div>
<label for="config_ldap_cert_path">{{_('LDAP Certificate Path (Only needed for Client Certificate Authentication)')}}</label> <label for="config_ldap_cert_path">{{_('LDAP Certificate Path (Only needed for Client Certificate Authentication)')}}</label>
<div class="form-group input-group"> <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"> <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"> <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> </span>
</div> </div>
<label for="config_ldap_key_path">{{_('LDAP Keyfile Path (Only needed for Client Certificate Authentication)')}}</label> <label for="config_ldap_key_path">{{_('LDAP Keyfile Path (Only needed for Client Certificate Authentication)')}}</label>
<div class="form-group input-group"> <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"> <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"> <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> </span>
</div> </div>
</div> </div>
@ -384,7 +391,7 @@
<div class="form-group input-group"> <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"> <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"> <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> </span>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -395,7 +402,7 @@
<div class="form-group input-group"> <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"> <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"> <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> </span>
</div> </div>
{% if feature_support['rar'] %} {% if feature_support['rar'] %}
@ -403,7 +410,7 @@
<div class="form-group input-group"> <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"> <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"> <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> </span>
</div> </div>
{% endif %} {% endif %}
@ -412,8 +419,6 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="col-sm-12"> <div class="col-sm-12">
{% if not show_login_button %} {% if not show_login_button %}
<button type="submit" name="submit" class="btn btn-default">{{_('Save')}}</button> <button type="submit" name="submit" class="btn btn-default">{{_('Save')}}</button>
@ -428,6 +433,9 @@
</form> </form>
</div> </div>
{% endblock %} {% endblock %}
{% block modal %}
{{ filechooser_modal() }}
{% endblock %}
{% block js %} {% block js %}
<script type="text/javascript"> <script type="text/javascript">
$(document).on('change', '#config_use_google_drive', function() { $(document).on('change', '#config_use_google_drive', function() {

View File

@ -8,7 +8,10 @@
<div class="cover"> <div class="cover">
{% if entry.has_cover is defined %} {% 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"> <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> </a>
{% endif %} {% endif %}
</div> </div>

View File

@ -89,20 +89,7 @@
{% endblock %} {% endblock %}
{% block modal %} {% block modal %}
{% if g.allow_registration %} {% if g.allow_registration %}
<div id="DeleteDomain" class="modal fade" role="dialog"> {{ delete_confirm_modal() }}
<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>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block js %} {% block js %}

View File

@ -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="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"> <div class="cover">
<a href="{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].series[0].id )}}"> <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="img">
<span class="badge">{{entry.count}}</span> <img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/>
<span class="badge">{{entry.count}}</span>
</span>
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">

View File

@ -8,7 +8,10 @@
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books_rand"> <div class="col-sm-3 col-lg-2 col-xs-6 book" id="books_rand">
<div class="cover"> <div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <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> </a>
</div> </div>
<div class="meta"> <div class="meta">
@ -82,7 +85,10 @@
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books"> <div class="col-sm-3 col-lg-2 col-xs-6 book" id="books">
<div class="cover"> <div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <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 }}"/> <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> </a>
</div> </div>
<div class="meta"> <div class="meta">

View File

@ -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> <!DOCTYPE html>
<html lang="{{ g.user.locale }}"> <html lang="{{ g.user.locale }}">
<head> <head>
@ -189,8 +189,6 @@
</div> </div>
</div> </div>
{% block modal %}{% endblock %} {% block modal %}{% endblock %}
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
<!-- Include all compiled plugins (below), or include individual files as needed --> <!-- 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/context.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/plugins.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/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> <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"> <script type="text/javascript">
$(function() { $(function() {
$("#form-upload").uploadprogress({ $("#form-upload").uploadprogress({
@ -219,6 +210,13 @@
}); });
}); });
</script> </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 %} {% block js %}{% endblock %}
</body> </body>
</html> </html>

View File

@ -22,7 +22,7 @@
<button type="submit" name="forgot" value="forgot" class="btn btn-default">{{_('Forgot Password?')}}</button> <button type="submit" name="forgot" value="forgot" class="btn btn-default">{{_('Forgot Password?')}}</button>
{% endif %} {% endif %}
{% if config.config_remote_login %} {% 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 %} {% endif %}
{% if config.config_login_type == 2 %} {% if config.config_login_type == 2 %}
{% if 1 in oauth_check %} {% if 1 in oauth_check %}

View File

@ -37,7 +37,7 @@
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
{% macro delete_book(bookid) %} {% macro delete_book() %}
{% if g.user.role_delete_books() %} {% if g.user.role_delete_books() %}
<div class="modal fade" id="deleteModal" role="dialog" aria-labelledby="metaDeleteLabel"> <div class="modal fade" id="deleteModal" role="dialog" aria-labelledby="metaDeleteLabel">
<div class="modal-dialog"> <div class="modal-dialog">
@ -68,3 +68,56 @@
</div> </div>
{% endif %} {% endif %}
{% endmacro %} {% 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 %}

View File

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

View File

@ -43,7 +43,10 @@
<div class="cover"> <div class="cover">
{% if entry.has_cover is defined %} {% 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"> <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> </a>
{% endif %} {% endif %}
</div> </div>

View File

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

View File

@ -2,23 +2,38 @@
{% block body %} {% block body %}
<div class="discover"> <div class="discover">
<h2>{{title}}</h2> <h2>{{title}}</h2>
{% if g.user.role_download() %} {% 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> <a id="shelf_down" href="{{ url_for('shelf.show_simpleshelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a>
{% endif %} {% endif %}
{% if g.user.is_authenticated %} {% if g.user.is_authenticated %}
{% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %} {% 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> <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') }} </a> <a id="edit_shelf" href="{{ url_for('shelf.edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf Properties') }} </a>
<a id="order_shelf" href="{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </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 %}
{% endif %} {% endif %}
<div class="row display-flex"> <div class="row display-flex">
{% for entry in entries %} {% for entry in entries %}
<div class="col-sm-3 col-lg-2 col-xs-6 book"> <div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover"> <div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <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> </a>
</div> </div>
<div class="meta"> <div class="meta">
@ -68,7 +83,7 @@
{% endfor %} {% endfor %}
</div> </div>
</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-dialog modal-sm">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header bg-danger text-center"> <div class="modal-header bg-danger text-center">
@ -82,6 +97,9 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div-->
{% endblock %} {% endblock %}
{% block modal %}
{{ delete_confirm_modal() }}
{% endblock %}

View File

@ -5,30 +5,39 @@
<div>{{_('Drag to Rearrange Order')}}</div> <div>{{_('Drag to Rearrange Order')}}</div>
<div id="sortTrue" class="list-group"> <div id="sortTrue" class="list-group">
{% for entry in entries %} {% 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="row">
<div class="col-lg-2 col-sm-4 hidden-xs"> <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>
<div class="col-lg-10 col-sm-8 col-xs-12"> <div class="col-lg-10 col-sm-8 col-xs-12">
{{entry['title']}} {% if entry['visible'] %}
{% if entry['series']|length > 0 %} {{entry['Books']['title']}}
{% if entry['Books']['series']|length > 0 %}
<br> <br>
{{entry['series_index']}} - {{entry['series'][0].name}} {{entry['Books']['series_index']}} - {{entry['Books']['series'][0].name}}
{% endif %} {% endif %}
<br> <br>
{% for author in entry['author'] %} {% for author in entry['Books']['author'] %}
{{author.name.replace('|',',')}} {{author.name.replace('|',',')}}
{% if not loop.last %} {% if not loop.last %}
&amp; &amp;
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% else %}
{{_('Hidden Book')}}
<br>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </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> <a href="{{ url_for('shelf.show_shelf', shelf_id=shelf.id) }}" id="shelf_back" class="btn btn-default">{{_('Back')}}</a>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -55,27 +55,14 @@
</div> </div>
<div class="btn-group" role="group" aria-label="Download, send to Kindle, reading"> <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 %} {% if entry.data|length %}
<div class="btn-group" role="group"> <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="btnGroupDrop{{entry.id}}{{format.format|lower}}" class="btn btn-primary" role="button">
{% for format in entry.data %} <span class="glyphicon glyphicon-download"></span>{{format.format}} ({{ format.uncompressed_size|filesizeformat }})
<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"> </a>
<span class="glyphicon glyphicon-download"></span>{{format.format}} ({{ format.uncompressed_size|filesizeformat }}) {% endfor %}
</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 %}
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@ -63,7 +63,7 @@
<label>{{ _('Kobo Sync Token')}}</label> <label>{{ _('Kobo Sync Token')}}</label>
<div class="form-group col"> <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> <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> </div>
{% endif %} {% endif %}
</div> </div>
@ -82,8 +82,8 @@
<label for="Show_detail_random">{{_('Show Random Books in Detail View')}}</label> <label for="Show_detail_random">{{_('Show Random Books in Detail View')}}</label>
</div> </div>
{% if ( g.user and g.user.role_admin() and not new_user ) %} {% 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_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" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add allowed/Denied Custom Column Values')}}</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 %} {% endif %}
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
@ -125,19 +125,15 @@
</div> </div>
{% endif %} {% endif %}
{% 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>
<div class="col-sm-12"> <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 %} {% if not profile %}
<a href="{{ url_for('admin.admin') }}" id="back" class="btn btn-default">{{_('Cancel')}}</a> <a href="{{ url_for('admin.admin') }}" id="back" class="btn btn-default">{{_('Cancel')}}</a>
{% endif %} {% 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> </div>
</form> </form>
</div> </div>
@ -157,23 +153,10 @@
</div> </div>
</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 %} {% endblock %}
{% block modal %} {% block modal %}
{{ restrict_modal() }} {{ restrict_modal() }}
{{ delete_confirm_modal() }}
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.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

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

105
cps/ub.py
View File

@ -26,10 +26,8 @@ import uuid
from flask import session as flask_session from flask import session as flask_session
from binascii import hexlify from binascii import hexlify
from flask import g
from flask_babel import gettext as _
from flask_login import AnonymousUserMixin, current_user from flask_login import AnonymousUserMixin, current_user
from werkzeug.local import LocalProxy
try: try:
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
oauth_support = True oauth_support = True
@ -45,7 +43,7 @@ from sqlalchemy import Column, ForeignKey
from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float, JSON from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float, JSON
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.orm import backref, relationship, sessionmaker, Session from sqlalchemy.orm import backref, relationship, sessionmaker, Session, scoped_session
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from . import constants from . import constants
@ -57,73 +55,6 @@ Base = declarative_base()
searched_ids = {} searched_ids = {}
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 store_ids(result): def store_ids(result):
ids = list() ids = list()
for element in result: for element in result:
@ -521,7 +452,7 @@ def migrate_Database(session):
if not engine.dialect.has_table(engine.connect(), "archived_book"): if not engine.dialect.has_table(engine.connect(), "archived_book"):
ArchivedBook.__table__.create(bind=engine) ArchivedBook.__table__.create(bind=engine)
if not engine.dialect.has_table(engine.connect(), "registration"): if not engine.dialect.has_table(engine.connect(), "registration"):
ReadBook.__table__.create(bind=engine) Registration.__table__.create(bind=engine)
with engine.connect() as conn: with engine.connect() as conn:
conn.execute("insert into registration (domain, allow) values('%.%',1)") conn.execute("insert into registration (domain, allow) values('%.%',1)")
session.commit() session.commit()
@ -570,12 +501,16 @@ def migrate_Database(session):
for book_shelf in session.query(BookShelf).all(): for book_shelf in session.query(BookShelf).all():
book_shelf.date_added = datetime.datetime.now() book_shelf.date_added = datetime.datetime.now()
session.commit() session.commit()
# Handle table exists, but no content try:
cnt = session.query(Registration).count() # Handle table exists, but no content
if not cnt: cnt = session.query(Registration).count()
with engine.connect() as conn: if not cnt:
conn.execute("insert into registration (domain, allow) values('%.%',1)") with engine.connect() as conn:
session.commit() conn.execute("insert into registration (domain, allow) values('%.%',1)")
session.commit()
except exc.OperationalError: # Database is not writeable
print('Settings database is not writeable. Exiting...')
sys.exit(2)
try: try:
session.query(exists().where(BookShelf.order)).scalar() session.query(exists().where(BookShelf.order)).scalar()
except exc.OperationalError: # Database is not compatible, some columns are missing except exc.OperationalError: # Database is not compatible, some columns are missing
@ -660,7 +595,7 @@ def migrate_Database(session):
session.commit() session.commit()
except exc.OperationalError: except exc.OperationalError:
print('Settings database is not writeable. Exiting...') print('Settings database is not writeable. Exiting...')
sys.exit(1) sys.exit(2)
def clean_database(session): def clean_database(session):
@ -678,13 +613,19 @@ def update_download(book_id, user_id):
if not check: if not check:
new_download = Downloads(user_id=user_id, book_id=book_id) new_download = Downloads(user_id=user_id, book_id=book_id)
session.add(new_download) session.add(new_download)
session.commit() try:
session.commit()
except exc.OperationalError:
session.rollback()
# Delete non exisiting downloaded books in calibre-web's own database # Delete non exisiting downloaded books in calibre-web's own database
def delete_download(book_id): def delete_download(book_id):
session.query(Downloads).filter(book_id == Downloads.book_id).delete() session.query(Downloads).filter(book_id == Downloads.book_id).delete()
session.commit() try:
session.commit()
except exc.OperationalError:
session.rollback()
# Generate user Guest (translated text), as anonymous user, no rights # Generate user Guest (translated text), as anonymous user, no rights
def create_anonymous_user(session): def create_anonymous_user(session):
@ -725,7 +666,7 @@ def init_db(app_db_path):
app_DB_path = app_db_path app_DB_path = app_db_path
engine = create_engine(u'sqlite:///{0}'.format(app_db_path), echo=False) engine = create_engine(u'sqlite:///{0}'.format(app_db_path), echo=False)
Session = sessionmaker() Session = scoped_session(sessionmaker())
Session.configure(bind=engine) Session.configure(bind=engine)
session = Session() session = Session()

View File

@ -66,14 +66,6 @@ except ImportError as e:
log.debug('Cannot import fb2, extracting fb2 metadata will not work: %s', e) log.debug('Cannot import fb2, extracting fb2 metadata will not work: %s', e)
use_fb2_meta = False use_fb2_meta = False
try:
from PIL import Image as PILImage
from PIL import __version__ as PILversion
use_PIL = True
except ImportError as e:
log.debug('Cannot import Pillow, using png and webp images as cover will not work: %s', e)
use_PIL = False
def process(tmp_file_path, original_file_name, original_file_extension, rarExecutable): def process(tmp_file_path, original_file_name, original_file_extension, rarExecutable):
meta = None meta = None
@ -179,10 +171,6 @@ def get_versions():
XVersion = 'v'+'.'.join(map(str, lxmlversion)) XVersion = 'v'+'.'.join(map(str, lxmlversion))
else: else:
XVersion = u'not installed' XVersion = u'not installed'
if use_PIL:
PILVersion = 'v' + PILversion
else:
PILVersion = u'not installed'
if comic.use_comic_meta: if comic.use_comic_meta:
ComicVersion = comic.comic_version or u'installed' ComicVersion = comic.comic_version or u'installed'
else: else:
@ -191,7 +179,7 @@ def get_versions():
'PyPdf': PVersion, 'PyPdf': PVersion,
'lxml':XVersion, 'lxml':XVersion,
'Wand': WVersion, 'Wand': WVersion,
'Pillow': PILVersion, # 'Pillow': PILVersion,
'Comic_API': ComicVersion} 'Comic_API': ComicVersion}

88
cps/usermanagement.py Normal file
View File

@ -0,0 +1,88 @@
# -*- 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 base64
import binascii
from sqlalchemy.sql.expression import func
from werkzeug.security import check_password_hash
from flask_login import login_required
from . import lm, ub, config, constants, services
try:
from functools import wraps
except ImportError:
pass # We're not using Python 3
def login_required_if_no_ano(func):
@wraps(func)
def decorated_view(*args, **kwargs):
if config.config_anonbrowse == 1:
return func(*args, **kwargs)
return login_required(func)(*args, **kwargs)
return decorated_view
def _fetch_user_by_name(username):
return ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first()
@lm.user_loader
def load_user(user_id):
return ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
@lm.request_loader
def load_user_from_request(request):
if config.config_allow_reverse_proxy_header_login:
rp_header_name = config.config_reverse_proxy_login_header_name
if rp_header_name:
rp_header_username = request.headers.get(rp_header_name)
if rp_header_username:
user = _fetch_user_by_name(rp_header_username)
if user:
return user
auth_header = request.headers.get("Authorization")
if auth_header:
user = load_user_from_auth_header(auth_header)
if user:
return user
return
def load_user_from_auth_header(header_val):
if header_val.startswith('Basic '):
header_val = header_val.replace('Basic ', '', 1)
basic_username = basic_password = ''
try:
header_val = base64.b64decode(header_val).decode('utf-8')
basic_username = header_val.split(':')[0]
basic_password = header_val.split(':')[1]
except (TypeError, UnicodeDecodeError, binascii.Error):
pass
user = _fetch_user_by_name(basic_username)
if user and config.config_login_type == constants.LOGIN_LDAP and services.ldap:
if services.ldap.bind_user(str(user.password), basic_password):
return user
if user and check_password_hash(str(user.password), basic_password):
return user
return

View File

@ -22,46 +22,40 @@
from __future__ import division, print_function, unicode_literals from __future__ import division, print_function, unicode_literals
import os import os
import base64
from datetime import datetime from datetime import datetime
import json import json
import mimetypes import mimetypes
import traceback import chardet # dependency of requests
import binascii
import re
from babel.dates import format_date from babel.dates import format_date
from babel import Locale as LC from babel import Locale as LC
from babel.core import UnknownLocaleError from babel.core import UnknownLocaleError
from flask import Blueprint, jsonify from flask import Blueprint, jsonify
from flask import render_template, request, redirect, send_from_directory, make_response, g, flash, abort, url_for from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for
from flask import session as flask_session from flask import session as flask_session
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import login_user, logout_user, login_required, current_user, confirm_login from flask_login import login_user, logout_user, login_required, current_user
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
from sqlalchemy.sql.expression import text, func, true, false, not_, and_, or_ from sqlalchemy.sql.expression import text, func, false, not_, and_
from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.orm.attributes import flag_modified
from werkzeug.exceptions import default_exceptions
from sqlalchemy.sql.functions import coalesce from sqlalchemy.sql.functions import coalesce
from .services.worker import WorkerThread from .services.worker import WorkerThread
try:
from werkzeug.exceptions import FailedDependency
except ImportError:
from werkzeug.exceptions import UnprocessableEntity as FailedDependency
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from . import constants, logger, isoLanguages, services from . import constants, logger, isoLanguages, services
from . import lm, babel, db, ub, config, get_locale, app from . import babel, db, ub, config, get_locale, app
from . import calibre_db from . import calibre_db, shelf
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
from .helper import check_valid_domain, render_task_status, \ from .helper import check_valid_domain, render_task_status, \
get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \ get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \
send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password
from .pagination import Pagination from .pagination import Pagination
from .redirect import redirect_back from .redirect import redirect_back
from .usermanagement import login_required_if_no_ano
from .render_template import render_title_template
feature_support = { feature_support = {
'ldap': bool(services.ldap), 'ldap': bool(services.ldap),
@ -71,7 +65,6 @@ feature_support = {
try: try:
from .oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status from .oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status
feature_support['oauth'] = True feature_support['oauth'] = True
except ImportError: except ImportError:
feature_support['oauth'] = False feature_support['oauth'] = False
@ -82,55 +75,12 @@ try:
except ImportError: except ImportError:
pass # We're not using Python 3 pass # We're not using Python 3
try: try:
from natsort import natsorted as sort from natsort import natsorted as sort
except ImportError: except ImportError:
sort = sorted # Just use regular sort then, may cause issues with badly named pages in cbz/cbr files sort = sorted # Just use regular sort then, may cause issues with badly named pages in cbz/cbr files
# 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
# 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 feature_support['ldap']:
# Only way of catching the LDAPException upon logging in with LDAP server down
@app.errorhandler(services.ldap.LDAPException)
def handle_exception(e):
log.debug('LDAP server not accessible while trying to login to opds feed')
return error_http(FailedDependency())
# @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))
@app.after_request @app.after_request
def add_security_headers(resp): def add_security_headers(resp):
# resp.headers['Content-Security-Policy']= "script-src 'self' https://www.googleapis.com https://api.douban.com https://comicvine.gamespot.com;" # resp.headers['Content-Security-Policy']= "script-src 'self' https://www.googleapis.com https://api.douban.com https://comicvine.gamespot.com;"
@ -146,104 +96,6 @@ log = logger.create()
# ################################### Login logic and rights management ############################################### # ################################### Login logic and rights management ###############################################
def _fetch_user_by_name(username):
return ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first()
@lm.user_loader
def load_user(user_id):
return ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
@lm.request_loader
def load_user_from_request(request):
if config.config_allow_reverse_proxy_header_login:
rp_header_name = config.config_reverse_proxy_login_header_name
if rp_header_name:
rp_header_username = request.headers.get(rp_header_name)
if rp_header_username:
user = _fetch_user_by_name(rp_header_username)
if user:
return user
auth_header = request.headers.get("Authorization")
if auth_header:
user = load_user_from_auth_header(auth_header)
if user:
return user
return
def load_user_from_auth_header(header_val):
if header_val.startswith('Basic '):
header_val = header_val.replace('Basic ', '', 1)
basic_username = basic_password = ''
try:
header_val = base64.b64decode(header_val).decode('utf-8')
basic_username = header_val.split(':')[0]
basic_password = header_val.split(':')[1]
except (TypeError, UnicodeDecodeError, binascii.Error):
pass
user = _fetch_user_by_name(basic_username)
if user and config.config_login_type == constants.LOGIN_LDAP and services.ldap:
if services.ldap.bind_user(str(user.password), basic_password):
return user
if user and check_password_hash(str(user.password), basic_password):
return user
return
def login_required_if_no_ano(func):
@wraps(func)
def decorated_view(*args, **kwargs):
if config.config_anonbrowse == 1:
return func(*args, **kwargs)
return login_required(func)(*args, **kwargs)
return decorated_view
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
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
def download_required(f): def download_required(f):
@ -265,155 +117,6 @@ def viewer_required(f):
return inner return inner
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
# ################################### Helper functions ################################################################
@web.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') and '/static/' not in request.path:
return redirect(url_for('admin.basic_configuration'))
@app.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.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.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)
# ################################### data provider functions ######################################################### # ################################### data provider functions #########################################################
@ -432,7 +135,10 @@ def bookmark(book_id, book_format):
ub.Bookmark.book_id == book_id, ub.Bookmark.book_id == book_id,
ub.Bookmark.format == book_format)).delete() ub.Bookmark.format == book_format)).delete()
if not bookmark_key: if not bookmark_key:
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return "", 204 return "", 204
lbookmark = ub.Bookmark(user_id=current_user.id, lbookmark = ub.Bookmark(user_id=current_user.id,
@ -440,7 +146,10 @@ def bookmark(book_id, book_format):
format=book_format, format=book_format,
bookmark_key=bookmark_key) bookmark_key=bookmark_key)
ub.session.merge(lbookmark) ub.session.merge(lbookmark)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return "", 201 return "", 201
@ -465,7 +174,10 @@ def toggle_read(book_id):
kobo_reading_state.statistics = ub.KoboStatistics() kobo_reading_state.statistics = ub.KoboStatistics()
book.kobo_reading_state = kobo_reading_state book.kobo_reading_state = kobo_reading_state
ub.session.merge(book) ub.session.merge(book)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
else: else:
try: try:
calibre_db.update_title_sort(config) calibre_db.update_title_sort(config)
@ -499,7 +211,10 @@ def toggle_archived(book_id):
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id) archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)
archived_book.is_archived = True archived_book.is_archived = True
ub.session.merge(archived_book) ub.session.merge(archived_book)
ub.session.commit() try:
ub.session.commit()
except OperationalError:
ub.session.rollback()
return "" return ""
@ -620,8 +335,6 @@ def get_matching_tags():
title_input = request.args.get('book_title') or '' title_input = request.args.get('book_title') or ''
include_tag_inputs = request.args.getlist('include_tag') or '' include_tag_inputs = request.args.getlist('include_tag') or ''
exclude_tag_inputs = request.args.getlist('exclude_tag') or '' exclude_tag_inputs = request.args.getlist('exclude_tag') or ''
# include_extension_inputs = request.args.getlist('include_extension') or ''
# exclude_extension_inputs = request.args.getlist('exclude_extension') or ''
q = q.filter(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + author_input + "%")), q = q.filter(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + author_input + "%")),
func.lower(db.Books.title).ilike("%" + title_input + "%")) func.lower(db.Books.title).ilike("%" + title_input + "%"))
if len(include_tag_inputs) > 0: if len(include_tag_inputs) > 0:
@ -638,14 +351,6 @@ def get_matching_tags():
return json_dumps return json_dumps
# Returns the template for rendering and includes the instance name
def render_title_template(*args, **kwargs):
sidebar = ub.get_sidebar_config(kwargs)
return render_template(instance=config.config_calibre_web_title, sidebar=sidebar,
accept=constants.EXTENSIONS_UPLOAD,
*args, **kwargs)
def render_books_list(data, sort, book_id, page): def render_books_list(data, sort, book_id, page):
order = [db.Books.timestamp.desc()] order = [db.Books.timestamp.desc()]
if sort == 'stored': if sort == 'stored':
@ -749,8 +454,6 @@ def render_hot_books(page):
entries.append(downloadBook) entries.append(downloadBook)
else: else:
ub.delete_download(book.Downloads.book_id) ub.delete_download(book.Downloads.book_id)
# ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete()
# ub.session.commit()
numBooks = entries.__len__() numBooks = entries.__len__()
pagination = Pagination(page, config.config_books_per_page, numBooks) pagination = Pagination(page, config.config_books_per_page, numBooks)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
@ -905,7 +608,8 @@ def render_language_books(page, name, order):
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name, return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name,
title=_(u"Language: %(name)s", name=lang_name), page="language") title=_(u"Language: %(name)s", name=lang_name), page="language")
def render_read_books(page, are_read, as_xml=False, order=None, *args, **kwargs):
def render_read_books(page, are_read, as_xml=False, order=None):
order = order or [] order = order or []
if not config.config_read_column: if not config.config_read_column:
if are_read: if are_read:
@ -917,7 +621,7 @@ def render_read_books(page, are_read, as_xml=False, order=None, *args, **kwargs)
db.Books, db.Books,
db_filter, db_filter,
order, order,
ub.ReadBook, db.Books.id==ub.ReadBook.book_id) ub.ReadBook, db.Books.id == ub.ReadBook.book_id)
else: else:
try: try:
if are_read: if are_read:
@ -1086,11 +790,12 @@ def update_table_settings():
except AttributeError: except AttributeError:
pass pass
ub.session.commit() ub.session.commit()
except InvalidRequestError: except (InvalidRequestError, OperationalError):
log.error("Invalid request received: %r ", request, ) log.error("Invalid request received: %r ", request, )
return "Invalid request", 400 return "Invalid request", 400
return "" return ""
@web.route("/author") @web.route("/author")
@login_required_if_no_ano @login_required_if_no_ano
def author_list(): def author_list():
@ -1323,6 +1028,7 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
rating_low = term.get("ratinghigh") rating_low = term.get("ratinghigh")
rating_high = term.get("ratinglow") rating_high = term.get("ratinglow")
description = term.get("comment") description = term.get("comment")
read_status = term.get("read_status")
if author_name: if author_name:
author_name = author_name.strip().lower().replace(',', '|') author_name = author_name.strip().lower().replace(',', '|')
if book_title: if book_title:
@ -1340,7 +1046,7 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
if include_tag_inputs or exclude_tag_inputs or include_series_inputs or exclude_series_inputs or \ if include_tag_inputs or exclude_tag_inputs or include_series_inputs or exclude_series_inputs or \
include_languages_inputs or exclude_languages_inputs or author_name or book_title or \ include_languages_inputs or exclude_languages_inputs or author_name or book_title or \
publisher or pub_start or pub_end or rating_low or rating_high or description or cc_present or \ publisher or pub_start or pub_end or rating_low or rating_high or description or cc_present or \
include_extension_inputs or exclude_extension_inputs: include_extension_inputs or exclude_extension_inputs or read_status:
searchterm.extend((author_name.replace('|', ','), book_title, publisher)) searchterm.extend((author_name.replace('|', ','), book_title, publisher))
if pub_start: if pub_start:
try: try:
@ -1358,8 +1064,12 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
pub_start = u"" pub_start = u""
tag_names = calibre_db.session.query(db.Tags).filter(db.Tags.id.in_(include_tag_inputs)).all() tag_names = calibre_db.session.query(db.Tags).filter(db.Tags.id.in_(include_tag_inputs)).all()
searchterm.extend(tag.name for tag in tag_names) searchterm.extend(tag.name for tag in tag_names)
tag_names = calibre_db.session.query(db.Tags).filter(db.Tags.id.in_(exclude_tag_inputs)).all()
searchterm.extend(tag.name for tag in tag_names)
serie_names = calibre_db.session.query(db.Series).filter(db.Series.id.in_(include_series_inputs)).all() serie_names = calibre_db.session.query(db.Series).filter(db.Series.id.in_(include_series_inputs)).all()
searchterm.extend(serie.name for serie in serie_names) searchterm.extend(serie.name for serie in serie_names)
serie_names = calibre_db.session.query(db.Series).filter(db.Series.id.in_(exclude_series_inputs)).all()
searchterm.extend(serie.name for serie in serie_names)
language_names = calibre_db.session.query(db.Languages).\ language_names = calibre_db.session.query(db.Languages).\
filter(db.Languages.id.in_(include_languages_inputs)).all() filter(db.Languages.id.in_(include_languages_inputs)).all()
if language_names: if language_names:
@ -1369,6 +1079,8 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
searchterm.extend([_(u"Rating <= %(rating)s", rating=rating_high)]) searchterm.extend([_(u"Rating <= %(rating)s", rating=rating_high)])
if rating_low: if rating_low:
searchterm.extend([_(u"Rating >= %(rating)s", rating=rating_low)]) searchterm.extend([_(u"Rating >= %(rating)s", rating=rating_low)])
if read_status:
searchterm.extend([_(u"Read Status = %(status)s", status=read_status)])
searchterm.extend(ext for ext in include_extension_inputs) searchterm.extend(ext for ext in include_extension_inputs)
searchterm.extend(ext for ext in exclude_extension_inputs) searchterm.extend(ext for ext in exclude_extension_inputs)
# handle custom columns # handle custom columns
@ -1385,6 +1097,23 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
q = q.filter(db.Books.pubdate >= pub_start) q = q.filter(db.Books.pubdate >= pub_start)
if pub_end: if pub_end:
q = q.filter(db.Books.pubdate <= pub_end) q = q.filter(db.Books.pubdate <= pub_end)
if read_status:
if config.config_read_column:
if read_status=="True":
q = q.join(db.cc_classes[config.config_read_column], isouter=True) \
.filter(db.cc_classes[config.config_read_column].value == True)
else:
q = q.join(db.cc_classes[config.config_read_column], isouter=True) \
.filter(coalesce(db.cc_classes[config.config_read_column].value, False) != True)
else:
if read_status == "True":
q = q.join(ub.ReadBook, db.Books.id==ub.ReadBook.book_id, isouter=True)\
.filter(ub.ReadBook.user_id == int(current_user.id),
ub.ReadBook.read_status == ub.ReadBook.STATUS_FINISHED)
else:
q = q.join(ub.ReadBook, db.Books.id == ub.ReadBook.book_id, isouter=True) \
.filter(ub.ReadBook.user_id == int(current_user.id),
coalesce(ub.ReadBook.read_status, 0) != ub.ReadBook.STATUS_FINISHED)
if publisher: if publisher:
q = q.filter(db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + publisher + "%"))) q = q.filter(db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + publisher + "%")))
for tag in include_tag_inputs: for tag in include_tag_inputs:
@ -1487,8 +1216,14 @@ def serve_book(book_id, book_format, anyname):
headers = Headers() headers = Headers()
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream") headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
df = getFileFromEbooksFolder(book.path, data.name + "." + book_format) df = getFileFromEbooksFolder(book.path, data.name + "." + book_format)
return do_gdrive_download(df, headers) return do_gdrive_download(df, headers, (book_format.upper() == 'TXT'))
else: else:
if book_format.upper() == 'TXT':
rawdata = open(os.path.join(config.config_calibre_dir, book.path, data.name + "." + book_format),
"rb").read()
result = chardet.detect(rawdata)
return make_response(
rawdata.decode(result['encoding']).encode('utf-8'))
return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format) return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format)
@ -1674,89 +1409,7 @@ def logout():
return redirect(url_for('web.login')) return redirect(url_for('web.login'))
@web.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('web.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")
@web.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'))
@web.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
# ################################### Users own configuration ######################################################### # ################################### Users own configuration #########################################################
@ -1839,14 +1492,11 @@ def profile():
ub.session.rollback() ub.session.rollback()
flash(_(u"Found an existing account for this e-mail address."), category="error") flash(_(u"Found an existing account for this e-mail address."), category="error")
log.debug(u"Found an existing account for this e-mail address.") log.debug(u"Found an existing account for this e-mail address.")
'''return render_title_template("user_edit.html", except OperationalError as e:
content=current_user, ub.session.rollback()
translations=translations, log.error("Database error: %s", e)
kobo_support=kobo_support, flash(_(u"Database error: %(error)s.", error=e), category="error")
title=_(u"%(name)s's profile", name=current_user.nickname),
page="me",
registered_oauth=local_oauth_check,
oauth_status=oauth_status)'''
return render_title_template("user_edit.html", return render_title_template("user_edit.html",
translations=translations, translations=translations,
profile=1, profile=1,
@ -1900,14 +1550,6 @@ def read_book(book_id, book_format):
log.debug(u"Start comic reader for %d", book_id) log.debug(u"Start comic reader for %d", book_id)
return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book"), return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book"),
extension=fileExt) extension=fileExt)
# if feature_support['rar']:
# extensionList = ["cbr","cbt","cbz"]
# else:
# extensionList = ["cbt","cbz"]
# for fileext in extensionList:
# if book_format.lower() == fileext:
# return render_title_template('readcbr.html', comicfile=book_id,
# extension=fileext, title=_(u"Read a Book"), book=book)
log.debug(u"Error opening eBook. File does not exist or file is not accessible") log.debug(u"Error opening eBook. File does not exist or file is not accessible")
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error") flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
return redirect(url_for("web.index")) return redirect(url_for("web.index"))

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ oauth2client>=4.0.0,<4.1.4
uritemplate>=3.0.0,<3.1.0 uritemplate>=3.0.0,<3.1.0
pyasn1-modules>=0.0.8,<0.3.0 pyasn1-modules>=0.0.8,<0.3.0
pyasn1>=0.1.9,<0.5.0 pyasn1>=0.1.9,<0.5.0
PyDrive>=1.3.1,<1.4.0 PyDrive2>=1.3.1,<1.8.0
PyYAML>=3.12 PyYAML>=3.12
rsa==3.4.2,<4.1.0 rsa==3.4.2,<4.1.0
six>=1.10.0,<1.15.0 six>=1.10.0,<1.15.0
@ -26,7 +26,6 @@ SQLAlchemy-Utils>=0.33.5,<0.37.0
# extracting metadata # extracting metadata
lxml>=3.8.0,<4.6.0 lxml>=3.8.0,<4.6.0
Pillow>=4.0.0,<7.2.0
rarfile>=2.7 rarfile>=2.7
# other # other

View File

@ -1,5 +1,5 @@
Babel>=1.3, <2.9 Babel>=1.3, <2.9
Flask-Babel>=0.11.1,<1.1.0 Flask-Babel>=0.11.1,<2.1.0
Flask-Login>=0.3.2,<0.5.1 Flask-Login>=0.3.2,<0.5.1
Flask-Principal>=0.3.2,<0.5.1 Flask-Principal>=0.3.2,<0.5.1
singledispatch>=3.4.0.0,<3.5.0.0 singledispatch>=3.4.0.0,<3.5.0.0
@ -11,5 +11,5 @@ pytz>=2016.10
requests>=2.11.1,<2.25.0 requests>=2.11.1,<2.25.0
SQLAlchemy>=1.3.0,<1.4.0 SQLAlchemy>=1.3.0,<1.4.0
tornado>=4.1,<6.2 tornado>=4.1,<6.2
Wand>=0.4.4,<0.6.0 Wand>=0.4.4,<0.7.0
unidecode>=0.04.19,<1.2.0 unidecode>=0.04.19,<1.2.0

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff