mirror of
https://github.com/janeczku/calibre-web
synced 2024-11-24 10:37:23 +00:00
Merge branch 'Develop' into master
This commit is contained in:
commit
e269bab186
10
cps.py
10
cps.py
@ -41,6 +41,8 @@ from cps.shelf import shelf
|
||||
from cps.admin import admi
|
||||
from cps.gdrive import gdrive
|
||||
from cps.editbooks import editbook
|
||||
from cps.remotelogin import remotelogin
|
||||
from cps.error_handler import init_errorhandler
|
||||
|
||||
try:
|
||||
from cps.kobo import kobo, get_kobo_activated
|
||||
@ -58,14 +60,18 @@ except ImportError:
|
||||
|
||||
def main():
|
||||
app = create_app()
|
||||
|
||||
init_errorhandler()
|
||||
|
||||
app.register_blueprint(web)
|
||||
app.register_blueprint(opds)
|
||||
app.register_blueprint(jinjia)
|
||||
app.register_blueprint(about)
|
||||
app.register_blueprint(shelf)
|
||||
app.register_blueprint(admi)
|
||||
if config.config_use_google_drive:
|
||||
app.register_blueprint(gdrive)
|
||||
app.register_blueprint(remotelogin)
|
||||
# if config.config_use_google_drive:
|
||||
app.register_blueprint(gdrive)
|
||||
app.register_blueprint(editbook)
|
||||
if kobo_available:
|
||||
app.register_blueprint(kobo)
|
||||
|
@ -94,7 +94,8 @@ def create_app():
|
||||
app.root_path = app.root_path.decode('utf-8')
|
||||
app.instance_path = app.instance_path.decode('utf-8')
|
||||
|
||||
cache_buster.init_cache_busting(app)
|
||||
if os.environ.get('FLASK_DEBUG'):
|
||||
cache_buster.init_cache_busting(app)
|
||||
|
||||
log.info('Starting Calibre Web...')
|
||||
Principal(app)
|
||||
|
@ -31,7 +31,7 @@ import werkzeug, flask, flask_login, flask_principal, jinja2
|
||||
from flask_babel import gettext as _
|
||||
|
||||
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants
|
||||
from .web import render_title_template
|
||||
from .render_template import render_title_template
|
||||
try:
|
||||
from flask_login import __version__ as flask_loginVersion
|
||||
except ImportError:
|
||||
|
495
cps/admin.py
495
cps/admin.py
@ -5,7 +5,7 @@
|
||||
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
|
||||
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
|
||||
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
|
||||
# apetresc, nanu-c, mutschler
|
||||
# apetresc, nanu-c, mutschler, GammaC0de, vuolter
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -26,24 +26,31 @@ import re
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
import operator
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from babel import Locale as LC
|
||||
from babel.dates import format_datetime
|
||||
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory
|
||||
from flask_login import login_required, current_user, logout_user
|
||||
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g
|
||||
from flask_login import login_required, current_user, logout_user, confirm_login
|
||||
from flask_babel import gettext as _
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
|
||||
from sqlalchemy.sql.expression import func
|
||||
from sqlalchemy.sql.expression import func, or_
|
||||
|
||||
from . import constants, logger, helper, services
|
||||
from .cli import filepicker
|
||||
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils
|
||||
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash
|
||||
from .gdriveutils import is_gdrive_ready, gdrive_support
|
||||
from .web import admin_required, render_title_template, before_request, unconfigured
|
||||
from .render_template import render_title_template, get_sidebar_config
|
||||
from . import debug_info
|
||||
|
||||
try:
|
||||
from functools import wraps
|
||||
except ImportError:
|
||||
pass # We're not using Python 3
|
||||
|
||||
log = logger.create()
|
||||
|
||||
feature_support = {
|
||||
@ -72,6 +79,49 @@ feature_support['gdrive'] = gdrive_support
|
||||
admi = Blueprint('admin', __name__)
|
||||
|
||||
|
||||
def admin_required(f):
|
||||
"""
|
||||
Checks if current_user.role == 1
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
if current_user.role_admin():
|
||||
return f(*args, **kwargs)
|
||||
abort(403)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def unconfigured(f):
|
||||
"""
|
||||
Checks if calibre-web instance is not configured
|
||||
"""
|
||||
@wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
if not config.db_configured:
|
||||
return f(*args, **kwargs)
|
||||
abort(403)
|
||||
|
||||
return inner
|
||||
|
||||
@admi.before_app_request
|
||||
def before_request():
|
||||
if current_user.is_authenticated:
|
||||
confirm_login()
|
||||
g.constants = constants
|
||||
g.user = current_user
|
||||
g.allow_registration = config.config_public_reg
|
||||
g.allow_anonymous = config.config_anonbrowse
|
||||
g.allow_upload = config.config_uploading
|
||||
g.current_theme = config.config_theme
|
||||
g.config_authors_max = config.config_authors_max
|
||||
g.shelves_access = ub.session.query(ub.Shelf).filter(
|
||||
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
|
||||
if not config.db_configured and request.endpoint not in (
|
||||
'admin.basic_configuration', 'login', 'admin.config_pathchooser') and '/static/' not in request.path:
|
||||
return redirect(url_for('admin.basic_configuration'))
|
||||
|
||||
|
||||
@admi.route("/admin")
|
||||
@login_required
|
||||
@ -143,7 +193,7 @@ def admin():
|
||||
@admin_required
|
||||
def configuration():
|
||||
if request.method == "POST":
|
||||
return _configuration_update_helper()
|
||||
return _configuration_update_helper(True)
|
||||
return _configuration_result()
|
||||
|
||||
|
||||
@ -195,6 +245,21 @@ def update_view_configuration():
|
||||
return view_configuration()
|
||||
|
||||
|
||||
@admi.route("/ajax/loaddialogtexts/<element_id>")
|
||||
@login_required
|
||||
def load_dialogtexts(element_id):
|
||||
texts = { "header": "", "main": "" }
|
||||
if element_id == "config_delete_kobo_token":
|
||||
texts["main"] = _('Do you really want to delete the Kobo Token?')
|
||||
elif element_id == "btndeletedomain":
|
||||
texts["main"] = _('Do you really want to delete this domain?')
|
||||
elif element_id == "btndeluser":
|
||||
texts["main"] = _('Do you really want to delete this user?')
|
||||
elif element_id == "delete_shelf":
|
||||
texts["main"] = _('Are you sure you want to delete this shelf?')
|
||||
return json.dumps(texts)
|
||||
|
||||
|
||||
@admi.route("/ajax/editdomain/<int:allow>", methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
@ -206,7 +271,10 @@ def edit_domain(allow):
|
||||
vals = request.form.to_dict()
|
||||
answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first()
|
||||
answer.domain = vals['value'].replace('*', '%').replace('?', '_').lower()
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
return ""
|
||||
|
||||
|
||||
@ -220,7 +288,10 @@ def add_domain(allow):
|
||||
if not check:
|
||||
new_domain = ub.Registration(domain=domain_name, allow=allow)
|
||||
ub.session.add(new_domain)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
return ""
|
||||
|
||||
|
||||
@ -228,14 +299,23 @@ def add_domain(allow):
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_domain():
|
||||
domain_id = request.form.to_dict()['domainid'].replace('*', '%').replace('?', '_').lower()
|
||||
ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete()
|
||||
ub.session.commit()
|
||||
# If last domain was deleted, add all domains by default
|
||||
if not ub.session.query(ub.Registration).filter(ub.Registration.allow==1).count():
|
||||
new_domain = ub.Registration(domain="%.%",allow=1)
|
||||
ub.session.add(new_domain)
|
||||
ub.session.commit()
|
||||
try:
|
||||
domain_id = request.form.to_dict()['domainid'].replace('*', '%').replace('?', '_').lower()
|
||||
ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
# If last domain was deleted, add all domains by default
|
||||
if not ub.session.query(ub.Registration).filter(ub.Registration.allow==1).count():
|
||||
new_domain = ub.Registration(domain="%.%",allow=1)
|
||||
ub.session.add(new_domain)
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
except KeyError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
@ -250,10 +330,11 @@ def list_domain(allow):
|
||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
return response
|
||||
|
||||
@admi.route("/ajax/editrestriction/<int:res_type>", methods=['POST'])
|
||||
@admi.route("/ajax/editrestriction/<int:res_type>", defaults={"user_id":0}, methods=['POST'])
|
||||
@admi.route("/ajax/editrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def edit_restriction(res_type):
|
||||
def edit_restriction(res_type, user_id):
|
||||
element = request.form.to_dict()
|
||||
if element['id'].startswith('a'):
|
||||
if res_type == 0: # Tags as template
|
||||
@ -267,25 +348,29 @@ def edit_restriction(res_type):
|
||||
config.config_allowed_column_value = ','.join(elementlist)
|
||||
config.save()
|
||||
if res_type == 2: # Tags per user
|
||||
usr_id = os.path.split(request.referrer)[-1]
|
||||
if usr_id.isdigit() == True:
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||
if isinstance(user_id, int):
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||
else:
|
||||
usr = current_user
|
||||
elementlist = usr.list_allowed_tags()
|
||||
elementlist[int(element['id'][1:])]=element['Element']
|
||||
usr.allowed_tags = ','.join(elementlist)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
if res_type == 3: # CColumn per user
|
||||
usr_id = os.path.split(request.referrer)[-1]
|
||||
if usr_id.isdigit() == True:
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||
if isinstance(user_id, int):
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||
else:
|
||||
usr = current_user
|
||||
elementlist = usr.list_allowed_column_values()
|
||||
elementlist[int(element['id'][1:])]=element['Element']
|
||||
usr.allowed_column_value = ','.join(elementlist)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
if element['id'].startswith('d'):
|
||||
if res_type == 0: # Tags as template
|
||||
elementlist = config.list_denied_tags()
|
||||
@ -298,25 +383,29 @@ def edit_restriction(res_type):
|
||||
config.config_denied_column_value = ','.join(elementlist)
|
||||
config.save()
|
||||
if res_type == 2: # Tags per user
|
||||
usr_id = os.path.split(request.referrer)[-1]
|
||||
if usr_id.isdigit() == True:
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||
if isinstance(user_id, int):
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||
else:
|
||||
usr = current_user
|
||||
elementlist = usr.list_denied_tags()
|
||||
elementlist[int(element['id'][1:])]=element['Element']
|
||||
usr.denied_tags = ','.join(elementlist)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
if res_type == 3: # CColumn per user
|
||||
usr_id = os.path.split(request.referrer)[-1]
|
||||
if usr_id.isdigit() == True:
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||
if isinstance(user_id, int):
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||
else:
|
||||
usr = current_user
|
||||
elementlist = usr.list_denied_column_values()
|
||||
elementlist[int(element['id'][1:])]=element['Element']
|
||||
usr.denied_column_value = ','.join(elementlist)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
return ""
|
||||
|
||||
def restriction_addition(element, list_func):
|
||||
@ -335,10 +424,11 @@ def restriction_deletion(element, list_func):
|
||||
return ','.join(elementlist)
|
||||
|
||||
|
||||
@admi.route("/ajax/addrestriction/<int:res_type>", methods=['POST'])
|
||||
@admi.route("/ajax/addrestriction/<int:res_type>", defaults={"user_id":0}, methods=['POST'])
|
||||
@admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def add_restriction(res_type):
|
||||
def add_restriction(res_type, user_id):
|
||||
element = request.form.to_dict()
|
||||
if res_type == 0: # Tags as template
|
||||
if 'submit_allow' in element:
|
||||
@ -355,35 +445,46 @@ def add_restriction(res_type):
|
||||
config.config_denied_column_value = restriction_addition(element, config.list_allowed_column_values)
|
||||
config.save()
|
||||
if res_type == 2: # Tags per user
|
||||
usr_id = os.path.split(request.referrer)[-1]
|
||||
if usr_id.isdigit() == True:
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||
if isinstance(user_id, int):
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||
else:
|
||||
usr = current_user
|
||||
if 'submit_allow' in element:
|
||||
usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
elif 'submit_deny' in element:
|
||||
usr.denied_tags = restriction_addition(element, usr.list_denied_tags)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
if res_type == 3: # CustomC per user
|
||||
usr_id = os.path.split(request.referrer)[-1]
|
||||
if usr_id.isdigit() == True:
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||
if isinstance(user_id, int):
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||
else:
|
||||
usr = current_user
|
||||
if 'submit_allow' in element:
|
||||
usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
elif 'submit_deny' in element:
|
||||
usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
return ""
|
||||
|
||||
@admi.route("/ajax/deleterestriction/<int:res_type>", methods=['POST'])
|
||||
@admi.route("/ajax/deleterestriction/<int:res_type>", defaults={"user_id":0}, methods=['POST'])
|
||||
@admi.route("/ajax/deleterestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_restriction(res_type):
|
||||
def delete_restriction(res_type, user_id):
|
||||
element = request.form.to_dict()
|
||||
if res_type == 0: # Tags as template
|
||||
if element['id'].startswith('a'):
|
||||
@ -400,36 +501,46 @@ def delete_restriction(res_type):
|
||||
config.config_denied_column_value = restriction_deletion(element, config.list_denied_column_values)
|
||||
config.save()
|
||||
elif res_type == 2: # Tags per user
|
||||
usr_id = os.path.split(request.referrer)[-1]
|
||||
if usr_id.isdigit() == True:
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||
if isinstance(user_id, int):
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||
else:
|
||||
usr = current_user
|
||||
if element['id'].startswith('a'):
|
||||
usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
elif element['id'].startswith('d'):
|
||||
usr.denied_tags = restriction_deletion(element, usr.list_denied_tags)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
elif res_type == 3: # Columns per user
|
||||
usr_id = os.path.split(request.referrer)[-1]
|
||||
if usr_id.isdigit() == True: # select current user if admins are editing their own rights
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
||||
if isinstance(user_id, int):
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||
else:
|
||||
usr = current_user
|
||||
if element['id'].startswith('a'):
|
||||
usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
elif element['id'].startswith('d'):
|
||||
usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
return ""
|
||||
|
||||
|
||||
@admi.route("/ajax/listrestriction/<int:res_type>")
|
||||
@admi.route("/ajax/listrestriction/<int:res_type>", defaults={"user_id":0})
|
||||
@admi.route("/ajax/listrestriction/<int:res_type>/<int:user_id>")
|
||||
@login_required
|
||||
@admin_required
|
||||
def list_restriction(res_type):
|
||||
def list_restriction(res_type, user_id):
|
||||
if res_type == 0: # Tags as template
|
||||
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
|
||||
for i,x in enumerate(config.list_denied_tags()) if x != '' ]
|
||||
@ -443,9 +554,8 @@ def list_restriction(res_type):
|
||||
for i,x in enumerate(config.list_allowed_column_values()) if x != '']
|
||||
json_dumps = restrict + allow
|
||||
elif res_type == 2: # Tags per user
|
||||
usr_id = os.path.split(request.referrer)[-1]
|
||||
if usr_id.isdigit() == True:
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == usr_id).first()
|
||||
if isinstance(user_id, int):
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
|
||||
else:
|
||||
usr = current_user
|
||||
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
|
||||
@ -454,9 +564,8 @@ def list_restriction(res_type):
|
||||
for i,x in enumerate(usr.list_allowed_tags()) if x != '']
|
||||
json_dumps = restrict + allow
|
||||
elif res_type == 3: # CustomC per user
|
||||
usr_id = os.path.split(request.referrer)[-1]
|
||||
if usr_id.isdigit() == True:
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id==usr_id).first()
|
||||
if isinstance(user_id, int):
|
||||
usr = ub.session.query(ub.User).filter(ub.User.id==user_id).first()
|
||||
else:
|
||||
usr = current_user
|
||||
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
|
||||
@ -471,14 +580,108 @@ def list_restriction(res_type):
|
||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
return response
|
||||
|
||||
@admi.route("/basicconfig/pathchooser/")
|
||||
@unconfigured
|
||||
def config_pathchooser():
|
||||
if filepicker:
|
||||
return pathchooser()
|
||||
abort(403)
|
||||
|
||||
@admi.route("/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
|
||||
def basic_configuration():
|
||||
logout_user()
|
||||
if request.method == "POST":
|
||||
return _configuration_update_helper()
|
||||
return _configuration_result()
|
||||
return _configuration_update_helper(configured=filepicker)
|
||||
return _configuration_result(configured=filepicker)
|
||||
|
||||
|
||||
def _config_int(to_save, x, func=int):
|
||||
@ -633,13 +836,13 @@ def _configuration_ldap_helper(to_save, gdriveError):
|
||||
return reboot_required, None
|
||||
|
||||
|
||||
def _configuration_update_helper():
|
||||
def _configuration_update_helper(configured):
|
||||
reboot_required = False
|
||||
db_change = False
|
||||
to_save = request.form.to_dict()
|
||||
gdriveError = None
|
||||
|
||||
to_save['config_calibre_dir'] = re.sub('[\\/]metadata\.db$',
|
||||
to_save['config_calibre_dir'] = re.sub(r'[\\/]metadata\.db$',
|
||||
'',
|
||||
to_save['config_calibre_dir'],
|
||||
flags=re.IGNORECASE)
|
||||
@ -653,11 +856,15 @@ def _configuration_update_helper():
|
||||
|
||||
reboot_required |= _config_string(to_save, "config_keyfile")
|
||||
if config.config_keyfile and not os.path.isfile(config.config_keyfile):
|
||||
return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'), gdriveError)
|
||||
return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'),
|
||||
gdriveError,
|
||||
configured)
|
||||
|
||||
reboot_required |= _config_string(to_save, "config_certfile")
|
||||
if config.config_certfile and not os.path.isfile(config.config_certfile):
|
||||
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'), gdriveError)
|
||||
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'),
|
||||
gdriveError,
|
||||
configured)
|
||||
|
||||
_config_checkbox_int(to_save, "config_uploading")
|
||||
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
|
||||
@ -722,10 +929,10 @@ def _configuration_update_helper():
|
||||
if "config_rarfile_location" in to_save:
|
||||
unrar_status = helper.check_unrar(config.config_rarfile_location)
|
||||
if unrar_status:
|
||||
return _configuration_result(unrar_status, gdriveError)
|
||||
return _configuration_result(unrar_status, gdriveError, configured)
|
||||
except (OperationalError, InvalidRequestError):
|
||||
ub.session.rollback()
|
||||
_configuration_result(_(u"Settings DB is not Writeable"), gdriveError)
|
||||
_configuration_result(_(u"Settings DB is not Writeable"), gdriveError, configured)
|
||||
|
||||
try:
|
||||
metadata_db = os.path.join(config.config_calibre_dir, "metadata.db")
|
||||
@ -733,11 +940,13 @@ def _configuration_update_helper():
|
||||
gdriveutils.downloadFile(None, "metadata.db", metadata_db)
|
||||
db_change = True
|
||||
except Exception as e:
|
||||
return _configuration_result('%s' % e, gdriveError)
|
||||
return _configuration_result('%s' % e, gdriveError, configured)
|
||||
|
||||
if db_change:
|
||||
if not calibre_db.setup_db(config, ub.app_DB_path):
|
||||
return _configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdriveError)
|
||||
return _configuration_result(_('DB Location is not Valid, Please Enter Correct Path'),
|
||||
gdriveError,
|
||||
configured)
|
||||
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
|
||||
flash(_(u"DB is not Writeable"), category="warning")
|
||||
|
||||
@ -746,10 +955,10 @@ def _configuration_update_helper():
|
||||
if reboot_required:
|
||||
web_server.stop(True)
|
||||
|
||||
return _configuration_result(None, gdriveError)
|
||||
return _configuration_result(None, gdriveError, configured)
|
||||
|
||||
|
||||
def _configuration_result(error_flash=None, gdriveError=None):
|
||||
def _configuration_result(error_flash=None, gdriveError=None, configured=True):
|
||||
gdrive_authenticate = not is_gdrive_ready()
|
||||
gdrivefolders = []
|
||||
if gdriveError is None:
|
||||
@ -770,7 +979,7 @@ def _configuration_result(error_flash=None, gdriveError=None):
|
||||
|
||||
return render_title_template("config_edit.html", config=config, provider=oauthblueprints,
|
||||
show_back_button=show_back_button, show_login_button=show_login_button,
|
||||
show_authenticate_google_drive=gdrive_authenticate,
|
||||
show_authenticate_google_drive=gdrive_authenticate, filepicker=configured,
|
||||
gdriveError=gdriveError, gdrivefolders=gdrivefolders, feature_support=feature_support,
|
||||
title=_(u"Basic Configuration"), page="config")
|
||||
|
||||
@ -816,7 +1025,10 @@ def _handle_new_user(to_save, content,languages, translations, kobo_support):
|
||||
content.allowed_column_value = config.config_allowed_column_value
|
||||
content.denied_column_value = config.config_denied_column_value
|
||||
ub.session.add(content)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
flash(_(u"User '%(user)s' created", user=content.nickname), category="success")
|
||||
return redirect(url_for('admin.admin'))
|
||||
except IntegrityError:
|
||||
@ -832,7 +1044,10 @@ def _handle_edit_user(to_save, content,languages, translations, kobo_support):
|
||||
if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
|
||||
ub.User.id != content.id).count():
|
||||
ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success")
|
||||
return redirect(url_for('admin.admin'))
|
||||
else:
|
||||
@ -855,7 +1070,7 @@ def _handle_edit_user(to_save, content,languages, translations, kobo_support):
|
||||
content.role &= ~constants.ROLE_ANONYMOUS
|
||||
|
||||
val = [int(k[5:]) for k in to_save if k.startswith('show_')]
|
||||
sidebar = ub.get_sidebar_config()
|
||||
sidebar = get_sidebar_config()
|
||||
for element in sidebar:
|
||||
value = element['visibility']
|
||||
if value in val and not content.check_visibility(value):
|
||||
@ -907,7 +1122,10 @@ def _handle_edit_user(to_save, content,languages, translations, kobo_support):
|
||||
if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail:
|
||||
content.kindle_mail = to_save["kindle_mail"]
|
||||
try:
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success")
|
||||
except IntegrityError:
|
||||
ub.session.rollback()
|
||||
@ -1119,3 +1337,110 @@ def get_updater_status():
|
||||
except Exception:
|
||||
status['status'] = 11
|
||||
return json.dumps(status)
|
||||
|
||||
|
||||
@admi.route('/import_ldap_users')
|
||||
@login_required
|
||||
@admin_required
|
||||
def import_ldap_users():
|
||||
showtext = {}
|
||||
try:
|
||||
new_users = services.ldap.get_group_members(config.config_ldap_group_name)
|
||||
except (services.ldap.LDAPException, TypeError, AttributeError, KeyError) as e:
|
||||
log.debug_or_exception(e)
|
||||
showtext['text'] = _(u'Error: %(ldaperror)s', ldaperror=e)
|
||||
return json.dumps(showtext)
|
||||
if not new_users:
|
||||
log.debug('LDAP empty response')
|
||||
showtext['text'] = _(u'Error: No user returned in response of LDAP server')
|
||||
return json.dumps(showtext)
|
||||
|
||||
imported = 0
|
||||
for username in new_users:
|
||||
user = username.decode('utf-8')
|
||||
if '=' in user:
|
||||
# if member object field is empty take user object as filter
|
||||
if config.config_ldap_member_user_object:
|
||||
query_filter = config.config_ldap_member_user_object
|
||||
else:
|
||||
query_filter = config.config_ldap_user_object
|
||||
try:
|
||||
user_identifier = extract_user_identifier(user, query_filter)
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
continue
|
||||
else:
|
||||
user_identifier = user
|
||||
query_filter = None
|
||||
try:
|
||||
user_data = services.ldap.get_object_details(user=user_identifier, query_filter=query_filter)
|
||||
except AttributeError as e:
|
||||
log.debug_or_exception(e)
|
||||
continue
|
||||
if user_data:
|
||||
user_login_field = extract_dynamic_field_from_filter(user, config.config_ldap_user_object)
|
||||
|
||||
username = user_data[user_login_field][0].decode('utf-8')
|
||||
# check for duplicate username
|
||||
if ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first():
|
||||
# if ub.session.query(ub.User).filter(ub.User.nickname == username).first():
|
||||
log.warning("LDAP User %s Already in Database", user_data)
|
||||
continue
|
||||
|
||||
kindlemail = ''
|
||||
if 'mail' in user_data:
|
||||
useremail = user_data['mail'][0].decode('utf-8')
|
||||
if (len(user_data['mail']) > 1):
|
||||
kindlemail = user_data['mail'][1].decode('utf-8')
|
||||
|
||||
else:
|
||||
log.debug('No Mail Field Found in LDAP Response')
|
||||
useremail = username + '@email.com'
|
||||
# check for duplicate email
|
||||
if ub.session.query(ub.User).filter(func.lower(ub.User.email) == useremail.lower()).first():
|
||||
log.warning("LDAP Email %s Already in Database", user_data)
|
||||
continue
|
||||
content = ub.User()
|
||||
content.nickname = username
|
||||
content.password = '' # dummy password which will be replaced by ldap one
|
||||
content.email = useremail
|
||||
content.kindle_mail = kindlemail
|
||||
content.role = config.config_default_role
|
||||
content.sidebar_view = config.config_default_show
|
||||
content.allowed_tags = config.config_allowed_tags
|
||||
content.denied_tags = config.config_denied_tags
|
||||
content.allowed_column_value = config.config_allowed_column_value
|
||||
content.denied_column_value = config.config_denied_column_value
|
||||
ub.session.add(content)
|
||||
try:
|
||||
ub.session.commit()
|
||||
imported +=1
|
||||
except Exception as e:
|
||||
log.warning("Failed to create LDAP user: %s - %s", user, e)
|
||||
ub.session.rollback()
|
||||
showtext['text'] = _(u'Failed to Create at Least One LDAP User')
|
||||
else:
|
||||
log.warning("LDAP User: %s Not Found", user)
|
||||
showtext['text'] = _(u'At Least One LDAP User Not Found in Database')
|
||||
if not showtext:
|
||||
showtext['text'] = _(u'{} User Successfully Imported'.format(imported))
|
||||
return json.dumps(showtext)
|
||||
|
||||
|
||||
def extract_user_data_from_field(user, field):
|
||||
match = re.search(field + "=([\d\s\w-]+)", user, re.IGNORECASE | re.UNICODE)
|
||||
if match:
|
||||
return match.group(1)
|
||||
else:
|
||||
raise Exception("Could Not Parse LDAP User: {}".format(user))
|
||||
|
||||
def extract_dynamic_field_from_filter(user, filter):
|
||||
match = re.search("([a-zA-Z0-9-]+)=%s", filter, re.IGNORECASE | re.UNICODE)
|
||||
if match:
|
||||
return match.group(1)
|
||||
else:
|
||||
raise Exception("Could Not Parse LDAP Userfield: {}", user)
|
||||
|
||||
def extract_user_identifier(user, filter):
|
||||
dynamic_field = extract_dynamic_field_from_filter(user, filter)
|
||||
return extract_user_data_from_field(user, dynamic_field)
|
||||
|
@ -45,6 +45,7 @@ parser.add_argument('-v', '--version', action='version', help='Shows version num
|
||||
version=version_info())
|
||||
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
|
||||
parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password')
|
||||
parser.add_argument('-f', action='store_true', help='Enables filepicker in unconfigured mode')
|
||||
args = parser.parse_args()
|
||||
|
||||
if sys.version_info < (3, 0):
|
||||
@ -110,3 +111,6 @@ if ipadress:
|
||||
|
||||
# handle and check user password argument
|
||||
user_password = args.s or None
|
||||
|
||||
# Handles enableing of filepicker
|
||||
filepicker = args.f or None
|
||||
|
41
cps/comic.py
41
cps/comic.py
@ -18,21 +18,21 @@
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
import io
|
||||
|
||||
from . import logger, isoLanguages
|
||||
from .constants import BookMeta
|
||||
|
||||
try:
|
||||
from PIL import Image as PILImage
|
||||
use_PIL = True
|
||||
except ImportError as e:
|
||||
use_PIL = False
|
||||
|
||||
|
||||
log = logger.create()
|
||||
|
||||
|
||||
try:
|
||||
from wand.image import Image
|
||||
use_IM = True
|
||||
except (ImportError, RuntimeError) as e:
|
||||
use_IM = False
|
||||
|
||||
|
||||
try:
|
||||
from comicapi.comicarchive import ComicArchive, MetaDataStyle
|
||||
use_comic_meta = True
|
||||
@ -52,20 +52,23 @@ except (ImportError, LookupError) as e:
|
||||
use_rarfile = False
|
||||
use_comic_meta = False
|
||||
|
||||
NO_JPEG_EXTENSIONS = ['.png', '.webp', '.bmp']
|
||||
COVER_EXTENSIONS = ['.png', '.webp', '.bmp', '.jpg', '.jpeg']
|
||||
|
||||
def _cover_processing(tmp_file_name, img, extension):
|
||||
if use_PIL:
|
||||
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg')
|
||||
if use_IM:
|
||||
# convert to jpg because calibre only supports jpg
|
||||
if extension in ('.png', '.webp'):
|
||||
imgc = PILImage.open(io.BytesIO(img))
|
||||
im = imgc.convert('RGB')
|
||||
tmp_bytesio = io.BytesIO()
|
||||
im.save(tmp_bytesio, format='JPEG')
|
||||
img = tmp_bytesio.getvalue()
|
||||
if extension in NO_JPEG_EXTENSIONS:
|
||||
with Image(filename=tmp_file_name) as imgc:
|
||||
imgc.format = 'jpeg'
|
||||
imgc.transform_colorspace('rgb')
|
||||
imgc.save(tmp_cover_name)
|
||||
return tmp_cover_name
|
||||
|
||||
if not img:
|
||||
return None
|
||||
|
||||
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg')
|
||||
with open(tmp_cover_name, 'wb') as f:
|
||||
f.write(img)
|
||||
return tmp_cover_name
|
||||
@ -80,7 +83,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
||||
ext = os.path.splitext(name)
|
||||
if len(ext) > 1:
|
||||
extension = ext[1].lower()
|
||||
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
|
||||
if extension in COVER_EXTENSIONS:
|
||||
cover_data = archive.getPage(index)
|
||||
break
|
||||
else:
|
||||
@ -90,7 +93,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
||||
ext = os.path.splitext(name)
|
||||
if len(ext) > 1:
|
||||
extension = ext[1].lower()
|
||||
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
|
||||
if extension in COVER_EXTENSIONS:
|
||||
cover_data = cf.read(name)
|
||||
break
|
||||
elif original_file_extension.upper() == '.CBT':
|
||||
@ -99,7 +102,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
||||
ext = os.path.splitext(name)
|
||||
if len(ext) > 1:
|
||||
extension = ext[1].lower()
|
||||
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
|
||||
if extension in COVER_EXTENSIONS:
|
||||
cover_data = cf.extractfile(name).read()
|
||||
break
|
||||
elif original_file_extension.upper() == '.CBR' and use_rarfile:
|
||||
@ -110,7 +113,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
||||
ext = os.path.splitext(name)
|
||||
if len(ext) > 1:
|
||||
extension = ext[1].lower()
|
||||
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
|
||||
if extension in COVER_EXTENSIONS:
|
||||
cover_data = cf.read(name)
|
||||
break
|
||||
except Exception as e:
|
||||
|
@ -22,6 +22,7 @@ import os
|
||||
import sys
|
||||
|
||||
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from . import constants, cli, logger, ub
|
||||
@ -271,6 +272,14 @@ class _ConfigSQL(object):
|
||||
setattr(self, field, new_value)
|
||||
return True
|
||||
|
||||
def toDict(self):
|
||||
storage = {}
|
||||
for k, v in self.__dict__.items():
|
||||
if k[0] != '_' or k.endswith("password"):
|
||||
storage[k] = v
|
||||
return storage
|
||||
|
||||
|
||||
def load(self):
|
||||
'''Load all configuration values from the underlying storage.'''
|
||||
s = self._read_from_storage() # type: _Settings
|
||||
@ -295,7 +304,11 @@ class _ConfigSQL(object):
|
||||
log.warning("Log path %s not valid, falling back to default", self.config_logfile)
|
||||
self.config_logfile = logfile
|
||||
self._session.merge(s)
|
||||
self._session.commit()
|
||||
try:
|
||||
self._session.commit()
|
||||
except OperationalError as e:
|
||||
log.error('Database error: %s', e)
|
||||
self._session.rollback()
|
||||
|
||||
def save(self):
|
||||
'''Apply all configuration values to the underlying storage.'''
|
||||
@ -309,7 +322,11 @@ class _ConfigSQL(object):
|
||||
|
||||
log.debug("_ConfigSQL updating storage")
|
||||
self._session.merge(s)
|
||||
self._session.commit()
|
||||
try:
|
||||
self._session.commit()
|
||||
except OperationalError as e:
|
||||
log.error('Database error: %s', e)
|
||||
self._session.rollback()
|
||||
self.load()
|
||||
|
||||
def invalidate(self, error=None):
|
||||
@ -350,7 +367,10 @@ def _migrate_table(session, orm_class):
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
session.commit()
|
||||
try:
|
||||
session.commit()
|
||||
except OperationalError:
|
||||
session.rollback()
|
||||
|
||||
|
||||
def autodetect_calibre_binary():
|
||||
|
13
cps/db.py
13
cps/db.py
@ -32,9 +32,9 @@ from sqlalchemy.orm import relationship, sessionmaker, scoped_session
|
||||
from sqlalchemy.orm.collections import InstrumentedList
|
||||
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from flask_login import current_user
|
||||
from sqlalchemy.sql.expression import and_, true, false, text, func, or_
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from flask_login import current_user
|
||||
from babel import Locale as LC
|
||||
from babel.core import UnknownLocaleError
|
||||
from flask_babel import gettext as _
|
||||
@ -425,18 +425,19 @@ class CalibreDB():
|
||||
# instances alive once they reach the end of their respective scopes
|
||||
instances = WeakSet()
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, expire_on_commit=True):
|
||||
""" Initialize a new CalibreDB session
|
||||
"""
|
||||
self.session = None
|
||||
if self._init:
|
||||
self.initSession()
|
||||
self.initSession(expire_on_commit)
|
||||
|
||||
self.instances.add(self)
|
||||
|
||||
|
||||
def initSession(self):
|
||||
def initSession(self, expire_on_commit=True):
|
||||
self.session = self.session_factory()
|
||||
self.session.expire_on_commit = expire_on_commit
|
||||
self.update_title_sort(self.config)
|
||||
|
||||
@classmethod
|
||||
@ -444,6 +445,8 @@ class CalibreDB():
|
||||
cls.config = config
|
||||
cls.dispose()
|
||||
|
||||
# toDo: if db changed -> delete shelfs, delete download books, delete read boks, kobo sync??
|
||||
|
||||
if not config.config_calibre_dir:
|
||||
config.invalidate()
|
||||
return False
|
||||
@ -764,5 +767,5 @@ def lcase(s):
|
||||
return unidecode.unidecode(s.lower())
|
||||
except Exception as e:
|
||||
log = logger.create()
|
||||
log.exception(e)
|
||||
log.debug_or_exception(e)
|
||||
return s.lower()
|
||||
|
@ -44,8 +44,12 @@ def assemble_logfiles(file_name):
|
||||
def send_debug():
|
||||
file_list = glob.glob(logger.get_logfile(config.config_logfile) + '*')
|
||||
file_list.extend(glob.glob(logger.get_accesslogfile(config.config_access_logfile) + '*'))
|
||||
for element in [logger.LOG_TO_STDOUT, logger.LOG_TO_STDERR]:
|
||||
if element in file_list:
|
||||
file_list.remove(element)
|
||||
memory_zip = io.BytesIO()
|
||||
with zipfile.ZipFile(memory_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr('settings.txt', json.dumps(config.toDict()))
|
||||
zf.writestr('libs.txt', json.dumps(collect_stats()))
|
||||
for fp in file_list:
|
||||
zf.write(fp, os.path.basename(fp))
|
||||
|
@ -37,13 +37,38 @@ from . import config, get_locale, ub, db
|
||||
from . import calibre_db
|
||||
from .services.worker import WorkerThread
|
||||
from .tasks.upload import TaskUpload
|
||||
from .web import login_required_if_no_ano, render_title_template, edit_required, upload_required
|
||||
from .render_template import render_title_template
|
||||
from .usermanagement import login_required_if_no_ano
|
||||
|
||||
try:
|
||||
from functools import wraps
|
||||
except ImportError:
|
||||
pass # We're not using Python 3
|
||||
|
||||
|
||||
editbook = Blueprint('editbook', __name__)
|
||||
log = logger.create()
|
||||
|
||||
|
||||
def upload_required(f):
|
||||
@wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
if current_user.role_upload() or current_user.role_admin():
|
||||
return f(*args, **kwargs)
|
||||
abort(403)
|
||||
|
||||
return inner
|
||||
|
||||
def edit_required(f):
|
||||
@wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
if current_user.role_edit() or current_user.role_admin():
|
||||
return f(*args, **kwargs)
|
||||
abort(403)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
# Modifies different Database objects, first check if elements have to be added to database, than check
|
||||
# if elements have to be deleted, because they are no longer used
|
||||
def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type):
|
||||
@ -259,7 +284,7 @@ def delete_book(book_id, book_format, jsonResponse):
|
||||
filter(db.Data.format == book_format).delete()
|
||||
calibre_db.session.commit()
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.debug_or_exception(e)
|
||||
calibre_db.session.rollback()
|
||||
else:
|
||||
# book not found
|
||||
@ -287,7 +312,7 @@ def delete_book(book_id, book_format, jsonResponse):
|
||||
def render_edit_book(book_id):
|
||||
calibre_db.update_title_sort(config)
|
||||
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
||||
book = calibre_db.get_filtered_book(book_id)
|
||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||
if not book:
|
||||
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
|
||||
return redirect(url_for("web.index"))
|
||||
@ -716,7 +741,7 @@ def edit_book(book_id):
|
||||
flash(error, category="error")
|
||||
return render_edit_book(book_id)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.debug_or_exception(e)
|
||||
calibre_db.session.rollback()
|
||||
flash(_("Error editing book, please check logfile for details"), category="error")
|
||||
return redirect(url_for('web.show_book', book_id=book.id))
|
||||
|
73
cps/error_handler.py
Normal file
73
cps/error_handler.py
Normal 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))
|
@ -35,9 +35,9 @@ from flask_babel import gettext as _
|
||||
from flask_login import login_required
|
||||
|
||||
from . import logger, gdriveutils, config, ub, calibre_db
|
||||
from .web import admin_required
|
||||
from .admin import admin_required
|
||||
|
||||
gdrive = Blueprint('gdrive', __name__)
|
||||
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
|
||||
log = logger.create()
|
||||
|
||||
try:
|
||||
@ -50,7 +50,7 @@ current_milli_time = lambda: int(round(time() * 1000))
|
||||
gdrive_watch_callback_token = 'target=calibreweb-watch_files'
|
||||
|
||||
|
||||
@gdrive.route("/gdrive/authenticate")
|
||||
@gdrive.route("/authenticate")
|
||||
@login_required
|
||||
@admin_required
|
||||
def authenticate_google_drive():
|
||||
@ -63,7 +63,7 @@ def authenticate_google_drive():
|
||||
return redirect(authUrl)
|
||||
|
||||
|
||||
@gdrive.route("/gdrive/callback")
|
||||
@gdrive.route("/callback")
|
||||
def google_drive_callback():
|
||||
auth_code = request.args.get('code')
|
||||
if not auth_code:
|
||||
@ -77,18 +77,14 @@ def google_drive_callback():
|
||||
return redirect(url_for('admin.configuration'))
|
||||
|
||||
|
||||
@gdrive.route("/gdrive/watch/subscribe")
|
||||
@gdrive.route("/watch/subscribe")
|
||||
@login_required
|
||||
@admin_required
|
||||
def watch_gdrive():
|
||||
if not config.config_google_drive_watch_changes_response:
|
||||
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
|
||||
filedata = json.load(settings)
|
||||
if filedata['web']['redirect_uris'][0].endswith('/'):
|
||||
filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-((len('/gdrive/callback')+1))]
|
||||
else:
|
||||
filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-(len('/gdrive/callback'))]
|
||||
address = '%s/gdrive/watch/callback' % filedata['web']['redirect_uris'][0]
|
||||
address = filedata['web']['redirect_uris'][0].rstrip('/').replace('/gdrive/callback', '/gdrive/watch/callback')
|
||||
notification_id = str(uuid4())
|
||||
try:
|
||||
result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id,
|
||||
@ -98,14 +94,15 @@ def watch_gdrive():
|
||||
except HttpError as e:
|
||||
reason=json.loads(e.content)['error']['errors'][0]
|
||||
if reason['reason'] == u'push.webhookUrlUnauthorized':
|
||||
flash(_(u'Callback domain is not verified, please follow steps to verify domain in google developer console'), category="error")
|
||||
flash(_(u'Callback domain is not verified, '
|
||||
u'please follow steps to verify domain in google developer console'), category="error")
|
||||
else:
|
||||
flash(reason['message'], category="error")
|
||||
|
||||
return redirect(url_for('admin.configuration'))
|
||||
|
||||
|
||||
@gdrive.route("/gdrive/watch/revoke")
|
||||
@gdrive.route("/watch/revoke")
|
||||
@login_required
|
||||
@admin_required
|
||||
def revoke_watch_gdrive():
|
||||
@ -121,14 +118,14 @@ def revoke_watch_gdrive():
|
||||
return redirect(url_for('admin.configuration'))
|
||||
|
||||
|
||||
@gdrive.route("/gdrive/watch/callback", methods=['GET', 'POST'])
|
||||
@gdrive.route("/watch/callback", methods=['GET', 'POST'])
|
||||
def on_received_watch_confirmation():
|
||||
if not config.config_google_drive_watch_changes_response:
|
||||
return ''
|
||||
if request.headers.get('X-Goog-Channel-Token') != gdrive_watch_callback_token \
|
||||
or request.headers.get('X-Goog-Resource-State') != 'change' \
|
||||
or not request.data:
|
||||
return '' # redirect(url_for('admin.configuration'))
|
||||
return ''
|
||||
|
||||
log.debug('%r', request.headers)
|
||||
log.debug('%r', request.data)
|
||||
@ -146,15 +143,18 @@ def on_received_watch_confirmation():
|
||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
|
||||
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
||||
and response['file']['md5Checksum'] != hashlib.md5(dbpath):
|
||||
tmpDir = tempfile.gettempdir()
|
||||
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
|
||||
if not os.path.isdir(tmp_dir):
|
||||
os.mkdir(tmp_dir)
|
||||
|
||||
log.info('Database file updated')
|
||||
copyfile(dbpath, os.path.join(tmpDir, "metadata.db_" + str(current_milli_time())))
|
||||
copyfile(dbpath, os.path.join(tmp_dir, "metadata.db_" + str(current_milli_time())))
|
||||
log.info('Backing up existing and downloading updated metadata.db')
|
||||
gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmpDir, "tmp_metadata.db"))
|
||||
gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmp_dir, "tmp_metadata.db"))
|
||||
log.info('Setting up new DB')
|
||||
# prevent error on windows, as os.rename does on exisiting files
|
||||
move(os.path.join(tmpDir, "tmp_metadata.db"), dbpath)
|
||||
# prevent error on windows, as os.rename does on existing files, also allow cross hdd move
|
||||
move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath)
|
||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.debug_or_exception(e)
|
||||
return ''
|
||||
|
@ -32,16 +32,25 @@ from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
||||
|
||||
try:
|
||||
from pydrive.auth import GoogleAuth
|
||||
from pydrive.drive import GoogleDrive
|
||||
from pydrive.auth import RefreshError
|
||||
from apiclient import errors
|
||||
from httplib2 import ServerNotFoundError
|
||||
gdrive_support = True
|
||||
importError = None
|
||||
except ImportError as err:
|
||||
importError = err
|
||||
gdrive_support = True
|
||||
except ImportError as e:
|
||||
importError = e
|
||||
gdrive_support = False
|
||||
try:
|
||||
from pydrive2.auth import GoogleAuth
|
||||
from pydrive2.drive import GoogleDrive
|
||||
from pydrive2.auth import RefreshError
|
||||
except ImportError as err:
|
||||
try:
|
||||
from pydrive.auth import GoogleAuth
|
||||
from pydrive.drive import GoogleDrive
|
||||
from pydrive.auth import RefreshError
|
||||
except ImportError as err:
|
||||
importError = err
|
||||
gdrive_support = False
|
||||
|
||||
from . import logger, cli, config
|
||||
from .constants import CONFIG_DIR as _CONFIG_DIR
|
||||
@ -91,7 +100,7 @@ class Singleton:
|
||||
except AttributeError:
|
||||
self._instance = self._decorated()
|
||||
return self._instance
|
||||
except ImportError as e:
|
||||
except (ImportError, NameError) as e:
|
||||
log.debug(e)
|
||||
return None
|
||||
|
||||
@ -190,7 +199,7 @@ def getDrive(drive=None, gauth=None):
|
||||
except RefreshError as e:
|
||||
log.error("Google Drive error: %s", e)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.debug_or_exception(e)
|
||||
else:
|
||||
# Initialize the saved creds
|
||||
gauth.Authorize()
|
||||
@ -208,7 +217,7 @@ def listRootFolders():
|
||||
drive = getDrive(Gdrive.Instance().drive)
|
||||
folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false"
|
||||
fileList = drive.ListFile({'q': folder}).GetList()
|
||||
except ServerNotFoundError as e:
|
||||
except (ServerNotFoundError, ssl.SSLError) as e:
|
||||
log.info("GDrive Error %s" % e)
|
||||
fileList = []
|
||||
return fileList
|
||||
@ -547,21 +556,24 @@ def partial(total_byte_len, part_size_limit):
|
||||
return s
|
||||
|
||||
# downloads files in chunks from gdrive
|
||||
def do_gdrive_download(df, headers):
|
||||
def do_gdrive_download(df, headers, convert_encoding=False):
|
||||
total_size = int(df.metadata.get('fileSize'))
|
||||
download_url = df.metadata.get('downloadUrl')
|
||||
s = partial(total_size, 1024 * 1024) # I'm downloading BIG files, so 100M chunk size is fine for me
|
||||
|
||||
def stream():
|
||||
def stream(convert_encoding):
|
||||
for byte in s:
|
||||
headers = {"Range": 'bytes=%s-%s' % (byte[0], byte[1])}
|
||||
resp, content = df.auth.Get_Http_Object().request(download_url, headers=headers)
|
||||
if resp.status == 206:
|
||||
if convert_encoding:
|
||||
result = chardet.detect(content)
|
||||
content = content.decode(result['encoding']).encode('utf-8')
|
||||
yield content
|
||||
else:
|
||||
log.warning('An error occurred: %s', resp)
|
||||
return
|
||||
return Response(stream_with_context(stream()), headers=headers)
|
||||
return Response(stream_with_context(stream(convert_encoding)), headers=headers)
|
||||
|
||||
|
||||
_SETTINGS_YAML_TEMPLATE = """
|
||||
|
@ -24,10 +24,7 @@ import io
|
||||
import mimetypes
|
||||
import re
|
||||
import shutil
|
||||
import glob
|
||||
import time
|
||||
import zipfile
|
||||
import json
|
||||
import unicodedata
|
||||
from datetime import datetime, timedelta
|
||||
from tempfile import gettempdir
|
||||
@ -53,13 +50,6 @@ try:
|
||||
except ImportError:
|
||||
use_unidecode = False
|
||||
|
||||
try:
|
||||
from PIL import Image as PILImage
|
||||
from PIL import UnidentifiedImageError
|
||||
use_PIL = True
|
||||
except ImportError:
|
||||
use_PIL = False
|
||||
|
||||
from . import calibre_db
|
||||
from .tasks.convert import TaskConvert
|
||||
from . import logger, config, get_locale, db, ub
|
||||
@ -69,9 +59,16 @@ from .subproc_wrapper import process_wait
|
||||
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS
|
||||
from .tasks.mail import TaskEmail
|
||||
|
||||
|
||||
log = logger.create()
|
||||
|
||||
try:
|
||||
from wand.image import Image
|
||||
from wand.exceptions import MissingDelegateError
|
||||
use_IM = True
|
||||
except (ImportError, RuntimeError) as e:
|
||||
log.debug('Cannot import Image, generating covers from non jpg files will not work: %s', e)
|
||||
use_IM = False
|
||||
|
||||
|
||||
# Convert existing book entry to new format
|
||||
def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, user_id, kindle_mail=None):
|
||||
@ -112,21 +109,21 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
|
||||
def send_test_mail(kindle_mail, user_name):
|
||||
WorkerThread.add(user_name, TaskEmail(_(u'Calibre-Web test e-mail'), None, None,
|
||||
config.get_mail_settings(), kindle_mail, _(u"Test e-mail"),
|
||||
_(u'This e-mail has been sent via Calibre-Web.')))
|
||||
_(u'This e-mail has been sent via Calibre-Web.')))
|
||||
return
|
||||
|
||||
|
||||
# Send registration email or password reset email, depending on parameter resend (False means welcome email)
|
||||
def send_registration_mail(e_mail, user_name, default_password, resend=False):
|
||||
text = "Hello %s!\r\n" % user_name
|
||||
txt = "Hello %s!\r\n" % user_name
|
||||
if not resend:
|
||||
text += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n"
|
||||
text += "Please log in to your account using the following informations:\r\n"
|
||||
text += "User name: %s\r\n" % user_name
|
||||
text += "Password: %s\r\n" % default_password
|
||||
text += "Don't forget to change your password after first login.\r\n"
|
||||
text += "Sincerely\r\n\r\n"
|
||||
text += "Your Calibre-Web team"
|
||||
txt += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n"
|
||||
txt += "Please log in to your account using the following informations:\r\n"
|
||||
txt += "User name: %s\r\n" % user_name
|
||||
txt += "Password: %s\r\n" % default_password
|
||||
txt += "Don't forget to change your password after first login.\r\n"
|
||||
txt += "Sincerely\r\n\r\n"
|
||||
txt += "Your Calibre-Web team"
|
||||
WorkerThread.add(None, TaskEmail(
|
||||
subject=_(u'Get Started with Calibre-Web'),
|
||||
filepath=None,
|
||||
@ -134,7 +131,7 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
|
||||
settings=config.get_mail_settings(),
|
||||
recipient=e_mail,
|
||||
taskMessage=_(u"Registration e-mail for user: %(name)s", name=user_name),
|
||||
text=text
|
||||
text=txt
|
||||
))
|
||||
|
||||
return
|
||||
@ -180,7 +177,7 @@ def check_send_to_kindle(entry):
|
||||
'convert': 0,
|
||||
'text': _('Send %(format)s to Kindle', format='Pdf')})
|
||||
if config.config_converterpath:
|
||||
if 'EPUB' in formats and not 'MOBI' in formats:
|
||||
if 'EPUB' in formats and 'MOBI' not in formats:
|
||||
bookformats.append({'format': 'Mobi',
|
||||
'convert':1,
|
||||
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
|
||||
@ -565,8 +562,7 @@ def get_book_cover_internal(book, use_generic_cover_on_failure):
|
||||
log.error('%s/cover.jpg not found on Google Drive', book.path)
|
||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
# traceback.print_exc()
|
||||
log.debug_or_exception(e)
|
||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||
else:
|
||||
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
|
||||
@ -589,16 +585,15 @@ def save_cover_from_url(url, book_path):
|
||||
requests.exceptions.Timeout) as ex:
|
||||
log.info(u'Cover Download Error %s', ex)
|
||||
return False, _("Error Downloading Cover")
|
||||
except UnidentifiedImageError as ex:
|
||||
except MissingDelegateError as ex:
|
||||
log.info(u'File Format Error %s', ex)
|
||||
return False, _("Cover Format Error")
|
||||
|
||||
|
||||
def save_cover_from_filestorage(filepath, saved_filename, img):
|
||||
if hasattr(img, '_content'):
|
||||
f = open(os.path.join(filepath, saved_filename), "wb")
|
||||
f.write(img._content)
|
||||
f.close()
|
||||
if hasattr(img,"metadata"):
|
||||
img.save(filename=os.path.join(filepath, saved_filename))
|
||||
img.close()
|
||||
else:
|
||||
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file
|
||||
if not os.path.exists(filepath):
|
||||
@ -619,31 +614,33 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
|
||||
def save_cover(img, book_path):
|
||||
content_type = img.headers.get('content-type')
|
||||
|
||||
if use_PIL:
|
||||
if content_type not in ('image/jpeg', 'image/png', 'image/webp'):
|
||||
log.error("Only jpg/jpeg/png/webp files are supported as coverfile")
|
||||
return False, _("Only jpg/jpeg/png/webp files are supported as coverfile")
|
||||
if use_IM:
|
||||
if content_type not in ('image/jpeg', 'image/png', 'image/webp', 'image/bmp'):
|
||||
log.error("Only jpg/jpeg/png/webp/bmp files are supported as coverfile")
|
||||
return False, _("Only jpg/jpeg/png/webp/bmp files are supported as coverfile")
|
||||
# convert to jpg because calibre only supports jpg
|
||||
if content_type in ('image/png', 'image/webp'):
|
||||
if content_type != 'image/jpg':
|
||||
if hasattr(img, 'stream'):
|
||||
imgc = PILImage.open(img.stream)
|
||||
imgc = Image(blob=img.stream)
|
||||
else:
|
||||
imgc = PILImage.open(io.BytesIO(img.content))
|
||||
im = imgc.convert('RGB')
|
||||
tmp_bytesio = io.BytesIO()
|
||||
im.save(tmp_bytesio, format='JPEG')
|
||||
img._content = tmp_bytesio.getvalue()
|
||||
imgc = Image(blob=io.BytesIO(img.content))
|
||||
imgc.format = 'jpeg'
|
||||
imgc.transform_colorspace("rgb")
|
||||
img = imgc
|
||||
else:
|
||||
if content_type not in 'image/jpeg':
|
||||
log.error("Only jpg/jpeg files are supported as coverfile")
|
||||
return False, _("Only jpg/jpeg files are supported as coverfile")
|
||||
|
||||
if config.config_use_google_drive:
|
||||
tmpDir = gettempdir()
|
||||
ret, message = save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img)
|
||||
tmp_dir = os.path.join(gettempdir(), 'calibre_web')
|
||||
|
||||
if not os.path.isdir(tmp_dir):
|
||||
os.mkdir(tmp_dir)
|
||||
ret, message = save_cover_from_filestorage(tmp_dir, "uploaded_cover.jpg", img)
|
||||
if ret is True:
|
||||
gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'),
|
||||
os.path.join(tmpDir, "uploaded_cover.jpg"))
|
||||
gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg').replace("\\","/"),
|
||||
os.path.join(tmp_dir, "uploaded_cover.jpg"))
|
||||
log.info("Cover is saved on Google Drive")
|
||||
return True, None
|
||||
else:
|
||||
@ -697,7 +694,7 @@ def check_unrar(unrarLocation):
|
||||
log.debug("unrar version %s", version)
|
||||
break
|
||||
except (OSError, UnicodeDecodeError) as err:
|
||||
log.exception(err)
|
||||
log.debug_or_exception(err)
|
||||
return _('Error excecuting UnRar')
|
||||
|
||||
|
||||
@ -827,4 +824,3 @@ def get_download_link(book_id, book_format, client):
|
||||
return do_download_file(book, book_format, client, data1, headers)
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
|
144
cps/kobo.py
144
cps/kobo.py
@ -43,6 +43,8 @@ from flask_login import current_user
|
||||
from werkzeug.datastructures import Headers
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.sql.expression import and_, or_
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from sqlalchemy.orm import load_only
|
||||
from sqlalchemy.exc import StatementError
|
||||
import requests
|
||||
|
||||
@ -56,6 +58,8 @@ KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]}
|
||||
KOBO_STOREAPI_URL = "https://storeapi.kobo.com"
|
||||
KOBO_IMAGEHOST_URL = "https://kbimages1-a.akamaihd.net"
|
||||
|
||||
SYNC_ITEM_LIMIT = 5
|
||||
|
||||
kobo = Blueprint("kobo", __name__, url_prefix="/kobo/<auth_token>")
|
||||
kobo_auth.disable_failed_auth_redirect_for_blueprint(kobo)
|
||||
kobo_auth.register_url_value_preprocessor(kobo)
|
||||
@ -142,68 +146,80 @@ def HandleSyncRequest():
|
||||
new_books_last_modified = sync_token.books_last_modified
|
||||
new_books_last_created = sync_token.books_last_created
|
||||
new_reading_state_last_modified = sync_token.reading_state_last_modified
|
||||
new_archived_last_modified = datetime.datetime.min
|
||||
sync_results = []
|
||||
|
||||
# We reload the book database so that the user get's a fresh view of the library
|
||||
# in case of external changes (e.g: adding a book through Calibre).
|
||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||
|
||||
archived_books = (
|
||||
ub.session.query(ub.ArchivedBook)
|
||||
.filter(ub.ArchivedBook.user_id == int(current_user.id))
|
||||
.all()
|
||||
)
|
||||
|
||||
# We join-in books that have had their Archived bit recently modified in order to either:
|
||||
# * Restore them to the user's device.
|
||||
# * Delete them from the user's device.
|
||||
# (Ideally we would use a join for this logic, however cross-database joins don't look trivial in SqlAlchemy.)
|
||||
recently_restored_or_archived_books = []
|
||||
archived_book_ids = {}
|
||||
new_archived_last_modified = datetime.datetime.min
|
||||
for archived_book in archived_books:
|
||||
if archived_book.last_modified > sync_token.archive_last_modified:
|
||||
recently_restored_or_archived_books.append(archived_book.book_id)
|
||||
if archived_book.is_archived:
|
||||
archived_book_ids[archived_book.book_id] = True
|
||||
new_archived_last_modified = max(
|
||||
new_archived_last_modified, archived_book.last_modified)
|
||||
|
||||
# sqlite gives unexpected results when performing the last_modified comparison without the datetime cast.
|
||||
# It looks like it's treating the db.Books.last_modified field as a string and may fail
|
||||
# the comparison because of the +00:00 suffix.
|
||||
changed_entries = (
|
||||
calibre_db.session.query(db.Books)
|
||||
.join(db.Data)
|
||||
.filter(or_(func.datetime(db.Books.last_modified) > sync_token.books_last_modified,
|
||||
db.Books.id.in_(recently_restored_or_archived_books)))
|
||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||
.all()
|
||||
)
|
||||
if sync_token.books_last_id > -1:
|
||||
changed_entries = (
|
||||
calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
|
||||
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||
.filter(db.Books.last_modified >= sync_token.books_last_modified)
|
||||
.filter(db.Books.id>sync_token.books_last_id)
|
||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||
.order_by(db.Books.last_modified)
|
||||
.order_by(db.Books.id)
|
||||
.limit(SYNC_ITEM_LIMIT)
|
||||
)
|
||||
else:
|
||||
changed_entries = (
|
||||
calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
|
||||
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||
.filter(db.Books.last_modified > sync_token.books_last_modified)
|
||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||
.order_by(db.Books.last_modified)
|
||||
.order_by(db.Books.id)
|
||||
.limit(SYNC_ITEM_LIMIT)
|
||||
)
|
||||
|
||||
reading_states_in_new_entitlements = []
|
||||
for book in changed_entries:
|
||||
kobo_reading_state = get_or_create_reading_state(book.id)
|
||||
kobo_reading_state = get_or_create_reading_state(book.Books.id)
|
||||
entitlement = {
|
||||
"BookEntitlement": create_book_entitlement(book, archived=(book.id in archived_book_ids)),
|
||||
"BookMetadata": get_metadata(book),
|
||||
"BookEntitlement": create_book_entitlement(book.Books, archived=(book.is_archived == True)),
|
||||
"BookMetadata": get_metadata(book.Books),
|
||||
}
|
||||
|
||||
if kobo_reading_state.last_modified > sync_token.reading_state_last_modified:
|
||||
entitlement["ReadingState"] = get_kobo_reading_state_response(book, kobo_reading_state)
|
||||
entitlement["ReadingState"] = get_kobo_reading_state_response(book.Books, kobo_reading_state)
|
||||
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
|
||||
reading_states_in_new_entitlements.append(book.id)
|
||||
reading_states_in_new_entitlements.append(book.Books.id)
|
||||
|
||||
if book.timestamp > sync_token.books_last_created:
|
||||
if book.Books.timestamp > sync_token.books_last_created:
|
||||
sync_results.append({"NewEntitlement": entitlement})
|
||||
else:
|
||||
sync_results.append({"ChangedEntitlement": entitlement})
|
||||
|
||||
new_books_last_modified = max(
|
||||
book.last_modified, new_books_last_modified
|
||||
book.Books.last_modified, new_books_last_modified
|
||||
)
|
||||
new_books_last_created = max(book.timestamp, new_books_last_created)
|
||||
new_books_last_created = max(book.Books.timestamp, new_books_last_created)
|
||||
|
||||
max_change = (changed_entries
|
||||
.from_self()
|
||||
.filter(ub.ArchivedBook.is_archived)
|
||||
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc())
|
||||
.first()
|
||||
)
|
||||
if max_change:
|
||||
max_change = max_change.last_modified
|
||||
else:
|
||||
max_change = new_archived_last_modified
|
||||
new_archived_last_modified = max(new_archived_last_modified, max_change)
|
||||
|
||||
# no. of books returned
|
||||
book_count = changed_entries.count()
|
||||
|
||||
# last entry:
|
||||
if book_count:
|
||||
books_last_id = changed_entries.all()[-1].Books.id or -1
|
||||
else:
|
||||
books_last_id = -1
|
||||
|
||||
# generate reading state data
|
||||
changed_reading_states = (
|
||||
ub.session.query(ub.KoboReadingState)
|
||||
.filter(and_(func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified,
|
||||
@ -225,11 +241,12 @@ def HandleSyncRequest():
|
||||
sync_token.books_last_modified = new_books_last_modified
|
||||
sync_token.archive_last_modified = new_archived_last_modified
|
||||
sync_token.reading_state_last_modified = new_reading_state_last_modified
|
||||
sync_token.books_last_id = books_last_id
|
||||
|
||||
return generate_sync_response(sync_token, sync_results)
|
||||
return generate_sync_response(sync_token, sync_results, book_count)
|
||||
|
||||
|
||||
def generate_sync_response(sync_token, sync_results):
|
||||
def generate_sync_response(sync_token, sync_results, set_cont=False):
|
||||
extra_headers = {}
|
||||
if config.config_kobo_proxy:
|
||||
# Merge in sync results from the official Kobo store.
|
||||
@ -245,6 +262,8 @@ def generate_sync_response(sync_token, sync_results):
|
||||
|
||||
except Exception as e:
|
||||
log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e))
|
||||
if set_cont:
|
||||
extra_headers["x-kobo-sync"] = "continue"
|
||||
sync_token.to_headers(extra_headers)
|
||||
|
||||
response = make_response(jsonify(sync_results), extra_headers)
|
||||
@ -443,8 +462,10 @@ def HandleTagCreate():
|
||||
items_unknown_to_calibre = add_items_to_shelf(items, shelf)
|
||||
if items_unknown_to_calibre:
|
||||
log.debug("Received request to add unknown books to a collection. Silently ignoring items.")
|
||||
ub.session.commit()
|
||||
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
return make_response(jsonify(str(shelf.uuid)), 201)
|
||||
|
||||
|
||||
@ -476,7 +497,10 @@ def HandleTagUpdate(tag_id):
|
||||
|
||||
shelf.name = name
|
||||
ub.session.merge(shelf)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
return make_response(' ', 200)
|
||||
|
||||
|
||||
@ -528,7 +552,10 @@ def HandleTagAddItem(tag_id):
|
||||
log.debug("Received request to add an unknown book to a collection. Silently ignoring item.")
|
||||
|
||||
ub.session.merge(shelf)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
|
||||
return make_response('', 201)
|
||||
|
||||
@ -569,7 +596,10 @@ def HandleTagRemoveItem(tag_id):
|
||||
shelf.books.filter(ub.BookShelf.book_id == book.id).delete()
|
||||
except KeyError:
|
||||
items_unknown_to_calibre.append(item)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
|
||||
if items_unknown_to_calibre:
|
||||
log.debug("Received request to remove an unknown book to a collecition. Silently ignoring item.")
|
||||
@ -615,7 +645,10 @@ def sync_shelves(sync_token, sync_results):
|
||||
"ChangedTag": tag
|
||||
})
|
||||
sync_token.tags_last_modified = new_tags_last_modified
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
|
||||
|
||||
# Creates a Kobo "Tag" object from a ub.Shelf object
|
||||
@ -696,7 +729,10 @@ def HandleStateRequest(book_uuid):
|
||||
abort(400, description="Malformed request data is missing 'ReadingStates' key")
|
||||
|
||||
ub.session.merge(kobo_reading_state)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
return jsonify({
|
||||
"RequestResult": "Success",
|
||||
"UpdateResults": [update_results_response],
|
||||
@ -734,7 +770,10 @@ def get_or_create_reading_state(book_id):
|
||||
kobo_reading_state.statistics = ub.KoboStatistics()
|
||||
book_read.kobo_reading_state = kobo_reading_state
|
||||
ub.session.add(book_read)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
return book_read.kobo_reading_state
|
||||
|
||||
|
||||
@ -837,7 +876,10 @@ def HandleBookDeletionRequest(book_uuid):
|
||||
archived_book.last_modified = datetime.datetime.utcnow()
|
||||
|
||||
ub.session.merge(archived_book)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
|
||||
return ("", 204)
|
||||
|
||||
|
@ -66,9 +66,10 @@ from os import urandom
|
||||
from flask import g, Blueprint, url_for, abort, request
|
||||
from flask_login import login_user, login_required
|
||||
from flask_babel import gettext as _
|
||||
from sqlalchemy.exc import OperationalError
|
||||
|
||||
from . import logger, ub, lm
|
||||
from .web import render_title_template
|
||||
from .render_template import render_title_template
|
||||
|
||||
try:
|
||||
from functools import wraps
|
||||
@ -147,7 +148,10 @@ def generate_auth_token(user_id):
|
||||
auth_token.token_type = 1
|
||||
|
||||
ub.session.add(auth_token)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
return render_title_template(
|
||||
"generate_kobo_auth_url.html",
|
||||
title=_(u"Kobo Setup"),
|
||||
@ -164,5 +168,8 @@ def delete_auth_token(user_id):
|
||||
# Invalidate any prevously generated Kobo Auth token for this user.
|
||||
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
||||
.filter(ub.RemoteAuthToken.token_type==1).delete()
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
return ""
|
||||
|
@ -41,10 +41,18 @@ logging.addLevelName(logging.WARNING, "WARN")
|
||||
logging.addLevelName(logging.CRITICAL, "CRIT")
|
||||
|
||||
|
||||
class _Logger(logging.Logger):
|
||||
|
||||
def debug_or_exception(self, message, *args, **kwargs):
|
||||
if is_debug_enabled():
|
||||
self.exception(message, stacklevel=2, *args, **kwargs)
|
||||
else:
|
||||
self.error(message, stacklevel=2, *args, **kwargs)
|
||||
|
||||
|
||||
def get(name=None):
|
||||
return logging.getLogger(name)
|
||||
|
||||
|
||||
def create():
|
||||
parent_frame = inspect.stack(0)[1]
|
||||
if hasattr(parent_frame, 'frame'):
|
||||
@ -54,7 +62,6 @@ def create():
|
||||
parent_module = inspect.getmodule(parent_frame)
|
||||
return get(parent_module.__name__)
|
||||
|
||||
|
||||
def is_debug_enabled():
|
||||
return logging.root.level <= logging.DEBUG
|
||||
|
||||
@ -99,6 +106,7 @@ def setup(log_file, log_level=None):
|
||||
May be called multiple times.
|
||||
'''
|
||||
log_level = log_level or DEFAULT_LOG_LEVEL
|
||||
logging.setLoggerClass(_Logger)
|
||||
logging.getLogger(__package__).setLevel(log_level)
|
||||
|
||||
r = logging.root
|
||||
|
@ -30,11 +30,12 @@ from flask_babel import gettext as _
|
||||
from flask_dance.consumer import oauth_authorized, oauth_error
|
||||
from flask_dance.contrib.github import make_github_blueprint, github
|
||||
from flask_dance.contrib.google import make_google_blueprint, google
|
||||
from flask_login import login_user, current_user
|
||||
from flask_login import login_user, current_user, login_required
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
from sqlalchemy.exc import OperationalError
|
||||
|
||||
from . import constants, logger, config, app, ub
|
||||
from .web import login_required
|
||||
|
||||
from .oauth import OAuthBackend, backend_resultcode
|
||||
|
||||
|
||||
@ -87,7 +88,7 @@ def register_user_with_oauth(user=None):
|
||||
try:
|
||||
ub.session.commit()
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.debug_or_exception(e)
|
||||
ub.session.rollback()
|
||||
|
||||
|
||||
@ -109,7 +110,10 @@ if ub.oauth_support:
|
||||
oauthProvider.provider_name = "google"
|
||||
oauthProvider.active = False
|
||||
ub.session.add(oauthProvider)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
|
||||
oauth_ids = ub.session.query(ub.OAuthProvider).all()
|
||||
ele1 = dict(provider_name='github',
|
||||
@ -203,7 +207,7 @@ if ub.oauth_support:
|
||||
ub.session.add(oauth_entry)
|
||||
ub.session.commit()
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.debug_or_exception(e)
|
||||
ub.session.rollback()
|
||||
|
||||
# Disable Flask-Dance's default behavior for saving the OAuth token
|
||||
@ -235,7 +239,7 @@ if ub.oauth_support:
|
||||
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
|
||||
return redirect(url_for('web.profile'))
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.debug_or_exception(e)
|
||||
ub.session.rollback()
|
||||
else:
|
||||
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
|
||||
@ -282,7 +286,7 @@ if ub.oauth_support:
|
||||
logout_oauth_user()
|
||||
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
log.debug_or_exception(e)
|
||||
ub.session.rollback()
|
||||
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
|
||||
except NoResultFound:
|
||||
|
@ -33,7 +33,8 @@ from werkzeug.security import check_password_hash
|
||||
from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
|
||||
from .helper import get_download_link, get_book_cover
|
||||
from .pagination import Pagination
|
||||
from .web import render_read_books, download_required, load_user_from_request
|
||||
from .web import render_read_books
|
||||
from .usermanagement import load_user_from_request
|
||||
from flask_babel import gettext as _
|
||||
from babel import Locale as LC
|
||||
from babel.core import UnknownLocaleError
|
||||
|
139
cps/remotelogin.py
Normal file
139
cps/remotelogin.py
Normal 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
116
cps/render_template.py
Normal 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)
|
@ -85,6 +85,7 @@ class SyncToken:
|
||||
"archive_last_modified": {"type": "string"},
|
||||
"reading_state_last_modified": {"type": "string"},
|
||||
"tags_last_modified": {"type": "string"},
|
||||
"books_last_id": {"type": "integer", "optional": True}
|
||||
},
|
||||
}
|
||||
|
||||
@ -96,6 +97,7 @@ class SyncToken:
|
||||
archive_last_modified=datetime.min,
|
||||
reading_state_last_modified=datetime.min,
|
||||
tags_last_modified=datetime.min,
|
||||
books_last_id=-1
|
||||
):
|
||||
self.raw_kobo_store_token = raw_kobo_store_token
|
||||
self.books_last_created = books_last_created
|
||||
@ -103,6 +105,7 @@ class SyncToken:
|
||||
self.archive_last_modified = archive_last_modified
|
||||
self.reading_state_last_modified = reading_state_last_modified
|
||||
self.tags_last_modified = tags_last_modified
|
||||
self.books_last_id = books_last_id
|
||||
|
||||
@staticmethod
|
||||
def from_headers(headers):
|
||||
@ -137,9 +140,12 @@ class SyncToken:
|
||||
archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified")
|
||||
reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified")
|
||||
tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified")
|
||||
books_last_id = data_json["books_last_id"]
|
||||
except TypeError:
|
||||
log.error("SyncToken timestamps don't parse to a datetime.")
|
||||
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
|
||||
except KeyError:
|
||||
books_last_id = -1
|
||||
|
||||
return SyncToken(
|
||||
raw_kobo_store_token=raw_kobo_store_token,
|
||||
@ -147,7 +153,8 @@ class SyncToken:
|
||||
books_last_modified=books_last_modified,
|
||||
archive_last_modified=archive_last_modified,
|
||||
reading_state_last_modified=reading_state_last_modified,
|
||||
tags_last_modified=tags_last_modified
|
||||
tags_last_modified=tags_last_modified,
|
||||
books_last_id=books_last_id
|
||||
)
|
||||
|
||||
def set_kobo_store_header(self, store_headers):
|
||||
@ -170,7 +177,8 @@ class SyncToken:
|
||||
"books_last_created": to_epoch_timestamp(self.books_last_created),
|
||||
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
|
||||
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified),
|
||||
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified)
|
||||
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified),
|
||||
"books_last_id":self.books_last_id
|
||||
},
|
||||
}
|
||||
return b64encode_json(token)
|
||||
|
@ -110,7 +110,7 @@ class WorkerThread(threading.Thread):
|
||||
# We don't use a daemon here because we don't want the tasks to just be abruptly halted, leading to
|
||||
# possible file / database corruption
|
||||
item = self.queue.get(timeout=1)
|
||||
except queue.Empty as ex:
|
||||
except queue.Empty:
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
@ -161,7 +161,7 @@ class CalibreTask:
|
||||
self.run(*args)
|
||||
except Exception as e:
|
||||
self._handleError(str(e))
|
||||
log.exception(e)
|
||||
log.debug_or_exception(e)
|
||||
|
||||
self.end_time = datetime.now()
|
||||
|
||||
@ -210,7 +210,6 @@ class CalibreTask:
|
||||
self._progress = x
|
||||
|
||||
def _handleError(self, error_message):
|
||||
log.exception(error_message)
|
||||
self.stat = STAT_FAIL
|
||||
self.progress = 1
|
||||
self.error = error_message
|
||||
|
155
cps/shelf.py
155
cps/shelf.py
@ -22,6 +22,7 @@
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
from datetime import datetime
|
||||
import sys
|
||||
|
||||
from flask import Blueprint, request, flash, redirect, url_for
|
||||
from flask_babel import gettext as _
|
||||
@ -29,8 +30,9 @@ from flask_login import login_required, current_user
|
||||
from sqlalchemy.sql.expression import func
|
||||
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
||||
|
||||
from . import logger, ub, calibre_db
|
||||
from .web import login_required_if_no_ano, render_title_template
|
||||
from . import logger, ub, calibre_db, db
|
||||
from .render_template import render_title_template
|
||||
from .usermanagement import login_required_if_no_ano
|
||||
|
||||
|
||||
shelf = Blueprint('shelf', __name__)
|
||||
@ -138,18 +140,14 @@ def search_to_shelf(shelf_id):
|
||||
books_for_shelf = ub.searched_ids[current_user.id]
|
||||
|
||||
if not books_for_shelf:
|
||||
log.error("Books are already part of %s", shelf)
|
||||
log.error("Books are already part of %s", shelf.name)
|
||||
flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()
|
||||
if maxOrder[0] is None:
|
||||
maxOrder = 0
|
||||
else:
|
||||
maxOrder = maxOrder[0]
|
||||
maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0
|
||||
|
||||
for book in books_for_shelf:
|
||||
maxOrder = maxOrder + 1
|
||||
maxOrder += 1
|
||||
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder))
|
||||
shelf.last_modified = datetime.utcnow()
|
||||
try:
|
||||
@ -322,8 +320,11 @@ def delete_shelf_helper(cur_shelf):
|
||||
ub.session.delete(cur_shelf)
|
||||
ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete()
|
||||
ub.session.add(ub.ShelfArchive(uuid=cur_shelf.uuid, user_id=cur_shelf.user_id))
|
||||
ub.session.commit()
|
||||
log.info("successfully deleted %s", cur_shelf)
|
||||
try:
|
||||
ub.session.commit()
|
||||
log.info("successfully deleted %s", cur_shelf)
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
|
||||
|
||||
|
||||
@ -333,44 +334,22 @@ def delete_shelf(shelf_id):
|
||||
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
try:
|
||||
delete_shelf_helper(cur_shelf)
|
||||
except (OperationalError, InvalidRequestError):
|
||||
except InvalidRequestError:
|
||||
ub.session.rollback()
|
||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
|
||||
@shelf.route("/shelf/<int:shelf_id>", defaults={'shelf_type': 1})
|
||||
@shelf.route("/shelf/<int:shelf_id>/<int:shelf_type>")
|
||||
@shelf.route("/simpleshelf/<int:shelf_id>")
|
||||
@login_required_if_no_ano
|
||||
def show_shelf(shelf_type, shelf_id):
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
def show_simpleshelf(shelf_id):
|
||||
return render_show_shelf(2, shelf_id, 1, None)
|
||||
|
||||
result = list()
|
||||
# user is allowed to access shelf
|
||||
if shelf and check_shelf_view_permissions(shelf):
|
||||
page = "shelf.html" if shelf_type == 1 else 'shelfdown.html'
|
||||
|
||||
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\
|
||||
.order_by(ub.BookShelf.order.asc()).all()
|
||||
for book in books_in_shelf:
|
||||
cur_book = calibre_db.get_filtered_book(book.book_id)
|
||||
if cur_book:
|
||||
result.append(cur_book)
|
||||
else:
|
||||
cur_book = calibre_db.get_book(book.book_id)
|
||||
if not cur_book:
|
||||
log.info('Not existing book %s in %s deleted', book.book_id, shelf)
|
||||
try:
|
||||
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete()
|
||||
ub.session.commit()
|
||||
except (OperationalError, InvalidRequestError):
|
||||
ub.session.rollback()
|
||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
||||
return render_title_template(page, entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name),
|
||||
shelf=shelf, page="shelf")
|
||||
else:
|
||||
flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error")
|
||||
return redirect(url_for("web.index"))
|
||||
@shelf.route("/shelf/<int:shelf_id>", defaults={"sort_param": "order", 'page': 1})
|
||||
@shelf.route("/shelf/<int:shelf_id>/<sort_param>", defaults={'page': 1})
|
||||
@shelf.route("/shelf/<int:shelf_id>/<sort_param>/<int:page>")
|
||||
@login_required_if_no_ano
|
||||
def show_shelf(shelf_id, sort_param, page):
|
||||
return render_show_shelf(1, shelf_id, page, sort_param)
|
||||
|
||||
|
||||
@shelf.route("/shelf/order/<int:shelf_id>", methods=["GET", "POST"])
|
||||
@ -394,22 +373,80 @@ def order_shelf(shelf_id):
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
result = list()
|
||||
if shelf and check_shelf_view_permissions(shelf):
|
||||
books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
|
||||
.order_by(ub.BookShelf.order.asc()).all()
|
||||
for book in books_in_shelf2:
|
||||
cur_book = calibre_db.get_filtered_book(book.book_id)
|
||||
if cur_book:
|
||||
result.append({'title': cur_book.title,
|
||||
'id': cur_book.id,
|
||||
'author': cur_book.authors,
|
||||
'series': cur_book.series,
|
||||
'series_index': cur_book.series_index})
|
||||
else:
|
||||
cur_book = calibre_db.get_book(book.book_id)
|
||||
result.append({'title': _('Hidden Book'),
|
||||
'id': cur_book.id,
|
||||
'author': [],
|
||||
'series': []})
|
||||
result = calibre_db.session.query(db.Books)\
|
||||
.join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id , isouter=True) \
|
||||
.add_columns(calibre_db.common_filters().label("visible")) \
|
||||
.filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all()
|
||||
return render_title_template('shelf_order.html', entries=result,
|
||||
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
|
||||
shelf=shelf, page="shelforder")
|
||||
|
||||
def change_shelf_order(shelf_id, order):
|
||||
result = calibre_db.session.query(db.Books).join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id)\
|
||||
.filter(ub.BookShelf.shelf == shelf_id).order_by(*order).all()
|
||||
for index, entry in enumerate(result):
|
||||
book = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
|
||||
.filter(ub.BookShelf.book_id == entry.id).first()
|
||||
book.order = index
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
|
||||
def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
|
||||
# check user is allowed to access shelf
|
||||
if shelf and check_shelf_view_permissions(shelf):
|
||||
|
||||
if shelf_type == 1:
|
||||
# order = [ub.BookShelf.order.asc()]
|
||||
if sort_param == 'pubnew':
|
||||
change_shelf_order(shelf_id, [db.Books.pubdate.desc()])
|
||||
if sort_param == 'pubold':
|
||||
change_shelf_order(shelf_id, [db.Books.pubdate])
|
||||
if sort_param == 'abc':
|
||||
change_shelf_order(shelf_id, [db.Books.sort])
|
||||
if sort_param == 'zyx':
|
||||
change_shelf_order(shelf_id, [db.Books.sort.desc()])
|
||||
if sort_param == 'new':
|
||||
change_shelf_order(shelf_id, [db.Books.timestamp.desc()])
|
||||
if sort_param == 'old':
|
||||
change_shelf_order(shelf_id, [db.Books.timestamp])
|
||||
if sort_param == 'authaz':
|
||||
change_shelf_order(shelf_id, [db.Books.author_sort.asc()])
|
||||
if sort_param == 'authza':
|
||||
change_shelf_order(shelf_id, [db.Books.author_sort.desc()])
|
||||
page = "shelf.html"
|
||||
pagesize = 0
|
||||
else:
|
||||
pagesize = sys.maxsize
|
||||
page = 'shelfdown.html'
|
||||
|
||||
result, __, pagination = calibre_db.fill_indexpage(page_no, pagesize,
|
||||
db.Books,
|
||||
ub.BookShelf.shelf == shelf_id,
|
||||
[ub.BookShelf.order.asc()],
|
||||
ub.BookShelf,ub.BookShelf.book_id == db.Books.id)
|
||||
# delete chelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web
|
||||
wrong_entries = calibre_db.session.query(ub.BookShelf)\
|
||||
.join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True)\
|
||||
.filter(db.Books.id == None).all()
|
||||
for entry in wrong_entries:
|
||||
log.info('Not existing book {} in {} deleted'.format(entry.book_id, shelf))
|
||||
try:
|
||||
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == entry.book_id).delete()
|
||||
ub.session.commit()
|
||||
except (OperationalError, InvalidRequestError):
|
||||
ub.session.rollback()
|
||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
||||
|
||||
return render_title_template(page,
|
||||
entries=result,
|
||||
pagination=pagination,
|
||||
title=_(u"Shelf: '%(name)s'", name=shelf.name),
|
||||
shelf=shelf,
|
||||
page="shelf")
|
||||
else:
|
||||
flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error")
|
||||
return redirect(url_for("web.index"))
|
||||
|
@ -240,7 +240,7 @@ body.blur .row-fluid .col-sm-10 {
|
||||
|
||||
.col-sm-10 .book-meta > div.btn-toolbar:after {
|
||||
content: '';
|
||||
direction: block;
|
||||
direction: ltr;
|
||||
position: fixed;
|
||||
top: 120px;
|
||||
right: 0;
|
||||
@ -398,20 +398,17 @@ body.blur .row-fluid .col-sm-10 {
|
||||
|
||||
.shelforder #sortTrue > div:hover {
|
||||
background-color: hsla(0, 0%, 100%, .06) !important;
|
||||
cursor: move;
|
||||
cursor: grab;
|
||||
cursor: -webkit-grab;
|
||||
color: #eee
|
||||
}
|
||||
|
||||
.shelforder #sortTrue > div:active {
|
||||
cursor: grabbing;
|
||||
cursor: -webkit-grabbing
|
||||
}
|
||||
|
||||
.shelforder #sortTrue > div:before {
|
||||
content: "\EA53";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
margin-right: 30px;
|
||||
margin-left: 15px;
|
||||
vertical-align: bottom;
|
||||
@ -446,7 +443,7 @@ body.blur .row-fluid .col-sm-10 {
|
||||
|
||||
body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
||||
content: "\e155";
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
@ -494,7 +491,7 @@ body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
||||
}
|
||||
|
||||
#have_read_cb + label:before, #have_read_cb:checked + label:before {
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
font-size: 16px;
|
||||
height: 40px;
|
||||
width: 60px;
|
||||
@ -550,13 +547,12 @@ body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
||||
height: 60px;
|
||||
width: 50px;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
margin-top: -4px;
|
||||
margin: -4px 0 0;
|
||||
}
|
||||
|
||||
#archived_cb + label:before, #archived_cb:checked + label:before {
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
font-size: 16px;
|
||||
height: 40px;
|
||||
width: 60px;
|
||||
@ -618,7 +614,7 @@ div[aria-label="Edit/Delete book"] > .btn > span {
|
||||
|
||||
div[aria-label="Edit/Delete book"] > .btn > span:before {
|
||||
content: "\EA5d";
|
||||
font-family: plex-icons;
|
||||
font-family: plex-icons, serif;
|
||||
font-size: 20px;
|
||||
padding: 16px 15px;
|
||||
display: inline-block;
|
||||
@ -641,7 +637,7 @@ div[aria-label="Edit/Delete book"] > .btn > span:hover {
|
||||
width: 225px;
|
||||
max-width: 225px;
|
||||
position: relative !important;
|
||||
left: auto !important;
|
||||
left: auto !important;
|
||||
top: auto !important;
|
||||
-webkit-transform: none !important;
|
||||
-ms-transform: none !important;
|
||||
@ -760,7 +756,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a
|
||||
|
||||
.home-btn {
|
||||
color: hsla(0, 0%, 100%, .7);
|
||||
line-height: 34.29px;
|
||||
line-height: 34px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
@ -770,7 +766,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a
|
||||
|
||||
.home-btn > a {
|
||||
color: rgba(255, 255, 255, .7);
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
line-height: 60px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
@ -800,7 +796,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.home-btn > a:hove
|
||||
|
||||
.glyphicon-search:before {
|
||||
content: "\EA4F";
|
||||
font-family: plex-icons
|
||||
font-family: plex-icons, serif
|
||||
}
|
||||
|
||||
#nav_about:after, .profileDrop > span:after, .profileDrop > span:before {
|
||||
@ -833,7 +829,7 @@ body:not(.read-frame) {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
/* scroll bar fix for firefox */
|
||||
scrollbar-color: hsla(0, 0%, 100%, .2) transparent;
|
||||
scrollbar-color: hsla(0, 0%, 100%, .2) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
@ -966,7 +962,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d
|
||||
|
||||
#form-upload .form-group .btn:before {
|
||||
content: "\e043";
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
@ -991,7 +987,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d
|
||||
#form-upload .form-group .btn:after {
|
||||
content: "\EA13";
|
||||
position: absolute;
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-size: 8px;
|
||||
background: #3c444a;
|
||||
color: hsla(0, 0%, 100%, .7);
|
||||
@ -1019,7 +1015,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d
|
||||
text-transform: none;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
line-height: 1;
|
||||
@ -1075,7 +1071,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d
|
||||
|
||||
body > div.navbar.navbar-default.navbar-static-top > div > form > div > span > button:before {
|
||||
content: "\EA32";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
color: #eee;
|
||||
background: #555;
|
||||
font-size: 10px;
|
||||
@ -1097,7 +1093,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form > div > span > b
|
||||
body > div.navbar.navbar-default.navbar-static-top > div > form:before {
|
||||
content: "\EA4F";
|
||||
display: block;
|
||||
font-family: plex-icons;
|
||||
font-family: plex-icons, serif;
|
||||
position: absolute;
|
||||
color: #eee;
|
||||
font-weight: 400;
|
||||
@ -1120,7 +1116,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form:before {
|
||||
body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > div > span.input-group-btn:before {
|
||||
content: "\EA4F";
|
||||
display: block;
|
||||
font-family: plex-icons;
|
||||
font-family: plex-icons, serif;
|
||||
position: absolute;
|
||||
left: -298px;
|
||||
top: 8px;
|
||||
@ -1193,7 +1189,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.c
|
||||
|
||||
body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul > li > #top_admin > .glyphicon-dashboard::before {
|
||||
content: "\EA31";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-size: 20px
|
||||
}
|
||||
|
||||
@ -1272,7 +1268,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover {
|
||||
user-select: none
|
||||
}
|
||||
|
||||
.navigation li, .navigation li:not(ul>li) {
|
||||
.navigation li, .navigation li:not(ul > li) {
|
||||
border-radius: 0 4px 4px 0
|
||||
}
|
||||
|
||||
@ -1352,32 +1348,32 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover {
|
||||
|
||||
#nav_hot .glyphicon-fire::before {
|
||||
content: "\1F525";
|
||||
font-family: glyphicons regular
|
||||
font-family: glyphicons regular, serif
|
||||
}
|
||||
|
||||
.glyphicon-star:before {
|
||||
content: "\EA10";
|
||||
font-family: plex-icons-new
|
||||
font-family: plex-icons-new, serif
|
||||
}
|
||||
|
||||
#nav_rand .glyphicon-random::before {
|
||||
content: "\EA44";
|
||||
font-family: plex-icons-new
|
||||
font-family: plex-icons-new, serif
|
||||
}
|
||||
|
||||
.glyphicon-list::before {
|
||||
content: "\EA4D";
|
||||
font-family: plex-icons-new
|
||||
font-family: plex-icons-new, serif
|
||||
}
|
||||
|
||||
#nav_about .glyphicon-info-sign::before {
|
||||
content: "\EA26";
|
||||
font-family: plex-icons-new
|
||||
font-family: plex-icons-new, serif
|
||||
}
|
||||
|
||||
#nav_cat .glyphicon-inbox::before, .glyphicon-tags::before {
|
||||
content: "\E067";
|
||||
font-family: Glyphicons Regular;
|
||||
font-family: Glyphicons Regular, serif;
|
||||
margin-left: 2px
|
||||
}
|
||||
|
||||
@ -1423,7 +1419,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover {
|
||||
|
||||
.navigation .create-shelf a:before {
|
||||
content: "\EA13";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-size: 100%;
|
||||
padding-right: 10px;
|
||||
vertical-align: middle
|
||||
@ -1473,7 +1469,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover {
|
||||
|
||||
#books > .cover > a:before, #books_rand > .cover > a:before, .book.isotope-item > .cover > a:before, body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 > div.col-sm-12 > div.col-sm-2 > a:before {
|
||||
content: "\e352";
|
||||
font-family: Glyphicons Regular;
|
||||
font-family: Glyphicons Regular, serif;
|
||||
background: var(--color-secondary);
|
||||
border-radius: 50%;
|
||||
font-weight: 400;
|
||||
@ -1521,8 +1517,8 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
background: -webkit-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||
background: -o-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||
background: -webkit-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||
background: -o-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||
background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||
z-index: -9
|
||||
}
|
||||
@ -1562,8 +1558,8 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
background: -webkit-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||
background: -o-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||
background: -webkit-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||
background: -o-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||
background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%)
|
||||
}
|
||||
|
||||
@ -1739,7 +1735,7 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 {
|
||||
|
||||
body.me > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
||||
content: '';
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 6vw;
|
||||
@ -1947,7 +1943,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.pagination .page-next > a
|
||||
body > div.container-fluid > div > div.col-sm-10 > div.pagination .page-previous > a
|
||||
{
|
||||
top: 0;
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-weight: 100;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
line-height: 60px;
|
||||
@ -2026,7 +2022,7 @@ body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container > di
|
||||
|
||||
body.serieslist > div.container-fluid > div > div.col-sm-10:before {
|
||||
content: "\e044";
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
@ -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 {
|
||||
content: "\E067";
|
||||
font-family: Glyphicons Regular;
|
||||
font-family: Glyphicons Regular, serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
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 {
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
@ -2250,7 +2246,7 @@ body.langlist > div.container-fluid > div > div.col-sm-10 > div.container:before
|
||||
|
||||
body.advsearch > div.container-fluid > div > div.col-sm-10 > div.col-md-10.col-lg-6 {
|
||||
padding: 15px 10px 15px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
body.advsearch > div.container-fluid > div > div.col-sm-10 > div.col-md-10.col-lg-6 {
|
||||
@ -2492,7 +2488,6 @@ body > div.container-fluid > div > div.col-sm-10 > div.col-sm-8 > form > .btn.bt
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
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 {
|
||||
content: "\EA4F";
|
||||
font-family: plex-icons;
|
||||
font-family: plex-icons, serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
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 {
|
||||
content: "\EA59";
|
||||
font-family: plex-icons;
|
||||
font-family: plex-icons, serif;
|
||||
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 {
|
||||
content: "\e352";
|
||||
font-family: Glyphicons Regular;
|
||||
font-family: Glyphicons Regular, serif;
|
||||
font-size: 18px;
|
||||
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 {
|
||||
font-size: 20px;
|
||||
content: "\ea66";
|
||||
font-family: plex-icons
|
||||
font-family: plex-icons, serif
|
||||
}
|
||||
|
||||
.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);
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, .35);
|
||||
-o-transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
||||
transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
||||
transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4), -webkit-transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
||||
-webkit-transform-origin: center top;
|
||||
-ms-transform-origin: center top;
|
||||
@ -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 {
|
||||
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 {
|
||||
@ -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 {
|
||||
content: "\EA6D";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
position: absolute;
|
||||
color: hsla(0, 0%, 100%, .7);
|
||||
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 {
|
||||
content: "\EA5d";
|
||||
font-family: plex-icons;
|
||||
font-family: plex-icons, serif;
|
||||
position: absolute;
|
||||
color: hsla(0, 0%, 100%, .7);
|
||||
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 {
|
||||
content: "\E409";
|
||||
font-family: Glyphicons Regular;
|
||||
font-family: Glyphicons Regular, serif;
|
||||
position: absolute;
|
||||
color: hsla(0, 0%, 100%, .7);
|
||||
font-size: 20px;
|
||||
@ -3752,7 +3746,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a
|
||||
|
||||
.plexBack > a {
|
||||
color: rgba(255, 255, 255, .7);
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
-webkit-font-variant-ligatures: normal;
|
||||
font-variant-ligatures: normal;
|
||||
line-height: 60px;
|
||||
@ -3864,11 +3858,9 @@ body.login > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.l
|
||||
-webkit-transform: translateY(-50%);
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
border-style: solid;
|
||||
vertical-align: middle;
|
||||
-webkit-transition: border .2s, -webkit-transform .4s;
|
||||
-o-transition: border .2s, transform .4s;
|
||||
transition: border .2s, transform .4s;
|
||||
transition: border .2s, transform .4s, -webkit-transform .4s;
|
||||
margin: 9px 6px
|
||||
}
|
||||
@ -3887,11 +3879,9 @@ body.login > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.l
|
||||
-webkit-transform: translateY(-50%);
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
border-style: solid;
|
||||
vertical-align: middle;
|
||||
-webkit-transition: border .2s, -webkit-transform .4s;
|
||||
-o-transition: border .2s, transform .4s;
|
||||
transition: border .2s, transform .4s;
|
||||
transition: border .2s, transform .4s, -webkit-transform .4s;
|
||||
margin: 12px 6px
|
||||
}
|
||||
@ -3971,7 +3961,7 @@ body.author img.bg-blur[src=undefined] {
|
||||
|
||||
body.author:not(.authorlist) .undefined-img:before {
|
||||
content: "\e008";
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
@ -4120,7 +4110,7 @@ body.shelf.modal-open > .container-fluid {
|
||||
font-size: 18px;
|
||||
color: #999;
|
||||
display: inline-block;
|
||||
font-family: Glyphicons Regular;
|
||||
font-family: Glyphicons Regular, serif;
|
||||
font-style: normal;
|
||||
font-weight: 400
|
||||
}
|
||||
@ -4221,7 +4211,7 @@ body.shelf.modal-open > .container-fluid {
|
||||
|
||||
#remove-from-shelves > .btn > span:before {
|
||||
content: "\EA52";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
color: transparent;
|
||||
padding-left: 5px
|
||||
}
|
||||
@ -4233,7 +4223,7 @@ body.shelf.modal-open > .container-fluid {
|
||||
|
||||
#remove-from-shelves > a:first-of-type:before {
|
||||
content: "\EA4D";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
position: absolute;
|
||||
color: hsla(0, 0%, 100%, .45);
|
||||
font-style: normal;
|
||||
@ -4273,7 +4263,7 @@ body.shelf.modal-open > .container-fluid {
|
||||
content: "\E208";
|
||||
padding-right: 10px;
|
||||
display: block;
|
||||
font-family: Glyphicons Regular;
|
||||
font-family: Glyphicons Regular, serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
position: absolute;
|
||||
@ -4284,7 +4274,6 @@ body.shelf.modal-open > .container-fluid {
|
||||
opacity: .5;
|
||||
-webkit-transition: -webkit-transform .3s ease-out;
|
||||
-o-transition: transform .3s ease-out;
|
||||
transition: transform .3s ease-out;
|
||||
transition: transform .3s ease-out, -webkit-transform .3s ease-out;
|
||||
-webkit-transform: translate(0, -60px);
|
||||
-ms-transform: translate(0, -60px);
|
||||
@ -4344,7 +4333,7 @@ body.advanced_search > div.container-fluid > div > div.col-sm-10 > div.col-sm-8
|
||||
|
||||
.glyphicon-remove:before {
|
||||
content: "\EA52";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-weight: 400
|
||||
}
|
||||
|
||||
@ -4430,7 +4419,7 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div
|
||||
|
||||
body:not(.blur) #nav_new:before {
|
||||
content: "\EA4F";
|
||||
font-family: plex-icons;
|
||||
font-family: plex-icons, serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
@ -4456,7 +4445,7 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div
|
||||
color: hsla(0, 0%, 100%, .7);
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-size: 20px;
|
||||
font-stretch: 100%;
|
||||
font-style: normal;
|
||||
@ -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 {
|
||||
border: collapse
|
||||
border: collapse;
|
||||
}
|
||||
|
||||
body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.newuser.admin > div.container-fluid > div.row-fluid > div.col-sm-10::before {
|
||||
content: '';
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 6vw;
|
||||
@ -4661,7 +4650,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
||||
content: "\e352";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
font-family: Glyphicons Regular;
|
||||
font-family: Glyphicons Regular, serif;
|
||||
background: var(--color-secondary);
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
@ -4699,8 +4688,8 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
background: -webkit-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||
background: -o-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||
background: -webkit-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||
background: -o-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||
background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%)
|
||||
}
|
||||
|
||||
@ -4752,7 +4741,7 @@ body.admin td > a:hover {
|
||||
|
||||
.glyphicon-ok::before {
|
||||
content: "\EA55";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-weight: 400
|
||||
}
|
||||
|
||||
@ -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-size: auto, auto, cover !important;
|
||||
-webkit-background-size: auto, auto, cover !important;
|
||||
-moz-background-size: autom, auto, cover !important;
|
||||
-moz-background-size: auto, auto, cover !important;
|
||||
-o-background-size: auto, auto, cover !important;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
@ -4887,7 +4876,6 @@ body.read:not(.blur) a[href*=readbooks] {
|
||||
.tooltip.in {
|
||||
opacity: 1;
|
||||
-o-transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
||||
transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
||||
transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4), -webkit-transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
||||
-webkit-transform: translate(0) scale(1);
|
||||
-ms-transform: translate(0) scale(1);
|
||||
@ -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 {
|
||||
content: "\EA6D";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-size: 18px;
|
||||
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 {
|
||||
content: "\EA58";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-weight: 400;
|
||||
right: 20px;
|
||||
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 {
|
||||
content: "\EA57";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-weight: 400;
|
||||
right: 20px;
|
||||
position: absolute
|
||||
@ -5143,7 +5131,7 @@ body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.boot
|
||||
|
||||
.epub-back:before {
|
||||
content: "\EA1C";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-weight: 400;
|
||||
color: #4f4f4f;
|
||||
position: absolute;
|
||||
@ -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 {
|
||||
content: "\EA6D";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-size: 18px;
|
||||
color: hsla(0, 0%, 100%, .7);
|
||||
vertical-align: super
|
||||
@ -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 {
|
||||
content: "\EA13";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-size: 100%;
|
||||
padding-right: 10px;
|
||||
vertical-align: middle
|
||||
@ -5511,7 +5499,7 @@ body.admin.modal-open .navbar {
|
||||
content: "\E208";
|
||||
padding-right: 10px;
|
||||
display: block;
|
||||
font-family: Glyphicons Regular;
|
||||
font-family: Glyphicons Regular, serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
position: absolute;
|
||||
@ -5522,7 +5510,6 @@ body.admin.modal-open .navbar {
|
||||
opacity: .5;
|
||||
-webkit-transition: -webkit-transform .3s ease-out;
|
||||
-o-transition: transform .3s ease-out;
|
||||
transition: transform .3s ease-out;
|
||||
transition: transform .3s ease-out, -webkit-transform .3s ease-out;
|
||||
-webkit-transform: translate(0, -60px);
|
||||
-ms-transform: translate(0, -60px);
|
||||
@ -5576,22 +5563,22 @@ body.admin.modal-open .navbar {
|
||||
|
||||
#RestartDialog > .modal-dialog > .modal-content > .modal-header:before {
|
||||
content: "\EA4F";
|
||||
font-family: plex-icons-new
|
||||
font-family: plex-icons-new, serif
|
||||
}
|
||||
|
||||
#ShutdownDialog > .modal-dialog > .modal-content > .modal-header:before {
|
||||
content: "\E064";
|
||||
font-family: glyphicons regular
|
||||
font-family: glyphicons regular, serif
|
||||
}
|
||||
|
||||
#StatusDialog > .modal-dialog > .modal-content > .modal-header:before {
|
||||
content: "\EA15";
|
||||
font-family: plex-icons-new
|
||||
font-family: plex-icons-new, serif
|
||||
}
|
||||
|
||||
#deleteModal > .modal-dialog > .modal-content > .modal-header:before {
|
||||
content: "\EA6D";
|
||||
font-family: plex-icons-new
|
||||
font-family: plex-icons-new, serif
|
||||
}
|
||||
|
||||
#RestartDialog > .modal-dialog > .modal-content > .modal-header:after {
|
||||
@ -5982,7 +5969,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
||||
|
||||
.home-btn {
|
||||
height: 48px;
|
||||
line-height: 28.29px;
|
||||
line-height: 28px;
|
||||
right: 10px;
|
||||
left: auto
|
||||
}
|
||||
@ -5994,7 +5981,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
||||
|
||||
.plexBack {
|
||||
height: 48px;
|
||||
line-height: 28.29px;
|
||||
line-height: 28px;
|
||||
left: 48px;
|
||||
display: none
|
||||
}
|
||||
@ -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 {
|
||||
content: "\EA33";
|
||||
display: block;
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
position: fixed;
|
||||
left: 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 {
|
||||
content: "\e043";
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
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 {
|
||||
content: "\EA13";
|
||||
position: absolute;
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-size: 8px;
|
||||
background: #3c444a;
|
||||
color: #fff;
|
||||
@ -6296,7 +6283,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
||||
}
|
||||
|
||||
#top_admin, #top_tasks {
|
||||
padding: 11.5px 15px;
|
||||
padding: 12px 15px;
|
||||
font-size: 13px;
|
||||
line-height: 1.71428571;
|
||||
overflow: hidden
|
||||
@ -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 {
|
||||
position: relative;
|
||||
top: 0;
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
line-height: 1;
|
||||
border-radius: 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 {
|
||||
text-transform: none;
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
@ -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 {
|
||||
content: "\e008";
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
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);
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
font-size: 20px;
|
||||
font-stretch: 100%;
|
||||
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%);
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
border-style: solid;
|
||||
vertical-align: middle;
|
||||
-webkit-transition: border .2s, -webkit-transform .4s;
|
||||
-o-transition: border .2s, transform .4s;
|
||||
transition: border .2s, transform .4s;
|
||||
transition: border .2s, transform .4s, -webkit-transform .4s;
|
||||
margin: 12px 6px
|
||||
}
|
||||
@ -7048,18 +7033,16 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
||||
-webkit-transform: translateY(-50%);
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
border-style: solid;
|
||||
vertical-align: middle;
|
||||
-webkit-transition: border .2s, -webkit-transform .4s;
|
||||
-o-transition: border .2s, transform .4s;
|
||||
transition: border .2s, transform .4s;
|
||||
transition: border .2s, transform .4s, -webkit-transform .4s;
|
||||
margin: 9px 6px
|
||||
}
|
||||
|
||||
body.author:not(.authorlist) .blur-wrapper:before, body.author > .container-fluid > .row-fluid > .col-sm-10 > h2:after {
|
||||
content: "\e008";
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
font-weight: 400;
|
||||
z-index: 9;
|
||||
line-height: 1;
|
||||
@ -7390,7 +7373,6 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
||||
transform: translate3d(0, 0, 0);
|
||||
-webkit-transition: -webkit-transform .5s;
|
||||
-o-transition: transform .5s;
|
||||
transition: transform .5s;
|
||||
transition: transform .5s, -webkit-transform .5s;
|
||||
z-index: 99
|
||||
}
|
||||
@ -7405,7 +7387,6 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
||||
transform: translate3d(-240px, 0, 0);
|
||||
-webkit-transition: -webkit-transform .5s;
|
||||
-o-transition: transform .5s;
|
||||
transition: transform .5s;
|
||||
transition: transform .5s, -webkit-transform .5s;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
@ -7444,7 +7425,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
||||
text-align: center;
|
||||
min-width: 40px;
|
||||
pointer-events: none;
|
||||
color: #
|
||||
// color: #
|
||||
}
|
||||
|
||||
.col-xs-12 > .row > .col-xs-10 {
|
||||
@ -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 {
|
||||
content: "\e241";
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
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 {
|
||||
content: "\e007";
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
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 {
|
||||
content: "\e022";
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-family: 'Glyphicons Halflings', serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
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 {
|
||||
content: "\EA6D";
|
||||
font-family: plex-icons-new
|
||||
font-family: plex-icons-new, serif
|
||||
}
|
||||
|
||||
#DeleteDomain {
|
||||
@ -7799,7 +7780,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic
|
||||
content: "\E208";
|
||||
padding-right: 10px;
|
||||
display: block;
|
||||
font-family: Glyphicons Regular;
|
||||
font-family: Glyphicons Regular, serif;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
position: absolute;
|
||||
@ -7810,7 +7791,6 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic
|
||||
opacity: .5;
|
||||
-webkit-transition: -webkit-transform .3s ease-out;
|
||||
-o-transition: transform .3s ease-out;
|
||||
transition: transform .3s ease-out;
|
||||
transition: transform .3s ease-out, -webkit-transform .3s ease-out;
|
||||
-webkit-transform: translate(0, -60px);
|
||||
-ms-transform: translate(0, -60px);
|
||||
@ -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 {
|
||||
content: "\EA6D";
|
||||
font-family: plex-icons-new;
|
||||
font-family: plex-icons-new, serif;
|
||||
padding-right: 10px;
|
||||
font-size: 18px;
|
||||
color: #999;
|
||||
|
@ -1,11 +1,11 @@
|
||||
body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cover .badge{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
color: #fff;
|
||||
background-color: #cc7b19;
|
||||
border-radius: 0;
|
||||
padding: 0 8px;
|
||||
@ -15,3 +15,8 @@ body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{
|
||||
.cover{
|
||||
box-shadow: 0 0 4px rgba(0,0,0,.6);
|
||||
}
|
||||
|
||||
.cover .read{
|
||||
padding: 0 0px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
6
cps/static/css/libs/bootstrap-select.min.css
vendored
Normal file
6
cps/static/css/libs/bootstrap-select.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -25,10 +25,9 @@ body {
|
||||
overflow: hidden;
|
||||
-webkit-transition: -webkit-transform 0.4s, width 0.2s;
|
||||
-moz-transition: -webkit-transform 0.4s, width 0.2s;
|
||||
-ms-transition: -webkit-transform 0.4s, width 0.2s;
|
||||
transition: -webkit-transform 0.4s, width 0.2s;
|
||||
-moz-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
|
||||
-webkit-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
|
||||
-ms-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@ -45,7 +44,7 @@ body {
|
||||
text-align: center;
|
||||
-webkit-transition: opacity 0.5s;
|
||||
-moz-transition: opacity 0.5s;
|
||||
-ms-transition: opacity 0.5s;
|
||||
transition: opacity 0.5s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@ -79,7 +78,6 @@ body {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
-moz-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
||||
-ms-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
||||
box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
||||
}
|
||||
|
||||
@ -121,7 +119,6 @@ body {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
@ -147,7 +144,7 @@ body {
|
||||
height: 100%;
|
||||
-webkit-transition: -webkit-transform 0.5s;
|
||||
-moz-transition: -moz-transform 0.5s;
|
||||
-ms-transition: -moz-transform 0.5s;
|
||||
transition: -moz-transform 0.5s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@ -183,7 +180,6 @@ body {
|
||||
height: 14px;
|
||||
-moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||
-ms-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
@ -232,7 +228,6 @@ body {
|
||||
|
||||
input::-webkit-input-placeholder { color: #454545; }
|
||||
input:-moz-placeholder { color: #454545; }
|
||||
input:-ms-placeholder { color: #454545; }
|
||||
|
||||
#divider {
|
||||
position: absolute;
|
||||
@ -268,18 +263,18 @@ input:-ms-placeholder { color: #454545; }
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
visibility: hidden;
|
||||
-webkit-transition: visibility 0 ease 0.5s;
|
||||
-moz-transition: visibility 0 ease 0.5s;
|
||||
-ms-transition: visibility 0 ease 0.5s;
|
||||
-webkit-transition: visibility 0s ease 0.5s;
|
||||
-moz-transition: visibility 0s ease 0.5s;
|
||||
transition: visibility 0s ease 0.5s;
|
||||
}
|
||||
|
||||
#sidebar.open #tocView,
|
||||
#sidebar.open #bookmarksView {
|
||||
overflow-y: auto;
|
||||
visibility: visible;
|
||||
-webkit-transition: visibility 0 ease 0;
|
||||
-moz-transition: visibility 0 ease 0;
|
||||
-ms-transition: visibility 0 ease 0;
|
||||
-webkit-transition: visibility 0s ease 0s;
|
||||
-moz-transition: visibility 0s ease 0s;
|
||||
transition: visibility 0s ease 0s;
|
||||
}
|
||||
|
||||
#sidebar.open #tocView {
|
||||
@ -495,9 +490,8 @@ input:-ms-placeholder { color: #454545; }
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
// width: 50%;
|
||||
width: 630px;
|
||||
|
||||
height: auto;
|
||||
z-index: 2000;
|
||||
visibility: hidden;
|
||||
@ -518,7 +512,6 @@ input:-ms-placeholder { color: #454545; }
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
-webkit-transition: all 0.3s;
|
||||
-moz-transition: all 0.3s;
|
||||
-ms-transition: all 0.3s;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
@ -588,7 +581,6 @@ input:-ms-placeholder { color: #454545; }
|
||||
opacity: 0;
|
||||
-webkit-transition: all 0.3s;
|
||||
-moz-transition: all 0.3s;
|
||||
-ms-transition: all 0.3s;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
@ -601,7 +593,7 @@ input:-ms-placeholder { color: #454545; }
|
||||
}
|
||||
|
||||
.md-content > .closer {
|
||||
font-size: 18px;
|
||||
//font-size: 18px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
@ -663,7 +655,7 @@ input:-ms-placeholder { color: #454545; }
|
||||
-ms-transform: translate(0, 0);
|
||||
-webkit-transition: -webkit-transform .3s;
|
||||
-moz-transition: -moz-transform .3s;
|
||||
-ms-transition: -moz-transform .3s;
|
||||
transition: -moz-transform .3s;
|
||||
}
|
||||
|
||||
#main.closed {
|
||||
@ -778,7 +770,7 @@ and (orientation : landscape)
|
||||
}
|
||||
|
||||
[class^="icon-"]:before, [class*=" icon-"]:before {
|
||||
font-family: "fontello";
|
||||
font-family: "fontello", serif;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
speak: none;
|
||||
|
@ -116,6 +116,7 @@ a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.container-fluid .discover{ margin-bottom: 50px; }
|
||||
@ -132,12 +133,19 @@ a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.container-fluid .book .cover img {
|
||||
.container-fluid .book .cover span.img {
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.container-fluid .book .cover span img {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
border: 1px solid #fff;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
-webkit-box-shadow: 0 5px 8px -6px #777;
|
||||
-moz-box-shadow: 0 5px 8px -6px #777;
|
||||
box-shadow: 0 5px 8px -6px #777;
|
||||
@ -206,11 +214,22 @@ span.glyphicon.glyphicon-tags {
|
||||
.navbar-default .navbar-toggle .icon-bar {background-color: #000; }
|
||||
.navbar-default .navbar-toggle {border-color: #000; }
|
||||
.cover { margin-bottom: 10px; }
|
||||
|
||||
.cover .badge{
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
background-color: #777;
|
||||
color: #000;
|
||||
border-radius: 10px;
|
||||
background-color: #fff;
|
||||
}
|
||||
.cover .read{
|
||||
left: auto;
|
||||
right: 2px;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
}
|
||||
.cover-height { max-height: 100px;}
|
||||
|
||||
@ -241,7 +260,7 @@ span.glyphicon.glyphicon-tags {
|
||||
.button-link {color: #fff; }
|
||||
.btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open .dropdown-toggle.btn-primary{ background-color: #1C5484; }
|
||||
.btn-primary.disabled, .btn-primary[disabled], fieldset[disabled] .btn-primary, .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled:active, .btn-primary[disabled]:active, fieldset[disabled] .btn-primary:active, .btn-primary.disabled.active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary.active { background-color: #89B9E2; }
|
||||
.btn-toolbar>.btn+.btn, .btn-toolbar>.btn-group+.btn, .btn-toolbar>.btn+.btn-group, .btn-toolbar>.btn-group+.btn-group { margin-left: 0px; }
|
||||
.btn-toolbar>.btn+.btn, .btn-toolbar>.btn-group+.btn, .btn-toolbar>.btn+.btn-group, .btn-toolbar>.btn-group+.btn-group { margin-left: 0; }
|
||||
.panel-body {background-color: #f5f5f5; }
|
||||
.spinner {margin: 0 41%; }
|
||||
.spinner2 {margin: 0 41%; }
|
||||
@ -311,11 +330,11 @@ input.pill:not(:checked) + label .glyphicon { display: none; }
|
||||
.editable-input { display:inline-block; }
|
||||
|
||||
.editable-cancel {
|
||||
margin-bottom: 0px !important;
|
||||
margin-bottom: 0 !important;
|
||||
margin-left: 7px !important;
|
||||
}
|
||||
|
||||
.editable-submit { margin-bottom: 0px !important; }
|
||||
.editable-submit { margin-bottom: 0 !important; }
|
||||
.filterheader { margin-bottom: 20px; }
|
||||
.errorlink { margin-top: 20px; }
|
||||
.emailconfig { margin-top: 10px; }
|
||||
@ -326,7 +345,7 @@ input.pill:not(:checked) + label .glyphicon { display: none; }
|
||||
}
|
||||
|
||||
div.log {
|
||||
font-family: Courier New;
|
||||
font-family: Courier New, serif;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
height: 700px;
|
||||
|
@ -249,18 +249,26 @@ promisePublishers.done(function() {
|
||||
);
|
||||
});
|
||||
|
||||
$("#search").on("change input.typeahead:selected", function() {
|
||||
$("#search").on("change input.typeahead:selected", function(event) {
|
||||
if (event.target.type == "search" && event.target.tagName == "INPUT") {
|
||||
return;
|
||||
}
|
||||
var form = $("form").serialize();
|
||||
$.getJSON( getPath() + "/get_matching_tags", form, function( data ) {
|
||||
$(".tags_click").each(function() {
|
||||
if ($.inArray(parseInt($(this).children("input").first().val(), 10), data.tags) === -1 ) {
|
||||
if (!($(this).hasClass("active"))) {
|
||||
$(this).addClass("disabled");
|
||||
if ($.inArray(parseInt($(this).val(), 10), data.tags) === -1) {
|
||||
if(!$(this).prop("selected")) {
|
||||
$(this).prop("disabled", true);
|
||||
}
|
||||
} else {
|
||||
$(this).removeClass("disabled");
|
||||
$(this).prop("disabled", false);
|
||||
}
|
||||
});
|
||||
$("#include_tag option:selected").each(function () {
|
||||
$("#exclude_tag").find("[value="+$(this).val()+"]").prop("disabled", true);
|
||||
});
|
||||
$('#include_tag').selectpicker("refresh");
|
||||
$('#exclude_tag').selectpicker("refresh");
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -19,16 +19,9 @@ var direction = 0; // Descending order
|
||||
var sort = 0; // Show sorted entries
|
||||
|
||||
$("#sort_name").click(function() {
|
||||
var class_name = $("h1").attr('Class') + "_sort_name";
|
||||
var className = $("h1").attr("Class") + "_sort_name";
|
||||
var obj = {};
|
||||
obj[class_name] = sort;
|
||||
/*$.ajax({
|
||||
method:"post",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
url: window.location.pathname + "/../../ajax/view",
|
||||
data: JSON.stringify({obj}),
|
||||
});*/
|
||||
obj[className] = sort;
|
||||
|
||||
var count = 0;
|
||||
var index = 0;
|
||||
|
9
cps/static/js/libs/bootstrap-select.min.js
vendored
Normal file
9
cps/static/js/libs/bootstrap-select.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
8
cps/static/js/libs/bootstrap-select/defaults-cs.min.js
vendored
Normal file
8
cps/static/js/libs/bootstrap-select/defaults-cs.min.js
vendored
Normal 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"}});
|
8
cps/static/js/libs/bootstrap-select/defaults-de.min.js
vendored
Normal file
8
cps/static/js/libs/bootstrap-select/defaults-de.min.js
vendored
Normal 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:", "}});
|
8
cps/static/js/libs/bootstrap-select/defaults-es.min.js
vendored
Normal file
8
cps/static/js/libs/bootstrap-select/defaults-es.min.js
vendored
Normal 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"}});
|
8
cps/static/js/libs/bootstrap-select/defaults-fi.min.js
vendored
Normal file
8
cps/static/js/libs/bootstrap-select/defaults-fi.min.js
vendored
Normal 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:", "}});
|
8
cps/static/js/libs/bootstrap-select/defaults-fr.min.js
vendored
Normal file
8
cps/static/js/libs/bootstrap-select/defaults-fr.min.js
vendored
Normal 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"}});
|
8
cps/static/js/libs/bootstrap-select/defaults-hu.min.js
vendored
Normal file
8
cps/static/js/libs/bootstrap-select/defaults-hu.min.js
vendored
Normal 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:", "}});
|
8
cps/static/js/libs/bootstrap-select/defaults-it.min.js
vendored
Normal file
8
cps/static/js/libs/bootstrap-select/defaults-it.min.js
vendored
Normal 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"}});
|
8
cps/static/js/libs/bootstrap-select/defaults-ja.min.js
vendored
Normal file
8
cps/static/js/libs/bootstrap-select/defaults-ja.min.js
vendored
Normal 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:", "}});
|
8
cps/static/js/libs/bootstrap-select/defaults-km.min.js
vendored
Normal file
8
cps/static/js/libs/bootstrap-select/defaults-km.min.js
vendored
Normal 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:", "}});
|
8
cps/static/js/libs/bootstrap-select/defaults-nl.min.js
vendored
Normal file
8
cps/static/js/libs/bootstrap-select/defaults-nl.min.js
vendored
Normal 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:", "}});
|
8
cps/static/js/libs/bootstrap-select/defaults-pl.min.js
vendored
Normal file
8
cps/static/js/libs/bootstrap-select/defaults-pl.min.js
vendored
Normal 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:", "}});
|
8
cps/static/js/libs/bootstrap-select/defaults-ru.min.js
vendored
Normal file
8
cps/static/js/libs/bootstrap-select/defaults-ru.min.js
vendored
Normal 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:", "}});
|
8
cps/static/js/libs/bootstrap-select/defaults-sv.min.js
vendored
Normal file
8
cps/static/js/libs/bootstrap-select/defaults-sv.min.js
vendored
Normal 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:", "}});
|
8
cps/static/js/libs/bootstrap-select/defaults-tr.min.js
vendored
Normal file
8
cps/static/js/libs/bootstrap-select/defaults-tr.min.js
vendored
Normal 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:", "}});
|
8
cps/static/js/libs/bootstrap-select/defaults-zh_Hans_CN.min.js
vendored
Normal file
8
cps/static/js/libs/bootstrap-select/defaults-zh_Hans_CN.min.js
vendored
Normal 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"}});
|
@ -110,6 +110,34 @@ $(document).ready(function() {
|
||||
}
|
||||
});
|
||||
|
||||
function ConfirmDialog(id, dataValue, yesFn, noFn) {
|
||||
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
|
||||
var path = src.substring(0, src.lastIndexOf("/"));
|
||||
var $confirm = $("#GeneralDeleteModal");
|
||||
// var dataValue= e.data('value'); // target.data('value');
|
||||
$confirm.modal('show');
|
||||
$.ajax({
|
||||
method:"get",
|
||||
dataType: "json",
|
||||
url: path + "/../../ajax/loaddialogtexts/" + id,
|
||||
success: function success(data) {
|
||||
$("#header").html(data.header);
|
||||
$("#text").html(data.main);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$("#btnConfirmYes").off('click').click(function () {
|
||||
yesFn(dataValue);
|
||||
$confirm.modal("hide");
|
||||
});
|
||||
$("#btnConfirmNo").off('click').click(function () {
|
||||
if (typeof noFn !== 'undefined') {
|
||||
noFn(dataValue);
|
||||
}
|
||||
$confirm.modal("hide");
|
||||
});
|
||||
}
|
||||
|
||||
$("#delete_confirm").click(function() {
|
||||
//get data-id attribute of the clicked element
|
||||
@ -213,6 +241,56 @@ $(function() {
|
||||
});
|
||||
}
|
||||
|
||||
function fillFileTable(path, type, folder, filt) {
|
||||
if (window.location.pathname.endsWith("/basicconfig")) {
|
||||
var request_path = "/../basicconfig/pathchooser/";
|
||||
} else {
|
||||
var request_path = "/../../ajax/pathchooser/";
|
||||
}
|
||||
$.ajax({
|
||||
dataType: "json",
|
||||
data: {
|
||||
path: path,
|
||||
folder: folder,
|
||||
filter: filt
|
||||
},
|
||||
url: window.location.pathname + request_path,
|
||||
success: function success(data) {
|
||||
if ($("#element_selected").text() ==="") {
|
||||
$("#element_selected").text(data.cwd);
|
||||
}
|
||||
$("#file_table > tbody > tr").each(function () {
|
||||
if ($(this).attr("id") !== "parent") {
|
||||
$(this).closest("tr").remove();
|
||||
} else {
|
||||
if(data.absolute && data.parentdir !== "") {
|
||||
$(this)[0].attributes['data-path'].value = data.parentdir;
|
||||
} else {
|
||||
$(this)[0].attributes['data-path'].value = "..";
|
||||
}
|
||||
}
|
||||
});
|
||||
if (data.parentdir !== "") {
|
||||
$("#parent").removeClass('hidden')
|
||||
} else {
|
||||
$("#parent").addClass('hidden')
|
||||
}
|
||||
// console.log(data);
|
||||
data.files.forEach(function(entry) {
|
||||
if(entry.type === "dir") {
|
||||
var type = "<span class=\"glyphicon glyphicon-folder-close\"></span>";
|
||||
} else {
|
||||
var type = "";
|
||||
}
|
||||
$("<tr class=\"tr-clickable\" data-type=\"" + entry.type + "\" data-path=\"" +
|
||||
entry.fullpath + "\"><td>" + type + "</td><td>" + entry.name + "</td><td>" +
|
||||
entry.size + "</td></tr>").appendTo($("#file_table"));
|
||||
});
|
||||
},
|
||||
timeout: 2000
|
||||
});
|
||||
}
|
||||
|
||||
$(".discover .row").isotope({
|
||||
// options
|
||||
itemSelector : ".book",
|
||||
@ -402,18 +480,98 @@ $(function() {
|
||||
$("#config_delete_kobo_token").show();
|
||||
});
|
||||
|
||||
$("#btndeletetoken").click(function() {
|
||||
//get data-id attribute of the clicked element
|
||||
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
|
||||
var path = src.substring(0, src.lastIndexOf("/"));
|
||||
// var domainId = $(this).value("domainId");
|
||||
$.ajax({
|
||||
method:"get",
|
||||
url: path + "/../../kobo_auth/deleteauthtoken/" + this.value,
|
||||
});
|
||||
$("#modalDeleteToken").modal("hide");
|
||||
$("#config_delete_kobo_token").hide();
|
||||
$("#config_delete_kobo_token").click(function() {
|
||||
ConfirmDialog(
|
||||
$(this).attr('id'),
|
||||
$(this).data('value'),
|
||||
function (value) {
|
||||
var pathname = document.getElementsByTagName("script");
|
||||
var src = pathname[pathname.length - 1].src;
|
||||
var path = src.substring(0, src.lastIndexOf("/"));
|
||||
$.ajax({
|
||||
method: "get",
|
||||
url: path + "/../../kobo_auth/deleteauthtoken/" + value,
|
||||
});
|
||||
$("#config_delete_kobo_token").hide();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#toggle_order_shelf").click(function() {
|
||||
$("#new").toggleClass("disabled");
|
||||
$("#old").toggleClass("disabled");
|
||||
$("#asc").toggleClass("disabled");
|
||||
$("#desc").toggleClass("disabled");
|
||||
$("#auth_az").toggleClass("disabled");
|
||||
$("#auth_za").toggleClass("disabled");
|
||||
$("#pub_new").toggleClass("disabled");
|
||||
$("#pub_old").toggleClass("disabled");
|
||||
var alternative_text = $("#toggle_order_shelf").data('alt-text');
|
||||
$("#toggle_order_shelf")[0].attributes['data-alt-text'].value = $("#toggle_order_shelf").html();
|
||||
$("#toggle_order_shelf").html(alternative_text);
|
||||
});
|
||||
|
||||
$("#btndeluser").click(function() {
|
||||
ConfirmDialog(
|
||||
$(this).attr('id'),
|
||||
$(this).data('value'),
|
||||
function(value){
|
||||
var subform = $('#user_submit').closest("form");
|
||||
subform.submit(function(eventObj) {
|
||||
$(this).append('<input type="hidden" name="delete" value="True" />');
|
||||
return true;
|
||||
});
|
||||
subform.submit();
|
||||
}
|
||||
);
|
||||
});
|
||||
$("#user_submit").click(function() {
|
||||
this.closest("form").submit();
|
||||
});
|
||||
|
||||
$("#delete_shelf").click(function() {
|
||||
ConfirmDialog(
|
||||
$(this).attr('id'),
|
||||
$(this).data('value'),
|
||||
function(value){
|
||||
window.location.href = window.location.pathname + "/../../shelf/delete/" + value
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
|
||||
$("#fileModal").on("show.bs.modal", function(e) {
|
||||
var target = $(e.relatedTarget);
|
||||
var path = $("#" + target.data("link"))[0].value;
|
||||
var folder = target.data("folderonly");
|
||||
var filter = target.data("filefilter");
|
||||
$("#element_selected").text(path);
|
||||
$("#file_confirm")[0].attributes["data-link"].value = target.data("link");
|
||||
$("#file_confirm")[0].attributes["data-folderonly"].value = (typeof folder === 'undefined') ? false : true;
|
||||
$("#file_confirm")[0].attributes["data-filefilter"].value = (typeof filter === 'undefined') ? "" : filter;
|
||||
$("#file_confirm")[0].attributes["data-newfile"].value = target.data("newfile");
|
||||
fillFileTable(path,"dir", folder, filter);
|
||||
});
|
||||
|
||||
$("#file_confirm").click(function() {
|
||||
$("#" + $(this).data("link"))[0].value = $("#element_selected").text()
|
||||
});
|
||||
|
||||
$(document).on("click", ".tr-clickable", function() {
|
||||
var path = this.attributes["data-path"].value;
|
||||
var type = this.attributes["data-type"].value;
|
||||
var folder = $(file_confirm).data("folderonly");
|
||||
var filter = $(file_confirm).data("filefilter");
|
||||
var newfile = $(file_confirm).data("newfile");
|
||||
if (newfile !== 'undefined') {
|
||||
$("#element_selected").text(path + $("#new_file".text()));
|
||||
} else {
|
||||
$("#element_selected").text(path);
|
||||
}
|
||||
if(type === "dir") {
|
||||
fillFileTable(path, type, folder, filter);
|
||||
}
|
||||
});
|
||||
|
||||
$(window).resize(function() {
|
||||
|
@ -45,14 +45,13 @@ $(function() {
|
||||
if (selections.length < 1) {
|
||||
$("#delete_selection").addClass("disabled");
|
||||
$("#delete_selection").attr("aria-disabled", true);
|
||||
}
|
||||
else{
|
||||
} else {
|
||||
$("#delete_selection").removeClass("disabled");
|
||||
$("#delete_selection").attr("aria-disabled", false);
|
||||
}
|
||||
});
|
||||
$("#delete_selection").click(function() {
|
||||
$("#books-table").bootstrapTable('uncheckAll');
|
||||
$("#books-table").bootstrapTable("uncheckAll");
|
||||
});
|
||||
|
||||
$("#merge_confirm").click(function() {
|
||||
@ -63,8 +62,8 @@ $(function() {
|
||||
url: window.location.pathname + "/../../ajax/mergebooks",
|
||||
data: JSON.stringify({"Merge_books":selections}),
|
||||
success: function success() {
|
||||
$('#books-table').bootstrapTable('refresh');
|
||||
$("#books-table").bootstrapTable('uncheckAll');
|
||||
$("#books-table").bootstrapTable("refresh");
|
||||
$("#books-table").bootstrapTable("uncheckAll");
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -76,11 +75,11 @@ $(function() {
|
||||
dataType: "json",
|
||||
url: window.location.pathname + "/../../ajax/simulatemerge",
|
||||
data: JSON.stringify({"Merge_books":selections}),
|
||||
success: function success(book_titles) {
|
||||
$.each(book_titles.from, function(i, item) {
|
||||
success: function success(booTitles) {
|
||||
$.each(booTitles.from, function(i, item) {
|
||||
$("<span>- " + item + "</span>").appendTo("#merge_from");
|
||||
});
|
||||
$('#merge_to').text("- " + book_titles.to);
|
||||
$("#merge_to").text("- " + booTitles.to);
|
||||
|
||||
}
|
||||
});
|
||||
@ -126,34 +125,35 @@ $(function() {
|
||||
formatNoMatches: function () {
|
||||
return "";
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
onEditableSave: function (field, row, oldvalue, $el) {
|
||||
if (field === 'title' || field === 'authors') {
|
||||
$.ajax({
|
||||
method:"get",
|
||||
dataType: "json",
|
||||
url: window.location.pathname + "/../../ajax/sort_value/" + field + '/' + row.id,
|
||||
success: function success(data) {
|
||||
var key = Object.keys(data)[0]
|
||||
$("#books-table").bootstrapTable('updateCellByUniqueId', {
|
||||
id: row.id,
|
||||
field: key,
|
||||
value: data[key]
|
||||
});
|
||||
console.log(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (field === "title" || field === "authors") {
|
||||
$.ajax({
|
||||
method:"get",
|
||||
dataType: "json",
|
||||
url: window.location.pathname + "/../../ajax/sort_value/" + field + "/" + row.id,
|
||||
success: function success(data) {
|
||||
var key = Object.keys(data)[0];
|
||||
$("#books-table").bootstrapTable("updateCellByUniqueId", {
|
||||
id: row.id,
|
||||
field: key,
|
||||
value: data[key]
|
||||
});
|
||||
// console.log(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
onColumnSwitch: function (field, checked) {
|
||||
var visible = $("#books-table").bootstrapTable('getVisibleColumns');
|
||||
var hidden = $("#books-table").bootstrapTable('getHiddenColumns');
|
||||
var visibility =[]
|
||||
var st = ""
|
||||
var visible = $("#books-table").bootstrapTable("getVisibleColumns");
|
||||
var hidden = $("#books-table").bootstrapTable("getHiddenColumns");
|
||||
var st = "";
|
||||
visible.forEach(function(item) {
|
||||
st += "\""+ item.field + "\":\"" +"true"+ "\","
|
||||
st += "\"" + item.field + "\":\"" + "true" + "\",";
|
||||
});
|
||||
hidden.forEach(function(item) {
|
||||
st += "\""+ item.field + "\":\"" +"false"+ "\","
|
||||
st += "\"" + item.field + "\":\"" + "false" + "\",";
|
||||
});
|
||||
st = st.slice(0, -1);
|
||||
$.ajax({
|
||||
@ -208,15 +208,13 @@ $(function() {
|
||||
},
|
||||
striped: false
|
||||
});
|
||||
$("#btndeletedomain").click(function() {
|
||||
//get data-id attribute of the clicked element
|
||||
var domainId = $(this).data("domainId");
|
||||
|
||||
function domain_handle(domainId) {
|
||||
$.ajax({
|
||||
method:"post",
|
||||
url: window.location.pathname + "/../../ajax/deletedomain",
|
||||
data: {"domainid":domainId}
|
||||
});
|
||||
$("#DeleteDomain").modal("hide");
|
||||
$.ajax({
|
||||
method:"get",
|
||||
url: window.location.pathname + "/../../ajax/domainlist/1",
|
||||
@ -235,12 +233,16 @@ $(function() {
|
||||
$("#domain-deny-table").bootstrapTable("load", data);
|
||||
}
|
||||
});
|
||||
}
|
||||
$("#domain-allow-table").on("click-cell.bs.table", function (field, value, row, $element) {
|
||||
if (value === 2) {
|
||||
ConfirmDialog("btndeletedomain", $element.id, domain_handle);
|
||||
}
|
||||
});
|
||||
//triggered when modal is about to be shown
|
||||
$("#DeleteDomain").on("show.bs.modal", function(e) {
|
||||
//get data-id attribute of the clicked element and store in button
|
||||
var domainId = $(e.relatedTarget).data("domain-id");
|
||||
$(e.currentTarget).find("#btndeletedomain").data("domainId", domainId);
|
||||
$("#domain-deny-table").on("click-cell.bs.table", function (field, value, row, $element) {
|
||||
if (value === 2) {
|
||||
ConfirmDialog("btndeletedomain", $element.id, domain_handle);
|
||||
}
|
||||
});
|
||||
|
||||
$("#restrictModal").on("hidden.bs.modal", function () {
|
||||
@ -253,14 +255,14 @@ $(function() {
|
||||
$("#h3").addClass("hidden");
|
||||
$("#h4").addClass("hidden");
|
||||
});
|
||||
function startTable(type) {
|
||||
function startTable(type, user_id) {
|
||||
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
|
||||
var path = src.substring(0, src.lastIndexOf("/"));
|
||||
$("#restrict-elements-table").bootstrapTable({
|
||||
formatNoMatches: function () {
|
||||
return "";
|
||||
},
|
||||
url: path + "/../../ajax/listrestriction/" + type,
|
||||
url: path + "/../../ajax/listrestriction/" + type + "/" + user_id,
|
||||
rowStyle: function(row) {
|
||||
// console.log('Reihe :' + row + " Index :" + index);
|
||||
if (row.id.charAt(0) === "a") {
|
||||
@ -274,13 +276,13 @@ $(function() {
|
||||
$.ajax ({
|
||||
type: "Post",
|
||||
data: "id=" + row.id + "&type=" + row.type + "&Element=" + encodeURIComponent(row.Element),
|
||||
url: path + "/../../ajax/deleterestriction/" + type,
|
||||
url: path + "/../../ajax/deleterestriction/" + type + "/" + user_id,
|
||||
async: true,
|
||||
timeout: 900,
|
||||
success:function() {
|
||||
$.ajax({
|
||||
method:"get",
|
||||
url: path + "/../../ajax/listrestriction/" + type,
|
||||
url: path + "/../../ajax/listrestriction/" + type + "/" + user_id,
|
||||
async: true,
|
||||
timeout: 900,
|
||||
success:function(data) {
|
||||
@ -296,7 +298,7 @@ $(function() {
|
||||
$("#restrict-elements-table").removeClass("table-hover");
|
||||
$("#restrict-elements-table").on("editable-save.bs.table", function (e, field, row) {
|
||||
$.ajax({
|
||||
url: path + "/../../ajax/editrestriction/" + type,
|
||||
url: path + "/../../ajax/editrestriction/" + type + "/" + user_id,
|
||||
type: "Post",
|
||||
data: row
|
||||
});
|
||||
@ -304,13 +306,13 @@ $(function() {
|
||||
$("[id^=submit_]").click(function() {
|
||||
$(this)[0].blur();
|
||||
$.ajax({
|
||||
url: path + "/../../ajax/addrestriction/" + type,
|
||||
url: path + "/../../ajax/addrestriction/" + type + "/" + user_id,
|
||||
type: "Post",
|
||||
data: $(this).closest("form").serialize() + "&" + $(this)[0].name + "=",
|
||||
success: function () {
|
||||
$.ajax ({
|
||||
method:"get",
|
||||
url: path + "/../../ajax/listrestriction/" + type,
|
||||
url: path + "/../../ajax/listrestriction/" + type + "/" + user_id,
|
||||
async: true,
|
||||
timeout: 900,
|
||||
success:function(data) {
|
||||
@ -323,21 +325,21 @@ $(function() {
|
||||
});
|
||||
}
|
||||
$("#get_column_values").on("click", function() {
|
||||
startTable(1);
|
||||
startTable(1, 0);
|
||||
$("#h2").removeClass("hidden");
|
||||
});
|
||||
|
||||
$("#get_tags").on("click", function() {
|
||||
startTable(0);
|
||||
startTable(0, 0);
|
||||
$("#h1").removeClass("hidden");
|
||||
});
|
||||
$("#get_user_column_values").on("click", function() {
|
||||
startTable(3);
|
||||
startTable(3, $(this).data('id'));
|
||||
$("#h4").removeClass("hidden");
|
||||
});
|
||||
|
||||
$("#get_user_tags").on("click", function() {
|
||||
startTable(2);
|
||||
startTable(2, $(this).data('id'));
|
||||
$(this)[0].blur();
|
||||
$("#h3").removeClass("hidden");
|
||||
});
|
||||
@ -347,7 +349,7 @@ $(function() {
|
||||
/* Function for deleting domain restrictions */
|
||||
function TableActions (value, row) {
|
||||
return [
|
||||
"<a class=\"danger remove\" data-toggle=\"modal\" data-target=\"#DeleteDomain\" data-domain-id=\"" + row.id
|
||||
"<a class=\"danger remove\" data-value=\"" + row.id
|
||||
+ "\" title=\"Remove\">",
|
||||
"<i class=\"glyphicon glyphicon-trash\"></i>",
|
||||
"</a>"
|
||||
|
@ -9,7 +9,7 @@ from shutil import copyfile
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from cps.services.worker import CalibreTask
|
||||
from cps import calibre_db, db
|
||||
from cps import db
|
||||
from cps import logger, config
|
||||
from cps.subproc_wrapper import process_open
|
||||
from flask_babel import gettext as _
|
||||
@ -33,8 +33,9 @@ class TaskConvert(CalibreTask):
|
||||
def run(self, worker_thread):
|
||||
self.worker_thread = worker_thread
|
||||
if config.config_use_google_drive:
|
||||
cur_book = calibre_db.get_book(self.bookid)
|
||||
data = calibre_db.get_book_format(self.bookid, self.settings['old_book_format'])
|
||||
worker_db = db.CalibreDB(expire_on_commit=False)
|
||||
cur_book = worker_db.get_book(self.bookid)
|
||||
data = worker_db.get_book_format(self.bookid, self.settings['old_book_format'])
|
||||
df = gdriveutils.getFileFromEbooksFolder(cur_book.path,
|
||||
data.name + "." + self.settings['old_book_format'].lower())
|
||||
if df:
|
||||
@ -44,10 +45,12 @@ class TaskConvert(CalibreTask):
|
||||
if not os.path.exists(os.path.join(config.config_calibre_dir, cur_book.path)):
|
||||
os.makedirs(os.path.join(config.config_calibre_dir, cur_book.path))
|
||||
df.GetContentFile(datafile)
|
||||
worker_db.session.close()
|
||||
else:
|
||||
error_message = _(u"%(format)s not found on Google Drive: %(fn)s",
|
||||
format=self.settings['old_book_format'],
|
||||
fn=data.name + "." + self.settings['old_book_format'].lower())
|
||||
worker_db.session.close()
|
||||
return error_message
|
||||
|
||||
filename = self._convert_ebook_format()
|
||||
@ -71,21 +74,23 @@ class TaskConvert(CalibreTask):
|
||||
|
||||
def _convert_ebook_format(self):
|
||||
error_message = None
|
||||
local_session = db.CalibreDB().session
|
||||
local_db = db.CalibreDB(expire_on_commit=False)
|
||||
file_path = self.file_path
|
||||
book_id = self.bookid
|
||||
format_old_ext = u'.' + self.settings['old_book_format'].lower()
|
||||
format_new_ext = u'.' + self.settings['new_book_format'].lower()
|
||||
|
||||
# check to see if destination format already exists -
|
||||
# check to see if destination format already exists - or if book is in database
|
||||
# if it does - mark the conversion task as complete and return a success
|
||||
# this will allow send to kindle workflow to continue to work
|
||||
if os.path.isfile(file_path + format_new_ext):
|
||||
if os.path.isfile(file_path + format_new_ext) or\
|
||||
local_db.get_book_format(self.bookid, self.settings['new_book_format']):
|
||||
log.info("Book id %d already converted to %s", book_id, format_new_ext)
|
||||
cur_book = calibre_db.get_book(book_id)
|
||||
cur_book = local_db.get_book(book_id)
|
||||
self.results['path'] = file_path
|
||||
self.results['title'] = cur_book.title
|
||||
self._handleSuccess()
|
||||
local_db.session.close()
|
||||
return os.path.basename(file_path + format_new_ext)
|
||||
else:
|
||||
log.info("Book id %d - target format of %s does not exist. Moving forward with convert.",
|
||||
@ -105,18 +110,18 @@ class TaskConvert(CalibreTask):
|
||||
check, error_message = self._convert_calibre(file_path, format_old_ext, format_new_ext)
|
||||
|
||||
if check == 0:
|
||||
cur_book = calibre_db.get_book(book_id)
|
||||
cur_book = local_db.get_book(book_id)
|
||||
if os.path.isfile(file_path + format_new_ext):
|
||||
# self.db_queue.join()
|
||||
new_format = db.Data(name=cur_book.data[0].name,
|
||||
book_format=self.settings['new_book_format'].upper(),
|
||||
book=book_id, uncompressed_size=os.path.getsize(file_path + format_new_ext))
|
||||
try:
|
||||
local_session.merge(new_format)
|
||||
local_session.commit()
|
||||
local_db.session.merge(new_format)
|
||||
local_db.session.commit()
|
||||
except SQLAlchemyError as e:
|
||||
local_session.rollback()
|
||||
local_db.session.rollback()
|
||||
log.error("Database error: %s", e)
|
||||
local_db.session.close()
|
||||
return
|
||||
self.results['path'] = cur_book.path
|
||||
self.results['title'] = cur_book.title
|
||||
@ -125,6 +130,7 @@ class TaskConvert(CalibreTask):
|
||||
return os.path.basename(file_path + format_new_ext)
|
||||
else:
|
||||
error_message = _('%(format)s format not found on disk', format=format_new_ext.upper())
|
||||
local_db.session.close()
|
||||
log.info("ebook converter failed with error while converting book")
|
||||
if not error_message:
|
||||
error_message = _('Ebook converter failed with unknown error')
|
||||
|
@ -167,7 +167,7 @@ class TaskEmail(CalibreTask):
|
||||
smtplib.stderr = org_smtpstderr
|
||||
|
||||
except (MemoryError) as e:
|
||||
log.exception(e)
|
||||
log.debug_or_exception(e)
|
||||
self._handleError(u'MemoryError sending email: ' + str(e))
|
||||
# return None
|
||||
except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
|
||||
@ -178,7 +178,7 @@ class TaskEmail(CalibreTask):
|
||||
elif hasattr(e, "args"):
|
||||
text = '\n'.join(e.args)
|
||||
else:
|
||||
log.exception(e)
|
||||
log.debug_or_exception(e)
|
||||
text = ''
|
||||
self._handleError(u'Smtplib Error sending email: ' + text)
|
||||
# return None
|
||||
@ -225,7 +225,7 @@ class TaskEmail(CalibreTask):
|
||||
data = file_.read()
|
||||
file_.close()
|
||||
except IOError as e:
|
||||
log.exception(e)
|
||||
log.debug_or_exception(e)
|
||||
log.error(u'The requested file could not be read. Maybe wrong permissions?')
|
||||
return None
|
||||
|
||||
|
@ -36,7 +36,10 @@
|
||||
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
|
||||
<div class="cover">
|
||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" />
|
||||
<span class="img">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" />
|
||||
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
|
@ -197,7 +197,8 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block modal %}
|
||||
{{ delete_book(book.id) }}
|
||||
{{ delete_book() }}
|
||||
{{ delete_confirm_modal() }}
|
||||
|
||||
<div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
|
@ -61,7 +61,7 @@
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% block modal %}
|
||||
{{ delete_book(0) }}
|
||||
{{ delete_book() }}
|
||||
{% if g.user.role_edit() %}
|
||||
<div class="modal fade" id="mergeModal" role="dialog" aria-labelledby="metaMergeLabel">
|
||||
<div class="modal-dialog">
|
||||
|
@ -16,12 +16,19 @@
|
||||
<div id="collapseOne" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<label for="config_calibre_dir">{{_('Location of Calibre Database')}}</label>
|
||||
<div class="form-group required input-group">
|
||||
<div class="form-group required{% if filepicker %} input-group{% endif %}">
|
||||
<input type="text" class="form-control" id="config_calibre_dir" name="config_calibre_dir" value="{% if config.config_calibre_dir != None %}{{ config.config_calibre_dir }}{% endif %}" autocomplete="off">
|
||||
{% if filepicker %}
|
||||
<span class="input-group-btn">
|
||||
<button type="button" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
<button type="button" data-toggle="modal" id="calibre_modal_path" data-link="config_calibre_dir" data-filefilter="metadata.db" data-target="#fileModal" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not filepicker %}
|
||||
<div class="form-group">
|
||||
<label id="filepicker-hint">{{_('To activate serverside filepicker start Calibre-Web with -f optionn')}}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if feature_support['gdrive'] %}
|
||||
<div class="form-group required">
|
||||
<input type="checkbox" id="config_use_google_drive" name="config_use_google_drive" data-control="gdrive_settings" {% if config.config_use_google_drive %}checked{% endif %} >
|
||||
@ -94,14 +101,14 @@
|
||||
<div class="form-group input-group">
|
||||
<input type="text" class="form-control" id="config_certfile" name="config_certfile" value="{% if config.config_certfile != None %}{{ config.config_certfile }}{% endif %}" autocomplete="off">
|
||||
<span class="input-group-btn">
|
||||
<button type="button" id="certfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
<button type="button" data-toggle="modal" data-link="config_certfile" data-target="#fileModal" id="certfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
<label for="config_calibre_dir" >{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label>
|
||||
<div class="form-group input-group">
|
||||
<input type="text" class="form-control" id="config_keyfile" name="config_keyfile" value="{% if config.config_keyfile != None %}{{ config.config_keyfile }}{% endif %}" autocomplete="off">
|
||||
<span class="input-group-btn">
|
||||
<button type="button" id="keyfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
<button type="button" id="keyfile_path" data-toggle="modal" data-link="config_keyfile" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -268,21 +275,21 @@
|
||||
<div class="form-group input-group">
|
||||
<input type="text" class="form-control" id="config_ldap_cacert_path" name="config_ldap_cacert_path" value="{% if config.config_ldap_cacert_path != None %}{{ config.config_ldap_cacert_path }}{% endif %}" autocomplete="off">
|
||||
<span class="input-group-btn">
|
||||
<button type="button" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
<button type="button" id="library_path" data-toggle="modal" data-link="config_ldap_cacert_path" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
<label for="config_ldap_cert_path">{{_('LDAP Certificate Path (Only needed for Client Certificate Authentication)')}}</label>
|
||||
<div class="form-group input-group">
|
||||
<input type="text" class="form-control" id="config_ldap_cert_path" name="config_ldap_cert_path" value="{% if config.config_ldap_cert_path != None %}{{ config.config_ldap_cert_path }}{% endif %}" autocomplete="off">
|
||||
<span class="input-group-btn">
|
||||
<button type="button" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
<button type="button" id="library_path" data-toggle="modal" data-link="config_ldap_cert_path" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
<label for="config_ldap_key_path">{{_('LDAP Keyfile Path (Only needed for Client Certificate Authentication)')}}</label>
|
||||
<div class="form-group input-group">
|
||||
<input type="text" class="form-control" id="config_ldap_key_path" name="config_ldap_key_path" value="{% if config.config_ldap_key_path != None %}{{ config.config_ldap_key_path }}{% endif %}" autocomplete="off">
|
||||
<span class="input-group-btn">
|
||||
<button type="button" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
<button type="button" id="library_path" data-toggle="modal" data-link="config_ldap_key_path" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -384,7 +391,7 @@
|
||||
<div class="form-group input-group">
|
||||
<input type="text" class="form-control" id="config_converterpath" name="config_converterpath" value="{% if config.config_converterpath != None %}{{ config.config_converterpath }}{% endif %}" autocomplete="off">
|
||||
<span class="input-group-btn">
|
||||
<button type="button" id="converter_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
<button type="button" data-toggle="modal" id="converter_modal_path" data-link="config_converterpath" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -395,7 +402,7 @@
|
||||
<div class="form-group input-group">
|
||||
<input type="text" class="form-control" id="config_kepubifypath" name="config_kepubifypath" value="{% if config.config_kepubifypath != None %}{{ config.config_kepubifypath }}{% endif %}" autocomplete="off">
|
||||
<span class="input-group-btn">
|
||||
<button type="button" id="kepubify_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
<button type="button" id="kepubify_path" data-toggle="modal" data-link="config_kepubifypath" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
{% if feature_support['rar'] %}
|
||||
@ -403,7 +410,7 @@
|
||||
<div class="form-group input-group">
|
||||
<input type="text" class="form-control" id="config_rarfile_location" name="config_rarfile_location" value="{% if config.config_rarfile_location != None %}{{ config.config_rarfile_location }}{% endif %}" autocomplete="off">
|
||||
<span class="input-group-btn">
|
||||
<button type="button" id="unrar_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
<button type="button" id="unrar_path" data-toggle="modal" data-link="config_rarfile_location" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -412,8 +419,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-sm-12">
|
||||
{% if not show_login_button %}
|
||||
<button type="submit" name="submit" class="btn btn-default">{{_('Save')}}</button>
|
||||
@ -428,6 +433,9 @@
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block modal %}
|
||||
{{ filechooser_modal() }}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
<script type="text/javascript">
|
||||
$(document).on('change', '#config_use_google_drive', function() {
|
||||
|
@ -8,7 +8,10 @@
|
||||
<div class="cover">
|
||||
{% if entry.has_cover is defined %}
|
||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
<span class="img">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -89,20 +89,7 @@
|
||||
{% endblock %}
|
||||
{% block modal %}
|
||||
{% if g.allow_registration %}
|
||||
<div id="DeleteDomain" class="modal fade" role="dialog">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<!-- Modal content-->
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger">
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<p>{{_('Are you sure you want to delete this domain?')}}</p>
|
||||
<button type="button" class="btn btn-danger" id="btndeletedomain" >{{_('Delete')}}</button>
|
||||
<button type="button" class="btn btn-default" id="btncancel" data-dismiss="modal">{{_('Cancel')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ delete_confirm_modal() }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
|
@ -28,8 +28,10 @@
|
||||
<div class="col-sm-3 col-lg-2 col-xs-6 book sortable" {% if entry[0].sort %}data-name="{{entry[0].series[0].name}}"{% endif %} data-id="{% if entry[0].series[0].name %}{{entry[0].series[0].name}}{% endif %}">
|
||||
<div class="cover">
|
||||
<a href="{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].series[0].id )}}">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/>
|
||||
<span class="badge">{{entry.count}}</span>
|
||||
<span class="img">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/>
|
||||
<span class="badge">{{entry.count}}</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
|
@ -8,7 +8,10 @@
|
||||
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books_rand">
|
||||
<div class="cover">
|
||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
<span class="img">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
@ -82,7 +85,10 @@
|
||||
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books">
|
||||
<div class="cover">
|
||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<span class="img">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}"/>
|
||||
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% from 'modal_dialogs.html' import restrict_modal, delete_book %}
|
||||
{% from 'modal_dialogs.html' import restrict_modal, delete_book, filechooser_modal, delete_confirm_modal %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ g.user.locale }}">
|
||||
<head>
|
||||
@ -189,8 +189,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{% block modal %}{% endblock %}
|
||||
|
||||
|
||||
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
||||
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
|
||||
<!-- Include all compiled plugins (below), or include individual files as needed -->
|
||||
@ -200,14 +198,7 @@
|
||||
<script src="{{ url_for('static', filename='js/libs/context.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/plugins.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/jquery.form.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/uploadprogress.js') }}"> </script>
|
||||
{% if g.current_theme == 1 %}
|
||||
<script src="{{ url_for('static', filename='js/libs/jquery.visible.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/compromise.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/readmore.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/caliBlur.js') }}"></script>
|
||||
{% endif %}
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$("#form-upload").uploadprogress({
|
||||
@ -219,6 +210,13 @@
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
{% if g.current_theme == 1 %}
|
||||
<script src="{{ url_for('static', filename='js/libs/jquery.visible.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/compromise.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/readmore.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/caliBlur.js') }}"></script>
|
||||
{% endif %}
|
||||
{% block js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -22,7 +22,7 @@
|
||||
<button type="submit" name="forgot" value="forgot" class="btn btn-default">{{_('Forgot Password?')}}</button>
|
||||
{% endif %}
|
||||
{% if config.config_remote_login %}
|
||||
<a href="{{url_for('web.remote_login')}}" class="pull-right">{{_('Log in with Magic Link')}}</a>
|
||||
<a href="{{url_for('remotelogin.remote_login')}}" id="remote_login" class="pull-right">{{_('Log in with Magic Link')}}</a>
|
||||
{% endif %}
|
||||
{% if config.config_login_type == 2 %}
|
||||
{% if 1 in oauth_check %}
|
||||
|
@ -37,7 +37,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
{% macro delete_book(bookid) %}
|
||||
{% macro delete_book() %}
|
||||
{% if g.user.role_delete_books() %}
|
||||
<div class="modal fade" id="deleteModal" role="dialog" aria-labelledby="metaDeleteLabel">
|
||||
<div class="modal-dialog">
|
||||
@ -68,3 +68,56 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{% macro filechooser_modal() %}
|
||||
<div class="modal fade" id="fileModal" role="dialog" aria-labelledby="metafileLabel">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-info text-center">
|
||||
<span>{{_('Choose File Location')}}</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table id="file_table" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{_('type')}}</th>
|
||||
<th>{{_('name')}}</th>
|
||||
<th>{{_('size')}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody">
|
||||
<tr class="tr-clickable hidden" id="parent" data-type="dir" data-path="..">
|
||||
<td><span class="glyphicon glyphicon-folder-close"></span></td>
|
||||
<td title="{{_('Parent Directory')}}"><span class="parentdir">..</span></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="text-left" id="element_selected"></div>
|
||||
<input type="button" class="btn btn-primary" data-path="" data-link="" data-folderonly="" data-filefilter="" data-newfile="" value="{{_('Select')}}" name="file_confirm" id="file_confirm" data-dismiss="modal">
|
||||
<button type="button" id="file_abort" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro delete_confirm_modal() %}
|
||||
<div id="GeneralDeleteModal" class="modal fade" role="Dialog">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger text-center">
|
||||
<span id="header"></span>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<span id="text"></span>
|
||||
<p></p>
|
||||
<button id="btnConfirmYes" type="button" class="btn btn btn-danger">{{_('Delete')}}</button>
|
||||
<button id="btnConfirmNo" type="button" class="btn btn-default">{{_('Cancel')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endmacro %}
|
||||
|
@ -4,7 +4,7 @@
|
||||
<h2 style="margin-top: 0">{{_('Magic Link - Authorise New Device')}}</h2>
|
||||
<p>
|
||||
{{_('On another device, login and visit:')}}
|
||||
<h4><a href="{{verify_url}}">{{verify_url}}</a></b>
|
||||
<h4><a id="verify_url" href="{{verify_url}}">{{verify_url}}</a></b>
|
||||
</h4>
|
||||
<p>
|
||||
{{_('Once verified, you will automatically be logged in on this device.')}}
|
||||
@ -20,7 +20,7 @@
|
||||
(function () {
|
||||
// Poll the server to check if the user has authenticated
|
||||
var t = setInterval(function () {
|
||||
$.post('{{url_for("web.token_verified")}}', { token: '{{token}}' })
|
||||
$.post('{{url_for("remotelogin.token_verified")}}', { token: '{{token}}' })
|
||||
.done(function(response) {
|
||||
if (response.status === 'success') {
|
||||
// Wait a tick so cookies are updated
|
||||
|
@ -43,7 +43,10 @@
|
||||
<div class="cover">
|
||||
{% if entry.has_cover is defined %}
|
||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
<span class="img">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -31,87 +31,87 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label for="include_tag">{{_('Tags')}}</label>
|
||||
<div class="form-group" id="tag">
|
||||
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
|
||||
{% for tag in tags %}
|
||||
<label id="tag_{{tag.id}}" class="btn btn-primary tags_click">
|
||||
<input type="checkbox" autocomplete="off" name="include_tag" id="include_tag" value="{{tag.id}}">{{tag.name}}</input>
|
||||
</label>
|
||||
{% endfor %}
|
||||
<div class="form-group">
|
||||
<label for="read_status">{{_('Read Status')}}</label>
|
||||
<select name="read_status" id="read_status" class="form-control">
|
||||
<option value="" selected></option>
|
||||
<option value="True" >{{_('Yes')}}</option>
|
||||
<option value="False" >{{_('No')}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-sm-6" id="tag">
|
||||
<div><label for="include_tag">{{_('Tags')}}</label></div>
|
||||
<select class="selectpicker" name="include_tag" id="include_tag" data-live-search="true" data-style="btn-primary" multiple>
|
||||
{% for tag in tags %}
|
||||
<option class="tags_click" value="{{tag.id}}">{{tag.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-sm-6">
|
||||
<div><label for="exclude_tag">{{_('Exclude Tags')}}</label></div>
|
||||
<select class="selectpicker" name="exclude_tag" id="exclude_tag" data-live-search="true" data-style="btn-danger" multiple>
|
||||
{% for tag in tags %}
|
||||
<option class="tags_click" value="{{tag.id}}">{{tag.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<label for="exclude_tag">{{_('Exclude Tags')}}</label>
|
||||
<div class="form-group">
|
||||
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
|
||||
{% for tag in tags %}
|
||||
<label id="exclude_tag_{{tag.id}}" class="btn btn-danger tags_click">
|
||||
<input type="checkbox" autocomplete="off" name="exclude_tag" id="exclude_tag" value="{{tag.id}}">{{tag.name}}</input>
|
||||
</label>
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="form-group col-sm-6">
|
||||
<div><label for="include_serie">{{_('Series')}}</label></div>
|
||||
<select class="selectpicker" name="include_serie" id="include_serie" data-live-search="true" data-style="btn-primary" multiple>
|
||||
{% for serie in series %}
|
||||
<option value="{{serie.id}}">{{serie.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<label for="include_serie">{{_('Series')}}</label>
|
||||
<div class="form-group">
|
||||
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
|
||||
{% for serie in series %}
|
||||
<label id="serie_{{serie.id}}" class="btn btn-primary serie_click">
|
||||
<input type="checkbox" autocomplete="off" name="include_serie" id="include_serie" value="{{serie.id}}">{{serie.name}}</input>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<label for="exclude_serie">{{_('Exclude Series')}}</label>
|
||||
<div class="form-group">
|
||||
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
|
||||
{% for serie in series %}
|
||||
<label id="exclude_serie_{{serie.id}}" class="btn btn-danger serie_click">
|
||||
<input type="checkbox" autocomplete="off" name="exclude_serie" id="exclude_serie" value="{{serie.id}}">{{serie.name}}</input>
|
||||
</label>
|
||||
{% endfor %}
|
||||
<div class="form-group col-sm-6">
|
||||
<div><label for="exclude_serie">{{_('Exclude Series')}}</label></div>
|
||||
<select class="selectpicker" name="exclude_serie" id="exclude_serie" data-live-search="true" data-style="btn-danger" multiple>
|
||||
{% for serie in series %}
|
||||
<option value="{{serie.id}}">{{serie.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% if languages %}
|
||||
<label for="include_language">{{_('Languages')}}</label>
|
||||
<div class="form-group">
|
||||
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
|
||||
<div class="row">
|
||||
<div class="form-group col-sm-6">
|
||||
<div><label for="include_language">{{_('Languages')}}</label></div>
|
||||
<select class="selectpicker" name="include_language" id="include_language" data-live-search="true" data-style="btn-primary" multiple>
|
||||
{% for language in languages %}
|
||||
<label id="language_{{language.id}}" class="btn btn-primary serie_click">
|
||||
<input type="checkbox" autocomplete="off" name="include_language" id="include_language" value="{{language.id}}">{{language.name}}</input>
|
||||
</label>
|
||||
<option value="{{language.id}}">{{language.name}}</option>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</select>
|
||||
</div>
|
||||
<label for="exclude_language">{{_('Exclude Languages')}}</label>
|
||||
<div class="form-group">
|
||||
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
|
||||
<div class="form-group col-sm-6">
|
||||
<div><label for="exclude_language">{{_('Exclude Languages')}}</label></div>
|
||||
<select class="selectpicker" name="exclude_language" id="exclude_language" data-live-search="true" data-style="btn-danger" multiple>
|
||||
{% for language in languages %}
|
||||
<label id="exclude_language_{{language.id}}" class="btn btn-danger language_click">
|
||||
<input type="checkbox" autocomplete="off" name="exclude_language" id="exclude_language" value="{{language.id}}">{{language.name}}</input>
|
||||
</label>
|
||||
<option value="{{language.id}}">{{language.name}}</option>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</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 %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<label for="exclude_extension">{{_('Exclude Extensions')}}</label>
|
||||
<div class="form-group">
|
||||
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
|
||||
{% endif%}
|
||||
<div class="row">
|
||||
<div class="form-group col-sm-6">
|
||||
<div><label for="include_extension">{{_('Extensions')}}</label></div>
|
||||
<select id="include_extension" class="selectpicker" name="include_extension" id="include_extension" data-live-search="true" data-style="btn-primary" multiple>
|
||||
{% for extension in extensions %}
|
||||
<label id="exclude_extension_{{extension.format}}" class="btn btn-danger extension_click">
|
||||
<input type="checkbox" autocomplete="off" name="exclude_extension" id="exclude_extension" value="{{extension.format}}">{{extension.format}}</input>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<option value="{{extension.format}}">{{extension.format}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div 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 class="row">
|
||||
<div class="form-group col-sm-6">
|
||||
@ -189,10 +189,13 @@
|
||||
<script src="{{ url_for('static', filename='js/libs/bootstrap-rating-input.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/typeahead.bundle.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/edit_books.js') }}"></script>
|
||||
<script>
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/libs/bootstrap-select.min.js')}}"></script>
|
||||
{% if not g.user.locale == 'en' %}
|
||||
<script src="{{ url_for('static', filename='js/libs/bootstrap-select/defaults-' + g.user.locale + '.min.js') }}" charset="UTF-8"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block header %}
|
||||
<link href="{{ url_for('static', filename='css/libs/typeahead.css') }}" rel="stylesheet" media="screen">
|
||||
<link href="{{ url_for('static', filename='css/libs/bootstrap-datepicker3.min.css') }}" rel="stylesheet" media="screen">
|
||||
<link href="{{ url_for('static', filename='css/libs/bootstrap-select.min.css') }}" rel="stylesheet" >
|
||||
{% endblock %}
|
||||
|
@ -2,23 +2,38 @@
|
||||
{% block body %}
|
||||
<div class="discover">
|
||||
<h2>{{title}}</h2>
|
||||
{% if g.user.role_download() %}
|
||||
<a id="shelf_down" href="{{ url_for('shelf.show_shelf', shelf_type=2, shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a>
|
||||
{% if g.user.role_download() %}
|
||||
<a id="shelf_down" href="{{ url_for('shelf.show_simpleshelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a>
|
||||
{% endif %}
|
||||
{% if g.user.is_authenticated %}
|
||||
{% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
|
||||
<div id="delete_shelf" data-toggle="modal" data-target="#DeleteShelfDialog" class="btn btn-danger">{{ _('Delete this Shelf') }} </div>
|
||||
<a id="edit_shelf" href="{{ url_for('shelf.edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf') }} </a>
|
||||
<a id="order_shelf" href="{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a>
|
||||
<div class="btn btn-danger" id="delete_shelf" data-value="{{ shelf.id }}">{{ _('Delete this Shelf') }}</div>
|
||||
<a id="edit_shelf" href="{{ url_for('shelf.edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf Properties') }} </a>
|
||||
{% if entries.__len__() %}
|
||||
<a id="order_shelf" href="{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Arrange books manually') }} </a>
|
||||
<button id="toggle_order_shelf" type="button" data-alt-text="{{ _('Disable Change order') }}" class="btn btn-primary">{{ _('Enable Change order') }}</button>
|
||||
<div class="filterheader hidden-xs hidden-sm">
|
||||
<a data-toggle="tooltip" title="{{_('Sort according to book date, newest first')}}" id="new" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort according to book date, oldest first')}}" id="old" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort title in alphabetical order')}}" id="asc" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort title in reverse alphabetical order')}}" id="desc" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort authors in alphabetical order')}}" id="auth_az" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='authaz')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort authors in reverse alphabetical order')}}" id="auth_za" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='authza')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort according to publishing date, newest first')}}" id="pub_new" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort according to publishing date, oldest first')}}" id="pub_old" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="row display-flex">
|
||||
|
||||
{% for entry in entries %}
|
||||
<div class="col-sm-3 col-lg-2 col-xs-6 book">
|
||||
<div class="cover">
|
||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
<span class="img">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
@ -68,7 +83,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div id="DeleteShelfDialog" class="modal fade" role="dialog">
|
||||
<!--div id="DeleteShelfDialog" class="modal fade" role="dialog">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger text-center">
|
||||
@ -82,6 +97,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div-->
|
||||
|
||||
{% endblock %}
|
||||
{% block modal %}
|
||||
{{ delete_confirm_modal() }}
|
||||
{% endblock %}
|
||||
|
@ -5,30 +5,39 @@
|
||||
<div>{{_('Drag to Rearrange Order')}}</div>
|
||||
<div id="sortTrue" class="list-group">
|
||||
{% for entry in entries %}
|
||||
<div id="{{entry['id']}}" class="list-group-item">
|
||||
<div id="{{entry['Books']['id']}}" class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col-lg-2 col-sm-4 hidden-xs">
|
||||
<img class="cover-height" src="{{ url_for('web.get_cover', book_id=entry['id']) }}">
|
||||
{% if entry['visible'] %}
|
||||
<img class="cover-height" src="{{ url_for('web.get_cover', book_id=entry['Books']['id']) }}">
|
||||
{% else %}
|
||||
<img class="cover-height" src="{{ url_for('static', filename='generic_cover.jpg') }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-lg-10 col-sm-8 col-xs-12">
|
||||
{{entry['title']}}
|
||||
{% if entry['series']|length > 0 %}
|
||||
{% if entry['visible'] %}
|
||||
{{entry['Books']['title']}}
|
||||
{% if entry['Books']['series']|length > 0 %}
|
||||
<br>
|
||||
{{entry['series_index']}} - {{entry['series'][0].name}}
|
||||
{{entry['Books']['series_index']}} - {{entry['Books']['series'][0].name}}
|
||||
{% endif %}
|
||||
<br>
|
||||
{% for author in entry['author'] %}
|
||||
{% for author in entry['Books']['author'] %}
|
||||
{{author.name.replace('|',',')}}
|
||||
{% if not loop.last %}
|
||||
&
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{{_('Hidden Book')}}
|
||||
<br>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button onclick="sendData('{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}')" class="btn btn-default" id="ChangeOrder">{{_('Change order')}}</button>
|
||||
<button onclick="sendData('{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}')" class="btn btn-default" id="ChangeOrder">{{_('Save')}}</button>
|
||||
<a href="{{ url_for('shelf.show_shelf', shelf_id=shelf.id) }}" id="shelf_back" class="btn btn-default">{{_('Back')}}</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -55,27 +55,14 @@
|
||||
</div>
|
||||
|
||||
<div class="btn-group" role="group" aria-label="Download, send to Kindle, reading">
|
||||
{% if g.user.role_download() %}
|
||||
{% if g.user.role_download() %}
|
||||
{% if entry.data|length %}
|
||||
<div class="btn-group" role="group">
|
||||
{% if entry.data|length < 2 %}
|
||||
|
||||
{% for format in entry.data %}
|
||||
<a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}" id="btnGroupDrop1{{format.format|lower}}" class="btn btn-primary" role="button">
|
||||
<span class="glyphicon glyphicon-download"></span>{{format.format}} ({{ format.uncompressed_size|filesizeformat }})
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-download"></span> {{_('Download')}}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
||||
{% for format in entry.data %}
|
||||
<li><a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}">{{format.format}} ({{ format.uncompressed_size|filesizeformat }})</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% for format in entry.data %}
|
||||
<a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}" id="btnGroupDrop{{entry.id}}{{format.format|lower}}" class="btn btn-primary" role="button">
|
||||
<span class="glyphicon glyphicon-download"></span>{{format.format}} ({{ format.uncompressed_size|filesizeformat }})
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
@ -63,7 +63,7 @@
|
||||
<label>{{ _('Kobo Sync Token')}}</label>
|
||||
<div class="form-group col">
|
||||
<a class="btn btn-default" id="config_create_kobo_token" data-toggle="modal" data-target="#modal_kobo_token" data-remote="false" href="{{ url_for('kobo_auth.generate_auth_token', user_id=content.id) }}">{{_('Create/View')}}</a>
|
||||
<div class="btn btn-danger" id="config_delete_kobo_token" data-toggle="modal" data-target="#modalDeleteToken" data-remote="false" {% if not content.remote_auth_token.first() %} style="display: none;" {% endif %}>{{_('Delete')}}</div>
|
||||
<div class="btn btn-danger" id="config_delete_kobo_token" data-value="{{ content.id }}" data-remote="false" {% if not content.remote_auth_token.first() %} style="display: none;" {% endif %}>{{_('Delete')}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -82,8 +82,8 @@
|
||||
<label for="Show_detail_random">{{_('Show Random Books in Detail View')}}</label>
|
||||
</div>
|
||||
{% if ( g.user and g.user.role_admin() and not new_user ) %}
|
||||
<a href="#" id="get_user_tags" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add Allowed/Denied Tags')}}</a>
|
||||
<a href="#" id="get_user_column_values" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add allowed/Denied Custom Column Values')}}</a>
|
||||
<a href="#" id="get_user_tags" class="btn btn-default" data-id="{{content.id}}" data-toggle="modal" data-target="#restrictModal">{{_('Add Allowed/Denied Tags')}}</a>
|
||||
<a href="#" id="get_user_column_values" data-id="{{content.id}}" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add allowed/Denied Custom Column Values')}}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
@ -125,19 +125,15 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="delete" name="delete"> {{_('Delete User')}}
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<button type="submit" id="submit" class="btn btn-default">{{_('Save')}}</button>
|
||||
<div id="user_submit" class="btn btn-default">{{_('Save')}}</div>
|
||||
{% if not profile %}
|
||||
<a href="{{ url_for('admin.admin') }}" id="back" class="btn btn-default">{{_('Cancel')}}</a>
|
||||
{% endif %}
|
||||
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
|
||||
<div class="btn btn-danger" id="btndeluser" data-value="{{ content.id }}" data-remote="false" >{{_('Delete User')}}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -157,23 +153,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modalDeleteToken" class="modal fade" role="dialog">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger">
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<p>{{_('Do you really want to delete the Kobo Token?')}}</p>
|
||||
<button type="button" class="btn btn-danger" id="btndeletetoken" value="{{content.id}}">{{_('Delete')}}</button>
|
||||
<button type="button" class="btn btn-default" id="btncancel" data-dismiss="modal">{{_('Back')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block modal %}
|
||||
{{ restrict_modal() }}
|
||||
{{ delete_confirm_modal() }}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
|
||||
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
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
Binary file not shown.
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
105
cps/ub.py
@ -26,10 +26,8 @@ import uuid
|
||||
from flask import session as flask_session
|
||||
from binascii import hexlify
|
||||
|
||||
from flask import g
|
||||
from flask_babel import gettext as _
|
||||
from flask_login import AnonymousUserMixin, current_user
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
try:
|
||||
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
|
||||
oauth_support = True
|
||||
@ -45,7 +43,7 @@ from sqlalchemy import Column, ForeignKey
|
||||
from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float, JSON
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from sqlalchemy.orm import backref, relationship, sessionmaker, Session
|
||||
from sqlalchemy.orm import backref, relationship, sessionmaker, Session, scoped_session
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
from . import constants
|
||||
@ -57,73 +55,6 @@ Base = declarative_base()
|
||||
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):
|
||||
ids = list()
|
||||
for element in result:
|
||||
@ -521,7 +452,7 @@ def migrate_Database(session):
|
||||
if not engine.dialect.has_table(engine.connect(), "archived_book"):
|
||||
ArchivedBook.__table__.create(bind=engine)
|
||||
if not engine.dialect.has_table(engine.connect(), "registration"):
|
||||
ReadBook.__table__.create(bind=engine)
|
||||
Registration.__table__.create(bind=engine)
|
||||
with engine.connect() as conn:
|
||||
conn.execute("insert into registration (domain, allow) values('%.%',1)")
|
||||
session.commit()
|
||||
@ -570,12 +501,16 @@ def migrate_Database(session):
|
||||
for book_shelf in session.query(BookShelf).all():
|
||||
book_shelf.date_added = datetime.datetime.now()
|
||||
session.commit()
|
||||
# Handle table exists, but no content
|
||||
cnt = session.query(Registration).count()
|
||||
if not cnt:
|
||||
with engine.connect() as conn:
|
||||
conn.execute("insert into registration (domain, allow) values('%.%',1)")
|
||||
session.commit()
|
||||
try:
|
||||
# Handle table exists, but no content
|
||||
cnt = session.query(Registration).count()
|
||||
if not cnt:
|
||||
with engine.connect() as conn:
|
||||
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:
|
||||
session.query(exists().where(BookShelf.order)).scalar()
|
||||
except exc.OperationalError: # Database is not compatible, some columns are missing
|
||||
@ -660,7 +595,7 @@ def migrate_Database(session):
|
||||
session.commit()
|
||||
except exc.OperationalError:
|
||||
print('Settings database is not writeable. Exiting...')
|
||||
sys.exit(1)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def clean_database(session):
|
||||
@ -678,13 +613,19 @@ def update_download(book_id, user_id):
|
||||
if not check:
|
||||
new_download = Downloads(user_id=user_id, book_id=book_id)
|
||||
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
|
||||
def delete_download(book_id):
|
||||
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
|
||||
def create_anonymous_user(session):
|
||||
@ -725,7 +666,7 @@ def init_db(app_db_path):
|
||||
app_DB_path = app_db_path
|
||||
engine = create_engine(u'sqlite:///{0}'.format(app_db_path), echo=False)
|
||||
|
||||
Session = sessionmaker()
|
||||
Session = scoped_session(sessionmaker())
|
||||
Session.configure(bind=engine)
|
||||
session = Session()
|
||||
|
||||
|
@ -66,14 +66,6 @@ except ImportError as e:
|
||||
log.debug('Cannot import fb2, extracting fb2 metadata will not work: %s', e)
|
||||
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):
|
||||
meta = None
|
||||
@ -179,10 +171,6 @@ def get_versions():
|
||||
XVersion = 'v'+'.'.join(map(str, lxmlversion))
|
||||
else:
|
||||
XVersion = u'not installed'
|
||||
if use_PIL:
|
||||
PILVersion = 'v' + PILversion
|
||||
else:
|
||||
PILVersion = u'not installed'
|
||||
if comic.use_comic_meta:
|
||||
ComicVersion = comic.comic_version or u'installed'
|
||||
else:
|
||||
@ -191,7 +179,7 @@ def get_versions():
|
||||
'PyPdf': PVersion,
|
||||
'lxml':XVersion,
|
||||
'Wand': WVersion,
|
||||
'Pillow': PILVersion,
|
||||
# 'Pillow': PILVersion,
|
||||
'Comic_API': ComicVersion}
|
||||
|
||||
|
||||
|
88
cps/usermanagement.py
Normal file
88
cps/usermanagement.py
Normal 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
|
490
cps/web.py
490
cps/web.py
@ -22,46 +22,40 @@
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
import base64
|
||||
from datetime import datetime
|
||||
import json
|
||||
import mimetypes
|
||||
import traceback
|
||||
import binascii
|
||||
import re
|
||||
import chardet # dependency of requests
|
||||
|
||||
from babel.dates import format_date
|
||||
from babel import Locale as LC
|
||||
from babel.core import UnknownLocaleError
|
||||
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_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.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 werkzeug.exceptions import default_exceptions
|
||||
from sqlalchemy.sql.functions import coalesce
|
||||
|
||||
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.security import generate_password_hash, check_password_hash
|
||||
|
||||
from . import constants, logger, isoLanguages, services
|
||||
from . import lm, babel, db, ub, config, get_locale, app
|
||||
from . import calibre_db
|
||||
from . import babel, db, ub, config, get_locale, app
|
||||
from . import calibre_db, shelf
|
||||
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
|
||||
from .helper import check_valid_domain, render_task_status, \
|
||||
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
|
||||
from .pagination import Pagination
|
||||
from .redirect import redirect_back
|
||||
from .usermanagement import login_required_if_no_ano
|
||||
from .render_template import render_title_template
|
||||
|
||||
feature_support = {
|
||||
'ldap': bool(services.ldap),
|
||||
@ -71,7 +65,6 @@ feature_support = {
|
||||
|
||||
try:
|
||||
from .oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status
|
||||
|
||||
feature_support['oauth'] = True
|
||||
except ImportError:
|
||||
feature_support['oauth'] = False
|
||||
@ -82,55 +75,12 @@ try:
|
||||
except ImportError:
|
||||
pass # We're not using Python 3
|
||||
|
||||
|
||||
try:
|
||||
from natsort import natsorted as sort
|
||||
except ImportError:
|
||||
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
|
||||
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;"
|
||||
@ -146,104 +96,6 @@ log = logger.create()
|
||||
|
||||
|
||||
# ################################### 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):
|
||||
@ -265,155 +117,6 @@ def viewer_required(f):
|
||||
|
||||
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 #########################################################
|
||||
|
||||
|
||||
@ -432,7 +135,10 @@ def bookmark(book_id, book_format):
|
||||
ub.Bookmark.book_id == book_id,
|
||||
ub.Bookmark.format == book_format)).delete()
|
||||
if not bookmark_key:
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
return "", 204
|
||||
|
||||
lbookmark = ub.Bookmark(user_id=current_user.id,
|
||||
@ -440,7 +146,10 @@ def bookmark(book_id, book_format):
|
||||
format=book_format,
|
||||
bookmark_key=bookmark_key)
|
||||
ub.session.merge(lbookmark)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
return "", 201
|
||||
|
||||
|
||||
@ -465,7 +174,10 @@ def toggle_read(book_id):
|
||||
kobo_reading_state.statistics = ub.KoboStatistics()
|
||||
book.kobo_reading_state = kobo_reading_state
|
||||
ub.session.merge(book)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
else:
|
||||
try:
|
||||
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.is_archived = True
|
||||
ub.session.merge(archived_book)
|
||||
ub.session.commit()
|
||||
try:
|
||||
ub.session.commit()
|
||||
except OperationalError:
|
||||
ub.session.rollback()
|
||||
return ""
|
||||
|
||||
|
||||
@ -620,8 +335,6 @@ def get_matching_tags():
|
||||
title_input = request.args.get('book_title') or ''
|
||||
include_tag_inputs = request.args.getlist('include_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 + "%")),
|
||||
func.lower(db.Books.title).ilike("%" + title_input + "%"))
|
||||
if len(include_tag_inputs) > 0:
|
||||
@ -638,14 +351,6 @@ def get_matching_tags():
|
||||
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):
|
||||
order = [db.Books.timestamp.desc()]
|
||||
if sort == 'stored':
|
||||
@ -749,8 +454,6 @@ def render_hot_books(page):
|
||||
entries.append(downloadBook)
|
||||
else:
|
||||
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__()
|
||||
pagination = Pagination(page, config.config_books_per_page, numBooks)
|
||||
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,
|
||||
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 []
|
||||
if not config.config_read_column:
|
||||
if are_read:
|
||||
@ -917,7 +621,7 @@ def render_read_books(page, are_read, as_xml=False, order=None, *args, **kwargs)
|
||||
db.Books,
|
||||
db_filter,
|
||||
order,
|
||||
ub.ReadBook, db.Books.id==ub.ReadBook.book_id)
|
||||
ub.ReadBook, db.Books.id == ub.ReadBook.book_id)
|
||||
else:
|
||||
try:
|
||||
if are_read:
|
||||
@ -1086,11 +790,12 @@ def update_table_settings():
|
||||
except AttributeError:
|
||||
pass
|
||||
ub.session.commit()
|
||||
except InvalidRequestError:
|
||||
except (InvalidRequestError, OperationalError):
|
||||
log.error("Invalid request received: %r ", request, )
|
||||
return "Invalid request", 400
|
||||
return ""
|
||||
|
||||
|
||||
@web.route("/author")
|
||||
@login_required_if_no_ano
|
||||
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_high = term.get("ratinglow")
|
||||
description = term.get("comment")
|
||||
read_status = term.get("read_status")
|
||||
if author_name:
|
||||
author_name = author_name.strip().lower().replace(',', '|')
|
||||
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 \
|
||||
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 \
|
||||
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))
|
||||
if pub_start:
|
||||
try:
|
||||
@ -1358,8 +1064,12 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
||||
pub_start = u""
|
||||
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)
|
||||
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()
|
||||
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).\
|
||||
filter(db.Languages.id.in_(include_languages_inputs)).all()
|
||||
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)])
|
||||
if 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 exclude_extension_inputs)
|
||||
# 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)
|
||||
if 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:
|
||||
q = q.filter(db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + publisher + "%")))
|
||||
for tag in include_tag_inputs:
|
||||
@ -1487,8 +1216,14 @@ def serve_book(book_id, book_format, anyname):
|
||||
headers = Headers()
|
||||
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
||||
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:
|
||||
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)
|
||||
|
||||
|
||||
@ -1674,89 +1409,7 @@ def logout():
|
||||
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 #########################################################
|
||||
@ -1839,14 +1492,11 @@ def profile():
|
||||
ub.session.rollback()
|
||||
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.")
|
||||
'''return render_title_template("user_edit.html",
|
||||
content=current_user,
|
||||
translations=translations,
|
||||
kobo_support=kobo_support,
|
||||
title=_(u"%(name)s's profile", name=current_user.nickname),
|
||||
page="me",
|
||||
registered_oauth=local_oauth_check,
|
||||
oauth_status=oauth_status)'''
|
||||
except OperationalError as e:
|
||||
ub.session.rollback()
|
||||
log.error("Database error: %s", e)
|
||||
flash(_(u"Database error: %(error)s.", error=e), category="error")
|
||||
|
||||
return render_title_template("user_edit.html",
|
||||
translations=translations,
|
||||
profile=1,
|
||||
@ -1900,14 +1550,6 @@ def read_book(book_id, book_format):
|
||||
log.debug(u"Start comic reader for %d", book_id)
|
||||
return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book"),
|
||||
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")
|
||||
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
|
||||
return redirect(url_for("web.index"))
|
||||
|
466
messages.pot
466
messages.pot
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,7 @@ oauth2client>=4.0.0,<4.1.4
|
||||
uritemplate>=3.0.0,<3.1.0
|
||||
pyasn1-modules>=0.0.8,<0.3.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
|
||||
rsa==3.4.2,<4.1.0
|
||||
six>=1.10.0,<1.15.0
|
||||
@ -26,7 +26,6 @@ SQLAlchemy-Utils>=0.33.5,<0.37.0
|
||||
|
||||
# extracting metadata
|
||||
lxml>=3.8.0,<4.6.0
|
||||
Pillow>=4.0.0,<7.2.0
|
||||
rarfile>=2.7
|
||||
|
||||
# other
|
||||
|
@ -1,5 +1,5 @@
|
||||
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-Principal>=0.3.2,<0.5.1
|
||||
singledispatch>=3.4.0.0,<3.5.0.0
|
||||
@ -11,5 +11,5 @@ pytz>=2016.10
|
||||
requests>=2.11.1,<2.25.0
|
||||
SQLAlchemy>=1.3.0,<1.4.0
|
||||
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
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user