mirror of
https://github.com/janeczku/calibre-web
synced 2024-11-28 12:30:00 +00:00
Merge branch 'Develop' into master
This commit is contained in:
commit
e269bab186
10
cps.py
10
cps.py
@ -41,6 +41,8 @@ from cps.shelf import shelf
|
|||||||
from cps.admin import admi
|
from cps.admin import admi
|
||||||
from cps.gdrive import gdrive
|
from cps.gdrive import gdrive
|
||||||
from cps.editbooks import editbook
|
from cps.editbooks import editbook
|
||||||
|
from cps.remotelogin import remotelogin
|
||||||
|
from cps.error_handler import init_errorhandler
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from cps.kobo import kobo, get_kobo_activated
|
from cps.kobo import kobo, get_kobo_activated
|
||||||
@ -58,14 +60,18 @@ except ImportError:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
|
init_errorhandler()
|
||||||
|
|
||||||
app.register_blueprint(web)
|
app.register_blueprint(web)
|
||||||
app.register_blueprint(opds)
|
app.register_blueprint(opds)
|
||||||
app.register_blueprint(jinjia)
|
app.register_blueprint(jinjia)
|
||||||
app.register_blueprint(about)
|
app.register_blueprint(about)
|
||||||
app.register_blueprint(shelf)
|
app.register_blueprint(shelf)
|
||||||
app.register_blueprint(admi)
|
app.register_blueprint(admi)
|
||||||
if config.config_use_google_drive:
|
app.register_blueprint(remotelogin)
|
||||||
app.register_blueprint(gdrive)
|
# if config.config_use_google_drive:
|
||||||
|
app.register_blueprint(gdrive)
|
||||||
app.register_blueprint(editbook)
|
app.register_blueprint(editbook)
|
||||||
if kobo_available:
|
if kobo_available:
|
||||||
app.register_blueprint(kobo)
|
app.register_blueprint(kobo)
|
||||||
|
@ -94,7 +94,8 @@ def create_app():
|
|||||||
app.root_path = app.root_path.decode('utf-8')
|
app.root_path = app.root_path.decode('utf-8')
|
||||||
app.instance_path = app.instance_path.decode('utf-8')
|
app.instance_path = app.instance_path.decode('utf-8')
|
||||||
|
|
||||||
cache_buster.init_cache_busting(app)
|
if os.environ.get('FLASK_DEBUG'):
|
||||||
|
cache_buster.init_cache_busting(app)
|
||||||
|
|
||||||
log.info('Starting Calibre Web...')
|
log.info('Starting Calibre Web...')
|
||||||
Principal(app)
|
Principal(app)
|
||||||
|
@ -31,7 +31,7 @@ import werkzeug, flask, flask_login, flask_principal, jinja2
|
|||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants
|
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants
|
||||||
from .web import render_title_template
|
from .render_template import render_title_template
|
||||||
try:
|
try:
|
||||||
from flask_login import __version__ as flask_loginVersion
|
from flask_login import __version__ as flask_loginVersion
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
495
cps/admin.py
495
cps/admin.py
@ -5,7 +5,7 @@
|
|||||||
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
|
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
|
||||||
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
|
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
|
||||||
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
|
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
|
||||||
# apetresc, nanu-c, mutschler
|
# apetresc, nanu-c, mutschler, GammaC0de, vuolter
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -26,24 +26,31 @@ import re
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
import operator
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from babel import Locale as LC
|
from babel import Locale as LC
|
||||||
from babel.dates import format_datetime
|
from babel.dates import format_datetime
|
||||||
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory
|
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g
|
||||||
from flask_login import login_required, current_user, logout_user
|
from flask_login import login_required, current_user, logout_user, confirm_login
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
|
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
|
||||||
from sqlalchemy.sql.expression import func
|
from sqlalchemy.sql.expression import func, or_
|
||||||
|
|
||||||
from . import constants, logger, helper, services
|
from . import constants, logger, helper, services
|
||||||
|
from .cli import filepicker
|
||||||
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils
|
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils
|
||||||
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash
|
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash
|
||||||
from .gdriveutils import is_gdrive_ready, gdrive_support
|
from .gdriveutils import is_gdrive_ready, gdrive_support
|
||||||
from .web import admin_required, render_title_template, before_request, unconfigured
|
from .render_template import render_title_template, get_sidebar_config
|
||||||
from . import debug_info
|
from . import debug_info
|
||||||
|
|
||||||
|
try:
|
||||||
|
from functools import wraps
|
||||||
|
except ImportError:
|
||||||
|
pass # We're not using Python 3
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
feature_support = {
|
feature_support = {
|
||||||
@ -72,6 +79,49 @@ feature_support['gdrive'] = gdrive_support
|
|||||||
admi = Blueprint('admin', __name__)
|
admi = Blueprint('admin', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
def admin_required(f):
|
||||||
|
"""
|
||||||
|
Checks if current_user.role == 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
if current_user.role_admin():
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def unconfigured(f):
|
||||||
|
"""
|
||||||
|
Checks if calibre-web instance is not configured
|
||||||
|
"""
|
||||||
|
@wraps(f)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
if not config.db_configured:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
@admi.before_app_request
|
||||||
|
def before_request():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
confirm_login()
|
||||||
|
g.constants = constants
|
||||||
|
g.user = current_user
|
||||||
|
g.allow_registration = config.config_public_reg
|
||||||
|
g.allow_anonymous = config.config_anonbrowse
|
||||||
|
g.allow_upload = config.config_uploading
|
||||||
|
g.current_theme = config.config_theme
|
||||||
|
g.config_authors_max = config.config_authors_max
|
||||||
|
g.shelves_access = ub.session.query(ub.Shelf).filter(
|
||||||
|
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
|
||||||
|
if not config.db_configured and request.endpoint not in (
|
||||||
|
'admin.basic_configuration', 'login', 'admin.config_pathchooser') and '/static/' not in request.path:
|
||||||
|
return redirect(url_for('admin.basic_configuration'))
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin")
|
@admi.route("/admin")
|
||||||
@login_required
|
@login_required
|
||||||
@ -143,7 +193,7 @@ def admin():
|
|||||||
@admin_required
|
@admin_required
|
||||||
def configuration():
|
def configuration():
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
return _configuration_update_helper()
|
return _configuration_update_helper(True)
|
||||||
return _configuration_result()
|
return _configuration_result()
|
||||||
|
|
||||||
|
|
||||||
@ -195,6 +245,21 @@ def update_view_configuration():
|
|||||||
return view_configuration()
|
return view_configuration()
|
||||||
|
|
||||||
|
|
||||||
|
@admi.route("/ajax/loaddialogtexts/<element_id>")
|
||||||
|
@login_required
|
||||||
|
def load_dialogtexts(element_id):
|
||||||
|
texts = { "header": "", "main": "" }
|
||||||
|
if element_id == "config_delete_kobo_token":
|
||||||
|
texts["main"] = _('Do you really want to delete the Kobo Token?')
|
||||||
|
elif element_id == "btndeletedomain":
|
||||||
|
texts["main"] = _('Do you really want to delete this domain?')
|
||||||
|
elif element_id == "btndeluser":
|
||||||
|
texts["main"] = _('Do you really want to delete this user?')
|
||||||
|
elif element_id == "delete_shelf":
|
||||||
|
texts["main"] = _('Are you sure you want to delete this shelf?')
|
||||||
|
return json.dumps(texts)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/editdomain/<int:allow>", methods=['POST'])
|
@admi.route("/ajax/editdomain/<int:allow>", methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
@ -206,7 +271,10 @@ def edit_domain(allow):
|
|||||||
vals = request.form.to_dict()
|
vals = request.form.to_dict()
|
||||||
answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first()
|
answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first()
|
||||||
answer.domain = vals['value'].replace('*', '%').replace('?', '_').lower()
|
answer.domain = vals['value'].replace('*', '%').replace('?', '_').lower()
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@ -220,7 +288,10 @@ def add_domain(allow):
|
|||||||
if not check:
|
if not check:
|
||||||
new_domain = ub.Registration(domain=domain_name, allow=allow)
|
new_domain = ub.Registration(domain=domain_name, allow=allow)
|
||||||
ub.session.add(new_domain)
|
ub.session.add(new_domain)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@ -228,14 +299,23 @@ def add_domain(allow):
|
|||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete_domain():
|
def delete_domain():
|
||||||
domain_id = request.form.to_dict()['domainid'].replace('*', '%').replace('?', '_').lower()
|
try:
|
||||||
ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete()
|
domain_id = request.form.to_dict()['domainid'].replace('*', '%').replace('?', '_').lower()
|
||||||
ub.session.commit()
|
ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete()
|
||||||
# If last domain was deleted, add all domains by default
|
try:
|
||||||
if not ub.session.query(ub.Registration).filter(ub.Registration.allow==1).count():
|
ub.session.commit()
|
||||||
new_domain = ub.Registration(domain="%.%",allow=1)
|
except OperationalError:
|
||||||
ub.session.add(new_domain)
|
ub.session.rollback()
|
||||||
ub.session.commit()
|
# If last domain was deleted, add all domains by default
|
||||||
|
if not ub.session.query(ub.Registration).filter(ub.Registration.allow==1).count():
|
||||||
|
new_domain = ub.Registration(domain="%.%",allow=1)
|
||||||
|
ub.session.add(new_domain)
|
||||||
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@ -250,10 +330,11 @@ def list_domain(allow):
|
|||||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@admi.route("/ajax/editrestriction/<int:res_type>", methods=['POST'])
|
@admi.route("/ajax/editrestriction/<int:res_type>", defaults={"user_id":0}, methods=['POST'])
|
||||||
|
@admi.route("/ajax/editrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_restriction(res_type):
|
def edit_restriction(res_type, user_id):
|
||||||
element = request.form.to_dict()
|
element = request.form.to_dict()
|
||||||
if element['id'].startswith('a'):
|
if element['id'].startswith('a'):
|
||||||
if res_type == 0: # Tags as template
|
if res_type == 0: # Tags as template
|
||||||
@ -267,25 +348,29 @@ def edit_restriction(res_type):
|
|||||||
config.config_allowed_column_value = ','.join(elementlist)
|
config.config_allowed_column_value = ','.join(elementlist)
|
||||||
config.save()
|
config.save()
|
||||||
if res_type == 2: # Tags per user
|
if res_type == 2: # Tags per user
|
||||||
usr_id = os.path.split(request.referrer)[-1]
|
if isinstance(user_id, int):
|
||||||
if usr_id.isdigit() == True:
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
|
||||||
else:
|
else:
|
||||||
usr = current_user
|
usr = current_user
|
||||||
elementlist = usr.list_allowed_tags()
|
elementlist = usr.list_allowed_tags()
|
||||||
elementlist[int(element['id'][1:])]=element['Element']
|
elementlist[int(element['id'][1:])]=element['Element']
|
||||||
usr.allowed_tags = ','.join(elementlist)
|
usr.allowed_tags = ','.join(elementlist)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
if res_type == 3: # CColumn per user
|
if res_type == 3: # CColumn per user
|
||||||
usr_id = os.path.split(request.referrer)[-1]
|
if isinstance(user_id, int):
|
||||||
if usr_id.isdigit() == True:
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
|
||||||
else:
|
else:
|
||||||
usr = current_user
|
usr = current_user
|
||||||
elementlist = usr.list_allowed_column_values()
|
elementlist = usr.list_allowed_column_values()
|
||||||
elementlist[int(element['id'][1:])]=element['Element']
|
elementlist[int(element['id'][1:])]=element['Element']
|
||||||
usr.allowed_column_value = ','.join(elementlist)
|
usr.allowed_column_value = ','.join(elementlist)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
if element['id'].startswith('d'):
|
if element['id'].startswith('d'):
|
||||||
if res_type == 0: # Tags as template
|
if res_type == 0: # Tags as template
|
||||||
elementlist = config.list_denied_tags()
|
elementlist = config.list_denied_tags()
|
||||||
@ -298,25 +383,29 @@ def edit_restriction(res_type):
|
|||||||
config.config_denied_column_value = ','.join(elementlist)
|
config.config_denied_column_value = ','.join(elementlist)
|
||||||
config.save()
|
config.save()
|
||||||
if res_type == 2: # Tags per user
|
if res_type == 2: # Tags per user
|
||||||
usr_id = os.path.split(request.referrer)[-1]
|
if isinstance(user_id, int):
|
||||||
if usr_id.isdigit() == True:
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
|
||||||
else:
|
else:
|
||||||
usr = current_user
|
usr = current_user
|
||||||
elementlist = usr.list_denied_tags()
|
elementlist = usr.list_denied_tags()
|
||||||
elementlist[int(element['id'][1:])]=element['Element']
|
elementlist[int(element['id'][1:])]=element['Element']
|
||||||
usr.denied_tags = ','.join(elementlist)
|
usr.denied_tags = ','.join(elementlist)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
if res_type == 3: # CColumn per user
|
if res_type == 3: # CColumn per user
|
||||||
usr_id = os.path.split(request.referrer)[-1]
|
if isinstance(user_id, int):
|
||||||
if usr_id.isdigit() == True:
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
|
||||||
else:
|
else:
|
||||||
usr = current_user
|
usr = current_user
|
||||||
elementlist = usr.list_denied_column_values()
|
elementlist = usr.list_denied_column_values()
|
||||||
elementlist[int(element['id'][1:])]=element['Element']
|
elementlist[int(element['id'][1:])]=element['Element']
|
||||||
usr.denied_column_value = ','.join(elementlist)
|
usr.denied_column_value = ','.join(elementlist)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def restriction_addition(element, list_func):
|
def restriction_addition(element, list_func):
|
||||||
@ -335,10 +424,11 @@ def restriction_deletion(element, list_func):
|
|||||||
return ','.join(elementlist)
|
return ','.join(elementlist)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/addrestriction/<int:res_type>", methods=['POST'])
|
@admi.route("/ajax/addrestriction/<int:res_type>", defaults={"user_id":0}, methods=['POST'])
|
||||||
|
@admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def add_restriction(res_type):
|
def add_restriction(res_type, user_id):
|
||||||
element = request.form.to_dict()
|
element = request.form.to_dict()
|
||||||
if res_type == 0: # Tags as template
|
if res_type == 0: # Tags as template
|
||||||
if 'submit_allow' in element:
|
if 'submit_allow' in element:
|
||||||
@ -355,35 +445,46 @@ def add_restriction(res_type):
|
|||||||
config.config_denied_column_value = restriction_addition(element, config.list_allowed_column_values)
|
config.config_denied_column_value = restriction_addition(element, config.list_allowed_column_values)
|
||||||
config.save()
|
config.save()
|
||||||
if res_type == 2: # Tags per user
|
if res_type == 2: # Tags per user
|
||||||
usr_id = os.path.split(request.referrer)[-1]
|
if isinstance(user_id, int):
|
||||||
if usr_id.isdigit() == True:
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
|
||||||
else:
|
else:
|
||||||
usr = current_user
|
usr = current_user
|
||||||
if 'submit_allow' in element:
|
if 'submit_allow' in element:
|
||||||
usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags)
|
usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
elif 'submit_deny' in element:
|
elif 'submit_deny' in element:
|
||||||
usr.denied_tags = restriction_addition(element, usr.list_denied_tags)
|
usr.denied_tags = restriction_addition(element, usr.list_denied_tags)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
if res_type == 3: # CustomC per user
|
if res_type == 3: # CustomC per user
|
||||||
usr_id = os.path.split(request.referrer)[-1]
|
if isinstance(user_id, int):
|
||||||
if usr_id.isdigit() == True:
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
|
||||||
else:
|
else:
|
||||||
usr = current_user
|
usr = current_user
|
||||||
if 'submit_allow' in element:
|
if 'submit_allow' in element:
|
||||||
usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values)
|
usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
elif 'submit_deny' in element:
|
elif 'submit_deny' in element:
|
||||||
usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values)
|
usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@admi.route("/ajax/deleterestriction/<int:res_type>", methods=['POST'])
|
@admi.route("/ajax/deleterestriction/<int:res_type>", defaults={"user_id":0}, methods=['POST'])
|
||||||
|
@admi.route("/ajax/deleterestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete_restriction(res_type):
|
def delete_restriction(res_type, user_id):
|
||||||
element = request.form.to_dict()
|
element = request.form.to_dict()
|
||||||
if res_type == 0: # Tags as template
|
if res_type == 0: # Tags as template
|
||||||
if element['id'].startswith('a'):
|
if element['id'].startswith('a'):
|
||||||
@ -400,36 +501,46 @@ def delete_restriction(res_type):
|
|||||||
config.config_denied_column_value = restriction_deletion(element, config.list_denied_column_values)
|
config.config_denied_column_value = restriction_deletion(element, config.list_denied_column_values)
|
||||||
config.save()
|
config.save()
|
||||||
elif res_type == 2: # Tags per user
|
elif res_type == 2: # Tags per user
|
||||||
usr_id = os.path.split(request.referrer)[-1]
|
if isinstance(user_id, int):
|
||||||
if usr_id.isdigit() == True:
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
|
||||||
else:
|
else:
|
||||||
usr = current_user
|
usr = current_user
|
||||||
if element['id'].startswith('a'):
|
if element['id'].startswith('a'):
|
||||||
usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags)
|
usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
elif element['id'].startswith('d'):
|
elif element['id'].startswith('d'):
|
||||||
usr.denied_tags = restriction_deletion(element, usr.list_denied_tags)
|
usr.denied_tags = restriction_deletion(element, usr.list_denied_tags)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
elif res_type == 3: # Columns per user
|
elif res_type == 3: # Columns per user
|
||||||
usr_id = os.path.split(request.referrer)[-1]
|
if isinstance(user_id, int):
|
||||||
if usr_id.isdigit() == True: # select current user if admins are editing their own rights
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first()
|
|
||||||
else:
|
else:
|
||||||
usr = current_user
|
usr = current_user
|
||||||
if element['id'].startswith('a'):
|
if element['id'].startswith('a'):
|
||||||
usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values)
|
usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
elif element['id'].startswith('d'):
|
elif element['id'].startswith('d'):
|
||||||
usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values)
|
usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@admi.route("/ajax/listrestriction/<int:res_type>", defaults={"user_id":0})
|
||||||
@admi.route("/ajax/listrestriction/<int:res_type>")
|
@admi.route("/ajax/listrestriction/<int:res_type>/<int:user_id>")
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def list_restriction(res_type):
|
def list_restriction(res_type, user_id):
|
||||||
if res_type == 0: # Tags as template
|
if res_type == 0: # Tags as template
|
||||||
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
|
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
|
||||||
for i,x in enumerate(config.list_denied_tags()) if x != '' ]
|
for i,x in enumerate(config.list_denied_tags()) if x != '' ]
|
||||||
@ -443,9 +554,8 @@ def list_restriction(res_type):
|
|||||||
for i,x in enumerate(config.list_allowed_column_values()) if x != '']
|
for i,x in enumerate(config.list_allowed_column_values()) if x != '']
|
||||||
json_dumps = restrict + allow
|
json_dumps = restrict + allow
|
||||||
elif res_type == 2: # Tags per user
|
elif res_type == 2: # Tags per user
|
||||||
usr_id = os.path.split(request.referrer)[-1]
|
if isinstance(user_id, int):
|
||||||
if usr_id.isdigit() == True:
|
usr = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
|
||||||
usr = ub.session.query(ub.User).filter(ub.User.id == usr_id).first()
|
|
||||||
else:
|
else:
|
||||||
usr = current_user
|
usr = current_user
|
||||||
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
|
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
|
||||||
@ -454,9 +564,8 @@ def list_restriction(res_type):
|
|||||||
for i,x in enumerate(usr.list_allowed_tags()) if x != '']
|
for i,x in enumerate(usr.list_allowed_tags()) if x != '']
|
||||||
json_dumps = restrict + allow
|
json_dumps = restrict + allow
|
||||||
elif res_type == 3: # CustomC per user
|
elif res_type == 3: # CustomC per user
|
||||||
usr_id = os.path.split(request.referrer)[-1]
|
if isinstance(user_id, int):
|
||||||
if usr_id.isdigit() == True:
|
usr = ub.session.query(ub.User).filter(ub.User.id==user_id).first()
|
||||||
usr = ub.session.query(ub.User).filter(ub.User.id==usr_id).first()
|
|
||||||
else:
|
else:
|
||||||
usr = current_user
|
usr = current_user
|
||||||
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
|
restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) }
|
||||||
@ -471,14 +580,108 @@ def list_restriction(res_type):
|
|||||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@admi.route("/basicconfig/pathchooser/")
|
||||||
|
@unconfigured
|
||||||
|
def config_pathchooser():
|
||||||
|
if filepicker:
|
||||||
|
return pathchooser()
|
||||||
|
abort(403)
|
||||||
|
|
||||||
@admi.route("/config", methods=["GET", "POST"])
|
@admi.route("/ajax/pathchooser/")
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def ajax_pathchooser():
|
||||||
|
return pathchooser()
|
||||||
|
|
||||||
|
def pathchooser():
|
||||||
|
browse_for = "folder"
|
||||||
|
folder_only = request.args.get('folder', False) == "true"
|
||||||
|
file_filter = request.args.get('filter', "")
|
||||||
|
path = os.path.normpath(request.args.get('path', ""))
|
||||||
|
|
||||||
|
if os.path.isfile(path):
|
||||||
|
oldfile = path
|
||||||
|
path = os.path.dirname(path)
|
||||||
|
else:
|
||||||
|
oldfile = ""
|
||||||
|
|
||||||
|
abs = False
|
||||||
|
|
||||||
|
if os.path.isdir(path):
|
||||||
|
#if os.path.isabs(path):
|
||||||
|
cwd = os.path.realpath(path)
|
||||||
|
abs = True
|
||||||
|
#else:
|
||||||
|
# cwd = os.path.relpath(path)
|
||||||
|
else:
|
||||||
|
cwd = os.getcwd()
|
||||||
|
|
||||||
|
cwd = os.path.normpath(os.path.realpath(cwd))
|
||||||
|
parentdir = os.path.dirname(cwd)
|
||||||
|
if not abs:
|
||||||
|
if os.path.realpath(cwd) == os.path.realpath("/"):
|
||||||
|
cwd = os.path.relpath(cwd)
|
||||||
|
else:
|
||||||
|
cwd = os.path.relpath(cwd) + os.path.sep
|
||||||
|
parentdir = os.path.relpath(parentdir) + os.path.sep
|
||||||
|
|
||||||
|
if os.path.realpath(cwd) == os.path.realpath("/"):
|
||||||
|
parentdir = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
folders = os.listdir(cwd)
|
||||||
|
except Exception:
|
||||||
|
folders = []
|
||||||
|
|
||||||
|
files = []
|
||||||
|
# locale = get_locale()
|
||||||
|
for f in folders:
|
||||||
|
try:
|
||||||
|
data = {"name": f, "fullpath": os.path.join(cwd, f)}
|
||||||
|
data["sort"] = data["fullpath"].lower()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if os.path.isfile(os.path.join(cwd, f)):
|
||||||
|
if folder_only:
|
||||||
|
continue
|
||||||
|
if file_filter != "" and file_filter != f:
|
||||||
|
continue
|
||||||
|
data["type"] = "file"
|
||||||
|
data["size"] = os.path.getsize(os.path.join(cwd, f))
|
||||||
|
|
||||||
|
power = 0
|
||||||
|
while (data["size"] >> 10) > 0.3:
|
||||||
|
power += 1
|
||||||
|
data["size"] >>= 10
|
||||||
|
units = ("", "K", "M", "G", "T")
|
||||||
|
data["size"] = str(data["size"]) + " " + units[power] + "Byte"
|
||||||
|
else:
|
||||||
|
data["type"] = "dir"
|
||||||
|
data["size"] = ""
|
||||||
|
|
||||||
|
files.append(data)
|
||||||
|
|
||||||
|
files = sorted(files, key=operator.itemgetter("type", "sort"))
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"cwd": cwd,
|
||||||
|
"files": files,
|
||||||
|
"parentdir": parentdir,
|
||||||
|
"type": browse_for,
|
||||||
|
"oldfile": oldfile,
|
||||||
|
"absolute": abs,
|
||||||
|
}
|
||||||
|
return json.dumps(context)
|
||||||
|
|
||||||
|
|
||||||
|
@admi.route("/basicconfig", methods=["GET", "POST"])
|
||||||
@unconfigured
|
@unconfigured
|
||||||
def basic_configuration():
|
def basic_configuration():
|
||||||
logout_user()
|
logout_user()
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
return _configuration_update_helper()
|
return _configuration_update_helper(configured=filepicker)
|
||||||
return _configuration_result()
|
return _configuration_result(configured=filepicker)
|
||||||
|
|
||||||
|
|
||||||
def _config_int(to_save, x, func=int):
|
def _config_int(to_save, x, func=int):
|
||||||
@ -633,13 +836,13 @@ def _configuration_ldap_helper(to_save, gdriveError):
|
|||||||
return reboot_required, None
|
return reboot_required, None
|
||||||
|
|
||||||
|
|
||||||
def _configuration_update_helper():
|
def _configuration_update_helper(configured):
|
||||||
reboot_required = False
|
reboot_required = False
|
||||||
db_change = False
|
db_change = False
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
gdriveError = None
|
gdriveError = None
|
||||||
|
|
||||||
to_save['config_calibre_dir'] = re.sub('[\\/]metadata\.db$',
|
to_save['config_calibre_dir'] = re.sub(r'[\\/]metadata\.db$',
|
||||||
'',
|
'',
|
||||||
to_save['config_calibre_dir'],
|
to_save['config_calibre_dir'],
|
||||||
flags=re.IGNORECASE)
|
flags=re.IGNORECASE)
|
||||||
@ -653,11 +856,15 @@ def _configuration_update_helper():
|
|||||||
|
|
||||||
reboot_required |= _config_string(to_save, "config_keyfile")
|
reboot_required |= _config_string(to_save, "config_keyfile")
|
||||||
if config.config_keyfile and not os.path.isfile(config.config_keyfile):
|
if config.config_keyfile and not os.path.isfile(config.config_keyfile):
|
||||||
return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'), gdriveError)
|
return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'),
|
||||||
|
gdriveError,
|
||||||
|
configured)
|
||||||
|
|
||||||
reboot_required |= _config_string(to_save, "config_certfile")
|
reboot_required |= _config_string(to_save, "config_certfile")
|
||||||
if config.config_certfile and not os.path.isfile(config.config_certfile):
|
if config.config_certfile and not os.path.isfile(config.config_certfile):
|
||||||
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'), gdriveError)
|
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'),
|
||||||
|
gdriveError,
|
||||||
|
configured)
|
||||||
|
|
||||||
_config_checkbox_int(to_save, "config_uploading")
|
_config_checkbox_int(to_save, "config_uploading")
|
||||||
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
|
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
|
||||||
@ -722,10 +929,10 @@ def _configuration_update_helper():
|
|||||||
if "config_rarfile_location" in to_save:
|
if "config_rarfile_location" in to_save:
|
||||||
unrar_status = helper.check_unrar(config.config_rarfile_location)
|
unrar_status = helper.check_unrar(config.config_rarfile_location)
|
||||||
if unrar_status:
|
if unrar_status:
|
||||||
return _configuration_result(unrar_status, gdriveError)
|
return _configuration_result(unrar_status, gdriveError, configured)
|
||||||
except (OperationalError, InvalidRequestError):
|
except (OperationalError, InvalidRequestError):
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
_configuration_result(_(u"Settings DB is not Writeable"), gdriveError)
|
_configuration_result(_(u"Settings DB is not Writeable"), gdriveError, configured)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
metadata_db = os.path.join(config.config_calibre_dir, "metadata.db")
|
metadata_db = os.path.join(config.config_calibre_dir, "metadata.db")
|
||||||
@ -733,11 +940,13 @@ def _configuration_update_helper():
|
|||||||
gdriveutils.downloadFile(None, "metadata.db", metadata_db)
|
gdriveutils.downloadFile(None, "metadata.db", metadata_db)
|
||||||
db_change = True
|
db_change = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return _configuration_result('%s' % e, gdriveError)
|
return _configuration_result('%s' % e, gdriveError, configured)
|
||||||
|
|
||||||
if db_change:
|
if db_change:
|
||||||
if not calibre_db.setup_db(config, ub.app_DB_path):
|
if not calibre_db.setup_db(config, ub.app_DB_path):
|
||||||
return _configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdriveError)
|
return _configuration_result(_('DB Location is not Valid, Please Enter Correct Path'),
|
||||||
|
gdriveError,
|
||||||
|
configured)
|
||||||
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
|
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
|
||||||
flash(_(u"DB is not Writeable"), category="warning")
|
flash(_(u"DB is not Writeable"), category="warning")
|
||||||
|
|
||||||
@ -746,10 +955,10 @@ def _configuration_update_helper():
|
|||||||
if reboot_required:
|
if reboot_required:
|
||||||
web_server.stop(True)
|
web_server.stop(True)
|
||||||
|
|
||||||
return _configuration_result(None, gdriveError)
|
return _configuration_result(None, gdriveError, configured)
|
||||||
|
|
||||||
|
|
||||||
def _configuration_result(error_flash=None, gdriveError=None):
|
def _configuration_result(error_flash=None, gdriveError=None, configured=True):
|
||||||
gdrive_authenticate = not is_gdrive_ready()
|
gdrive_authenticate = not is_gdrive_ready()
|
||||||
gdrivefolders = []
|
gdrivefolders = []
|
||||||
if gdriveError is None:
|
if gdriveError is None:
|
||||||
@ -770,7 +979,7 @@ def _configuration_result(error_flash=None, gdriveError=None):
|
|||||||
|
|
||||||
return render_title_template("config_edit.html", config=config, provider=oauthblueprints,
|
return render_title_template("config_edit.html", config=config, provider=oauthblueprints,
|
||||||
show_back_button=show_back_button, show_login_button=show_login_button,
|
show_back_button=show_back_button, show_login_button=show_login_button,
|
||||||
show_authenticate_google_drive=gdrive_authenticate,
|
show_authenticate_google_drive=gdrive_authenticate, filepicker=configured,
|
||||||
gdriveError=gdriveError, gdrivefolders=gdrivefolders, feature_support=feature_support,
|
gdriveError=gdriveError, gdrivefolders=gdrivefolders, feature_support=feature_support,
|
||||||
title=_(u"Basic Configuration"), page="config")
|
title=_(u"Basic Configuration"), page="config")
|
||||||
|
|
||||||
@ -816,7 +1025,10 @@ def _handle_new_user(to_save, content,languages, translations, kobo_support):
|
|||||||
content.allowed_column_value = config.config_allowed_column_value
|
content.allowed_column_value = config.config_allowed_column_value
|
||||||
content.denied_column_value = config.config_denied_column_value
|
content.denied_column_value = config.config_denied_column_value
|
||||||
ub.session.add(content)
|
ub.session.add(content)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
flash(_(u"User '%(user)s' created", user=content.nickname), category="success")
|
flash(_(u"User '%(user)s' created", user=content.nickname), category="success")
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin'))
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
@ -832,7 +1044,10 @@ def _handle_edit_user(to_save, content,languages, translations, kobo_support):
|
|||||||
if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
|
if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
|
||||||
ub.User.id != content.id).count():
|
ub.User.id != content.id).count():
|
||||||
ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
|
ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success")
|
flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success")
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin'))
|
||||||
else:
|
else:
|
||||||
@ -855,7 +1070,7 @@ def _handle_edit_user(to_save, content,languages, translations, kobo_support):
|
|||||||
content.role &= ~constants.ROLE_ANONYMOUS
|
content.role &= ~constants.ROLE_ANONYMOUS
|
||||||
|
|
||||||
val = [int(k[5:]) for k in to_save if k.startswith('show_')]
|
val = [int(k[5:]) for k in to_save if k.startswith('show_')]
|
||||||
sidebar = ub.get_sidebar_config()
|
sidebar = get_sidebar_config()
|
||||||
for element in sidebar:
|
for element in sidebar:
|
||||||
value = element['visibility']
|
value = element['visibility']
|
||||||
if value in val and not content.check_visibility(value):
|
if value in val and not content.check_visibility(value):
|
||||||
@ -907,7 +1122,10 @@ def _handle_edit_user(to_save, content,languages, translations, kobo_support):
|
|||||||
if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail:
|
if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail:
|
||||||
content.kindle_mail = to_save["kindle_mail"]
|
content.kindle_mail = to_save["kindle_mail"]
|
||||||
try:
|
try:
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success")
|
flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success")
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
@ -1119,3 +1337,110 @@ def get_updater_status():
|
|||||||
except Exception:
|
except Exception:
|
||||||
status['status'] = 11
|
status['status'] = 11
|
||||||
return json.dumps(status)
|
return json.dumps(status)
|
||||||
|
|
||||||
|
|
||||||
|
@admi.route('/import_ldap_users')
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def import_ldap_users():
|
||||||
|
showtext = {}
|
||||||
|
try:
|
||||||
|
new_users = services.ldap.get_group_members(config.config_ldap_group_name)
|
||||||
|
except (services.ldap.LDAPException, TypeError, AttributeError, KeyError) as e:
|
||||||
|
log.debug_or_exception(e)
|
||||||
|
showtext['text'] = _(u'Error: %(ldaperror)s', ldaperror=e)
|
||||||
|
return json.dumps(showtext)
|
||||||
|
if not new_users:
|
||||||
|
log.debug('LDAP empty response')
|
||||||
|
showtext['text'] = _(u'Error: No user returned in response of LDAP server')
|
||||||
|
return json.dumps(showtext)
|
||||||
|
|
||||||
|
imported = 0
|
||||||
|
for username in new_users:
|
||||||
|
user = username.decode('utf-8')
|
||||||
|
if '=' in user:
|
||||||
|
# if member object field is empty take user object as filter
|
||||||
|
if config.config_ldap_member_user_object:
|
||||||
|
query_filter = config.config_ldap_member_user_object
|
||||||
|
else:
|
||||||
|
query_filter = config.config_ldap_user_object
|
||||||
|
try:
|
||||||
|
user_identifier = extract_user_identifier(user, query_filter)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(e)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
user_identifier = user
|
||||||
|
query_filter = None
|
||||||
|
try:
|
||||||
|
user_data = services.ldap.get_object_details(user=user_identifier, query_filter=query_filter)
|
||||||
|
except AttributeError as e:
|
||||||
|
log.debug_or_exception(e)
|
||||||
|
continue
|
||||||
|
if user_data:
|
||||||
|
user_login_field = extract_dynamic_field_from_filter(user, config.config_ldap_user_object)
|
||||||
|
|
||||||
|
username = user_data[user_login_field][0].decode('utf-8')
|
||||||
|
# check for duplicate username
|
||||||
|
if ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first():
|
||||||
|
# if ub.session.query(ub.User).filter(ub.User.nickname == username).first():
|
||||||
|
log.warning("LDAP User %s Already in Database", user_data)
|
||||||
|
continue
|
||||||
|
|
||||||
|
kindlemail = ''
|
||||||
|
if 'mail' in user_data:
|
||||||
|
useremail = user_data['mail'][0].decode('utf-8')
|
||||||
|
if (len(user_data['mail']) > 1):
|
||||||
|
kindlemail = user_data['mail'][1].decode('utf-8')
|
||||||
|
|
||||||
|
else:
|
||||||
|
log.debug('No Mail Field Found in LDAP Response')
|
||||||
|
useremail = username + '@email.com'
|
||||||
|
# check for duplicate email
|
||||||
|
if ub.session.query(ub.User).filter(func.lower(ub.User.email) == useremail.lower()).first():
|
||||||
|
log.warning("LDAP Email %s Already in Database", user_data)
|
||||||
|
continue
|
||||||
|
content = ub.User()
|
||||||
|
content.nickname = username
|
||||||
|
content.password = '' # dummy password which will be replaced by ldap one
|
||||||
|
content.email = useremail
|
||||||
|
content.kindle_mail = kindlemail
|
||||||
|
content.role = config.config_default_role
|
||||||
|
content.sidebar_view = config.config_default_show
|
||||||
|
content.allowed_tags = config.config_allowed_tags
|
||||||
|
content.denied_tags = config.config_denied_tags
|
||||||
|
content.allowed_column_value = config.config_allowed_column_value
|
||||||
|
content.denied_column_value = config.config_denied_column_value
|
||||||
|
ub.session.add(content)
|
||||||
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
imported +=1
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("Failed to create LDAP user: %s - %s", user, e)
|
||||||
|
ub.session.rollback()
|
||||||
|
showtext['text'] = _(u'Failed to Create at Least One LDAP User')
|
||||||
|
else:
|
||||||
|
log.warning("LDAP User: %s Not Found", user)
|
||||||
|
showtext['text'] = _(u'At Least One LDAP User Not Found in Database')
|
||||||
|
if not showtext:
|
||||||
|
showtext['text'] = _(u'{} User Successfully Imported'.format(imported))
|
||||||
|
return json.dumps(showtext)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_user_data_from_field(user, field):
|
||||||
|
match = re.search(field + "=([\d\s\w-]+)", user, re.IGNORECASE | re.UNICODE)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
else:
|
||||||
|
raise Exception("Could Not Parse LDAP User: {}".format(user))
|
||||||
|
|
||||||
|
def extract_dynamic_field_from_filter(user, filter):
|
||||||
|
match = re.search("([a-zA-Z0-9-]+)=%s", filter, re.IGNORECASE | re.UNICODE)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
else:
|
||||||
|
raise Exception("Could Not Parse LDAP Userfield: {}", user)
|
||||||
|
|
||||||
|
def extract_user_identifier(user, filter):
|
||||||
|
dynamic_field = extract_dynamic_field_from_filter(user, filter)
|
||||||
|
return extract_user_data_from_field(user, dynamic_field)
|
||||||
|
@ -45,6 +45,7 @@ parser.add_argument('-v', '--version', action='version', help='Shows version num
|
|||||||
version=version_info())
|
version=version_info())
|
||||||
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
|
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
|
||||||
parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password')
|
parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password')
|
||||||
|
parser.add_argument('-f', action='store_true', help='Enables filepicker in unconfigured mode')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if sys.version_info < (3, 0):
|
if sys.version_info < (3, 0):
|
||||||
@ -110,3 +111,6 @@ if ipadress:
|
|||||||
|
|
||||||
# handle and check user password argument
|
# handle and check user password argument
|
||||||
user_password = args.s or None
|
user_password = args.s or None
|
||||||
|
|
||||||
|
# Handles enableing of filepicker
|
||||||
|
filepicker = args.f or None
|
||||||
|
41
cps/comic.py
41
cps/comic.py
@ -18,21 +18,21 @@
|
|||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
from __future__ import division, print_function, unicode_literals
|
||||||
import os
|
import os
|
||||||
import io
|
|
||||||
|
|
||||||
from . import logger, isoLanguages
|
from . import logger, isoLanguages
|
||||||
from .constants import BookMeta
|
from .constants import BookMeta
|
||||||
|
|
||||||
try:
|
|
||||||
from PIL import Image as PILImage
|
|
||||||
use_PIL = True
|
|
||||||
except ImportError as e:
|
|
||||||
use_PIL = False
|
|
||||||
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from wand.image import Image
|
||||||
|
use_IM = True
|
||||||
|
except (ImportError, RuntimeError) as e:
|
||||||
|
use_IM = False
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from comicapi.comicarchive import ComicArchive, MetaDataStyle
|
from comicapi.comicarchive import ComicArchive, MetaDataStyle
|
||||||
use_comic_meta = True
|
use_comic_meta = True
|
||||||
@ -52,20 +52,23 @@ except (ImportError, LookupError) as e:
|
|||||||
use_rarfile = False
|
use_rarfile = False
|
||||||
use_comic_meta = False
|
use_comic_meta = False
|
||||||
|
|
||||||
|
NO_JPEG_EXTENSIONS = ['.png', '.webp', '.bmp']
|
||||||
|
COVER_EXTENSIONS = ['.png', '.webp', '.bmp', '.jpg', '.jpeg']
|
||||||
|
|
||||||
def _cover_processing(tmp_file_name, img, extension):
|
def _cover_processing(tmp_file_name, img, extension):
|
||||||
if use_PIL:
|
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg')
|
||||||
|
if use_IM:
|
||||||
# convert to jpg because calibre only supports jpg
|
# convert to jpg because calibre only supports jpg
|
||||||
if extension in ('.png', '.webp'):
|
if extension in NO_JPEG_EXTENSIONS:
|
||||||
imgc = PILImage.open(io.BytesIO(img))
|
with Image(filename=tmp_file_name) as imgc:
|
||||||
im = imgc.convert('RGB')
|
imgc.format = 'jpeg'
|
||||||
tmp_bytesio = io.BytesIO()
|
imgc.transform_colorspace('rgb')
|
||||||
im.save(tmp_bytesio, format='JPEG')
|
imgc.save(tmp_cover_name)
|
||||||
img = tmp_bytesio.getvalue()
|
return tmp_cover_name
|
||||||
|
|
||||||
if not img:
|
if not img:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg')
|
|
||||||
with open(tmp_cover_name, 'wb') as f:
|
with open(tmp_cover_name, 'wb') as f:
|
||||||
f.write(img)
|
f.write(img)
|
||||||
return tmp_cover_name
|
return tmp_cover_name
|
||||||
@ -80,7 +83,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
|||||||
ext = os.path.splitext(name)
|
ext = os.path.splitext(name)
|
||||||
if len(ext) > 1:
|
if len(ext) > 1:
|
||||||
extension = ext[1].lower()
|
extension = ext[1].lower()
|
||||||
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
|
if extension in COVER_EXTENSIONS:
|
||||||
cover_data = archive.getPage(index)
|
cover_data = archive.getPage(index)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@ -90,7 +93,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
|||||||
ext = os.path.splitext(name)
|
ext = os.path.splitext(name)
|
||||||
if len(ext) > 1:
|
if len(ext) > 1:
|
||||||
extension = ext[1].lower()
|
extension = ext[1].lower()
|
||||||
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
|
if extension in COVER_EXTENSIONS:
|
||||||
cover_data = cf.read(name)
|
cover_data = cf.read(name)
|
||||||
break
|
break
|
||||||
elif original_file_extension.upper() == '.CBT':
|
elif original_file_extension.upper() == '.CBT':
|
||||||
@ -99,7 +102,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
|||||||
ext = os.path.splitext(name)
|
ext = os.path.splitext(name)
|
||||||
if len(ext) > 1:
|
if len(ext) > 1:
|
||||||
extension = ext[1].lower()
|
extension = ext[1].lower()
|
||||||
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
|
if extension in COVER_EXTENSIONS:
|
||||||
cover_data = cf.extractfile(name).read()
|
cover_data = cf.extractfile(name).read()
|
||||||
break
|
break
|
||||||
elif original_file_extension.upper() == '.CBR' and use_rarfile:
|
elif original_file_extension.upper() == '.CBR' and use_rarfile:
|
||||||
@ -110,7 +113,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
|||||||
ext = os.path.splitext(name)
|
ext = os.path.splitext(name)
|
||||||
if len(ext) > 1:
|
if len(ext) > 1:
|
||||||
extension = ext[1].lower()
|
extension = ext[1].lower()
|
||||||
if extension in ('.jpg', '.jpeg', '.png', '.webp'):
|
if extension in COVER_EXTENSIONS:
|
||||||
cover_data = cf.read(name)
|
cover_data = cf.read(name)
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -22,6 +22,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
|
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
|
||||||
|
from sqlalchemy.exc import OperationalError
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
from . import constants, cli, logger, ub
|
from . import constants, cli, logger, ub
|
||||||
@ -271,6 +272,14 @@ class _ConfigSQL(object):
|
|||||||
setattr(self, field, new_value)
|
setattr(self, field, new_value)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def toDict(self):
|
||||||
|
storage = {}
|
||||||
|
for k, v in self.__dict__.items():
|
||||||
|
if k[0] != '_' or k.endswith("password"):
|
||||||
|
storage[k] = v
|
||||||
|
return storage
|
||||||
|
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
'''Load all configuration values from the underlying storage.'''
|
'''Load all configuration values from the underlying storage.'''
|
||||||
s = self._read_from_storage() # type: _Settings
|
s = self._read_from_storage() # type: _Settings
|
||||||
@ -295,7 +304,11 @@ class _ConfigSQL(object):
|
|||||||
log.warning("Log path %s not valid, falling back to default", self.config_logfile)
|
log.warning("Log path %s not valid, falling back to default", self.config_logfile)
|
||||||
self.config_logfile = logfile
|
self.config_logfile = logfile
|
||||||
self._session.merge(s)
|
self._session.merge(s)
|
||||||
self._session.commit()
|
try:
|
||||||
|
self._session.commit()
|
||||||
|
except OperationalError as e:
|
||||||
|
log.error('Database error: %s', e)
|
||||||
|
self._session.rollback()
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
'''Apply all configuration values to the underlying storage.'''
|
'''Apply all configuration values to the underlying storage.'''
|
||||||
@ -309,7 +322,11 @@ class _ConfigSQL(object):
|
|||||||
|
|
||||||
log.debug("_ConfigSQL updating storage")
|
log.debug("_ConfigSQL updating storage")
|
||||||
self._session.merge(s)
|
self._session.merge(s)
|
||||||
self._session.commit()
|
try:
|
||||||
|
self._session.commit()
|
||||||
|
except OperationalError as e:
|
||||||
|
log.error('Database error: %s', e)
|
||||||
|
self._session.rollback()
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
def invalidate(self, error=None):
|
def invalidate(self, error=None):
|
||||||
@ -350,7 +367,10 @@ def _migrate_table(session, orm_class):
|
|||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
if changed:
|
if changed:
|
||||||
session.commit()
|
try:
|
||||||
|
session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
|
||||||
def autodetect_calibre_binary():
|
def autodetect_calibre_binary():
|
||||||
|
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.orm.collections import InstrumentedList
|
||||||
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
|
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
from flask_login import current_user
|
|
||||||
from sqlalchemy.sql.expression import and_, true, false, text, func, or_
|
from sqlalchemy.sql.expression import and_, true, false, text, func, or_
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
|
from flask_login import current_user
|
||||||
from babel import Locale as LC
|
from babel import Locale as LC
|
||||||
from babel.core import UnknownLocaleError
|
from babel.core import UnknownLocaleError
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
@ -425,18 +425,19 @@ class CalibreDB():
|
|||||||
# instances alive once they reach the end of their respective scopes
|
# instances alive once they reach the end of their respective scopes
|
||||||
instances = WeakSet()
|
instances = WeakSet()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, expire_on_commit=True):
|
||||||
""" Initialize a new CalibreDB session
|
""" Initialize a new CalibreDB session
|
||||||
"""
|
"""
|
||||||
self.session = None
|
self.session = None
|
||||||
if self._init:
|
if self._init:
|
||||||
self.initSession()
|
self.initSession(expire_on_commit)
|
||||||
|
|
||||||
self.instances.add(self)
|
self.instances.add(self)
|
||||||
|
|
||||||
|
|
||||||
def initSession(self):
|
def initSession(self, expire_on_commit=True):
|
||||||
self.session = self.session_factory()
|
self.session = self.session_factory()
|
||||||
|
self.session.expire_on_commit = expire_on_commit
|
||||||
self.update_title_sort(self.config)
|
self.update_title_sort(self.config)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -444,6 +445,8 @@ class CalibreDB():
|
|||||||
cls.config = config
|
cls.config = config
|
||||||
cls.dispose()
|
cls.dispose()
|
||||||
|
|
||||||
|
# toDo: if db changed -> delete shelfs, delete download books, delete read boks, kobo sync??
|
||||||
|
|
||||||
if not config.config_calibre_dir:
|
if not config.config_calibre_dir:
|
||||||
config.invalidate()
|
config.invalidate()
|
||||||
return False
|
return False
|
||||||
@ -764,5 +767,5 @@ def lcase(s):
|
|||||||
return unidecode.unidecode(s.lower())
|
return unidecode.unidecode(s.lower())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
log.exception(e)
|
log.debug_or_exception(e)
|
||||||
return s.lower()
|
return s.lower()
|
||||||
|
@ -44,8 +44,12 @@ def assemble_logfiles(file_name):
|
|||||||
def send_debug():
|
def send_debug():
|
||||||
file_list = glob.glob(logger.get_logfile(config.config_logfile) + '*')
|
file_list = glob.glob(logger.get_logfile(config.config_logfile) + '*')
|
||||||
file_list.extend(glob.glob(logger.get_accesslogfile(config.config_access_logfile) + '*'))
|
file_list.extend(glob.glob(logger.get_accesslogfile(config.config_access_logfile) + '*'))
|
||||||
|
for element in [logger.LOG_TO_STDOUT, logger.LOG_TO_STDERR]:
|
||||||
|
if element in file_list:
|
||||||
|
file_list.remove(element)
|
||||||
memory_zip = io.BytesIO()
|
memory_zip = io.BytesIO()
|
||||||
with zipfile.ZipFile(memory_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
|
with zipfile.ZipFile(memory_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
zf.writestr('settings.txt', json.dumps(config.toDict()))
|
||||||
zf.writestr('libs.txt', json.dumps(collect_stats()))
|
zf.writestr('libs.txt', json.dumps(collect_stats()))
|
||||||
for fp in file_list:
|
for fp in file_list:
|
||||||
zf.write(fp, os.path.basename(fp))
|
zf.write(fp, os.path.basename(fp))
|
||||||
|
@ -37,13 +37,38 @@ from . import config, get_locale, ub, db
|
|||||||
from . import calibre_db
|
from . import calibre_db
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
from .tasks.upload import TaskUpload
|
from .tasks.upload import TaskUpload
|
||||||
from .web import login_required_if_no_ano, render_title_template, edit_required, upload_required
|
from .render_template import render_title_template
|
||||||
|
from .usermanagement import login_required_if_no_ano
|
||||||
|
|
||||||
|
try:
|
||||||
|
from functools import wraps
|
||||||
|
except ImportError:
|
||||||
|
pass # We're not using Python 3
|
||||||
|
|
||||||
|
|
||||||
editbook = Blueprint('editbook', __name__)
|
editbook = Blueprint('editbook', __name__)
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
def upload_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
if current_user.role_upload() or current_user.role_admin():
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
def edit_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
if current_user.role_edit() or current_user.role_admin():
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
# Modifies different Database objects, first check if elements have to be added to database, than check
|
# Modifies different Database objects, first check if elements have to be added to database, than check
|
||||||
# if elements have to be deleted, because they are no longer used
|
# if elements have to be deleted, because they are no longer used
|
||||||
def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type):
|
def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type):
|
||||||
@ -259,7 +284,7 @@ def delete_book(book_id, book_format, jsonResponse):
|
|||||||
filter(db.Data.format == book_format).delete()
|
filter(db.Data.format == book_format).delete()
|
||||||
calibre_db.session.commit()
|
calibre_db.session.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.debug_or_exception(e)
|
||||||
calibre_db.session.rollback()
|
calibre_db.session.rollback()
|
||||||
else:
|
else:
|
||||||
# book not found
|
# book not found
|
||||||
@ -287,7 +312,7 @@ def delete_book(book_id, book_format, jsonResponse):
|
|||||||
def render_edit_book(book_id):
|
def render_edit_book(book_id):
|
||||||
calibre_db.update_title_sort(config)
|
calibre_db.update_title_sort(config)
|
||||||
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
||||||
book = calibre_db.get_filtered_book(book_id)
|
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||||
if not book:
|
if not book:
|
||||||
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
|
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
|
||||||
return redirect(url_for("web.index"))
|
return redirect(url_for("web.index"))
|
||||||
@ -716,7 +741,7 @@ def edit_book(book_id):
|
|||||||
flash(error, category="error")
|
flash(error, category="error")
|
||||||
return render_edit_book(book_id)
|
return render_edit_book(book_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.debug_or_exception(e)
|
||||||
calibre_db.session.rollback()
|
calibre_db.session.rollback()
|
||||||
flash(_("Error editing book, please check logfile for details"), category="error")
|
flash(_("Error editing book, please check logfile for details"), category="error")
|
||||||
return redirect(url_for('web.show_book', book_id=book.id))
|
return redirect(url_for('web.show_book', book_id=book.id))
|
||||||
|
73
cps/error_handler.py
Normal file
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 flask_login import login_required
|
||||||
|
|
||||||
from . import logger, gdriveutils, config, ub, calibre_db
|
from . import logger, gdriveutils, config, ub, calibre_db
|
||||||
from .web import admin_required
|
from .admin import admin_required
|
||||||
|
|
||||||
gdrive = Blueprint('gdrive', __name__)
|
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -50,7 +50,7 @@ current_milli_time = lambda: int(round(time() * 1000))
|
|||||||
gdrive_watch_callback_token = 'target=calibreweb-watch_files'
|
gdrive_watch_callback_token = 'target=calibreweb-watch_files'
|
||||||
|
|
||||||
|
|
||||||
@gdrive.route("/gdrive/authenticate")
|
@gdrive.route("/authenticate")
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def authenticate_google_drive():
|
def authenticate_google_drive():
|
||||||
@ -63,7 +63,7 @@ def authenticate_google_drive():
|
|||||||
return redirect(authUrl)
|
return redirect(authUrl)
|
||||||
|
|
||||||
|
|
||||||
@gdrive.route("/gdrive/callback")
|
@gdrive.route("/callback")
|
||||||
def google_drive_callback():
|
def google_drive_callback():
|
||||||
auth_code = request.args.get('code')
|
auth_code = request.args.get('code')
|
||||||
if not auth_code:
|
if not auth_code:
|
||||||
@ -77,18 +77,14 @@ def google_drive_callback():
|
|||||||
return redirect(url_for('admin.configuration'))
|
return redirect(url_for('admin.configuration'))
|
||||||
|
|
||||||
|
|
||||||
@gdrive.route("/gdrive/watch/subscribe")
|
@gdrive.route("/watch/subscribe")
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def watch_gdrive():
|
def watch_gdrive():
|
||||||
if not config.config_google_drive_watch_changes_response:
|
if not config.config_google_drive_watch_changes_response:
|
||||||
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
|
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
|
||||||
filedata = json.load(settings)
|
filedata = json.load(settings)
|
||||||
if filedata['web']['redirect_uris'][0].endswith('/'):
|
address = filedata['web']['redirect_uris'][0].rstrip('/').replace('/gdrive/callback', '/gdrive/watch/callback')
|
||||||
filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-((len('/gdrive/callback')+1))]
|
|
||||||
else:
|
|
||||||
filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-(len('/gdrive/callback'))]
|
|
||||||
address = '%s/gdrive/watch/callback' % filedata['web']['redirect_uris'][0]
|
|
||||||
notification_id = str(uuid4())
|
notification_id = str(uuid4())
|
||||||
try:
|
try:
|
||||||
result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id,
|
result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id,
|
||||||
@ -98,14 +94,15 @@ def watch_gdrive():
|
|||||||
except HttpError as e:
|
except HttpError as e:
|
||||||
reason=json.loads(e.content)['error']['errors'][0]
|
reason=json.loads(e.content)['error']['errors'][0]
|
||||||
if reason['reason'] == u'push.webhookUrlUnauthorized':
|
if reason['reason'] == u'push.webhookUrlUnauthorized':
|
||||||
flash(_(u'Callback domain is not verified, please follow steps to verify domain in google developer console'), category="error")
|
flash(_(u'Callback domain is not verified, '
|
||||||
|
u'please follow steps to verify domain in google developer console'), category="error")
|
||||||
else:
|
else:
|
||||||
flash(reason['message'], category="error")
|
flash(reason['message'], category="error")
|
||||||
|
|
||||||
return redirect(url_for('admin.configuration'))
|
return redirect(url_for('admin.configuration'))
|
||||||
|
|
||||||
|
|
||||||
@gdrive.route("/gdrive/watch/revoke")
|
@gdrive.route("/watch/revoke")
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def revoke_watch_gdrive():
|
def revoke_watch_gdrive():
|
||||||
@ -121,14 +118,14 @@ def revoke_watch_gdrive():
|
|||||||
return redirect(url_for('admin.configuration'))
|
return redirect(url_for('admin.configuration'))
|
||||||
|
|
||||||
|
|
||||||
@gdrive.route("/gdrive/watch/callback", methods=['GET', 'POST'])
|
@gdrive.route("/watch/callback", methods=['GET', 'POST'])
|
||||||
def on_received_watch_confirmation():
|
def on_received_watch_confirmation():
|
||||||
if not config.config_google_drive_watch_changes_response:
|
if not config.config_google_drive_watch_changes_response:
|
||||||
return ''
|
return ''
|
||||||
if request.headers.get('X-Goog-Channel-Token') != gdrive_watch_callback_token \
|
if request.headers.get('X-Goog-Channel-Token') != gdrive_watch_callback_token \
|
||||||
or request.headers.get('X-Goog-Resource-State') != 'change' \
|
or request.headers.get('X-Goog-Resource-State') != 'change' \
|
||||||
or not request.data:
|
or not request.data:
|
||||||
return '' # redirect(url_for('admin.configuration'))
|
return ''
|
||||||
|
|
||||||
log.debug('%r', request.headers)
|
log.debug('%r', request.headers)
|
||||||
log.debug('%r', request.data)
|
log.debug('%r', request.data)
|
||||||
@ -146,15 +143,18 @@ def on_received_watch_confirmation():
|
|||||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
|
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
|
||||||
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
||||||
and response['file']['md5Checksum'] != hashlib.md5(dbpath):
|
and response['file']['md5Checksum'] != hashlib.md5(dbpath):
|
||||||
tmpDir = tempfile.gettempdir()
|
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
|
||||||
|
if not os.path.isdir(tmp_dir):
|
||||||
|
os.mkdir(tmp_dir)
|
||||||
|
|
||||||
log.info('Database file updated')
|
log.info('Database file updated')
|
||||||
copyfile(dbpath, os.path.join(tmpDir, "metadata.db_" + str(current_milli_time())))
|
copyfile(dbpath, os.path.join(tmp_dir, "metadata.db_" + str(current_milli_time())))
|
||||||
log.info('Backing up existing and downloading updated metadata.db')
|
log.info('Backing up existing and downloading updated metadata.db')
|
||||||
gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmpDir, "tmp_metadata.db"))
|
gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmp_dir, "tmp_metadata.db"))
|
||||||
log.info('Setting up new DB')
|
log.info('Setting up new DB')
|
||||||
# prevent error on windows, as os.rename does on exisiting files
|
# prevent error on windows, as os.rename does on existing files, also allow cross hdd move
|
||||||
move(os.path.join(tmpDir, "tmp_metadata.db"), dbpath)
|
move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath)
|
||||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.debug_or_exception(e)
|
||||||
return ''
|
return ''
|
||||||
|
@ -32,16 +32,25 @@ from sqlalchemy.ext.declarative import declarative_base
|
|||||||
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pydrive.auth import GoogleAuth
|
|
||||||
from pydrive.drive import GoogleDrive
|
|
||||||
from pydrive.auth import RefreshError
|
|
||||||
from apiclient import errors
|
from apiclient import errors
|
||||||
from httplib2 import ServerNotFoundError
|
from httplib2 import ServerNotFoundError
|
||||||
gdrive_support = True
|
|
||||||
importError = None
|
importError = None
|
||||||
except ImportError as err:
|
gdrive_support = True
|
||||||
importError = err
|
except ImportError as e:
|
||||||
|
importError = e
|
||||||
gdrive_support = False
|
gdrive_support = False
|
||||||
|
try:
|
||||||
|
from pydrive2.auth import GoogleAuth
|
||||||
|
from pydrive2.drive import GoogleDrive
|
||||||
|
from pydrive2.auth import RefreshError
|
||||||
|
except ImportError as err:
|
||||||
|
try:
|
||||||
|
from pydrive.auth import GoogleAuth
|
||||||
|
from pydrive.drive import GoogleDrive
|
||||||
|
from pydrive.auth import RefreshError
|
||||||
|
except ImportError as err:
|
||||||
|
importError = err
|
||||||
|
gdrive_support = False
|
||||||
|
|
||||||
from . import logger, cli, config
|
from . import logger, cli, config
|
||||||
from .constants import CONFIG_DIR as _CONFIG_DIR
|
from .constants import CONFIG_DIR as _CONFIG_DIR
|
||||||
@ -91,7 +100,7 @@ class Singleton:
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
self._instance = self._decorated()
|
self._instance = self._decorated()
|
||||||
return self._instance
|
return self._instance
|
||||||
except ImportError as e:
|
except (ImportError, NameError) as e:
|
||||||
log.debug(e)
|
log.debug(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -190,7 +199,7 @@ def getDrive(drive=None, gauth=None):
|
|||||||
except RefreshError as e:
|
except RefreshError as e:
|
||||||
log.error("Google Drive error: %s", e)
|
log.error("Google Drive error: %s", e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.debug_or_exception(e)
|
||||||
else:
|
else:
|
||||||
# Initialize the saved creds
|
# Initialize the saved creds
|
||||||
gauth.Authorize()
|
gauth.Authorize()
|
||||||
@ -208,7 +217,7 @@ def listRootFolders():
|
|||||||
drive = getDrive(Gdrive.Instance().drive)
|
drive = getDrive(Gdrive.Instance().drive)
|
||||||
folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false"
|
folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false"
|
||||||
fileList = drive.ListFile({'q': folder}).GetList()
|
fileList = drive.ListFile({'q': folder}).GetList()
|
||||||
except ServerNotFoundError as e:
|
except (ServerNotFoundError, ssl.SSLError) as e:
|
||||||
log.info("GDrive Error %s" % e)
|
log.info("GDrive Error %s" % e)
|
||||||
fileList = []
|
fileList = []
|
||||||
return fileList
|
return fileList
|
||||||
@ -547,21 +556,24 @@ def partial(total_byte_len, part_size_limit):
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
# downloads files in chunks from gdrive
|
# downloads files in chunks from gdrive
|
||||||
def do_gdrive_download(df, headers):
|
def do_gdrive_download(df, headers, convert_encoding=False):
|
||||||
total_size = int(df.metadata.get('fileSize'))
|
total_size = int(df.metadata.get('fileSize'))
|
||||||
download_url = df.metadata.get('downloadUrl')
|
download_url = df.metadata.get('downloadUrl')
|
||||||
s = partial(total_size, 1024 * 1024) # I'm downloading BIG files, so 100M chunk size is fine for me
|
s = partial(total_size, 1024 * 1024) # I'm downloading BIG files, so 100M chunk size is fine for me
|
||||||
|
|
||||||
def stream():
|
def stream(convert_encoding):
|
||||||
for byte in s:
|
for byte in s:
|
||||||
headers = {"Range": 'bytes=%s-%s' % (byte[0], byte[1])}
|
headers = {"Range": 'bytes=%s-%s' % (byte[0], byte[1])}
|
||||||
resp, content = df.auth.Get_Http_Object().request(download_url, headers=headers)
|
resp, content = df.auth.Get_Http_Object().request(download_url, headers=headers)
|
||||||
if resp.status == 206:
|
if resp.status == 206:
|
||||||
|
if convert_encoding:
|
||||||
|
result = chardet.detect(content)
|
||||||
|
content = content.decode(result['encoding']).encode('utf-8')
|
||||||
yield content
|
yield content
|
||||||
else:
|
else:
|
||||||
log.warning('An error occurred: %s', resp)
|
log.warning('An error occurred: %s', resp)
|
||||||
return
|
return
|
||||||
return Response(stream_with_context(stream()), headers=headers)
|
return Response(stream_with_context(stream(convert_encoding)), headers=headers)
|
||||||
|
|
||||||
|
|
||||||
_SETTINGS_YAML_TEMPLATE = """
|
_SETTINGS_YAML_TEMPLATE = """
|
||||||
|
@ -24,10 +24,7 @@ import io
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import glob
|
|
||||||
import time
|
import time
|
||||||
import zipfile
|
|
||||||
import json
|
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from tempfile import gettempdir
|
from tempfile import gettempdir
|
||||||
@ -53,13 +50,6 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
use_unidecode = False
|
use_unidecode = False
|
||||||
|
|
||||||
try:
|
|
||||||
from PIL import Image as PILImage
|
|
||||||
from PIL import UnidentifiedImageError
|
|
||||||
use_PIL = True
|
|
||||||
except ImportError:
|
|
||||||
use_PIL = False
|
|
||||||
|
|
||||||
from . import calibre_db
|
from . import calibre_db
|
||||||
from .tasks.convert import TaskConvert
|
from .tasks.convert import TaskConvert
|
||||||
from . import logger, config, get_locale, db, ub
|
from . import logger, config, get_locale, db, ub
|
||||||
@ -69,9 +59,16 @@ from .subproc_wrapper import process_wait
|
|||||||
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS
|
from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS
|
||||||
from .tasks.mail import TaskEmail
|
from .tasks.mail import TaskEmail
|
||||||
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from wand.image import Image
|
||||||
|
from wand.exceptions import MissingDelegateError
|
||||||
|
use_IM = True
|
||||||
|
except (ImportError, RuntimeError) as e:
|
||||||
|
log.debug('Cannot import Image, generating covers from non jpg files will not work: %s', e)
|
||||||
|
use_IM = False
|
||||||
|
|
||||||
|
|
||||||
# Convert existing book entry to new format
|
# Convert existing book entry to new format
|
||||||
def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, user_id, kindle_mail=None):
|
def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, user_id, kindle_mail=None):
|
||||||
@ -112,21 +109,21 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
|
|||||||
def send_test_mail(kindle_mail, user_name):
|
def send_test_mail(kindle_mail, user_name):
|
||||||
WorkerThread.add(user_name, TaskEmail(_(u'Calibre-Web test e-mail'), None, None,
|
WorkerThread.add(user_name, TaskEmail(_(u'Calibre-Web test e-mail'), None, None,
|
||||||
config.get_mail_settings(), kindle_mail, _(u"Test e-mail"),
|
config.get_mail_settings(), kindle_mail, _(u"Test e-mail"),
|
||||||
_(u'This e-mail has been sent via Calibre-Web.')))
|
_(u'This e-mail has been sent via Calibre-Web.')))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
# Send registration email or password reset email, depending on parameter resend (False means welcome email)
|
# Send registration email or password reset email, depending on parameter resend (False means welcome email)
|
||||||
def send_registration_mail(e_mail, user_name, default_password, resend=False):
|
def send_registration_mail(e_mail, user_name, default_password, resend=False):
|
||||||
text = "Hello %s!\r\n" % user_name
|
txt = "Hello %s!\r\n" % user_name
|
||||||
if not resend:
|
if not resend:
|
||||||
text += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n"
|
txt += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n"
|
||||||
text += "Please log in to your account using the following informations:\r\n"
|
txt += "Please log in to your account using the following informations:\r\n"
|
||||||
text += "User name: %s\r\n" % user_name
|
txt += "User name: %s\r\n" % user_name
|
||||||
text += "Password: %s\r\n" % default_password
|
txt += "Password: %s\r\n" % default_password
|
||||||
text += "Don't forget to change your password after first login.\r\n"
|
txt += "Don't forget to change your password after first login.\r\n"
|
||||||
text += "Sincerely\r\n\r\n"
|
txt += "Sincerely\r\n\r\n"
|
||||||
text += "Your Calibre-Web team"
|
txt += "Your Calibre-Web team"
|
||||||
WorkerThread.add(None, TaskEmail(
|
WorkerThread.add(None, TaskEmail(
|
||||||
subject=_(u'Get Started with Calibre-Web'),
|
subject=_(u'Get Started with Calibre-Web'),
|
||||||
filepath=None,
|
filepath=None,
|
||||||
@ -134,7 +131,7 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
|
|||||||
settings=config.get_mail_settings(),
|
settings=config.get_mail_settings(),
|
||||||
recipient=e_mail,
|
recipient=e_mail,
|
||||||
taskMessage=_(u"Registration e-mail for user: %(name)s", name=user_name),
|
taskMessage=_(u"Registration e-mail for user: %(name)s", name=user_name),
|
||||||
text=text
|
text=txt
|
||||||
))
|
))
|
||||||
|
|
||||||
return
|
return
|
||||||
@ -180,7 +177,7 @@ def check_send_to_kindle(entry):
|
|||||||
'convert': 0,
|
'convert': 0,
|
||||||
'text': _('Send %(format)s to Kindle', format='Pdf')})
|
'text': _('Send %(format)s to Kindle', format='Pdf')})
|
||||||
if config.config_converterpath:
|
if config.config_converterpath:
|
||||||
if 'EPUB' in formats and not 'MOBI' in formats:
|
if 'EPUB' in formats and 'MOBI' not in formats:
|
||||||
bookformats.append({'format': 'Mobi',
|
bookformats.append({'format': 'Mobi',
|
||||||
'convert':1,
|
'convert':1,
|
||||||
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
|
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
|
||||||
@ -565,8 +562,7 @@ def get_book_cover_internal(book, use_generic_cover_on_failure):
|
|||||||
log.error('%s/cover.jpg not found on Google Drive', book.path)
|
log.error('%s/cover.jpg not found on Google Drive', book.path)
|
||||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.debug_or_exception(e)
|
||||||
# traceback.print_exc()
|
|
||||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
else:
|
else:
|
||||||
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
|
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
|
||||||
@ -589,16 +585,15 @@ def save_cover_from_url(url, book_path):
|
|||||||
requests.exceptions.Timeout) as ex:
|
requests.exceptions.Timeout) as ex:
|
||||||
log.info(u'Cover Download Error %s', ex)
|
log.info(u'Cover Download Error %s', ex)
|
||||||
return False, _("Error Downloading Cover")
|
return False, _("Error Downloading Cover")
|
||||||
except UnidentifiedImageError as ex:
|
except MissingDelegateError as ex:
|
||||||
log.info(u'File Format Error %s', ex)
|
log.info(u'File Format Error %s', ex)
|
||||||
return False, _("Cover Format Error")
|
return False, _("Cover Format Error")
|
||||||
|
|
||||||
|
|
||||||
def save_cover_from_filestorage(filepath, saved_filename, img):
|
def save_cover_from_filestorage(filepath, saved_filename, img):
|
||||||
if hasattr(img, '_content'):
|
if hasattr(img,"metadata"):
|
||||||
f = open(os.path.join(filepath, saved_filename), "wb")
|
img.save(filename=os.path.join(filepath, saved_filename))
|
||||||
f.write(img._content)
|
img.close()
|
||||||
f.close()
|
|
||||||
else:
|
else:
|
||||||
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file
|
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file
|
||||||
if not os.path.exists(filepath):
|
if not os.path.exists(filepath):
|
||||||
@ -619,31 +614,33 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
|
|||||||
def save_cover(img, book_path):
|
def save_cover(img, book_path):
|
||||||
content_type = img.headers.get('content-type')
|
content_type = img.headers.get('content-type')
|
||||||
|
|
||||||
if use_PIL:
|
if use_IM:
|
||||||
if content_type not in ('image/jpeg', 'image/png', 'image/webp'):
|
if content_type not in ('image/jpeg', 'image/png', 'image/webp', 'image/bmp'):
|
||||||
log.error("Only jpg/jpeg/png/webp files are supported as coverfile")
|
log.error("Only jpg/jpeg/png/webp/bmp files are supported as coverfile")
|
||||||
return False, _("Only jpg/jpeg/png/webp files are supported as coverfile")
|
return False, _("Only jpg/jpeg/png/webp/bmp files are supported as coverfile")
|
||||||
# convert to jpg because calibre only supports jpg
|
# convert to jpg because calibre only supports jpg
|
||||||
if content_type in ('image/png', 'image/webp'):
|
if content_type != 'image/jpg':
|
||||||
if hasattr(img, 'stream'):
|
if hasattr(img, 'stream'):
|
||||||
imgc = PILImage.open(img.stream)
|
imgc = Image(blob=img.stream)
|
||||||
else:
|
else:
|
||||||
imgc = PILImage.open(io.BytesIO(img.content))
|
imgc = Image(blob=io.BytesIO(img.content))
|
||||||
im = imgc.convert('RGB')
|
imgc.format = 'jpeg'
|
||||||
tmp_bytesio = io.BytesIO()
|
imgc.transform_colorspace("rgb")
|
||||||
im.save(tmp_bytesio, format='JPEG')
|
img = imgc
|
||||||
img._content = tmp_bytesio.getvalue()
|
|
||||||
else:
|
else:
|
||||||
if content_type not in 'image/jpeg':
|
if content_type not in 'image/jpeg':
|
||||||
log.error("Only jpg/jpeg files are supported as coverfile")
|
log.error("Only jpg/jpeg files are supported as coverfile")
|
||||||
return False, _("Only jpg/jpeg files are supported as coverfile")
|
return False, _("Only jpg/jpeg files are supported as coverfile")
|
||||||
|
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
tmpDir = gettempdir()
|
tmp_dir = os.path.join(gettempdir(), 'calibre_web')
|
||||||
ret, message = save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img)
|
|
||||||
|
if not os.path.isdir(tmp_dir):
|
||||||
|
os.mkdir(tmp_dir)
|
||||||
|
ret, message = save_cover_from_filestorage(tmp_dir, "uploaded_cover.jpg", img)
|
||||||
if ret is True:
|
if ret is True:
|
||||||
gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'),
|
gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg').replace("\\","/"),
|
||||||
os.path.join(tmpDir, "uploaded_cover.jpg"))
|
os.path.join(tmp_dir, "uploaded_cover.jpg"))
|
||||||
log.info("Cover is saved on Google Drive")
|
log.info("Cover is saved on Google Drive")
|
||||||
return True, None
|
return True, None
|
||||||
else:
|
else:
|
||||||
@ -697,7 +694,7 @@ def check_unrar(unrarLocation):
|
|||||||
log.debug("unrar version %s", version)
|
log.debug("unrar version %s", version)
|
||||||
break
|
break
|
||||||
except (OSError, UnicodeDecodeError) as err:
|
except (OSError, UnicodeDecodeError) as err:
|
||||||
log.exception(err)
|
log.debug_or_exception(err)
|
||||||
return _('Error excecuting UnRar')
|
return _('Error excecuting UnRar')
|
||||||
|
|
||||||
|
|
||||||
@ -827,4 +824,3 @@ def get_download_link(book_id, book_format, client):
|
|||||||
return do_download_file(book, book_format, client, data1, headers)
|
return do_download_file(book, book_format, client, data1, headers)
|
||||||
else:
|
else:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
144
cps/kobo.py
144
cps/kobo.py
@ -43,6 +43,8 @@ from flask_login import current_user
|
|||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy.sql.expression import and_, or_
|
from sqlalchemy.sql.expression import and_, or_
|
||||||
|
from sqlalchemy.exc import OperationalError
|
||||||
|
from sqlalchemy.orm import load_only
|
||||||
from sqlalchemy.exc import StatementError
|
from sqlalchemy.exc import StatementError
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@ -56,6 +58,8 @@ KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]}
|
|||||||
KOBO_STOREAPI_URL = "https://storeapi.kobo.com"
|
KOBO_STOREAPI_URL = "https://storeapi.kobo.com"
|
||||||
KOBO_IMAGEHOST_URL = "https://kbimages1-a.akamaihd.net"
|
KOBO_IMAGEHOST_URL = "https://kbimages1-a.akamaihd.net"
|
||||||
|
|
||||||
|
SYNC_ITEM_LIMIT = 5
|
||||||
|
|
||||||
kobo = Blueprint("kobo", __name__, url_prefix="/kobo/<auth_token>")
|
kobo = Blueprint("kobo", __name__, url_prefix="/kobo/<auth_token>")
|
||||||
kobo_auth.disable_failed_auth_redirect_for_blueprint(kobo)
|
kobo_auth.disable_failed_auth_redirect_for_blueprint(kobo)
|
||||||
kobo_auth.register_url_value_preprocessor(kobo)
|
kobo_auth.register_url_value_preprocessor(kobo)
|
||||||
@ -142,68 +146,80 @@ def HandleSyncRequest():
|
|||||||
new_books_last_modified = sync_token.books_last_modified
|
new_books_last_modified = sync_token.books_last_modified
|
||||||
new_books_last_created = sync_token.books_last_created
|
new_books_last_created = sync_token.books_last_created
|
||||||
new_reading_state_last_modified = sync_token.reading_state_last_modified
|
new_reading_state_last_modified = sync_token.reading_state_last_modified
|
||||||
|
new_archived_last_modified = datetime.datetime.min
|
||||||
sync_results = []
|
sync_results = []
|
||||||
|
|
||||||
# We reload the book database so that the user get's a fresh view of the library
|
# We reload the book database so that the user get's a fresh view of the library
|
||||||
# in case of external changes (e.g: adding a book through Calibre).
|
# in case of external changes (e.g: adding a book through Calibre).
|
||||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||||
|
|
||||||
archived_books = (
|
if sync_token.books_last_id > -1:
|
||||||
ub.session.query(ub.ArchivedBook)
|
changed_entries = (
|
||||||
.filter(ub.ArchivedBook.user_id == int(current_user.id))
|
calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
|
||||||
.all()
|
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||||
)
|
.filter(db.Books.last_modified >= sync_token.books_last_modified)
|
||||||
|
.filter(db.Books.id>sync_token.books_last_id)
|
||||||
# We join-in books that have had their Archived bit recently modified in order to either:
|
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||||
# * Restore them to the user's device.
|
.order_by(db.Books.last_modified)
|
||||||
# * Delete them from the user's device.
|
.order_by(db.Books.id)
|
||||||
# (Ideally we would use a join for this logic, however cross-database joins don't look trivial in SqlAlchemy.)
|
.limit(SYNC_ITEM_LIMIT)
|
||||||
recently_restored_or_archived_books = []
|
)
|
||||||
archived_book_ids = {}
|
else:
|
||||||
new_archived_last_modified = datetime.datetime.min
|
changed_entries = (
|
||||||
for archived_book in archived_books:
|
calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
|
||||||
if archived_book.last_modified > sync_token.archive_last_modified:
|
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||||
recently_restored_or_archived_books.append(archived_book.book_id)
|
.filter(db.Books.last_modified > sync_token.books_last_modified)
|
||||||
if archived_book.is_archived:
|
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||||
archived_book_ids[archived_book.book_id] = True
|
.order_by(db.Books.last_modified)
|
||||||
new_archived_last_modified = max(
|
.order_by(db.Books.id)
|
||||||
new_archived_last_modified, archived_book.last_modified)
|
.limit(SYNC_ITEM_LIMIT)
|
||||||
|
)
|
||||||
# sqlite gives unexpected results when performing the last_modified comparison without the datetime cast.
|
|
||||||
# It looks like it's treating the db.Books.last_modified field as a string and may fail
|
|
||||||
# the comparison because of the +00:00 suffix.
|
|
||||||
changed_entries = (
|
|
||||||
calibre_db.session.query(db.Books)
|
|
||||||
.join(db.Data)
|
|
||||||
.filter(or_(func.datetime(db.Books.last_modified) > sync_token.books_last_modified,
|
|
||||||
db.Books.id.in_(recently_restored_or_archived_books)))
|
|
||||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
reading_states_in_new_entitlements = []
|
reading_states_in_new_entitlements = []
|
||||||
for book in changed_entries:
|
for book in changed_entries:
|
||||||
kobo_reading_state = get_or_create_reading_state(book.id)
|
kobo_reading_state = get_or_create_reading_state(book.Books.id)
|
||||||
entitlement = {
|
entitlement = {
|
||||||
"BookEntitlement": create_book_entitlement(book, archived=(book.id in archived_book_ids)),
|
"BookEntitlement": create_book_entitlement(book.Books, archived=(book.is_archived == True)),
|
||||||
"BookMetadata": get_metadata(book),
|
"BookMetadata": get_metadata(book.Books),
|
||||||
}
|
}
|
||||||
|
|
||||||
if kobo_reading_state.last_modified > sync_token.reading_state_last_modified:
|
if kobo_reading_state.last_modified > sync_token.reading_state_last_modified:
|
||||||
entitlement["ReadingState"] = get_kobo_reading_state_response(book, kobo_reading_state)
|
entitlement["ReadingState"] = get_kobo_reading_state_response(book.Books, kobo_reading_state)
|
||||||
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
|
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
|
||||||
reading_states_in_new_entitlements.append(book.id)
|
reading_states_in_new_entitlements.append(book.Books.id)
|
||||||
|
|
||||||
if book.timestamp > sync_token.books_last_created:
|
if book.Books.timestamp > sync_token.books_last_created:
|
||||||
sync_results.append({"NewEntitlement": entitlement})
|
sync_results.append({"NewEntitlement": entitlement})
|
||||||
else:
|
else:
|
||||||
sync_results.append({"ChangedEntitlement": entitlement})
|
sync_results.append({"ChangedEntitlement": entitlement})
|
||||||
|
|
||||||
new_books_last_modified = max(
|
new_books_last_modified = max(
|
||||||
book.last_modified, new_books_last_modified
|
book.Books.last_modified, new_books_last_modified
|
||||||
)
|
)
|
||||||
new_books_last_created = max(book.timestamp, new_books_last_created)
|
new_books_last_created = max(book.Books.timestamp, new_books_last_created)
|
||||||
|
|
||||||
|
max_change = (changed_entries
|
||||||
|
.from_self()
|
||||||
|
.filter(ub.ArchivedBook.is_archived)
|
||||||
|
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if max_change:
|
||||||
|
max_change = max_change.last_modified
|
||||||
|
else:
|
||||||
|
max_change = new_archived_last_modified
|
||||||
|
new_archived_last_modified = max(new_archived_last_modified, max_change)
|
||||||
|
|
||||||
|
# no. of books returned
|
||||||
|
book_count = changed_entries.count()
|
||||||
|
|
||||||
|
# last entry:
|
||||||
|
if book_count:
|
||||||
|
books_last_id = changed_entries.all()[-1].Books.id or -1
|
||||||
|
else:
|
||||||
|
books_last_id = -1
|
||||||
|
|
||||||
|
# generate reading state data
|
||||||
changed_reading_states = (
|
changed_reading_states = (
|
||||||
ub.session.query(ub.KoboReadingState)
|
ub.session.query(ub.KoboReadingState)
|
||||||
.filter(and_(func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified,
|
.filter(and_(func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified,
|
||||||
@ -225,11 +241,12 @@ def HandleSyncRequest():
|
|||||||
sync_token.books_last_modified = new_books_last_modified
|
sync_token.books_last_modified = new_books_last_modified
|
||||||
sync_token.archive_last_modified = new_archived_last_modified
|
sync_token.archive_last_modified = new_archived_last_modified
|
||||||
sync_token.reading_state_last_modified = new_reading_state_last_modified
|
sync_token.reading_state_last_modified = new_reading_state_last_modified
|
||||||
|
sync_token.books_last_id = books_last_id
|
||||||
|
|
||||||
return generate_sync_response(sync_token, sync_results)
|
return generate_sync_response(sync_token, sync_results, book_count)
|
||||||
|
|
||||||
|
|
||||||
def generate_sync_response(sync_token, sync_results):
|
def generate_sync_response(sync_token, sync_results, set_cont=False):
|
||||||
extra_headers = {}
|
extra_headers = {}
|
||||||
if config.config_kobo_proxy:
|
if config.config_kobo_proxy:
|
||||||
# Merge in sync results from the official Kobo store.
|
# Merge in sync results from the official Kobo store.
|
||||||
@ -245,6 +262,8 @@ def generate_sync_response(sync_token, sync_results):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e))
|
log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e))
|
||||||
|
if set_cont:
|
||||||
|
extra_headers["x-kobo-sync"] = "continue"
|
||||||
sync_token.to_headers(extra_headers)
|
sync_token.to_headers(extra_headers)
|
||||||
|
|
||||||
response = make_response(jsonify(sync_results), extra_headers)
|
response = make_response(jsonify(sync_results), extra_headers)
|
||||||
@ -443,8 +462,10 @@ def HandleTagCreate():
|
|||||||
items_unknown_to_calibre = add_items_to_shelf(items, shelf)
|
items_unknown_to_calibre = add_items_to_shelf(items, shelf)
|
||||||
if items_unknown_to_calibre:
|
if items_unknown_to_calibre:
|
||||||
log.debug("Received request to add unknown books to a collection. Silently ignoring items.")
|
log.debug("Received request to add unknown books to a collection. Silently ignoring items.")
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
return make_response(jsonify(str(shelf.uuid)), 201)
|
return make_response(jsonify(str(shelf.uuid)), 201)
|
||||||
|
|
||||||
|
|
||||||
@ -476,7 +497,10 @@ def HandleTagUpdate(tag_id):
|
|||||||
|
|
||||||
shelf.name = name
|
shelf.name = name
|
||||||
ub.session.merge(shelf)
|
ub.session.merge(shelf)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
return make_response(' ', 200)
|
return make_response(' ', 200)
|
||||||
|
|
||||||
|
|
||||||
@ -528,7 +552,10 @@ def HandleTagAddItem(tag_id):
|
|||||||
log.debug("Received request to add an unknown book to a collection. Silently ignoring item.")
|
log.debug("Received request to add an unknown book to a collection. Silently ignoring item.")
|
||||||
|
|
||||||
ub.session.merge(shelf)
|
ub.session.merge(shelf)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
|
|
||||||
return make_response('', 201)
|
return make_response('', 201)
|
||||||
|
|
||||||
@ -569,7 +596,10 @@ def HandleTagRemoveItem(tag_id):
|
|||||||
shelf.books.filter(ub.BookShelf.book_id == book.id).delete()
|
shelf.books.filter(ub.BookShelf.book_id == book.id).delete()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
items_unknown_to_calibre.append(item)
|
items_unknown_to_calibre.append(item)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
|
|
||||||
if items_unknown_to_calibre:
|
if items_unknown_to_calibre:
|
||||||
log.debug("Received request to remove an unknown book to a collecition. Silently ignoring item.")
|
log.debug("Received request to remove an unknown book to a collecition. Silently ignoring item.")
|
||||||
@ -615,7 +645,10 @@ def sync_shelves(sync_token, sync_results):
|
|||||||
"ChangedTag": tag
|
"ChangedTag": tag
|
||||||
})
|
})
|
||||||
sync_token.tags_last_modified = new_tags_last_modified
|
sync_token.tags_last_modified = new_tags_last_modified
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
|
|
||||||
|
|
||||||
# Creates a Kobo "Tag" object from a ub.Shelf object
|
# Creates a Kobo "Tag" object from a ub.Shelf object
|
||||||
@ -696,7 +729,10 @@ def HandleStateRequest(book_uuid):
|
|||||||
abort(400, description="Malformed request data is missing 'ReadingStates' key")
|
abort(400, description="Malformed request data is missing 'ReadingStates' key")
|
||||||
|
|
||||||
ub.session.merge(kobo_reading_state)
|
ub.session.merge(kobo_reading_state)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"RequestResult": "Success",
|
"RequestResult": "Success",
|
||||||
"UpdateResults": [update_results_response],
|
"UpdateResults": [update_results_response],
|
||||||
@ -734,7 +770,10 @@ def get_or_create_reading_state(book_id):
|
|||||||
kobo_reading_state.statistics = ub.KoboStatistics()
|
kobo_reading_state.statistics = ub.KoboStatistics()
|
||||||
book_read.kobo_reading_state = kobo_reading_state
|
book_read.kobo_reading_state = kobo_reading_state
|
||||||
ub.session.add(book_read)
|
ub.session.add(book_read)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
return book_read.kobo_reading_state
|
return book_read.kobo_reading_state
|
||||||
|
|
||||||
|
|
||||||
@ -837,7 +876,10 @@ def HandleBookDeletionRequest(book_uuid):
|
|||||||
archived_book.last_modified = datetime.datetime.utcnow()
|
archived_book.last_modified = datetime.datetime.utcnow()
|
||||||
|
|
||||||
ub.session.merge(archived_book)
|
ub.session.merge(archived_book)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
|
|
||||||
return ("", 204)
|
return ("", 204)
|
||||||
|
|
||||||
|
@ -66,9 +66,10 @@ from os import urandom
|
|||||||
from flask import g, Blueprint, url_for, abort, request
|
from flask import g, Blueprint, url_for, abort, request
|
||||||
from flask_login import login_user, login_required
|
from flask_login import login_user, login_required
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
from sqlalchemy.exc import OperationalError
|
||||||
|
|
||||||
from . import logger, ub, lm
|
from . import logger, ub, lm
|
||||||
from .web import render_title_template
|
from .render_template import render_title_template
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@ -147,7 +148,10 @@ def generate_auth_token(user_id):
|
|||||||
auth_token.token_type = 1
|
auth_token.token_type = 1
|
||||||
|
|
||||||
ub.session.add(auth_token)
|
ub.session.add(auth_token)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
return render_title_template(
|
return render_title_template(
|
||||||
"generate_kobo_auth_url.html",
|
"generate_kobo_auth_url.html",
|
||||||
title=_(u"Kobo Setup"),
|
title=_(u"Kobo Setup"),
|
||||||
@ -164,5 +168,8 @@ def delete_auth_token(user_id):
|
|||||||
# Invalidate any prevously generated Kobo Auth token for this user.
|
# Invalidate any prevously generated Kobo Auth token for this user.
|
||||||
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
||||||
.filter(ub.RemoteAuthToken.token_type==1).delete()
|
.filter(ub.RemoteAuthToken.token_type==1).delete()
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
return ""
|
return ""
|
||||||
|
@ -41,10 +41,18 @@ logging.addLevelName(logging.WARNING, "WARN")
|
|||||||
logging.addLevelName(logging.CRITICAL, "CRIT")
|
logging.addLevelName(logging.CRITICAL, "CRIT")
|
||||||
|
|
||||||
|
|
||||||
|
class _Logger(logging.Logger):
|
||||||
|
|
||||||
|
def debug_or_exception(self, message, *args, **kwargs):
|
||||||
|
if is_debug_enabled():
|
||||||
|
self.exception(message, stacklevel=2, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
self.error(message, stacklevel=2, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def get(name=None):
|
def get(name=None):
|
||||||
return logging.getLogger(name)
|
return logging.getLogger(name)
|
||||||
|
|
||||||
|
|
||||||
def create():
|
def create():
|
||||||
parent_frame = inspect.stack(0)[1]
|
parent_frame = inspect.stack(0)[1]
|
||||||
if hasattr(parent_frame, 'frame'):
|
if hasattr(parent_frame, 'frame'):
|
||||||
@ -54,7 +62,6 @@ def create():
|
|||||||
parent_module = inspect.getmodule(parent_frame)
|
parent_module = inspect.getmodule(parent_frame)
|
||||||
return get(parent_module.__name__)
|
return get(parent_module.__name__)
|
||||||
|
|
||||||
|
|
||||||
def is_debug_enabled():
|
def is_debug_enabled():
|
||||||
return logging.root.level <= logging.DEBUG
|
return logging.root.level <= logging.DEBUG
|
||||||
|
|
||||||
@ -99,6 +106,7 @@ def setup(log_file, log_level=None):
|
|||||||
May be called multiple times.
|
May be called multiple times.
|
||||||
'''
|
'''
|
||||||
log_level = log_level or DEFAULT_LOG_LEVEL
|
log_level = log_level or DEFAULT_LOG_LEVEL
|
||||||
|
logging.setLoggerClass(_Logger)
|
||||||
logging.getLogger(__package__).setLevel(log_level)
|
logging.getLogger(__package__).setLevel(log_level)
|
||||||
|
|
||||||
r = logging.root
|
r = logging.root
|
||||||
|
@ -30,11 +30,12 @@ from flask_babel import gettext as _
|
|||||||
from flask_dance.consumer import oauth_authorized, oauth_error
|
from flask_dance.consumer import oauth_authorized, oauth_error
|
||||||
from flask_dance.contrib.github import make_github_blueprint, github
|
from flask_dance.contrib.github import make_github_blueprint, github
|
||||||
from flask_dance.contrib.google import make_google_blueprint, google
|
from flask_dance.contrib.google import make_google_blueprint, google
|
||||||
from flask_login import login_user, current_user
|
from flask_login import login_user, current_user, login_required
|
||||||
from sqlalchemy.orm.exc import NoResultFound
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
from sqlalchemy.exc import OperationalError
|
||||||
|
|
||||||
from . import constants, logger, config, app, ub
|
from . import constants, logger, config, app, ub
|
||||||
from .web import login_required
|
|
||||||
from .oauth import OAuthBackend, backend_resultcode
|
from .oauth import OAuthBackend, backend_resultcode
|
||||||
|
|
||||||
|
|
||||||
@ -87,7 +88,7 @@ def register_user_with_oauth(user=None):
|
|||||||
try:
|
try:
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.debug_or_exception(e)
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
|
|
||||||
|
|
||||||
@ -109,7 +110,10 @@ if ub.oauth_support:
|
|||||||
oauthProvider.provider_name = "google"
|
oauthProvider.provider_name = "google"
|
||||||
oauthProvider.active = False
|
oauthProvider.active = False
|
||||||
ub.session.add(oauthProvider)
|
ub.session.add(oauthProvider)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
|
|
||||||
oauth_ids = ub.session.query(ub.OAuthProvider).all()
|
oauth_ids = ub.session.query(ub.OAuthProvider).all()
|
||||||
ele1 = dict(provider_name='github',
|
ele1 = dict(provider_name='github',
|
||||||
@ -203,7 +207,7 @@ if ub.oauth_support:
|
|||||||
ub.session.add(oauth_entry)
|
ub.session.add(oauth_entry)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.debug_or_exception(e)
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
|
|
||||||
# Disable Flask-Dance's default behavior for saving the OAuth token
|
# Disable Flask-Dance's default behavior for saving the OAuth token
|
||||||
@ -235,7 +239,7 @@ if ub.oauth_support:
|
|||||||
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
|
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
|
||||||
return redirect(url_for('web.profile'))
|
return redirect(url_for('web.profile'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.debug_or_exception(e)
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
else:
|
else:
|
||||||
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
|
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
|
||||||
@ -282,7 +286,7 @@ if ub.oauth_support:
|
|||||||
logout_oauth_user()
|
logout_oauth_user()
|
||||||
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
|
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.debug_or_exception(e)
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
|
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
|
@ -33,7 +33,8 @@ from werkzeug.security import check_password_hash
|
|||||||
from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
|
from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
|
||||||
from .helper import get_download_link, get_book_cover
|
from .helper import get_download_link, get_book_cover
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
from .web import render_read_books, download_required, load_user_from_request
|
from .web import render_read_books
|
||||||
|
from .usermanagement import load_user_from_request
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from babel import Locale as LC
|
from babel import Locale as LC
|
||||||
from babel.core import UnknownLocaleError
|
from babel.core import UnknownLocaleError
|
||||||
|
139
cps/remotelogin.py
Normal file
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"},
|
"archive_last_modified": {"type": "string"},
|
||||||
"reading_state_last_modified": {"type": "string"},
|
"reading_state_last_modified": {"type": "string"},
|
||||||
"tags_last_modified": {"type": "string"},
|
"tags_last_modified": {"type": "string"},
|
||||||
|
"books_last_id": {"type": "integer", "optional": True}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,6 +97,7 @@ class SyncToken:
|
|||||||
archive_last_modified=datetime.min,
|
archive_last_modified=datetime.min,
|
||||||
reading_state_last_modified=datetime.min,
|
reading_state_last_modified=datetime.min,
|
||||||
tags_last_modified=datetime.min,
|
tags_last_modified=datetime.min,
|
||||||
|
books_last_id=-1
|
||||||
):
|
):
|
||||||
self.raw_kobo_store_token = raw_kobo_store_token
|
self.raw_kobo_store_token = raw_kobo_store_token
|
||||||
self.books_last_created = books_last_created
|
self.books_last_created = books_last_created
|
||||||
@ -103,6 +105,7 @@ class SyncToken:
|
|||||||
self.archive_last_modified = archive_last_modified
|
self.archive_last_modified = archive_last_modified
|
||||||
self.reading_state_last_modified = reading_state_last_modified
|
self.reading_state_last_modified = reading_state_last_modified
|
||||||
self.tags_last_modified = tags_last_modified
|
self.tags_last_modified = tags_last_modified
|
||||||
|
self.books_last_id = books_last_id
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_headers(headers):
|
def from_headers(headers):
|
||||||
@ -137,9 +140,12 @@ class SyncToken:
|
|||||||
archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified")
|
archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified")
|
||||||
reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified")
|
reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified")
|
||||||
tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified")
|
tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified")
|
||||||
|
books_last_id = data_json["books_last_id"]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
log.error("SyncToken timestamps don't parse to a datetime.")
|
log.error("SyncToken timestamps don't parse to a datetime.")
|
||||||
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
|
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
|
||||||
|
except KeyError:
|
||||||
|
books_last_id = -1
|
||||||
|
|
||||||
return SyncToken(
|
return SyncToken(
|
||||||
raw_kobo_store_token=raw_kobo_store_token,
|
raw_kobo_store_token=raw_kobo_store_token,
|
||||||
@ -147,7 +153,8 @@ class SyncToken:
|
|||||||
books_last_modified=books_last_modified,
|
books_last_modified=books_last_modified,
|
||||||
archive_last_modified=archive_last_modified,
|
archive_last_modified=archive_last_modified,
|
||||||
reading_state_last_modified=reading_state_last_modified,
|
reading_state_last_modified=reading_state_last_modified,
|
||||||
tags_last_modified=tags_last_modified
|
tags_last_modified=tags_last_modified,
|
||||||
|
books_last_id=books_last_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_kobo_store_header(self, store_headers):
|
def set_kobo_store_header(self, store_headers):
|
||||||
@ -170,7 +177,8 @@ class SyncToken:
|
|||||||
"books_last_created": to_epoch_timestamp(self.books_last_created),
|
"books_last_created": to_epoch_timestamp(self.books_last_created),
|
||||||
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
|
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
|
||||||
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified),
|
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified),
|
||||||
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified)
|
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified),
|
||||||
|
"books_last_id":self.books_last_id
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return b64encode_json(token)
|
return b64encode_json(token)
|
||||||
|
@ -110,7 +110,7 @@ class WorkerThread(threading.Thread):
|
|||||||
# We don't use a daemon here because we don't want the tasks to just be abruptly halted, leading to
|
# We don't use a daemon here because we don't want the tasks to just be abruptly halted, leading to
|
||||||
# possible file / database corruption
|
# possible file / database corruption
|
||||||
item = self.queue.get(timeout=1)
|
item = self.queue.get(timeout=1)
|
||||||
except queue.Empty as ex:
|
except queue.Empty:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ class CalibreTask:
|
|||||||
self.run(*args)
|
self.run(*args)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._handleError(str(e))
|
self._handleError(str(e))
|
||||||
log.exception(e)
|
log.debug_or_exception(e)
|
||||||
|
|
||||||
self.end_time = datetime.now()
|
self.end_time = datetime.now()
|
||||||
|
|
||||||
@ -210,7 +210,6 @@ class CalibreTask:
|
|||||||
self._progress = x
|
self._progress = x
|
||||||
|
|
||||||
def _handleError(self, error_message):
|
def _handleError(self, error_message):
|
||||||
log.exception(error_message)
|
|
||||||
self.stat = STAT_FAIL
|
self.stat = STAT_FAIL
|
||||||
self.progress = 1
|
self.progress = 1
|
||||||
self.error = error_message
|
self.error = error_message
|
||||||
|
155
cps/shelf.py
155
cps/shelf.py
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
from __future__ import division, print_function, unicode_literals
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import sys
|
||||||
|
|
||||||
from flask import Blueprint, request, flash, redirect, url_for
|
from flask import Blueprint, request, flash, redirect, url_for
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
@ -29,8 +30,9 @@ from flask_login import login_required, current_user
|
|||||||
from sqlalchemy.sql.expression import func
|
from sqlalchemy.sql.expression import func
|
||||||
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
||||||
|
|
||||||
from . import logger, ub, calibre_db
|
from . import logger, ub, calibre_db, db
|
||||||
from .web import login_required_if_no_ano, render_title_template
|
from .render_template import render_title_template
|
||||||
|
from .usermanagement import login_required_if_no_ano
|
||||||
|
|
||||||
|
|
||||||
shelf = Blueprint('shelf', __name__)
|
shelf = Blueprint('shelf', __name__)
|
||||||
@ -138,18 +140,14 @@ def search_to_shelf(shelf_id):
|
|||||||
books_for_shelf = ub.searched_ids[current_user.id]
|
books_for_shelf = ub.searched_ids[current_user.id]
|
||||||
|
|
||||||
if not books_for_shelf:
|
if not books_for_shelf:
|
||||||
log.error("Books are already part of %s", shelf)
|
log.error("Books are already part of %s", shelf.name)
|
||||||
flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
|
flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()
|
maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0
|
||||||
if maxOrder[0] is None:
|
|
||||||
maxOrder = 0
|
|
||||||
else:
|
|
||||||
maxOrder = maxOrder[0]
|
|
||||||
|
|
||||||
for book in books_for_shelf:
|
for book in books_for_shelf:
|
||||||
maxOrder = maxOrder + 1
|
maxOrder += 1
|
||||||
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder))
|
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder))
|
||||||
shelf.last_modified = datetime.utcnow()
|
shelf.last_modified = datetime.utcnow()
|
||||||
try:
|
try:
|
||||||
@ -322,8 +320,11 @@ def delete_shelf_helper(cur_shelf):
|
|||||||
ub.session.delete(cur_shelf)
|
ub.session.delete(cur_shelf)
|
||||||
ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete()
|
ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete()
|
||||||
ub.session.add(ub.ShelfArchive(uuid=cur_shelf.uuid, user_id=cur_shelf.user_id))
|
ub.session.add(ub.ShelfArchive(uuid=cur_shelf.uuid, user_id=cur_shelf.user_id))
|
||||||
ub.session.commit()
|
try:
|
||||||
log.info("successfully deleted %s", cur_shelf)
|
ub.session.commit()
|
||||||
|
log.info("successfully deleted %s", cur_shelf)
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -333,44 +334,22 @@ def delete_shelf(shelf_id):
|
|||||||
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
try:
|
try:
|
||||||
delete_shelf_helper(cur_shelf)
|
delete_shelf_helper(cur_shelf)
|
||||||
except (OperationalError, InvalidRequestError):
|
except InvalidRequestError:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
flash(_(u"Settings DB is not Writeable"), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
|
@shelf.route("/simpleshelf/<int:shelf_id>")
|
||||||
@shelf.route("/shelf/<int:shelf_id>", defaults={'shelf_type': 1})
|
|
||||||
@shelf.route("/shelf/<int:shelf_id>/<int:shelf_type>")
|
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
def show_shelf(shelf_type, shelf_id):
|
def show_simpleshelf(shelf_id):
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
return render_show_shelf(2, shelf_id, 1, None)
|
||||||
|
|
||||||
result = list()
|
@shelf.route("/shelf/<int:shelf_id>", defaults={"sort_param": "order", 'page': 1})
|
||||||
# user is allowed to access shelf
|
@shelf.route("/shelf/<int:shelf_id>/<sort_param>", defaults={'page': 1})
|
||||||
if shelf and check_shelf_view_permissions(shelf):
|
@shelf.route("/shelf/<int:shelf_id>/<sort_param>/<int:page>")
|
||||||
page = "shelf.html" if shelf_type == 1 else 'shelfdown.html'
|
@login_required_if_no_ano
|
||||||
|
def show_shelf(shelf_id, sort_param, page):
|
||||||
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\
|
return render_show_shelf(1, shelf_id, page, sort_param)
|
||||||
.order_by(ub.BookShelf.order.asc()).all()
|
|
||||||
for book in books_in_shelf:
|
|
||||||
cur_book = calibre_db.get_filtered_book(book.book_id)
|
|
||||||
if cur_book:
|
|
||||||
result.append(cur_book)
|
|
||||||
else:
|
|
||||||
cur_book = calibre_db.get_book(book.book_id)
|
|
||||||
if not cur_book:
|
|
||||||
log.info('Not existing book %s in %s deleted', book.book_id, shelf)
|
|
||||||
try:
|
|
||||||
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete()
|
|
||||||
ub.session.commit()
|
|
||||||
except (OperationalError, InvalidRequestError):
|
|
||||||
ub.session.rollback()
|
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
|
||||||
return render_title_template(page, entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name),
|
|
||||||
shelf=shelf, page="shelf")
|
|
||||||
else:
|
|
||||||
flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error")
|
|
||||||
return redirect(url_for("web.index"))
|
|
||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/order/<int:shelf_id>", methods=["GET", "POST"])
|
@shelf.route("/shelf/order/<int:shelf_id>", methods=["GET", "POST"])
|
||||||
@ -394,22 +373,80 @@ def order_shelf(shelf_id):
|
|||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
result = list()
|
result = list()
|
||||||
if shelf and check_shelf_view_permissions(shelf):
|
if shelf and check_shelf_view_permissions(shelf):
|
||||||
books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
|
result = calibre_db.session.query(db.Books)\
|
||||||
.order_by(ub.BookShelf.order.asc()).all()
|
.join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id , isouter=True) \
|
||||||
for book in books_in_shelf2:
|
.add_columns(calibre_db.common_filters().label("visible")) \
|
||||||
cur_book = calibre_db.get_filtered_book(book.book_id)
|
.filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all()
|
||||||
if cur_book:
|
|
||||||
result.append({'title': cur_book.title,
|
|
||||||
'id': cur_book.id,
|
|
||||||
'author': cur_book.authors,
|
|
||||||
'series': cur_book.series,
|
|
||||||
'series_index': cur_book.series_index})
|
|
||||||
else:
|
|
||||||
cur_book = calibre_db.get_book(book.book_id)
|
|
||||||
result.append({'title': _('Hidden Book'),
|
|
||||||
'id': cur_book.id,
|
|
||||||
'author': [],
|
|
||||||
'series': []})
|
|
||||||
return render_title_template('shelf_order.html', entries=result,
|
return render_title_template('shelf_order.html', entries=result,
|
||||||
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
|
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
|
||||||
shelf=shelf, page="shelforder")
|
shelf=shelf, page="shelforder")
|
||||||
|
|
||||||
|
def change_shelf_order(shelf_id, order):
|
||||||
|
result = calibre_db.session.query(db.Books).join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id)\
|
||||||
|
.filter(ub.BookShelf.shelf == shelf_id).order_by(*order).all()
|
||||||
|
for index, entry in enumerate(result):
|
||||||
|
book = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
|
||||||
|
.filter(ub.BookShelf.book_id == entry.id).first()
|
||||||
|
book.order = index
|
||||||
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
|
|
||||||
|
def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
|
||||||
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
|
|
||||||
|
# check user is allowed to access shelf
|
||||||
|
if shelf and check_shelf_view_permissions(shelf):
|
||||||
|
|
||||||
|
if shelf_type == 1:
|
||||||
|
# order = [ub.BookShelf.order.asc()]
|
||||||
|
if sort_param == 'pubnew':
|
||||||
|
change_shelf_order(shelf_id, [db.Books.pubdate.desc()])
|
||||||
|
if sort_param == 'pubold':
|
||||||
|
change_shelf_order(shelf_id, [db.Books.pubdate])
|
||||||
|
if sort_param == 'abc':
|
||||||
|
change_shelf_order(shelf_id, [db.Books.sort])
|
||||||
|
if sort_param == 'zyx':
|
||||||
|
change_shelf_order(shelf_id, [db.Books.sort.desc()])
|
||||||
|
if sort_param == 'new':
|
||||||
|
change_shelf_order(shelf_id, [db.Books.timestamp.desc()])
|
||||||
|
if sort_param == 'old':
|
||||||
|
change_shelf_order(shelf_id, [db.Books.timestamp])
|
||||||
|
if sort_param == 'authaz':
|
||||||
|
change_shelf_order(shelf_id, [db.Books.author_sort.asc()])
|
||||||
|
if sort_param == 'authza':
|
||||||
|
change_shelf_order(shelf_id, [db.Books.author_sort.desc()])
|
||||||
|
page = "shelf.html"
|
||||||
|
pagesize = 0
|
||||||
|
else:
|
||||||
|
pagesize = sys.maxsize
|
||||||
|
page = 'shelfdown.html'
|
||||||
|
|
||||||
|
result, __, pagination = calibre_db.fill_indexpage(page_no, pagesize,
|
||||||
|
db.Books,
|
||||||
|
ub.BookShelf.shelf == shelf_id,
|
||||||
|
[ub.BookShelf.order.asc()],
|
||||||
|
ub.BookShelf,ub.BookShelf.book_id == db.Books.id)
|
||||||
|
# delete chelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web
|
||||||
|
wrong_entries = calibre_db.session.query(ub.BookShelf)\
|
||||||
|
.join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True)\
|
||||||
|
.filter(db.Books.id == None).all()
|
||||||
|
for entry in wrong_entries:
|
||||||
|
log.info('Not existing book {} in {} deleted'.format(entry.book_id, shelf))
|
||||||
|
try:
|
||||||
|
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == entry.book_id).delete()
|
||||||
|
ub.session.commit()
|
||||||
|
except (OperationalError, InvalidRequestError):
|
||||||
|
ub.session.rollback()
|
||||||
|
flash(_(u"Settings DB is not Writeable"), category="error")
|
||||||
|
|
||||||
|
return render_title_template(page,
|
||||||
|
entries=result,
|
||||||
|
pagination=pagination,
|
||||||
|
title=_(u"Shelf: '%(name)s'", name=shelf.name),
|
||||||
|
shelf=shelf,
|
||||||
|
page="shelf")
|
||||||
|
else:
|
||||||
|
flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error")
|
||||||
|
return redirect(url_for("web.index"))
|
||||||
|
@ -240,7 +240,7 @@ body.blur .row-fluid .col-sm-10 {
|
|||||||
|
|
||||||
.col-sm-10 .book-meta > div.btn-toolbar:after {
|
.col-sm-10 .book-meta > div.btn-toolbar:after {
|
||||||
content: '';
|
content: '';
|
||||||
direction: block;
|
direction: ltr;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 120px;
|
top: 120px;
|
||||||
right: 0;
|
right: 0;
|
||||||
@ -398,20 +398,17 @@ body.blur .row-fluid .col-sm-10 {
|
|||||||
|
|
||||||
.shelforder #sortTrue > div:hover {
|
.shelforder #sortTrue > div:hover {
|
||||||
background-color: hsla(0, 0%, 100%, .06) !important;
|
background-color: hsla(0, 0%, 100%, .06) !important;
|
||||||
cursor: move;
|
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
cursor: -webkit-grab;
|
|
||||||
color: #eee
|
color: #eee
|
||||||
}
|
}
|
||||||
|
|
||||||
.shelforder #sortTrue > div:active {
|
.shelforder #sortTrue > div:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
cursor: -webkit-grabbing
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.shelforder #sortTrue > div:before {
|
.shelforder #sortTrue > div:before {
|
||||||
content: "\EA53";
|
content: "\EA53";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
margin-right: 30px;
|
margin-right: 30px;
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
@ -446,7 +443,7 @@ body.blur .row-fluid .col-sm-10 {
|
|||||||
|
|
||||||
body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
||||||
content: "\e155";
|
content: "\e155";
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -494,7 +491,7 @@ body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#have_read_cb + label:before, #have_read_cb:checked + label:before {
|
#have_read_cb + label:before, #have_read_cb:checked + label:before {
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
@ -550,13 +547,12 @@ body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
|||||||
height: 60px;
|
height: 60px;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: -4px;
|
margin: -4px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#archived_cb + label:before, #archived_cb:checked + label:before {
|
#archived_cb + label:before, #archived_cb:checked + label:before {
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
@ -618,7 +614,7 @@ div[aria-label="Edit/Delete book"] > .btn > span {
|
|||||||
|
|
||||||
div[aria-label="Edit/Delete book"] > .btn > span:before {
|
div[aria-label="Edit/Delete book"] > .btn > span:before {
|
||||||
content: "\EA5d";
|
content: "\EA5d";
|
||||||
font-family: plex-icons;
|
font-family: plex-icons, serif;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
padding: 16px 15px;
|
padding: 16px 15px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -760,7 +756,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a
|
|||||||
|
|
||||||
.home-btn {
|
.home-btn {
|
||||||
color: hsla(0, 0%, 100%, .7);
|
color: hsla(0, 0%, 100%, .7);
|
||||||
line-height: 34.29px;
|
line-height: 34px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -770,7 +766,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a
|
|||||||
|
|
||||||
.home-btn > a {
|
.home-btn > a {
|
||||||
color: rgba(255, 255, 255, .7);
|
color: rgba(255, 255, 255, .7);
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
line-height: 60px;
|
line-height: 60px;
|
||||||
position: relative;
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -800,7 +796,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.home-btn > a:hove
|
|||||||
|
|
||||||
.glyphicon-search:before {
|
.glyphicon-search:before {
|
||||||
content: "\EA4F";
|
content: "\EA4F";
|
||||||
font-family: plex-icons
|
font-family: plex-icons, serif
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav_about:after, .profileDrop > span:after, .profileDrop > span:before {
|
#nav_about:after, .profileDrop > span:after, .profileDrop > span:before {
|
||||||
@ -966,7 +962,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d
|
|||||||
|
|
||||||
#form-upload .form-group .btn:before {
|
#form-upload .form-group .btn:before {
|
||||||
content: "\e043";
|
content: "\e043";
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -991,7 +987,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d
|
|||||||
#form-upload .form-group .btn:after {
|
#form-upload .form-group .btn:after {
|
||||||
content: "\EA13";
|
content: "\EA13";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
background: #3c444a;
|
background: #3c444a;
|
||||||
color: hsla(0, 0%, 100%, .7);
|
color: hsla(0, 0%, 100%, .7);
|
||||||
@ -1019,7 +1015,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d
|
|||||||
text-transform: none;
|
text-transform: none;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -1075,7 +1071,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d
|
|||||||
|
|
||||||
body > div.navbar.navbar-default.navbar-static-top > div > form > div > span > button:before {
|
body > div.navbar.navbar-default.navbar-static-top > div > form > div > span > button:before {
|
||||||
content: "\EA32";
|
content: "\EA32";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
color: #eee;
|
color: #eee;
|
||||||
background: #555;
|
background: #555;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
@ -1097,7 +1093,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form > div > span > b
|
|||||||
body > div.navbar.navbar-default.navbar-static-top > div > form:before {
|
body > div.navbar.navbar-default.navbar-static-top > div > form:before {
|
||||||
content: "\EA4F";
|
content: "\EA4F";
|
||||||
display: block;
|
display: block;
|
||||||
font-family: plex-icons;
|
font-family: plex-icons, serif;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: #eee;
|
color: #eee;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@ -1120,7 +1116,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form:before {
|
|||||||
body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > div > span.input-group-btn:before {
|
body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > div > span.input-group-btn:before {
|
||||||
content: "\EA4F";
|
content: "\EA4F";
|
||||||
display: block;
|
display: block;
|
||||||
font-family: plex-icons;
|
font-family: plex-icons, serif;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -298px;
|
left: -298px;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
@ -1193,7 +1189,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.c
|
|||||||
|
|
||||||
body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul > li > #top_admin > .glyphicon-dashboard::before {
|
body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul > li > #top_admin > .glyphicon-dashboard::before {
|
||||||
content: "\EA31";
|
content: "\EA31";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
font-size: 20px
|
font-size: 20px
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1272,7 +1268,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover {
|
|||||||
user-select: none
|
user-select: none
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation li, .navigation li:not(ul>li) {
|
.navigation li, .navigation li:not(ul > li) {
|
||||||
border-radius: 0 4px 4px 0
|
border-radius: 0 4px 4px 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1352,32 +1348,32 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover {
|
|||||||
|
|
||||||
#nav_hot .glyphicon-fire::before {
|
#nav_hot .glyphicon-fire::before {
|
||||||
content: "\1F525";
|
content: "\1F525";
|
||||||
font-family: glyphicons regular
|
font-family: glyphicons regular, serif
|
||||||
}
|
}
|
||||||
|
|
||||||
.glyphicon-star:before {
|
.glyphicon-star:before {
|
||||||
content: "\EA10";
|
content: "\EA10";
|
||||||
font-family: plex-icons-new
|
font-family: plex-icons-new, serif
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav_rand .glyphicon-random::before {
|
#nav_rand .glyphicon-random::before {
|
||||||
content: "\EA44";
|
content: "\EA44";
|
||||||
font-family: plex-icons-new
|
font-family: plex-icons-new, serif
|
||||||
}
|
}
|
||||||
|
|
||||||
.glyphicon-list::before {
|
.glyphicon-list::before {
|
||||||
content: "\EA4D";
|
content: "\EA4D";
|
||||||
font-family: plex-icons-new
|
font-family: plex-icons-new, serif
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav_about .glyphicon-info-sign::before {
|
#nav_about .glyphicon-info-sign::before {
|
||||||
content: "\EA26";
|
content: "\EA26";
|
||||||
font-family: plex-icons-new
|
font-family: plex-icons-new, serif
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav_cat .glyphicon-inbox::before, .glyphicon-tags::before {
|
#nav_cat .glyphicon-inbox::before, .glyphicon-tags::before {
|
||||||
content: "\E067";
|
content: "\E067";
|
||||||
font-family: Glyphicons Regular;
|
font-family: Glyphicons Regular, serif;
|
||||||
margin-left: 2px
|
margin-left: 2px
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1423,7 +1419,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover {
|
|||||||
|
|
||||||
.navigation .create-shelf a:before {
|
.navigation .create-shelf a:before {
|
||||||
content: "\EA13";
|
content: "\EA13";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
vertical-align: middle
|
vertical-align: middle
|
||||||
@ -1473,7 +1469,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover {
|
|||||||
|
|
||||||
#books > .cover > a:before, #books_rand > .cover > a:before, .book.isotope-item > .cover > a:before, body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 > div.col-sm-12 > div.col-sm-2 > a:before {
|
#books > .cover > a:before, #books_rand > .cover > a:before, .book.isotope-item > .cover > a:before, body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 > div.col-sm-12 > div.col-sm-2 > a:before {
|
||||||
content: "\e352";
|
content: "\e352";
|
||||||
font-family: Glyphicons Regular;
|
font-family: Glyphicons Regular, serif;
|
||||||
background: var(--color-secondary);
|
background: var(--color-secondary);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@ -1521,8 +1517,8 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
background: -webkit-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
background: -webkit-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||||
background: -o-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
background: -o-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||||
background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||||
z-index: -9
|
z-index: -9
|
||||||
}
|
}
|
||||||
@ -1562,8 +1558,8 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
background: -webkit-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
background: -webkit-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||||
background: -o-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
background: -o-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||||
background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%)
|
background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1739,7 +1735,7 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 {
|
|||||||
|
|
||||||
body.me > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
body.me > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
||||||
content: '';
|
content: '';
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 6vw;
|
font-size: 6vw;
|
||||||
@ -1947,7 +1943,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.pagination .page-next > a
|
|||||||
body > div.container-fluid > div > div.col-sm-10 > div.pagination .page-previous > a
|
body > div.container-fluid > div > div.col-sm-10 > div.pagination .page-previous > a
|
||||||
{
|
{
|
||||||
top: 0;
|
top: 0;
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
line-height: 60px;
|
line-height: 60px;
|
||||||
@ -2026,7 +2022,7 @@ body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container > di
|
|||||||
|
|
||||||
body.serieslist > div.container-fluid > div > div.col-sm-10:before {
|
body.serieslist > div.container-fluid > div > div.col-sm-10:before {
|
||||||
content: "\e044";
|
content: "\e044";
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -2131,7 +2127,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container
|
|||||||
|
|
||||||
body.catlist > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
body.catlist > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
||||||
content: "\E067";
|
content: "\E067";
|
||||||
font-family: Glyphicons Regular;
|
font-family: Glyphicons Regular, serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -2151,7 +2147,7 @@ body.catlist > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
|||||||
|
|
||||||
|
|
||||||
body.authorlist > div.container-fluid > div.row-fluid > div.col-sm-10:before, body.langlist > div.container-fluid > div > div.col-sm-10:before {
|
body.authorlist > div.container-fluid > div.row-fluid > div.col-sm-10:before, body.langlist > div.container-fluid > div > div.col-sm-10:before {
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -2492,7 +2488,6 @@ body > div.container-fluid > div > div.col-sm-10 > div.col-sm-8 > form > .btn.bt
|
|||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
resize: none;
|
|
||||||
resize: vertical
|
resize: vertical
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2838,7 +2833,7 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form
|
|||||||
|
|
||||||
body.advanced_search > div.container-fluid > div > div.col-sm-10 > div.col-sm-8:before {
|
body.advanced_search > div.container-fluid > div > div.col-sm-10 > div.col-sm-8:before {
|
||||||
content: "\EA4F";
|
content: "\EA4F";
|
||||||
font-family: plex-icons;
|
font-family: plex-icons, serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -3195,7 +3190,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div.
|
|||||||
|
|
||||||
#add-to-shelf > span.glyphicon.glyphicon-list:before {
|
#add-to-shelf > span.glyphicon.glyphicon-list:before {
|
||||||
content: "\EA59";
|
content: "\EA59";
|
||||||
font-family: plex-icons;
|
font-family: plex-icons, serif;
|
||||||
font-size: 18px
|
font-size: 18px
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3207,7 +3202,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div.
|
|||||||
|
|
||||||
#read-in-browser > span.glyphicon-eye-open:before, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.glyphicon-eye-open:before {
|
#read-in-browser > span.glyphicon-eye-open:before, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.glyphicon-eye-open:before {
|
||||||
content: "\e352";
|
content: "\e352";
|
||||||
font-family: Glyphicons Regular;
|
font-family: Glyphicons Regular, serif;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
padding-right: 5px
|
padding-right: 5px
|
||||||
}
|
}
|
||||||
@ -3219,7 +3214,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div.
|
|||||||
#btnGroupDrop1 > span.glyphicon-download:before {
|
#btnGroupDrop1 > span.glyphicon-download:before {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
content: "\ea66";
|
content: "\ea66";
|
||||||
font-family: plex-icons
|
font-family: plex-icons, serif
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-sm-10 .book-meta > div.btn-toolbar {
|
.col-sm-10 .book-meta > div.btn-toolbar {
|
||||||
@ -3323,7 +3318,6 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd
|
|||||||
-webkit-box-shadow: 0 4px 10px rgba(0, 0, 0, .35);
|
-webkit-box-shadow: 0 4px 10px rgba(0, 0, 0, .35);
|
||||||
box-shadow: 0 4px 10px rgba(0, 0, 0, .35);
|
box-shadow: 0 4px 10px rgba(0, 0, 0, .35);
|
||||||
-o-transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
-o-transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
||||||
transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
|
||||||
transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4), -webkit-transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4), -webkit-transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
||||||
-webkit-transform-origin: center top;
|
-webkit-transform-origin: center top;
|
||||||
-ms-transform-origin: center top;
|
-ms-transform-origin: center top;
|
||||||
@ -3441,7 +3435,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover > .btn-primary:l
|
|||||||
|
|
||||||
.book-meta > div.more-stuff > .btn-toolbar > .btn-group[aria-label="Remove from shelves"] > a > .glyphicon-remove:before {
|
.book-meta > div.more-stuff > .btn-toolbar > .btn-group[aria-label="Remove from shelves"] > a > .glyphicon-remove:before {
|
||||||
content: "\ea64";
|
content: "\ea64";
|
||||||
font-family: plex-icons
|
font-family: plex-icons, serif
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > .col-sm-6 {
|
body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > .col-sm-6 {
|
||||||
@ -3555,7 +3549,7 @@ body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-b
|
|||||||
|
|
||||||
body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [data-target="#DeleteShelfDialog"]:before {
|
body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [data-target="#DeleteShelfDialog"]:before {
|
||||||
content: "\EA6D";
|
content: "\EA6D";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: hsla(0, 0%, 100%, .7);
|
color: hsla(0, 0%, 100%, .7);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@ -3585,7 +3579,7 @@ body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-b
|
|||||||
|
|
||||||
body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [href*=edit]:before {
|
body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [href*=edit]:before {
|
||||||
content: "\EA5d";
|
content: "\EA5d";
|
||||||
font-family: plex-icons;
|
font-family: plex-icons, serif;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: hsla(0, 0%, 100%, .7);
|
color: hsla(0, 0%, 100%, .7);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@ -3615,7 +3609,7 @@ body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-b
|
|||||||
|
|
||||||
body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [href*=order]:before {
|
body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [href*=order]:before {
|
||||||
content: "\E409";
|
content: "\E409";
|
||||||
font-family: Glyphicons Regular;
|
font-family: Glyphicons Regular, serif;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: hsla(0, 0%, 100%, .7);
|
color: hsla(0, 0%, 100%, .7);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@ -3752,7 +3746,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a
|
|||||||
|
|
||||||
.plexBack > a {
|
.plexBack > a {
|
||||||
color: rgba(255, 255, 255, .7);
|
color: rgba(255, 255, 255, .7);
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
-webkit-font-variant-ligatures: normal;
|
-webkit-font-variant-ligatures: normal;
|
||||||
font-variant-ligatures: normal;
|
font-variant-ligatures: normal;
|
||||||
line-height: 60px;
|
line-height: 60px;
|
||||||
@ -3864,11 +3858,9 @@ body.login > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.l
|
|||||||
-webkit-transform: translateY(-50%);
|
-webkit-transform: translateY(-50%);
|
||||||
-ms-transform: translateY(-50%);
|
-ms-transform: translateY(-50%);
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
border-style: solid;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
-webkit-transition: border .2s, -webkit-transform .4s;
|
-webkit-transition: border .2s, -webkit-transform .4s;
|
||||||
-o-transition: border .2s, transform .4s;
|
-o-transition: border .2s, transform .4s;
|
||||||
transition: border .2s, transform .4s;
|
|
||||||
transition: border .2s, transform .4s, -webkit-transform .4s;
|
transition: border .2s, transform .4s, -webkit-transform .4s;
|
||||||
margin: 9px 6px
|
margin: 9px 6px
|
||||||
}
|
}
|
||||||
@ -3887,11 +3879,9 @@ body.login > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.l
|
|||||||
-webkit-transform: translateY(-50%);
|
-webkit-transform: translateY(-50%);
|
||||||
-ms-transform: translateY(-50%);
|
-ms-transform: translateY(-50%);
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
border-style: solid;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
-webkit-transition: border .2s, -webkit-transform .4s;
|
-webkit-transition: border .2s, -webkit-transform .4s;
|
||||||
-o-transition: border .2s, transform .4s;
|
-o-transition: border .2s, transform .4s;
|
||||||
transition: border .2s, transform .4s;
|
|
||||||
transition: border .2s, transform .4s, -webkit-transform .4s;
|
transition: border .2s, transform .4s, -webkit-transform .4s;
|
||||||
margin: 12px 6px
|
margin: 12px 6px
|
||||||
}
|
}
|
||||||
@ -3971,7 +3961,7 @@ body.author img.bg-blur[src=undefined] {
|
|||||||
|
|
||||||
body.author:not(.authorlist) .undefined-img:before {
|
body.author:not(.authorlist) .undefined-img:before {
|
||||||
content: "\e008";
|
content: "\e008";
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -4120,7 +4110,7 @@ body.shelf.modal-open > .container-fluid {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #999;
|
color: #999;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: Glyphicons Regular;
|
font-family: Glyphicons Regular, serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400
|
font-weight: 400
|
||||||
}
|
}
|
||||||
@ -4221,7 +4211,7 @@ body.shelf.modal-open > .container-fluid {
|
|||||||
|
|
||||||
#remove-from-shelves > .btn > span:before {
|
#remove-from-shelves > .btn > span:before {
|
||||||
content: "\EA52";
|
content: "\EA52";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
padding-left: 5px
|
padding-left: 5px
|
||||||
}
|
}
|
||||||
@ -4233,7 +4223,7 @@ body.shelf.modal-open > .container-fluid {
|
|||||||
|
|
||||||
#remove-from-shelves > a:first-of-type:before {
|
#remove-from-shelves > a:first-of-type:before {
|
||||||
content: "\EA4D";
|
content: "\EA4D";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: hsla(0, 0%, 100%, .45);
|
color: hsla(0, 0%, 100%, .45);
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@ -4273,7 +4263,7 @@ body.shelf.modal-open > .container-fluid {
|
|||||||
content: "\E208";
|
content: "\E208";
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
display: block;
|
display: block;
|
||||||
font-family: Glyphicons Regular;
|
font-family: Glyphicons Regular, serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -4284,7 +4274,6 @@ body.shelf.modal-open > .container-fluid {
|
|||||||
opacity: .5;
|
opacity: .5;
|
||||||
-webkit-transition: -webkit-transform .3s ease-out;
|
-webkit-transition: -webkit-transform .3s ease-out;
|
||||||
-o-transition: transform .3s ease-out;
|
-o-transition: transform .3s ease-out;
|
||||||
transition: transform .3s ease-out;
|
|
||||||
transition: transform .3s ease-out, -webkit-transform .3s ease-out;
|
transition: transform .3s ease-out, -webkit-transform .3s ease-out;
|
||||||
-webkit-transform: translate(0, -60px);
|
-webkit-transform: translate(0, -60px);
|
||||||
-ms-transform: translate(0, -60px);
|
-ms-transform: translate(0, -60px);
|
||||||
@ -4344,7 +4333,7 @@ body.advanced_search > div.container-fluid > div > div.col-sm-10 > div.col-sm-8
|
|||||||
|
|
||||||
.glyphicon-remove:before {
|
.glyphicon-remove:before {
|
||||||
content: "\EA52";
|
content: "\EA52";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
font-weight: 400
|
font-weight: 400
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4430,7 +4419,7 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div
|
|||||||
|
|
||||||
body:not(.blur) #nav_new:before {
|
body:not(.blur) #nav_new:before {
|
||||||
content: "\EA4F";
|
content: "\EA4F";
|
||||||
font-family: plex-icons;
|
font-family: plex-icons, serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -4456,7 +4445,7 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div
|
|||||||
color: hsla(0, 0%, 100%, .7);
|
color: hsla(0, 0%, 100%, .7);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: block;
|
display: block;
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-stretch: 100%;
|
font-stretch: 100%;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@ -4552,12 +4541,12 @@ body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > d
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row > div.col .table > tbody > tr > td, body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row > div.col .table > tbody > tr > th, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > .table > tbody > tr > td, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > .table > tbody > tr > th {
|
body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row > div.col .table > tbody > tr > td, body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row > div.col .table > tbody > tr > th, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > .table > tbody > tr > td, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > .table > tbody > tr > th {
|
||||||
border: collapse
|
border: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.newuser.admin > div.container-fluid > div.row-fluid > div.col-sm-10::before {
|
body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.newuser.admin > div.container-fluid > div.row-fluid > div.col-sm-10::before {
|
||||||
content: '';
|
content: '';
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 6vw;
|
font-size: 6vw;
|
||||||
@ -4661,7 +4650,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
content: "\e352";
|
content: "\e352";
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-family: Glyphicons Regular;
|
font-family: Glyphicons Regular, serif;
|
||||||
background: var(--color-secondary);
|
background: var(--color-secondary);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@ -4699,8 +4688,8 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
background: -webkit-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
background: -webkit-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||||
background: -o-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
background: -o-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%);
|
||||||
background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%)
|
background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4752,7 +4741,7 @@ body.admin td > a:hover {
|
|||||||
|
|
||||||
.glyphicon-ok::before {
|
.glyphicon-ok::before {
|
||||||
content: "\EA55";
|
content: "\EA55";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
font-weight: 400
|
font-weight: 400
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4821,7 +4810,7 @@ body:not(.blur):not(.login):not(.me):not(.author):not(.editbook):not(.upload):no
|
|||||||
background-position: center center, center center, center center !important;
|
background-position: center center, center center, center center !important;
|
||||||
background-size: auto, auto, cover !important;
|
background-size: auto, auto, cover !important;
|
||||||
-webkit-background-size: auto, auto, cover !important;
|
-webkit-background-size: auto, auto, cover !important;
|
||||||
-moz-background-size: autom, auto, cover !important;
|
-moz-background-size: auto, auto, cover !important;
|
||||||
-o-background-size: auto, auto, cover !important;
|
-o-background-size: auto, auto, cover !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
@ -4887,7 +4876,6 @@ body.read:not(.blur) a[href*=readbooks] {
|
|||||||
.tooltip.in {
|
.tooltip.in {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
-o-transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
-o-transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
||||||
transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
|
||||||
transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4), -webkit-transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4), -webkit-transform .15s cubic-bezier(.6, .4, .2, 1.4);
|
||||||
-webkit-transform: translate(0) scale(1);
|
-webkit-transform: translate(0) scale(1);
|
||||||
-ms-transform: translate(0) scale(1);
|
-ms-transform: translate(0) scale(1);
|
||||||
@ -4987,7 +4975,7 @@ body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div
|
|||||||
|
|
||||||
body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-3 > div.text-center > #delete:before, body.upload > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-3 > div.text-center > #delete:before {
|
body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-3 > div.text-center > #delete:before, body.upload > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-3 > div.text-center > #delete:before {
|
||||||
content: "\EA6D";
|
content: "\EA6D";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: hsla(0, 0%, 100%, .7)
|
color: hsla(0, 0%, 100%, .7)
|
||||||
}
|
}
|
||||||
@ -5072,7 +5060,7 @@ body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.boot
|
|||||||
|
|
||||||
body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.bootstrap-table > div.fixed-table-container > div.fixed-table-body > #table > thead > tr > th > .th-inner.asc:after {
|
body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.bootstrap-table > div.fixed-table-container > div.fixed-table-body > #table > thead > tr > th > .th-inner.asc:after {
|
||||||
content: "\EA58";
|
content: "\EA58";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
position: absolute
|
position: absolute
|
||||||
@ -5080,7 +5068,7 @@ body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.boot
|
|||||||
|
|
||||||
body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.bootstrap-table > div.fixed-table-container > div.fixed-table-body > #table > thead > tr > th > .th-inner.desc:after {
|
body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.bootstrap-table > div.fixed-table-container > div.fixed-table-body > #table > thead > tr > th > .th-inner.desc:after {
|
||||||
content: "\EA57";
|
content: "\EA57";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
position: absolute
|
position: absolute
|
||||||
@ -5143,7 +5131,7 @@ body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.boot
|
|||||||
|
|
||||||
.epub-back:before {
|
.epub-back:before {
|
||||||
content: "\EA1C";
|
content: "\EA1C";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #4f4f4f;
|
color: #4f4f4f;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -5306,7 +5294,7 @@ body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm
|
|||||||
|
|
||||||
body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-3 > div.text-center > #delete:before, body.upload > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-3 > div.text-center > #delete:before {
|
body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-3 > div.text-center > #delete:before, body.upload > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-3 > div.text-center > #delete:before {
|
||||||
content: "\EA6D";
|
content: "\EA6D";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: hsla(0, 0%, 100%, .7);
|
color: hsla(0, 0%, 100%, .7);
|
||||||
vertical-align: super
|
vertical-align: super
|
||||||
@ -5466,7 +5454,7 @@ body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm
|
|||||||
|
|
||||||
#main-nav + #scnd-nav .create-shelf a:before {
|
#main-nav + #scnd-nav .create-shelf a:before {
|
||||||
content: "\EA13";
|
content: "\EA13";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
vertical-align: middle
|
vertical-align: middle
|
||||||
@ -5511,7 +5499,7 @@ body.admin.modal-open .navbar {
|
|||||||
content: "\E208";
|
content: "\E208";
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
display: block;
|
display: block;
|
||||||
font-family: Glyphicons Regular;
|
font-family: Glyphicons Regular, serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -5522,7 +5510,6 @@ body.admin.modal-open .navbar {
|
|||||||
opacity: .5;
|
opacity: .5;
|
||||||
-webkit-transition: -webkit-transform .3s ease-out;
|
-webkit-transition: -webkit-transform .3s ease-out;
|
||||||
-o-transition: transform .3s ease-out;
|
-o-transition: transform .3s ease-out;
|
||||||
transition: transform .3s ease-out;
|
|
||||||
transition: transform .3s ease-out, -webkit-transform .3s ease-out;
|
transition: transform .3s ease-out, -webkit-transform .3s ease-out;
|
||||||
-webkit-transform: translate(0, -60px);
|
-webkit-transform: translate(0, -60px);
|
||||||
-ms-transform: translate(0, -60px);
|
-ms-transform: translate(0, -60px);
|
||||||
@ -5576,22 +5563,22 @@ body.admin.modal-open .navbar {
|
|||||||
|
|
||||||
#RestartDialog > .modal-dialog > .modal-content > .modal-header:before {
|
#RestartDialog > .modal-dialog > .modal-content > .modal-header:before {
|
||||||
content: "\EA4F";
|
content: "\EA4F";
|
||||||
font-family: plex-icons-new
|
font-family: plex-icons-new, serif
|
||||||
}
|
}
|
||||||
|
|
||||||
#ShutdownDialog > .modal-dialog > .modal-content > .modal-header:before {
|
#ShutdownDialog > .modal-dialog > .modal-content > .modal-header:before {
|
||||||
content: "\E064";
|
content: "\E064";
|
||||||
font-family: glyphicons regular
|
font-family: glyphicons regular, serif
|
||||||
}
|
}
|
||||||
|
|
||||||
#StatusDialog > .modal-dialog > .modal-content > .modal-header:before {
|
#StatusDialog > .modal-dialog > .modal-content > .modal-header:before {
|
||||||
content: "\EA15";
|
content: "\EA15";
|
||||||
font-family: plex-icons-new
|
font-family: plex-icons-new, serif
|
||||||
}
|
}
|
||||||
|
|
||||||
#deleteModal > .modal-dialog > .modal-content > .modal-header:before {
|
#deleteModal > .modal-dialog > .modal-content > .modal-header:before {
|
||||||
content: "\EA6D";
|
content: "\EA6D";
|
||||||
font-family: plex-icons-new
|
font-family: plex-icons-new, serif
|
||||||
}
|
}
|
||||||
|
|
||||||
#RestartDialog > .modal-dialog > .modal-content > .modal-header:after {
|
#RestartDialog > .modal-dialog > .modal-content > .modal-header:after {
|
||||||
@ -5982,7 +5969,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
|
|
||||||
.home-btn {
|
.home-btn {
|
||||||
height: 48px;
|
height: 48px;
|
||||||
line-height: 28.29px;
|
line-height: 28px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
left: auto
|
left: auto
|
||||||
}
|
}
|
||||||
@ -5994,7 +5981,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
|
|
||||||
.plexBack {
|
.plexBack {
|
||||||
height: 48px;
|
height: 48px;
|
||||||
line-height: 28.29px;
|
line-height: 28px;
|
||||||
left: 48px;
|
left: 48px;
|
||||||
display: none
|
display: none
|
||||||
}
|
}
|
||||||
@ -6073,7 +6060,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > div > span.input-group-btn:before {
|
body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > div > span.input-group-btn:before {
|
||||||
content: "\EA33";
|
content: "\EA33";
|
||||||
display: block;
|
display: block;
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -6225,7 +6212,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
|
|
||||||
#form-upload .form-group .btn:before {
|
#form-upload .form-group .btn:before {
|
||||||
content: "\e043";
|
content: "\e043";
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@ -6243,7 +6230,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
#form-upload .form-group .btn:after {
|
#form-upload .form-group .btn:after {
|
||||||
content: "\EA13";
|
content: "\EA13";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
background: #3c444a;
|
background: #3c444a;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@ -6296,7 +6283,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
}
|
}
|
||||||
|
|
||||||
#top_admin, #top_tasks {
|
#top_admin, #top_tasks {
|
||||||
padding: 11.5px 15px;
|
padding: 12px 15px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.71428571;
|
line-height: 1.71428571;
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
@ -6305,7 +6292,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
#top_admin > .glyphicon, #top_tasks > .glyphicon-tasks {
|
#top_admin > .glyphicon, #top_tasks > .glyphicon-tasks {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
top: 0;
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: 0 0;
|
background: 0 0;
|
||||||
@ -6324,7 +6311,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
|
|
||||||
#top_tasks > .glyphicon-tasks::before, body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul > li > #top_admin > .glyphicon-dashboard::before {
|
#top_tasks > .glyphicon-tasks::before, body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul > li > #top_admin > .glyphicon-dashboard::before {
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
@ -6649,7 +6636,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
|
|
||||||
.author > .container-fluid > .row-fluid > .col-sm-10 > h2:after {
|
.author > .container-fluid > .row-fluid > .col-sm-10 > h2:after {
|
||||||
content: "\e008";
|
content: "\e008";
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -6854,7 +6841,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
color: hsla(0, 0%, 100%, .7);
|
color: hsla(0, 0%, 100%, .7);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: block;
|
display: block;
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-stretch: 100%;
|
font-stretch: 100%;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@ -7025,11 +7012,9 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
-webkit-transform: translateY(-50%);
|
-webkit-transform: translateY(-50%);
|
||||||
-ms-transform: translateY(-50%);
|
-ms-transform: translateY(-50%);
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
border-style: solid;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
-webkit-transition: border .2s, -webkit-transform .4s;
|
-webkit-transition: border .2s, -webkit-transform .4s;
|
||||||
-o-transition: border .2s, transform .4s;
|
-o-transition: border .2s, transform .4s;
|
||||||
transition: border .2s, transform .4s;
|
|
||||||
transition: border .2s, transform .4s, -webkit-transform .4s;
|
transition: border .2s, transform .4s, -webkit-transform .4s;
|
||||||
margin: 12px 6px
|
margin: 12px 6px
|
||||||
}
|
}
|
||||||
@ -7048,18 +7033,16 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
-webkit-transform: translateY(-50%);
|
-webkit-transform: translateY(-50%);
|
||||||
-ms-transform: translateY(-50%);
|
-ms-transform: translateY(-50%);
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
border-style: solid;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
-webkit-transition: border .2s, -webkit-transform .4s;
|
-webkit-transition: border .2s, -webkit-transform .4s;
|
||||||
-o-transition: border .2s, transform .4s;
|
-o-transition: border .2s, transform .4s;
|
||||||
transition: border .2s, transform .4s;
|
|
||||||
transition: border .2s, transform .4s, -webkit-transform .4s;
|
transition: border .2s, transform .4s, -webkit-transform .4s;
|
||||||
margin: 9px 6px
|
margin: 9px 6px
|
||||||
}
|
}
|
||||||
|
|
||||||
body.author:not(.authorlist) .blur-wrapper:before, body.author > .container-fluid > .row-fluid > .col-sm-10 > h2:after {
|
body.author:not(.authorlist) .blur-wrapper:before, body.author > .container-fluid > .row-fluid > .col-sm-10 > h2:after {
|
||||||
content: "\e008";
|
content: "\e008";
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -7390,7 +7373,6 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
-webkit-transition: -webkit-transform .5s;
|
-webkit-transition: -webkit-transform .5s;
|
||||||
-o-transition: transform .5s;
|
-o-transition: transform .5s;
|
||||||
transition: transform .5s;
|
|
||||||
transition: transform .5s, -webkit-transform .5s;
|
transition: transform .5s, -webkit-transform .5s;
|
||||||
z-index: 99
|
z-index: 99
|
||||||
}
|
}
|
||||||
@ -7405,7 +7387,6 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
transform: translate3d(-240px, 0, 0);
|
transform: translate3d(-240px, 0, 0);
|
||||||
-webkit-transition: -webkit-transform .5s;
|
-webkit-transition: -webkit-transform .5s;
|
||||||
-o-transition: transform .5s;
|
-o-transition: transform .5s;
|
||||||
transition: transform .5s;
|
|
||||||
transition: transform .5s, -webkit-transform .5s;
|
transition: transform .5s, -webkit-transform .5s;
|
||||||
top: 0;
|
top: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -7444,7 +7425,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: #
|
// color: #
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-xs-12 > .row > .col-xs-10 {
|
.col-xs-12 > .row > .col-xs-10 {
|
||||||
@ -7555,7 +7536,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
|
|
||||||
body.publisherlist > div.container-fluid > div > div.col-sm-10:before {
|
body.publisherlist > div.container-fluid > div > div.col-sm-10:before {
|
||||||
content: "\e241";
|
content: "\e241";
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -7575,7 +7556,7 @@ body.publisherlist > div.container-fluid > div > div.col-sm-10:before {
|
|||||||
|
|
||||||
body.ratingslist > div.container-fluid > div > div.col-sm-10:before {
|
body.ratingslist > div.container-fluid > div > div.col-sm-10:before {
|
||||||
content: "\e007";
|
content: "\e007";
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -7601,7 +7582,7 @@ body.ratingslist > div.container-fluid > div > div.col-sm-10:before {
|
|||||||
|
|
||||||
body.formatslist > div.container-fluid > div > div.col-sm-10:before {
|
body.formatslist > div.container-fluid > div > div.col-sm-10:before {
|
||||||
content: "\e022";
|
content: "\e022";
|
||||||
font-family: 'Glyphicons Halflings';
|
font-family: 'Glyphicons Halflings', serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -7776,7 +7757,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .editabl
|
|||||||
|
|
||||||
body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphicon-trash:before {
|
body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphicon-trash:before {
|
||||||
content: "\EA6D";
|
content: "\EA6D";
|
||||||
font-family: plex-icons-new
|
font-family: plex-icons-new, serif
|
||||||
}
|
}
|
||||||
|
|
||||||
#DeleteDomain {
|
#DeleteDomain {
|
||||||
@ -7799,7 +7780,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic
|
|||||||
content: "\E208";
|
content: "\E208";
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
display: block;
|
display: block;
|
||||||
font-family: Glyphicons Regular;
|
font-family: Glyphicons Regular, serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -7810,7 +7791,6 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic
|
|||||||
opacity: .5;
|
opacity: .5;
|
||||||
-webkit-transition: -webkit-transform .3s ease-out;
|
-webkit-transition: -webkit-transform .3s ease-out;
|
||||||
-o-transition: transform .3s ease-out;
|
-o-transition: transform .3s ease-out;
|
||||||
transition: transform .3s ease-out;
|
|
||||||
transition: transform .3s ease-out, -webkit-transform .3s ease-out;
|
transition: transform .3s ease-out, -webkit-transform .3s ease-out;
|
||||||
-webkit-transform: translate(0, -60px);
|
-webkit-transform: translate(0, -60px);
|
||||||
-ms-transform: translate(0, -60px);
|
-ms-transform: translate(0, -60px);
|
||||||
@ -7849,7 +7829,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic
|
|||||||
|
|
||||||
#DeleteDomain > .modal-dialog > .modal-content > .modal-header:before {
|
#DeleteDomain > .modal-dialog > .modal-content > .modal-header:before {
|
||||||
content: "\EA6D";
|
content: "\EA6D";
|
||||||
font-family: plex-icons-new;
|
font-family: plex-icons-new, serif;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #999;
|
color: #999;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{
|
body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cover .badge{
|
.cover .badge{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
color: #fff;
|
||||||
background-color: #cc7b19;
|
background-color: #cc7b19;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
@ -15,3 +15,8 @@ body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{
|
|||||||
.cover{
|
.cover{
|
||||||
box-shadow: 0 0 4px rgba(0,0,0,.6);
|
box-shadow: 0 0 4px rgba(0,0,0,.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cover .read{
|
||||||
|
padding: 0 0px;
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
|
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;
|
overflow: hidden;
|
||||||
-webkit-transition: -webkit-transform 0.4s, width 0.2s;
|
-webkit-transition: -webkit-transform 0.4s, width 0.2s;
|
||||||
-moz-transition: -webkit-transform 0.4s, width 0.2s;
|
-moz-transition: -webkit-transform 0.4s, width 0.2s;
|
||||||
-ms-transition: -webkit-transform 0.4s, width 0.2s;
|
transition: -webkit-transform 0.4s, width 0.2s;
|
||||||
-moz-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
|
-moz-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
|
||||||
-webkit-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
|
-webkit-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
|
||||||
-ms-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
|
|
||||||
box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
|
box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,7 +44,7 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
-webkit-transition: opacity 0.5s;
|
-webkit-transition: opacity 0.5s;
|
||||||
-moz-transition: opacity 0.5s;
|
-moz-transition: opacity 0.5s;
|
||||||
-ms-transition: opacity 0.5s;
|
transition: opacity 0.5s;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,7 +78,6 @@ body {
|
|||||||
color: rgba(0, 0, 0, 0.6);
|
color: rgba(0, 0, 0, 0.6);
|
||||||
-moz-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
-moz-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
||||||
-webkit-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
-webkit-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
||||||
-ms-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
|
||||||
box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +119,6 @@ body {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-khtml-user-select: none;
|
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -147,7 +144,7 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
-webkit-transition: -webkit-transform 0.5s;
|
-webkit-transition: -webkit-transform 0.5s;
|
||||||
-moz-transition: -moz-transform 0.5s;
|
-moz-transition: -moz-transform 0.5s;
|
||||||
-ms-transition: -moz-transform 0.5s;
|
transition: -moz-transform 0.5s;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +180,6 @@ body {
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
-moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
-moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||||
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||||
-ms-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,7 +228,6 @@ body {
|
|||||||
|
|
||||||
input::-webkit-input-placeholder { color: #454545; }
|
input::-webkit-input-placeholder { color: #454545; }
|
||||||
input:-moz-placeholder { color: #454545; }
|
input:-moz-placeholder { color: #454545; }
|
||||||
input:-ms-placeholder { color: #454545; }
|
|
||||||
|
|
||||||
#divider {
|
#divider {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -268,18 +263,18 @@ input:-ms-placeholder { color: #454545; }
|
|||||||
width: 25%;
|
width: 25%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
-webkit-transition: visibility 0 ease 0.5s;
|
-webkit-transition: visibility 0s ease 0.5s;
|
||||||
-moz-transition: visibility 0 ease 0.5s;
|
-moz-transition: visibility 0s ease 0.5s;
|
||||||
-ms-transition: visibility 0 ease 0.5s;
|
transition: visibility 0s ease 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar.open #tocView,
|
#sidebar.open #tocView,
|
||||||
#sidebar.open #bookmarksView {
|
#sidebar.open #bookmarksView {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
-webkit-transition: visibility 0 ease 0;
|
-webkit-transition: visibility 0s ease 0s;
|
||||||
-moz-transition: visibility 0 ease 0;
|
-moz-transition: visibility 0s ease 0s;
|
||||||
-ms-transition: visibility 0 ease 0;
|
transition: visibility 0s ease 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar.open #tocView {
|
#sidebar.open #tocView {
|
||||||
@ -495,9 +490,8 @@ input:-ms-placeholder { color: #454545; }
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
width: 50%;
|
// width: 50%;
|
||||||
width: 630px;
|
width: 630px;
|
||||||
|
|
||||||
height: auto;
|
height: auto;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@ -518,7 +512,6 @@ input:-ms-placeholder { color: #454545; }
|
|||||||
background: rgba(255, 255, 255, 0.8);
|
background: rgba(255, 255, 255, 0.8);
|
||||||
-webkit-transition: all 0.3s;
|
-webkit-transition: all 0.3s;
|
||||||
-moz-transition: all 0.3s;
|
-moz-transition: all 0.3s;
|
||||||
-ms-transition: all 0.3s;
|
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -588,7 +581,6 @@ input:-ms-placeholder { color: #454545; }
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
-webkit-transition: all 0.3s;
|
-webkit-transition: all 0.3s;
|
||||||
-moz-transition: all 0.3s;
|
-moz-transition: all 0.3s;
|
||||||
-ms-transition: all 0.3s;
|
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -601,7 +593,7 @@ input:-ms-placeholder { color: #454545; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.md-content > .closer {
|
.md-content > .closer {
|
||||||
font-size: 18px;
|
//font-size: 18px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -663,7 +655,7 @@ input:-ms-placeholder { color: #454545; }
|
|||||||
-ms-transform: translate(0, 0);
|
-ms-transform: translate(0, 0);
|
||||||
-webkit-transition: -webkit-transform .3s;
|
-webkit-transition: -webkit-transform .3s;
|
||||||
-moz-transition: -moz-transform .3s;
|
-moz-transition: -moz-transform .3s;
|
||||||
-ms-transition: -moz-transform .3s;
|
transition: -moz-transform .3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#main.closed {
|
#main.closed {
|
||||||
@ -778,7 +770,7 @@ and (orientation : landscape)
|
|||||||
}
|
}
|
||||||
|
|
||||||
[class^="icon-"]:before, [class*=" icon-"]:before {
|
[class^="icon-"]:before, [class*=" icon-"]:before {
|
||||||
font-family: "fontello";
|
font-family: "fontello", serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
speak: none;
|
speak: none;
|
||||||
|
@ -116,6 +116,7 @@ a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d
|
|||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-fluid .discover{ margin-bottom: 50px; }
|
.container-fluid .discover{ margin-bottom: 50px; }
|
||||||
@ -132,12 +133,19 @@ a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-fluid .book .cover img {
|
.container-fluid .book .cover span.img {
|
||||||
|
bottom: 0;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-fluid .book .cover span img {
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 100%;
|
|
||||||
bottom: 0;
|
|
||||||
position: absolute;
|
|
||||||
-webkit-box-shadow: 0 5px 8px -6px #777;
|
-webkit-box-shadow: 0 5px 8px -6px #777;
|
||||||
-moz-box-shadow: 0 5px 8px -6px #777;
|
-moz-box-shadow: 0 5px 8px -6px #777;
|
||||||
box-shadow: 0 5px 8px -6px #777;
|
box-shadow: 0 5px 8px -6px #777;
|
||||||
@ -206,11 +214,22 @@ span.glyphicon.glyphicon-tags {
|
|||||||
.navbar-default .navbar-toggle .icon-bar {background-color: #000; }
|
.navbar-default .navbar-toggle .icon-bar {background-color: #000; }
|
||||||
.navbar-default .navbar-toggle {border-color: #000; }
|
.navbar-default .navbar-toggle {border-color: #000; }
|
||||||
.cover { margin-bottom: 10px; }
|
.cover { margin-bottom: 10px; }
|
||||||
|
|
||||||
.cover .badge{
|
.cover .badge{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
background-color: #777;
|
color: #000;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.cover .read{
|
||||||
|
left: auto;
|
||||||
|
right: 2px;
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px;
|
||||||
}
|
}
|
||||||
.cover-height { max-height: 100px;}
|
.cover-height { max-height: 100px;}
|
||||||
|
|
||||||
@ -241,7 +260,7 @@ span.glyphicon.glyphicon-tags {
|
|||||||
.button-link {color: #fff; }
|
.button-link {color: #fff; }
|
||||||
.btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open .dropdown-toggle.btn-primary{ background-color: #1C5484; }
|
.btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open .dropdown-toggle.btn-primary{ background-color: #1C5484; }
|
||||||
.btn-primary.disabled, .btn-primary[disabled], fieldset[disabled] .btn-primary, .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled:active, .btn-primary[disabled]:active, fieldset[disabled] .btn-primary:active, .btn-primary.disabled.active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary.active { background-color: #89B9E2; }
|
.btn-primary.disabled, .btn-primary[disabled], fieldset[disabled] .btn-primary, .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled:active, .btn-primary[disabled]:active, fieldset[disabled] .btn-primary:active, .btn-primary.disabled.active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary.active { background-color: #89B9E2; }
|
||||||
.btn-toolbar>.btn+.btn, .btn-toolbar>.btn-group+.btn, .btn-toolbar>.btn+.btn-group, .btn-toolbar>.btn-group+.btn-group { margin-left: 0px; }
|
.btn-toolbar>.btn+.btn, .btn-toolbar>.btn-group+.btn, .btn-toolbar>.btn+.btn-group, .btn-toolbar>.btn-group+.btn-group { margin-left: 0; }
|
||||||
.panel-body {background-color: #f5f5f5; }
|
.panel-body {background-color: #f5f5f5; }
|
||||||
.spinner {margin: 0 41%; }
|
.spinner {margin: 0 41%; }
|
||||||
.spinner2 {margin: 0 41%; }
|
.spinner2 {margin: 0 41%; }
|
||||||
@ -311,11 +330,11 @@ input.pill:not(:checked) + label .glyphicon { display: none; }
|
|||||||
.editable-input { display:inline-block; }
|
.editable-input { display:inline-block; }
|
||||||
|
|
||||||
.editable-cancel {
|
.editable-cancel {
|
||||||
margin-bottom: 0px !important;
|
margin-bottom: 0 !important;
|
||||||
margin-left: 7px !important;
|
margin-left: 7px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editable-submit { margin-bottom: 0px !important; }
|
.editable-submit { margin-bottom: 0 !important; }
|
||||||
.filterheader { margin-bottom: 20px; }
|
.filterheader { margin-bottom: 20px; }
|
||||||
.errorlink { margin-top: 20px; }
|
.errorlink { margin-top: 20px; }
|
||||||
.emailconfig { margin-top: 10px; }
|
.emailconfig { margin-top: 10px; }
|
||||||
@ -326,7 +345,7 @@ input.pill:not(:checked) + label .glyphicon { display: none; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.log {
|
div.log {
|
||||||
font-family: Courier New;
|
font-family: Courier New, serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 700px;
|
height: 700px;
|
||||||
|
@ -249,18 +249,26 @@ promisePublishers.done(function() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#search").on("change input.typeahead:selected", function() {
|
$("#search").on("change input.typeahead:selected", function(event) {
|
||||||
|
if (event.target.type == "search" && event.target.tagName == "INPUT") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
var form = $("form").serialize();
|
var form = $("form").serialize();
|
||||||
$.getJSON( getPath() + "/get_matching_tags", form, function( data ) {
|
$.getJSON( getPath() + "/get_matching_tags", form, function( data ) {
|
||||||
$(".tags_click").each(function() {
|
$(".tags_click").each(function() {
|
||||||
if ($.inArray(parseInt($(this).children("input").first().val(), 10), data.tags) === -1 ) {
|
if ($.inArray(parseInt($(this).val(), 10), data.tags) === -1) {
|
||||||
if (!($(this).hasClass("active"))) {
|
if(!$(this).prop("selected")) {
|
||||||
$(this).addClass("disabled");
|
$(this).prop("disabled", true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$(this).removeClass("disabled");
|
$(this).prop("disabled", false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
$("#include_tag option:selected").each(function () {
|
||||||
|
$("#exclude_tag").find("[value="+$(this).val()+"]").prop("disabled", true);
|
||||||
|
});
|
||||||
|
$('#include_tag').selectpicker("refresh");
|
||||||
|
$('#exclude_tag').selectpicker("refresh");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -19,16 +19,9 @@ var direction = 0; // Descending order
|
|||||||
var sort = 0; // Show sorted entries
|
var sort = 0; // Show sorted entries
|
||||||
|
|
||||||
$("#sort_name").click(function() {
|
$("#sort_name").click(function() {
|
||||||
var class_name = $("h1").attr('Class') + "_sort_name";
|
var className = $("h1").attr("Class") + "_sort_name";
|
||||||
var obj = {};
|
var obj = {};
|
||||||
obj[class_name] = sort;
|
obj[className] = sort;
|
||||||
/*$.ajax({
|
|
||||||
method:"post",
|
|
||||||
contentType: "application/json; charset=utf-8",
|
|
||||||
dataType: "json",
|
|
||||||
url: window.location.pathname + "/../../ajax/view",
|
|
||||||
data: JSON.stringify({obj}),
|
|
||||||
});*/
|
|
||||||
|
|
||||||
var count = 0;
|
var count = 0;
|
||||||
var index = 0;
|
var index = 0;
|
||||||
|
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() {
|
$("#delete_confirm").click(function() {
|
||||||
//get data-id attribute of the clicked element
|
//get data-id attribute of the clicked element
|
||||||
@ -213,6 +241,56 @@ $(function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fillFileTable(path, type, folder, filt) {
|
||||||
|
if (window.location.pathname.endsWith("/basicconfig")) {
|
||||||
|
var request_path = "/../basicconfig/pathchooser/";
|
||||||
|
} else {
|
||||||
|
var request_path = "/../../ajax/pathchooser/";
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
dataType: "json",
|
||||||
|
data: {
|
||||||
|
path: path,
|
||||||
|
folder: folder,
|
||||||
|
filter: filt
|
||||||
|
},
|
||||||
|
url: window.location.pathname + request_path,
|
||||||
|
success: function success(data) {
|
||||||
|
if ($("#element_selected").text() ==="") {
|
||||||
|
$("#element_selected").text(data.cwd);
|
||||||
|
}
|
||||||
|
$("#file_table > tbody > tr").each(function () {
|
||||||
|
if ($(this).attr("id") !== "parent") {
|
||||||
|
$(this).closest("tr").remove();
|
||||||
|
} else {
|
||||||
|
if(data.absolute && data.parentdir !== "") {
|
||||||
|
$(this)[0].attributes['data-path'].value = data.parentdir;
|
||||||
|
} else {
|
||||||
|
$(this)[0].attributes['data-path'].value = "..";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (data.parentdir !== "") {
|
||||||
|
$("#parent").removeClass('hidden')
|
||||||
|
} else {
|
||||||
|
$("#parent").addClass('hidden')
|
||||||
|
}
|
||||||
|
// console.log(data);
|
||||||
|
data.files.forEach(function(entry) {
|
||||||
|
if(entry.type === "dir") {
|
||||||
|
var type = "<span class=\"glyphicon glyphicon-folder-close\"></span>";
|
||||||
|
} else {
|
||||||
|
var type = "";
|
||||||
|
}
|
||||||
|
$("<tr class=\"tr-clickable\" data-type=\"" + entry.type + "\" data-path=\"" +
|
||||||
|
entry.fullpath + "\"><td>" + type + "</td><td>" + entry.name + "</td><td>" +
|
||||||
|
entry.size + "</td></tr>").appendTo($("#file_table"));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
timeout: 2000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$(".discover .row").isotope({
|
$(".discover .row").isotope({
|
||||||
// options
|
// options
|
||||||
itemSelector : ".book",
|
itemSelector : ".book",
|
||||||
@ -402,18 +480,98 @@ $(function() {
|
|||||||
$("#config_delete_kobo_token").show();
|
$("#config_delete_kobo_token").show();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#btndeletetoken").click(function() {
|
$("#config_delete_kobo_token").click(function() {
|
||||||
//get data-id attribute of the clicked element
|
ConfirmDialog(
|
||||||
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
|
$(this).attr('id'),
|
||||||
var path = src.substring(0, src.lastIndexOf("/"));
|
$(this).data('value'),
|
||||||
// var domainId = $(this).value("domainId");
|
function (value) {
|
||||||
$.ajax({
|
var pathname = document.getElementsByTagName("script");
|
||||||
method:"get",
|
var src = pathname[pathname.length - 1].src;
|
||||||
url: path + "/../../kobo_auth/deleteauthtoken/" + this.value,
|
var path = src.substring(0, src.lastIndexOf("/"));
|
||||||
});
|
$.ajax({
|
||||||
$("#modalDeleteToken").modal("hide");
|
method: "get",
|
||||||
$("#config_delete_kobo_token").hide();
|
url: path + "/../../kobo_auth/deleteauthtoken/" + value,
|
||||||
|
});
|
||||||
|
$("#config_delete_kobo_token").hide();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#toggle_order_shelf").click(function() {
|
||||||
|
$("#new").toggleClass("disabled");
|
||||||
|
$("#old").toggleClass("disabled");
|
||||||
|
$("#asc").toggleClass("disabled");
|
||||||
|
$("#desc").toggleClass("disabled");
|
||||||
|
$("#auth_az").toggleClass("disabled");
|
||||||
|
$("#auth_za").toggleClass("disabled");
|
||||||
|
$("#pub_new").toggleClass("disabled");
|
||||||
|
$("#pub_old").toggleClass("disabled");
|
||||||
|
var alternative_text = $("#toggle_order_shelf").data('alt-text');
|
||||||
|
$("#toggle_order_shelf")[0].attributes['data-alt-text'].value = $("#toggle_order_shelf").html();
|
||||||
|
$("#toggle_order_shelf").html(alternative_text);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btndeluser").click(function() {
|
||||||
|
ConfirmDialog(
|
||||||
|
$(this).attr('id'),
|
||||||
|
$(this).data('value'),
|
||||||
|
function(value){
|
||||||
|
var subform = $('#user_submit').closest("form");
|
||||||
|
subform.submit(function(eventObj) {
|
||||||
|
$(this).append('<input type="hidden" name="delete" value="True" />');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
subform.submit();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
$("#user_submit").click(function() {
|
||||||
|
this.closest("form").submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#delete_shelf").click(function() {
|
||||||
|
ConfirmDialog(
|
||||||
|
$(this).attr('id'),
|
||||||
|
$(this).data('value'),
|
||||||
|
function(value){
|
||||||
|
window.location.href = window.location.pathname + "/../../shelf/delete/" + value
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$("#fileModal").on("show.bs.modal", function(e) {
|
||||||
|
var target = $(e.relatedTarget);
|
||||||
|
var path = $("#" + target.data("link"))[0].value;
|
||||||
|
var folder = target.data("folderonly");
|
||||||
|
var filter = target.data("filefilter");
|
||||||
|
$("#element_selected").text(path);
|
||||||
|
$("#file_confirm")[0].attributes["data-link"].value = target.data("link");
|
||||||
|
$("#file_confirm")[0].attributes["data-folderonly"].value = (typeof folder === 'undefined') ? false : true;
|
||||||
|
$("#file_confirm")[0].attributes["data-filefilter"].value = (typeof filter === 'undefined') ? "" : filter;
|
||||||
|
$("#file_confirm")[0].attributes["data-newfile"].value = target.data("newfile");
|
||||||
|
fillFileTable(path,"dir", folder, filter);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#file_confirm").click(function() {
|
||||||
|
$("#" + $(this).data("link"))[0].value = $("#element_selected").text()
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on("click", ".tr-clickable", function() {
|
||||||
|
var path = this.attributes["data-path"].value;
|
||||||
|
var type = this.attributes["data-type"].value;
|
||||||
|
var folder = $(file_confirm).data("folderonly");
|
||||||
|
var filter = $(file_confirm).data("filefilter");
|
||||||
|
var newfile = $(file_confirm).data("newfile");
|
||||||
|
if (newfile !== 'undefined') {
|
||||||
|
$("#element_selected").text(path + $("#new_file".text()));
|
||||||
|
} else {
|
||||||
|
$("#element_selected").text(path);
|
||||||
|
}
|
||||||
|
if(type === "dir") {
|
||||||
|
fillFileTable(path, type, folder, filter);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$(window).resize(function() {
|
$(window).resize(function() {
|
||||||
|
@ -45,14 +45,13 @@ $(function() {
|
|||||||
if (selections.length < 1) {
|
if (selections.length < 1) {
|
||||||
$("#delete_selection").addClass("disabled");
|
$("#delete_selection").addClass("disabled");
|
||||||
$("#delete_selection").attr("aria-disabled", true);
|
$("#delete_selection").attr("aria-disabled", true);
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
$("#delete_selection").removeClass("disabled");
|
$("#delete_selection").removeClass("disabled");
|
||||||
$("#delete_selection").attr("aria-disabled", false);
|
$("#delete_selection").attr("aria-disabled", false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$("#delete_selection").click(function() {
|
$("#delete_selection").click(function() {
|
||||||
$("#books-table").bootstrapTable('uncheckAll');
|
$("#books-table").bootstrapTable("uncheckAll");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#merge_confirm").click(function() {
|
$("#merge_confirm").click(function() {
|
||||||
@ -63,8 +62,8 @@ $(function() {
|
|||||||
url: window.location.pathname + "/../../ajax/mergebooks",
|
url: window.location.pathname + "/../../ajax/mergebooks",
|
||||||
data: JSON.stringify({"Merge_books":selections}),
|
data: JSON.stringify({"Merge_books":selections}),
|
||||||
success: function success() {
|
success: function success() {
|
||||||
$('#books-table').bootstrapTable('refresh');
|
$("#books-table").bootstrapTable("refresh");
|
||||||
$("#books-table").bootstrapTable('uncheckAll');
|
$("#books-table").bootstrapTable("uncheckAll");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -76,11 +75,11 @@ $(function() {
|
|||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../../ajax/simulatemerge",
|
url: window.location.pathname + "/../../ajax/simulatemerge",
|
||||||
data: JSON.stringify({"Merge_books":selections}),
|
data: JSON.stringify({"Merge_books":selections}),
|
||||||
success: function success(book_titles) {
|
success: function success(booTitles) {
|
||||||
$.each(book_titles.from, function(i, item) {
|
$.each(booTitles.from, function(i, item) {
|
||||||
$("<span>- " + item + "</span>").appendTo("#merge_from");
|
$("<span>- " + item + "</span>").appendTo("#merge_from");
|
||||||
});
|
});
|
||||||
$('#merge_to').text("- " + book_titles.to);
|
$("#merge_to").text("- " + booTitles.to);
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -126,34 +125,35 @@ $(function() {
|
|||||||
formatNoMatches: function () {
|
formatNoMatches: function () {
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
onEditableSave: function (field, row, oldvalue, $el) {
|
onEditableSave: function (field, row, oldvalue, $el) {
|
||||||
if (field === 'title' || field === 'authors') {
|
if (field === "title" || field === "authors") {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method:"get",
|
method:"get",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../../ajax/sort_value/" + field + '/' + row.id,
|
url: window.location.pathname + "/../../ajax/sort_value/" + field + "/" + row.id,
|
||||||
success: function success(data) {
|
success: function success(data) {
|
||||||
var key = Object.keys(data)[0]
|
var key = Object.keys(data)[0];
|
||||||
$("#books-table").bootstrapTable('updateCellByUniqueId', {
|
$("#books-table").bootstrapTable("updateCellByUniqueId", {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
field: key,
|
field: key,
|
||||||
value: data[key]
|
value: data[key]
|
||||||
});
|
});
|
||||||
console.log(data);
|
// console.log(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
onColumnSwitch: function (field, checked) {
|
onColumnSwitch: function (field, checked) {
|
||||||
var visible = $("#books-table").bootstrapTable('getVisibleColumns');
|
var visible = $("#books-table").bootstrapTable("getVisibleColumns");
|
||||||
var hidden = $("#books-table").bootstrapTable('getHiddenColumns');
|
var hidden = $("#books-table").bootstrapTable("getHiddenColumns");
|
||||||
var visibility =[]
|
var st = "";
|
||||||
var st = ""
|
|
||||||
visible.forEach(function(item) {
|
visible.forEach(function(item) {
|
||||||
st += "\""+ item.field + "\":\"" +"true"+ "\","
|
st += "\"" + item.field + "\":\"" + "true" + "\",";
|
||||||
});
|
});
|
||||||
hidden.forEach(function(item) {
|
hidden.forEach(function(item) {
|
||||||
st += "\""+ item.field + "\":\"" +"false"+ "\","
|
st += "\"" + item.field + "\":\"" + "false" + "\",";
|
||||||
});
|
});
|
||||||
st = st.slice(0, -1);
|
st = st.slice(0, -1);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@ -208,15 +208,13 @@ $(function() {
|
|||||||
},
|
},
|
||||||
striped: false
|
striped: false
|
||||||
});
|
});
|
||||||
$("#btndeletedomain").click(function() {
|
|
||||||
//get data-id attribute of the clicked element
|
function domain_handle(domainId) {
|
||||||
var domainId = $(this).data("domainId");
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method:"post",
|
method:"post",
|
||||||
url: window.location.pathname + "/../../ajax/deletedomain",
|
url: window.location.pathname + "/../../ajax/deletedomain",
|
||||||
data: {"domainid":domainId}
|
data: {"domainid":domainId}
|
||||||
});
|
});
|
||||||
$("#DeleteDomain").modal("hide");
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method:"get",
|
method:"get",
|
||||||
url: window.location.pathname + "/../../ajax/domainlist/1",
|
url: window.location.pathname + "/../../ajax/domainlist/1",
|
||||||
@ -235,12 +233,16 @@ $(function() {
|
|||||||
$("#domain-deny-table").bootstrapTable("load", data);
|
$("#domain-deny-table").bootstrapTable("load", data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
$("#domain-allow-table").on("click-cell.bs.table", function (field, value, row, $element) {
|
||||||
|
if (value === 2) {
|
||||||
|
ConfirmDialog("btndeletedomain", $element.id, domain_handle);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
//triggered when modal is about to be shown
|
$("#domain-deny-table").on("click-cell.bs.table", function (field, value, row, $element) {
|
||||||
$("#DeleteDomain").on("show.bs.modal", function(e) {
|
if (value === 2) {
|
||||||
//get data-id attribute of the clicked element and store in button
|
ConfirmDialog("btndeletedomain", $element.id, domain_handle);
|
||||||
var domainId = $(e.relatedTarget).data("domain-id");
|
}
|
||||||
$(e.currentTarget).find("#btndeletedomain").data("domainId", domainId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#restrictModal").on("hidden.bs.modal", function () {
|
$("#restrictModal").on("hidden.bs.modal", function () {
|
||||||
@ -253,14 +255,14 @@ $(function() {
|
|||||||
$("#h3").addClass("hidden");
|
$("#h3").addClass("hidden");
|
||||||
$("#h4").addClass("hidden");
|
$("#h4").addClass("hidden");
|
||||||
});
|
});
|
||||||
function startTable(type) {
|
function startTable(type, user_id) {
|
||||||
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
|
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
|
||||||
var path = src.substring(0, src.lastIndexOf("/"));
|
var path = src.substring(0, src.lastIndexOf("/"));
|
||||||
$("#restrict-elements-table").bootstrapTable({
|
$("#restrict-elements-table").bootstrapTable({
|
||||||
formatNoMatches: function () {
|
formatNoMatches: function () {
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
url: path + "/../../ajax/listrestriction/" + type,
|
url: path + "/../../ajax/listrestriction/" + type + "/" + user_id,
|
||||||
rowStyle: function(row) {
|
rowStyle: function(row) {
|
||||||
// console.log('Reihe :' + row + " Index :" + index);
|
// console.log('Reihe :' + row + " Index :" + index);
|
||||||
if (row.id.charAt(0) === "a") {
|
if (row.id.charAt(0) === "a") {
|
||||||
@ -274,13 +276,13 @@ $(function() {
|
|||||||
$.ajax ({
|
$.ajax ({
|
||||||
type: "Post",
|
type: "Post",
|
||||||
data: "id=" + row.id + "&type=" + row.type + "&Element=" + encodeURIComponent(row.Element),
|
data: "id=" + row.id + "&type=" + row.type + "&Element=" + encodeURIComponent(row.Element),
|
||||||
url: path + "/../../ajax/deleterestriction/" + type,
|
url: path + "/../../ajax/deleterestriction/" + type + "/" + user_id,
|
||||||
async: true,
|
async: true,
|
||||||
timeout: 900,
|
timeout: 900,
|
||||||
success:function() {
|
success:function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method:"get",
|
method:"get",
|
||||||
url: path + "/../../ajax/listrestriction/" + type,
|
url: path + "/../../ajax/listrestriction/" + type + "/" + user_id,
|
||||||
async: true,
|
async: true,
|
||||||
timeout: 900,
|
timeout: 900,
|
||||||
success:function(data) {
|
success:function(data) {
|
||||||
@ -296,7 +298,7 @@ $(function() {
|
|||||||
$("#restrict-elements-table").removeClass("table-hover");
|
$("#restrict-elements-table").removeClass("table-hover");
|
||||||
$("#restrict-elements-table").on("editable-save.bs.table", function (e, field, row) {
|
$("#restrict-elements-table").on("editable-save.bs.table", function (e, field, row) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: path + "/../../ajax/editrestriction/" + type,
|
url: path + "/../../ajax/editrestriction/" + type + "/" + user_id,
|
||||||
type: "Post",
|
type: "Post",
|
||||||
data: row
|
data: row
|
||||||
});
|
});
|
||||||
@ -304,13 +306,13 @@ $(function() {
|
|||||||
$("[id^=submit_]").click(function() {
|
$("[id^=submit_]").click(function() {
|
||||||
$(this)[0].blur();
|
$(this)[0].blur();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: path + "/../../ajax/addrestriction/" + type,
|
url: path + "/../../ajax/addrestriction/" + type + "/" + user_id,
|
||||||
type: "Post",
|
type: "Post",
|
||||||
data: $(this).closest("form").serialize() + "&" + $(this)[0].name + "=",
|
data: $(this).closest("form").serialize() + "&" + $(this)[0].name + "=",
|
||||||
success: function () {
|
success: function () {
|
||||||
$.ajax ({
|
$.ajax ({
|
||||||
method:"get",
|
method:"get",
|
||||||
url: path + "/../../ajax/listrestriction/" + type,
|
url: path + "/../../ajax/listrestriction/" + type + "/" + user_id,
|
||||||
async: true,
|
async: true,
|
||||||
timeout: 900,
|
timeout: 900,
|
||||||
success:function(data) {
|
success:function(data) {
|
||||||
@ -323,21 +325,21 @@ $(function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
$("#get_column_values").on("click", function() {
|
$("#get_column_values").on("click", function() {
|
||||||
startTable(1);
|
startTable(1, 0);
|
||||||
$("#h2").removeClass("hidden");
|
$("#h2").removeClass("hidden");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#get_tags").on("click", function() {
|
$("#get_tags").on("click", function() {
|
||||||
startTable(0);
|
startTable(0, 0);
|
||||||
$("#h1").removeClass("hidden");
|
$("#h1").removeClass("hidden");
|
||||||
});
|
});
|
||||||
$("#get_user_column_values").on("click", function() {
|
$("#get_user_column_values").on("click", function() {
|
||||||
startTable(3);
|
startTable(3, $(this).data('id'));
|
||||||
$("#h4").removeClass("hidden");
|
$("#h4").removeClass("hidden");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#get_user_tags").on("click", function() {
|
$("#get_user_tags").on("click", function() {
|
||||||
startTable(2);
|
startTable(2, $(this).data('id'));
|
||||||
$(this)[0].blur();
|
$(this)[0].blur();
|
||||||
$("#h3").removeClass("hidden");
|
$("#h3").removeClass("hidden");
|
||||||
});
|
});
|
||||||
@ -347,7 +349,7 @@ $(function() {
|
|||||||
/* Function for deleting domain restrictions */
|
/* Function for deleting domain restrictions */
|
||||||
function TableActions (value, row) {
|
function TableActions (value, row) {
|
||||||
return [
|
return [
|
||||||
"<a class=\"danger remove\" data-toggle=\"modal\" data-target=\"#DeleteDomain\" data-domain-id=\"" + row.id
|
"<a class=\"danger remove\" data-value=\"" + row.id
|
||||||
+ "\" title=\"Remove\">",
|
+ "\" title=\"Remove\">",
|
||||||
"<i class=\"glyphicon glyphicon-trash\"></i>",
|
"<i class=\"glyphicon glyphicon-trash\"></i>",
|
||||||
"</a>"
|
"</a>"
|
||||||
|
@ -9,7 +9,7 @@ from shutil import copyfile
|
|||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
from cps.services.worker import CalibreTask
|
from cps.services.worker import CalibreTask
|
||||||
from cps import calibre_db, db
|
from cps import db
|
||||||
from cps import logger, config
|
from cps import logger, config
|
||||||
from cps.subproc_wrapper import process_open
|
from cps.subproc_wrapper import process_open
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
@ -33,8 +33,9 @@ class TaskConvert(CalibreTask):
|
|||||||
def run(self, worker_thread):
|
def run(self, worker_thread):
|
||||||
self.worker_thread = worker_thread
|
self.worker_thread = worker_thread
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
cur_book = calibre_db.get_book(self.bookid)
|
worker_db = db.CalibreDB(expire_on_commit=False)
|
||||||
data = calibre_db.get_book_format(self.bookid, self.settings['old_book_format'])
|
cur_book = worker_db.get_book(self.bookid)
|
||||||
|
data = worker_db.get_book_format(self.bookid, self.settings['old_book_format'])
|
||||||
df = gdriveutils.getFileFromEbooksFolder(cur_book.path,
|
df = gdriveutils.getFileFromEbooksFolder(cur_book.path,
|
||||||
data.name + "." + self.settings['old_book_format'].lower())
|
data.name + "." + self.settings['old_book_format'].lower())
|
||||||
if df:
|
if df:
|
||||||
@ -44,10 +45,12 @@ class TaskConvert(CalibreTask):
|
|||||||
if not os.path.exists(os.path.join(config.config_calibre_dir, cur_book.path)):
|
if not os.path.exists(os.path.join(config.config_calibre_dir, cur_book.path)):
|
||||||
os.makedirs(os.path.join(config.config_calibre_dir, cur_book.path))
|
os.makedirs(os.path.join(config.config_calibre_dir, cur_book.path))
|
||||||
df.GetContentFile(datafile)
|
df.GetContentFile(datafile)
|
||||||
|
worker_db.session.close()
|
||||||
else:
|
else:
|
||||||
error_message = _(u"%(format)s not found on Google Drive: %(fn)s",
|
error_message = _(u"%(format)s not found on Google Drive: %(fn)s",
|
||||||
format=self.settings['old_book_format'],
|
format=self.settings['old_book_format'],
|
||||||
fn=data.name + "." + self.settings['old_book_format'].lower())
|
fn=data.name + "." + self.settings['old_book_format'].lower())
|
||||||
|
worker_db.session.close()
|
||||||
return error_message
|
return error_message
|
||||||
|
|
||||||
filename = self._convert_ebook_format()
|
filename = self._convert_ebook_format()
|
||||||
@ -71,21 +74,23 @@ class TaskConvert(CalibreTask):
|
|||||||
|
|
||||||
def _convert_ebook_format(self):
|
def _convert_ebook_format(self):
|
||||||
error_message = None
|
error_message = None
|
||||||
local_session = db.CalibreDB().session
|
local_db = db.CalibreDB(expire_on_commit=False)
|
||||||
file_path = self.file_path
|
file_path = self.file_path
|
||||||
book_id = self.bookid
|
book_id = self.bookid
|
||||||
format_old_ext = u'.' + self.settings['old_book_format'].lower()
|
format_old_ext = u'.' + self.settings['old_book_format'].lower()
|
||||||
format_new_ext = u'.' + self.settings['new_book_format'].lower()
|
format_new_ext = u'.' + self.settings['new_book_format'].lower()
|
||||||
|
|
||||||
# check to see if destination format already exists -
|
# check to see if destination format already exists - or if book is in database
|
||||||
# if it does - mark the conversion task as complete and return a success
|
# if it does - mark the conversion task as complete and return a success
|
||||||
# this will allow send to kindle workflow to continue to work
|
# this will allow send to kindle workflow to continue to work
|
||||||
if os.path.isfile(file_path + format_new_ext):
|
if os.path.isfile(file_path + format_new_ext) or\
|
||||||
|
local_db.get_book_format(self.bookid, self.settings['new_book_format']):
|
||||||
log.info("Book id %d already converted to %s", book_id, format_new_ext)
|
log.info("Book id %d already converted to %s", book_id, format_new_ext)
|
||||||
cur_book = calibre_db.get_book(book_id)
|
cur_book = local_db.get_book(book_id)
|
||||||
self.results['path'] = file_path
|
self.results['path'] = file_path
|
||||||
self.results['title'] = cur_book.title
|
self.results['title'] = cur_book.title
|
||||||
self._handleSuccess()
|
self._handleSuccess()
|
||||||
|
local_db.session.close()
|
||||||
return os.path.basename(file_path + format_new_ext)
|
return os.path.basename(file_path + format_new_ext)
|
||||||
else:
|
else:
|
||||||
log.info("Book id %d - target format of %s does not exist. Moving forward with convert.",
|
log.info("Book id %d - target format of %s does not exist. Moving forward with convert.",
|
||||||
@ -105,18 +110,18 @@ class TaskConvert(CalibreTask):
|
|||||||
check, error_message = self._convert_calibre(file_path, format_old_ext, format_new_ext)
|
check, error_message = self._convert_calibre(file_path, format_old_ext, format_new_ext)
|
||||||
|
|
||||||
if check == 0:
|
if check == 0:
|
||||||
cur_book = calibre_db.get_book(book_id)
|
cur_book = local_db.get_book(book_id)
|
||||||
if os.path.isfile(file_path + format_new_ext):
|
if os.path.isfile(file_path + format_new_ext):
|
||||||
# self.db_queue.join()
|
|
||||||
new_format = db.Data(name=cur_book.data[0].name,
|
new_format = db.Data(name=cur_book.data[0].name,
|
||||||
book_format=self.settings['new_book_format'].upper(),
|
book_format=self.settings['new_book_format'].upper(),
|
||||||
book=book_id, uncompressed_size=os.path.getsize(file_path + format_new_ext))
|
book=book_id, uncompressed_size=os.path.getsize(file_path + format_new_ext))
|
||||||
try:
|
try:
|
||||||
local_session.merge(new_format)
|
local_db.session.merge(new_format)
|
||||||
local_session.commit()
|
local_db.session.commit()
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
local_session.rollback()
|
local_db.session.rollback()
|
||||||
log.error("Database error: %s", e)
|
log.error("Database error: %s", e)
|
||||||
|
local_db.session.close()
|
||||||
return
|
return
|
||||||
self.results['path'] = cur_book.path
|
self.results['path'] = cur_book.path
|
||||||
self.results['title'] = cur_book.title
|
self.results['title'] = cur_book.title
|
||||||
@ -125,6 +130,7 @@ class TaskConvert(CalibreTask):
|
|||||||
return os.path.basename(file_path + format_new_ext)
|
return os.path.basename(file_path + format_new_ext)
|
||||||
else:
|
else:
|
||||||
error_message = _('%(format)s format not found on disk', format=format_new_ext.upper())
|
error_message = _('%(format)s format not found on disk', format=format_new_ext.upper())
|
||||||
|
local_db.session.close()
|
||||||
log.info("ebook converter failed with error while converting book")
|
log.info("ebook converter failed with error while converting book")
|
||||||
if not error_message:
|
if not error_message:
|
||||||
error_message = _('Ebook converter failed with unknown error')
|
error_message = _('Ebook converter failed with unknown error')
|
||||||
|
@ -167,7 +167,7 @@ class TaskEmail(CalibreTask):
|
|||||||
smtplib.stderr = org_smtpstderr
|
smtplib.stderr = org_smtpstderr
|
||||||
|
|
||||||
except (MemoryError) as e:
|
except (MemoryError) as e:
|
||||||
log.exception(e)
|
log.debug_or_exception(e)
|
||||||
self._handleError(u'MemoryError sending email: ' + str(e))
|
self._handleError(u'MemoryError sending email: ' + str(e))
|
||||||
# return None
|
# return None
|
||||||
except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
|
except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
|
||||||
@ -178,7 +178,7 @@ class TaskEmail(CalibreTask):
|
|||||||
elif hasattr(e, "args"):
|
elif hasattr(e, "args"):
|
||||||
text = '\n'.join(e.args)
|
text = '\n'.join(e.args)
|
||||||
else:
|
else:
|
||||||
log.exception(e)
|
log.debug_or_exception(e)
|
||||||
text = ''
|
text = ''
|
||||||
self._handleError(u'Smtplib Error sending email: ' + text)
|
self._handleError(u'Smtplib Error sending email: ' + text)
|
||||||
# return None
|
# return None
|
||||||
@ -225,7 +225,7 @@ class TaskEmail(CalibreTask):
|
|||||||
data = file_.read()
|
data = file_.read()
|
||||||
file_.close()
|
file_.close()
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
log.exception(e)
|
log.debug_or_exception(e)
|
||||||
log.error(u'The requested file could not be read. Maybe wrong permissions?')
|
log.error(u'The requested file could not be read. Maybe wrong permissions?')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -36,7 +36,10 @@
|
|||||||
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
|
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}">
|
<a href="{{ url_for('web.show_book', book_id=entry.id) }}">
|
||||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" />
|
<span class="img">
|
||||||
|
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" />
|
||||||
|
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
|
@ -197,7 +197,8 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal %}
|
{% block modal %}
|
||||||
{{ delete_book(book.id) }}
|
{{ delete_book() }}
|
||||||
|
{{ delete_confirm_modal() }}
|
||||||
|
|
||||||
<div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel">
|
<div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel">
|
||||||
<div class="modal-dialog modal-lg" role="document">
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
</table>
|
</table>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block modal %}
|
{% block modal %}
|
||||||
{{ delete_book(0) }}
|
{{ delete_book() }}
|
||||||
{% if g.user.role_edit() %}
|
{% if g.user.role_edit() %}
|
||||||
<div class="modal fade" id="mergeModal" role="dialog" aria-labelledby="metaMergeLabel">
|
<div class="modal fade" id="mergeModal" role="dialog" aria-labelledby="metaMergeLabel">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
@ -16,12 +16,19 @@
|
|||||||
<div id="collapseOne" class="panel-collapse collapse in">
|
<div id="collapseOne" class="panel-collapse collapse in">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<label for="config_calibre_dir">{{_('Location of Calibre Database')}}</label>
|
<label for="config_calibre_dir">{{_('Location of Calibre Database')}}</label>
|
||||||
<div class="form-group required input-group">
|
<div class="form-group required{% if filepicker %} input-group{% endif %}">
|
||||||
<input type="text" class="form-control" id="config_calibre_dir" name="config_calibre_dir" value="{% if config.config_calibre_dir != None %}{{ config.config_calibre_dir }}{% endif %}" autocomplete="off">
|
<input type="text" class="form-control" id="config_calibre_dir" name="config_calibre_dir" value="{% if config.config_calibre_dir != None %}{{ config.config_calibre_dir }}{% endif %}" autocomplete="off">
|
||||||
|
{% if filepicker %}
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
<button type="button" data-toggle="modal" id="calibre_modal_path" data-link="config_calibre_dir" data-filefilter="metadata.db" data-target="#fileModal" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||||
</span>
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if not filepicker %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label id="filepicker-hint">{{_('To activate serverside filepicker start Calibre-Web with -f optionn')}}</label>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if feature_support['gdrive'] %}
|
{% if feature_support['gdrive'] %}
|
||||||
<div class="form-group required">
|
<div class="form-group required">
|
||||||
<input type="checkbox" id="config_use_google_drive" name="config_use_google_drive" data-control="gdrive_settings" {% if config.config_use_google_drive %}checked{% endif %} >
|
<input type="checkbox" id="config_use_google_drive" name="config_use_google_drive" data-control="gdrive_settings" {% if config.config_use_google_drive %}checked{% endif %} >
|
||||||
@ -94,14 +101,14 @@
|
|||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" class="form-control" id="config_certfile" name="config_certfile" value="{% if config.config_certfile != None %}{{ config.config_certfile }}{% endif %}" autocomplete="off">
|
<input type="text" class="form-control" id="config_certfile" name="config_certfile" value="{% if config.config_certfile != None %}{{ config.config_certfile }}{% endif %}" autocomplete="off">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" id="certfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
<button type="button" data-toggle="modal" data-link="config_certfile" data-target="#fileModal" id="certfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<label for="config_calibre_dir" >{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label>
|
<label for="config_calibre_dir" >{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label>
|
||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" class="form-control" id="config_keyfile" name="config_keyfile" value="{% if config.config_keyfile != None %}{{ config.config_keyfile }}{% endif %}" autocomplete="off">
|
<input type="text" class="form-control" id="config_keyfile" name="config_keyfile" value="{% if config.config_keyfile != None %}{{ config.config_keyfile }}{% endif %}" autocomplete="off">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" id="keyfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
<button type="button" id="keyfile_path" data-toggle="modal" data-link="config_keyfile" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -268,21 +275,21 @@
|
|||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" class="form-control" id="config_ldap_cacert_path" name="config_ldap_cacert_path" value="{% if config.config_ldap_cacert_path != None %}{{ config.config_ldap_cacert_path }}{% endif %}" autocomplete="off">
|
<input type="text" class="form-control" id="config_ldap_cacert_path" name="config_ldap_cacert_path" value="{% if config.config_ldap_cacert_path != None %}{{ config.config_ldap_cacert_path }}{% endif %}" autocomplete="off">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
<button type="button" id="library_path" data-toggle="modal" data-link="config_ldap_cacert_path" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<label for="config_ldap_cert_path">{{_('LDAP Certificate Path (Only needed for Client Certificate Authentication)')}}</label>
|
<label for="config_ldap_cert_path">{{_('LDAP Certificate Path (Only needed for Client Certificate Authentication)')}}</label>
|
||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" class="form-control" id="config_ldap_cert_path" name="config_ldap_cert_path" value="{% if config.config_ldap_cert_path != None %}{{ config.config_ldap_cert_path }}{% endif %}" autocomplete="off">
|
<input type="text" class="form-control" id="config_ldap_cert_path" name="config_ldap_cert_path" value="{% if config.config_ldap_cert_path != None %}{{ config.config_ldap_cert_path }}{% endif %}" autocomplete="off">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
<button type="button" id="library_path" data-toggle="modal" data-link="config_ldap_cert_path" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<label for="config_ldap_key_path">{{_('LDAP Keyfile Path (Only needed for Client Certificate Authentication)')}}</label>
|
<label for="config_ldap_key_path">{{_('LDAP Keyfile Path (Only needed for Client Certificate Authentication)')}}</label>
|
||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" class="form-control" id="config_ldap_key_path" name="config_ldap_key_path" value="{% if config.config_ldap_key_path != None %}{{ config.config_ldap_key_path }}{% endif %}" autocomplete="off">
|
<input type="text" class="form-control" id="config_ldap_key_path" name="config_ldap_key_path" value="{% if config.config_ldap_key_path != None %}{{ config.config_ldap_key_path }}{% endif %}" autocomplete="off">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
<button type="button" id="library_path" data-toggle="modal" data-link="config_ldap_key_path" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -384,7 +391,7 @@
|
|||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" class="form-control" id="config_converterpath" name="config_converterpath" value="{% if config.config_converterpath != None %}{{ config.config_converterpath }}{% endif %}" autocomplete="off">
|
<input type="text" class="form-control" id="config_converterpath" name="config_converterpath" value="{% if config.config_converterpath != None %}{{ config.config_converterpath }}{% endif %}" autocomplete="off">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" id="converter_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
<button type="button" data-toggle="modal" id="converter_modal_path" data-link="config_converterpath" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -395,7 +402,7 @@
|
|||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" class="form-control" id="config_kepubifypath" name="config_kepubifypath" value="{% if config.config_kepubifypath != None %}{{ config.config_kepubifypath }}{% endif %}" autocomplete="off">
|
<input type="text" class="form-control" id="config_kepubifypath" name="config_kepubifypath" value="{% if config.config_kepubifypath != None %}{{ config.config_kepubifypath }}{% endif %}" autocomplete="off">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" id="kepubify_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
<button type="button" id="kepubify_path" data-toggle="modal" data-link="config_kepubifypath" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% if feature_support['rar'] %}
|
{% if feature_support['rar'] %}
|
||||||
@ -403,7 +410,7 @@
|
|||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" class="form-control" id="config_rarfile_location" name="config_rarfile_location" value="{% if config.config_rarfile_location != None %}{{ config.config_rarfile_location }}{% endif %}" autocomplete="off">
|
<input type="text" class="form-control" id="config_rarfile_location" name="config_rarfile_location" value="{% if config.config_rarfile_location != None %}{{ config.config_rarfile_location }}{% endif %}" autocomplete="off">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" id="unrar_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
<button type="button" id="unrar_path" data-toggle="modal" data-link="config_rarfile_location" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -412,8 +419,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
{% if not show_login_button %}
|
{% if not show_login_button %}
|
||||||
<button type="submit" name="submit" class="btn btn-default">{{_('Save')}}</button>
|
<button type="submit" name="submit" class="btn btn-default">{{_('Save')}}</button>
|
||||||
@ -428,6 +433,9 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block modal %}
|
||||||
|
{{ filechooser_modal() }}
|
||||||
|
{% endblock %}
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(document).on('change', '#config_use_google_drive', function() {
|
$(document).on('change', '#config_use_google_drive', function() {
|
||||||
|
@ -8,7 +8,10 @@
|
|||||||
<div class="cover">
|
<div class="cover">
|
||||||
{% if entry.has_cover is defined %}
|
{% if entry.has_cover is defined %}
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
<span class="img">
|
||||||
|
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||||
|
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -89,20 +89,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block modal %}
|
{% block modal %}
|
||||||
{% if g.allow_registration %}
|
{% if g.allow_registration %}
|
||||||
<div id="DeleteDomain" class="modal fade" role="dialog">
|
{{ delete_confirm_modal() }}
|
||||||
<div class="modal-dialog modal-sm">
|
|
||||||
<!-- Modal content-->
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header bg-danger">
|
|
||||||
</div>
|
|
||||||
<div class="modal-body text-center">
|
|
||||||
<p>{{_('Are you sure you want to delete this domain?')}}</p>
|
|
||||||
<button type="button" class="btn btn-danger" id="btndeletedomain" >{{_('Delete')}}</button>
|
|
||||||
<button type="button" class="btn btn-default" id="btncancel" data-dismiss="modal">{{_('Cancel')}}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
@ -28,8 +28,10 @@
|
|||||||
<div class="col-sm-3 col-lg-2 col-xs-6 book sortable" {% if entry[0].sort %}data-name="{{entry[0].series[0].name}}"{% endif %} data-id="{% if entry[0].series[0].name %}{{entry[0].series[0].name}}{% endif %}">
|
<div class="col-sm-3 col-lg-2 col-xs-6 book sortable" {% if entry[0].sort %}data-name="{{entry[0].series[0].name}}"{% endif %} data-id="{% if entry[0].series[0].name %}{{entry[0].series[0].name}}{% endif %}">
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<a href="{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].series[0].id )}}">
|
<a href="{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].series[0].id )}}">
|
||||||
<img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/>
|
<span class="img">
|
||||||
<span class="badge">{{entry.count}}</span>
|
<img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/>
|
||||||
|
<span class="badge">{{entry.count}}</span>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
|
@ -8,7 +8,10 @@
|
|||||||
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books_rand">
|
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books_rand">
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
<span class="img">
|
||||||
|
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||||
|
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
@ -82,7 +85,10 @@
|
|||||||
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books">
|
<div class="col-sm-3 col-lg-2 col-xs-6 book" id="books">
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||||
|
<span class="img">
|
||||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}"/>
|
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}"/>
|
||||||
|
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% from 'modal_dialogs.html' import restrict_modal, delete_book %}
|
{% from 'modal_dialogs.html' import restrict_modal, delete_book, filechooser_modal, delete_confirm_modal %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ g.user.locale }}">
|
<html lang="{{ g.user.locale }}">
|
||||||
<head>
|
<head>
|
||||||
@ -189,8 +189,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% block modal %}{% endblock %}
|
{% block modal %}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
||||||
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
|
||||||
<!-- Include all compiled plugins (below), or include individual files as needed -->
|
<!-- Include all compiled plugins (below), or include individual files as needed -->
|
||||||
@ -200,14 +198,7 @@
|
|||||||
<script src="{{ url_for('static', filename='js/libs/context.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/context.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/libs/plugins.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/plugins.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/libs/jquery.form.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/jquery.form.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
|
||||||
<script src="{{ url_for('static', filename='js/uploadprogress.js') }}"> </script>
|
<script src="{{ url_for('static', filename='js/uploadprogress.js') }}"> </script>
|
||||||
{% if g.current_theme == 1 %}
|
|
||||||
<script src="{{ url_for('static', filename='js/libs/jquery.visible.min.js') }}"></script>
|
|
||||||
<script src="{{ url_for('static', filename='js/libs/compromise.min.js') }}"></script>
|
|
||||||
<script src="{{ url_for('static', filename='js/libs/readmore.min.js') }}"></script>
|
|
||||||
<script src="{{ url_for('static', filename='js/caliBlur.js') }}"></script>
|
|
||||||
{% endif %}
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function() {
|
$(function() {
|
||||||
$("#form-upload").uploadprogress({
|
$("#form-upload").uploadprogress({
|
||||||
@ -219,6 +210,13 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
|
{% if g.current_theme == 1 %}
|
||||||
|
<script src="{{ url_for('static', filename='js/libs/jquery.visible.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/libs/compromise.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/libs/readmore.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/caliBlur.js') }}"></script>
|
||||||
|
{% endif %}
|
||||||
{% block js %}{% endblock %}
|
{% block js %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
<button type="submit" name="forgot" value="forgot" class="btn btn-default">{{_('Forgot Password?')}}</button>
|
<button type="submit" name="forgot" value="forgot" class="btn btn-default">{{_('Forgot Password?')}}</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if config.config_remote_login %}
|
{% if config.config_remote_login %}
|
||||||
<a href="{{url_for('web.remote_login')}}" class="pull-right">{{_('Log in with Magic Link')}}</a>
|
<a href="{{url_for('remotelogin.remote_login')}}" id="remote_login" class="pull-right">{{_('Log in with Magic Link')}}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if config.config_login_type == 2 %}
|
{% if config.config_login_type == 2 %}
|
||||||
{% if 1 in oauth_check %}
|
{% if 1 in oauth_check %}
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
{% macro delete_book(bookid) %}
|
{% macro delete_book() %}
|
||||||
{% if g.user.role_delete_books() %}
|
{% if g.user.role_delete_books() %}
|
||||||
<div class="modal fade" id="deleteModal" role="dialog" aria-labelledby="metaDeleteLabel">
|
<div class="modal fade" id="deleteModal" role="dialog" aria-labelledby="metaDeleteLabel">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
@ -68,3 +68,56 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
{% macro filechooser_modal() %}
|
||||||
|
<div class="modal fade" id="fileModal" role="dialog" aria-labelledby="metafileLabel">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-info text-center">
|
||||||
|
<span>{{_('Choose File Location')}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<table id="file_table" class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{_('type')}}</th>
|
||||||
|
<th>{{_('name')}}</th>
|
||||||
|
<th>{{_('size')}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tbody">
|
||||||
|
<tr class="tr-clickable hidden" id="parent" data-type="dir" data-path="..">
|
||||||
|
<td><span class="glyphicon glyphicon-folder-close"></span></td>
|
||||||
|
<td title="{{_('Parent Directory')}}"><span class="parentdir">..</span></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="text-left" id="element_selected"></div>
|
||||||
|
<input type="button" class="btn btn-primary" data-path="" data-link="" data-folderonly="" data-filefilter="" data-newfile="" value="{{_('Select')}}" name="file_confirm" id="file_confirm" data-dismiss="modal">
|
||||||
|
<button type="button" id="file_abort" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro delete_confirm_modal() %}
|
||||||
|
<div id="GeneralDeleteModal" class="modal fade" role="Dialog">
|
||||||
|
<div class="modal-dialog modal-sm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-danger text-center">
|
||||||
|
<span id="header"></span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center">
|
||||||
|
<span id="text"></span>
|
||||||
|
<p></p>
|
||||||
|
<button id="btnConfirmYes" type="button" class="btn btn btn-danger">{{_('Delete')}}</button>
|
||||||
|
<button id="btnConfirmNo" type="button" class="btn btn-default">{{_('Cancel')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endmacro %}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<h2 style="margin-top: 0">{{_('Magic Link - Authorise New Device')}}</h2>
|
<h2 style="margin-top: 0">{{_('Magic Link - Authorise New Device')}}</h2>
|
||||||
<p>
|
<p>
|
||||||
{{_('On another device, login and visit:')}}
|
{{_('On another device, login and visit:')}}
|
||||||
<h4><a href="{{verify_url}}">{{verify_url}}</a></b>
|
<h4><a id="verify_url" href="{{verify_url}}">{{verify_url}}</a></b>
|
||||||
</h4>
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
{{_('Once verified, you will automatically be logged in on this device.')}}
|
{{_('Once verified, you will automatically be logged in on this device.')}}
|
||||||
@ -20,7 +20,7 @@
|
|||||||
(function () {
|
(function () {
|
||||||
// Poll the server to check if the user has authenticated
|
// Poll the server to check if the user has authenticated
|
||||||
var t = setInterval(function () {
|
var t = setInterval(function () {
|
||||||
$.post('{{url_for("web.token_verified")}}', { token: '{{token}}' })
|
$.post('{{url_for("remotelogin.token_verified")}}', { token: '{{token}}' })
|
||||||
.done(function(response) {
|
.done(function(response) {
|
||||||
if (response.status === 'success') {
|
if (response.status === 'success') {
|
||||||
// Wait a tick so cookies are updated
|
// Wait a tick so cookies are updated
|
||||||
|
@ -43,7 +43,10 @@
|
|||||||
<div class="cover">
|
<div class="cover">
|
||||||
{% if entry.has_cover is defined %}
|
{% if entry.has_cover is defined %}
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
<span class="img">
|
||||||
|
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||||
|
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,87 +31,87 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label for="include_tag">{{_('Tags')}}</label>
|
<div class="form-group">
|
||||||
<div class="form-group" id="tag">
|
<label for="read_status">{{_('Read Status')}}</label>
|
||||||
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
|
<select name="read_status" id="read_status" class="form-control">
|
||||||
{% for tag in tags %}
|
<option value="" selected></option>
|
||||||
<label id="tag_{{tag.id}}" class="btn btn-primary tags_click">
|
<option value="True" >{{_('Yes')}}</option>
|
||||||
<input type="checkbox" autocomplete="off" name="include_tag" id="include_tag" value="{{tag.id}}">{{tag.name}}</input>
|
<option value="False" >{{_('No')}}</option>
|
||||||
</label>
|
</select>
|
||||||
{% endfor %}
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-sm-6" id="tag">
|
||||||
|
<div><label for="include_tag">{{_('Tags')}}</label></div>
|
||||||
|
<select class="selectpicker" name="include_tag" id="include_tag" data-live-search="true" data-style="btn-primary" multiple>
|
||||||
|
{% for tag in tags %}
|
||||||
|
<option class="tags_click" value="{{tag.id}}">{{tag.name}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-sm-6">
|
||||||
|
<div><label for="exclude_tag">{{_('Exclude Tags')}}</label></div>
|
||||||
|
<select class="selectpicker" name="exclude_tag" id="exclude_tag" data-live-search="true" data-style="btn-danger" multiple>
|
||||||
|
{% for tag in tags %}
|
||||||
|
<option class="tags_click" value="{{tag.id}}">{{tag.name}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label for="exclude_tag">{{_('Exclude Tags')}}</label>
|
<div class="row">
|
||||||
<div class="form-group">
|
<div class="form-group col-sm-6">
|
||||||
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
|
<div><label for="include_serie">{{_('Series')}}</label></div>
|
||||||
{% for tag in tags %}
|
<select class="selectpicker" name="include_serie" id="include_serie" data-live-search="true" data-style="btn-primary" multiple>
|
||||||
<label id="exclude_tag_{{tag.id}}" class="btn btn-danger tags_click">
|
{% for serie in series %}
|
||||||
<input type="checkbox" autocomplete="off" name="exclude_tag" id="exclude_tag" value="{{tag.id}}">{{tag.name}}</input>
|
<option value="{{serie.id}}">{{serie.name}}</option>
|
||||||
</label>
|
{% endfor %}
|
||||||
{% endfor %}
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="form-group col-sm-6">
|
||||||
<label for="include_serie">{{_('Series')}}</label>
|
<div><label for="exclude_serie">{{_('Exclude Series')}}</label></div>
|
||||||
<div class="form-group">
|
<select class="selectpicker" name="exclude_serie" id="exclude_serie" data-live-search="true" data-style="btn-danger" multiple>
|
||||||
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
|
{% for serie in series %}
|
||||||
{% for serie in series %}
|
<option value="{{serie.id}}">{{serie.name}}</option>
|
||||||
<label id="serie_{{serie.id}}" class="btn btn-primary serie_click">
|
{% endfor %}
|
||||||
<input type="checkbox" autocomplete="off" name="include_serie" id="include_serie" value="{{serie.id}}">{{serie.name}}</input>
|
</select>
|
||||||
</label>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<label for="exclude_serie">{{_('Exclude Series')}}</label>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
|
|
||||||
{% for serie in series %}
|
|
||||||
<label id="exclude_serie_{{serie.id}}" class="btn btn-danger serie_click">
|
|
||||||
<input type="checkbox" autocomplete="off" name="exclude_serie" id="exclude_serie" value="{{serie.id}}">{{serie.name}}</input>
|
|
||||||
</label>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if languages %}
|
{% if languages %}
|
||||||
<label for="include_language">{{_('Languages')}}</label>
|
<div class="row">
|
||||||
<div class="form-group">
|
<div class="form-group col-sm-6">
|
||||||
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
|
<div><label for="include_language">{{_('Languages')}}</label></div>
|
||||||
|
<select class="selectpicker" name="include_language" id="include_language" data-live-search="true" data-style="btn-primary" multiple>
|
||||||
{% for language in languages %}
|
{% for language in languages %}
|
||||||
<label id="language_{{language.id}}" class="btn btn-primary serie_click">
|
<option value="{{language.id}}">{{language.name}}</option>
|
||||||
<input type="checkbox" autocomplete="off" name="include_language" id="include_language" value="{{language.id}}">{{language.name}}</input>
|
|
||||||
</label>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<label for="exclude_language">{{_('Exclude Languages')}}</label>
|
<div class="form-group col-sm-6">
|
||||||
<div class="form-group">
|
<div><label for="exclude_language">{{_('Exclude Languages')}}</label></div>
|
||||||
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
|
<select class="selectpicker" name="exclude_language" id="exclude_language" data-live-search="true" data-style="btn-danger" multiple>
|
||||||
{% for language in languages %}
|
{% for language in languages %}
|
||||||
<label id="exclude_language_{{language.id}}" class="btn btn-danger language_click">
|
<option value="{{language.id}}">{{language.name}}</option>
|
||||||
<input type="checkbox" autocomplete="off" name="exclude_language" id="exclude_language" value="{{language.id}}">{{language.name}}</input>
|
|
||||||
</label>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</select>
|
||||||
</div>
|
|
||||||
{% endif%}
|
|
||||||
<label for="include_extension">{{_('Extensions')}}</label>
|
|
||||||
<div class="form-group" id="extension">
|
|
||||||
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
|
|
||||||
{% for extension in extensions %}
|
|
||||||
<label id="extension_{{extension.format}}" class="btn btn-primary extension_click">
|
|
||||||
<input type="checkbox" autocomplete="off" name="include_extension" id="include_extension" value="{{extension.format}}">{{extension.format}}</input>
|
|
||||||
</label>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label for="exclude_extension">{{_('Exclude Extensions')}}</label>
|
{% endif%}
|
||||||
<div class="form-group">
|
<div class="row">
|
||||||
<div class="btn-toolbar btn-toolbar-lg" data-toggle="buttons">
|
<div class="form-group col-sm-6">
|
||||||
|
<div><label for="include_extension">{{_('Extensions')}}</label></div>
|
||||||
|
<select id="include_extension" class="selectpicker" name="include_extension" id="include_extension" data-live-search="true" data-style="btn-primary" multiple>
|
||||||
{% for extension in extensions %}
|
{% for extension in extensions %}
|
||||||
<label id="exclude_extension_{{extension.format}}" class="btn btn-danger extension_click">
|
<option value="{{extension.format}}">{{extension.format}}</option>
|
||||||
<input type="checkbox" autocomplete="off" name="exclude_extension" id="exclude_extension" value="{{extension.format}}">{{extension.format}}</input>
|
{% endfor %}
|
||||||
</label>
|
</select>
|
||||||
{% endfor %}
|
</div>
|
||||||
</div>
|
<div class="form-group col-sm-6">
|
||||||
|
<div><label for="exclude_extension">{{_('Exclude Extensions')}}</label></div>
|
||||||
|
<select id="exclude_extension" class="selectpicker" name="exclude_extension" id="exclude_extension" data-live-search="true" data-style="btn-danger" multiple>
|
||||||
|
{% for extension in extensions %}
|
||||||
|
<option value="{{extension.format}}">{{extension.format}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-sm-6">
|
<div class="form-group col-sm-6">
|
||||||
@ -189,10 +189,13 @@
|
|||||||
<script src="{{ url_for('static', filename='js/libs/bootstrap-rating-input.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-rating-input.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/libs/typeahead.bundle.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/typeahead.bundle.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/edit_books.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/edit_books.js') }}"></script>
|
||||||
<script>
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-select.min.js')}}"></script>
|
||||||
</script>
|
{% if not g.user.locale == 'en' %}
|
||||||
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-select/defaults-' + g.user.locale + '.min.js') }}" charset="UTF-8"></script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<link href="{{ url_for('static', filename='css/libs/typeahead.css') }}" rel="stylesheet" media="screen">
|
<link href="{{ url_for('static', filename='css/libs/typeahead.css') }}" rel="stylesheet" media="screen">
|
||||||
<link href="{{ url_for('static', filename='css/libs/bootstrap-datepicker3.min.css') }}" rel="stylesheet" media="screen">
|
<link href="{{ url_for('static', filename='css/libs/bootstrap-datepicker3.min.css') }}" rel="stylesheet" media="screen">
|
||||||
|
<link href="{{ url_for('static', filename='css/libs/bootstrap-select.min.css') }}" rel="stylesheet" >
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -2,23 +2,38 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="discover">
|
<div class="discover">
|
||||||
<h2>{{title}}</h2>
|
<h2>{{title}}</h2>
|
||||||
{% if g.user.role_download() %}
|
{% if g.user.role_download() %}
|
||||||
<a id="shelf_down" href="{{ url_for('shelf.show_shelf', shelf_type=2, shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a>
|
<a id="shelf_down" href="{{ url_for('shelf.show_simpleshelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if g.user.is_authenticated %}
|
{% if g.user.is_authenticated %}
|
||||||
{% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
|
{% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
|
||||||
<div id="delete_shelf" data-toggle="modal" data-target="#DeleteShelfDialog" class="btn btn-danger">{{ _('Delete this Shelf') }} </div>
|
<div class="btn btn-danger" id="delete_shelf" data-value="{{ shelf.id }}">{{ _('Delete this Shelf') }}</div>
|
||||||
<a id="edit_shelf" href="{{ url_for('shelf.edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf') }} </a>
|
<a id="edit_shelf" href="{{ url_for('shelf.edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf Properties') }} </a>
|
||||||
<a id="order_shelf" href="{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a>
|
{% if entries.__len__() %}
|
||||||
|
<a id="order_shelf" href="{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Arrange books manually') }} </a>
|
||||||
|
<button id="toggle_order_shelf" type="button" data-alt-text="{{ _('Disable Change order') }}" class="btn btn-primary">{{ _('Enable Change order') }}</button>
|
||||||
|
<div class="filterheader hidden-xs hidden-sm">
|
||||||
|
<a data-toggle="tooltip" title="{{_('Sort according to book date, newest first')}}" id="new" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||||
|
<a data-toggle="tooltip" title="{{_('Sort according to book date, oldest first')}}" id="old" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||||
|
<a data-toggle="tooltip" title="{{_('Sort title in alphabetical order')}}" id="asc" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||||
|
<a data-toggle="tooltip" title="{{_('Sort title in reverse alphabetical order')}}" id="desc" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||||
|
<a data-toggle="tooltip" title="{{_('Sort authors in alphabetical order')}}" id="auth_az" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='authaz')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||||
|
<a data-toggle="tooltip" title="{{_('Sort authors in reverse alphabetical order')}}" id="auth_za" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='authza')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||||
|
<a data-toggle="tooltip" title="{{_('Sort according to publishing date, newest first')}}" id="pub_new" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||||
|
<a data-toggle="tooltip" title="{{_('Sort according to publishing date, oldest first')}}" id="pub_old" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="row display-flex">
|
<div class="row display-flex">
|
||||||
|
|
||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
<div class="col-sm-3 col-lg-2 col-xs-6 book">
|
<div class="col-sm-3 col-lg-2 col-xs-6 book">
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
<span class="img">
|
||||||
|
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
|
||||||
|
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
@ -68,7 +83,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="DeleteShelfDialog" class="modal fade" role="dialog">
|
<!--div id="DeleteShelfDialog" class="modal fade" role="dialog">
|
||||||
<div class="modal-dialog modal-sm">
|
<div class="modal-dialog modal-sm">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header bg-danger text-center">
|
<div class="modal-header bg-danger text-center">
|
||||||
@ -82,6 +97,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div-->
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block modal %}
|
||||||
|
{{ delete_confirm_modal() }}
|
||||||
|
{% endblock %}
|
||||||
|
@ -5,30 +5,39 @@
|
|||||||
<div>{{_('Drag to Rearrange Order')}}</div>
|
<div>{{_('Drag to Rearrange Order')}}</div>
|
||||||
<div id="sortTrue" class="list-group">
|
<div id="sortTrue" class="list-group">
|
||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
<div id="{{entry['id']}}" class="list-group-item">
|
<div id="{{entry['Books']['id']}}" class="list-group-item">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-2 col-sm-4 hidden-xs">
|
<div class="col-lg-2 col-sm-4 hidden-xs">
|
||||||
<img class="cover-height" src="{{ url_for('web.get_cover', book_id=entry['id']) }}">
|
{% if entry['visible'] %}
|
||||||
|
<img class="cover-height" src="{{ url_for('web.get_cover', book_id=entry['Books']['id']) }}">
|
||||||
|
{% else %}
|
||||||
|
<img class="cover-height" src="{{ url_for('static', filename='generic_cover.jpg') }}">
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-10 col-sm-8 col-xs-12">
|
<div class="col-lg-10 col-sm-8 col-xs-12">
|
||||||
{{entry['title']}}
|
{% if entry['visible'] %}
|
||||||
{% if entry['series']|length > 0 %}
|
{{entry['Books']['title']}}
|
||||||
|
{% if entry['Books']['series']|length > 0 %}
|
||||||
<br>
|
<br>
|
||||||
{{entry['series_index']}} - {{entry['series'][0].name}}
|
{{entry['Books']['series_index']}} - {{entry['Books']['series'][0].name}}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br>
|
<br>
|
||||||
{% for author in entry['author'] %}
|
{% for author in entry['Books']['author'] %}
|
||||||
{{author.name.replace('|',',')}}
|
{{author.name.replace('|',',')}}
|
||||||
{% if not loop.last %}
|
{% if not loop.last %}
|
||||||
&
|
&
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{{_('Hidden Book')}}
|
||||||
|
<br>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<button onclick="sendData('{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}')" class="btn btn-default" id="ChangeOrder">{{_('Change order')}}</button>
|
<button onclick="sendData('{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}')" class="btn btn-default" id="ChangeOrder">{{_('Save')}}</button>
|
||||||
<a href="{{ url_for('shelf.show_shelf', shelf_id=shelf.id) }}" id="shelf_back" class="btn btn-default">{{_('Back')}}</a>
|
<a href="{{ url_for('shelf.show_shelf', shelf_id=shelf.id) }}" id="shelf_back" class="btn btn-default">{{_('Back')}}</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -55,27 +55,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group" role="group" aria-label="Download, send to Kindle, reading">
|
<div class="btn-group" role="group" aria-label="Download, send to Kindle, reading">
|
||||||
{% if g.user.role_download() %}
|
{% if g.user.role_download() %}
|
||||||
{% if entry.data|length %}
|
{% if entry.data|length %}
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
{% if entry.data|length < 2 %}
|
{% for format in entry.data %}
|
||||||
|
<a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}" id="btnGroupDrop{{entry.id}}{{format.format|lower}}" class="btn btn-primary" role="button">
|
||||||
{% for format in entry.data %}
|
<span class="glyphicon glyphicon-download"></span>{{format.format}} ({{ format.uncompressed_size|filesizeformat }})
|
||||||
<a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}" id="btnGroupDrop1{{format.format|lower}}" class="btn btn-primary" role="button">
|
</a>
|
||||||
<span class="glyphicon glyphicon-download"></span>{{format.format}} ({{ format.uncompressed_size|filesizeformat }})
|
{% endfor %}
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
||||||
<span class="glyphicon glyphicon-download"></span> {{_('Download')}}
|
|
||||||
<span class="caret"></span>
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
|
||||||
{% for format in entry.data %}
|
|
||||||
<li><a href="{{ url_for('web.download_link', book_id=entry.id, book_format=format.format|lower, anyname=entry.id|string+'.'+format.format|lower) }}">{{format.format}} ({{ format.uncompressed_size|filesizeformat }})</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
<label>{{ _('Kobo Sync Token')}}</label>
|
<label>{{ _('Kobo Sync Token')}}</label>
|
||||||
<div class="form-group col">
|
<div class="form-group col">
|
||||||
<a class="btn btn-default" id="config_create_kobo_token" data-toggle="modal" data-target="#modal_kobo_token" data-remote="false" href="{{ url_for('kobo_auth.generate_auth_token', user_id=content.id) }}">{{_('Create/View')}}</a>
|
<a class="btn btn-default" id="config_create_kobo_token" data-toggle="modal" data-target="#modal_kobo_token" data-remote="false" href="{{ url_for('kobo_auth.generate_auth_token', user_id=content.id) }}">{{_('Create/View')}}</a>
|
||||||
<div class="btn btn-danger" id="config_delete_kobo_token" data-toggle="modal" data-target="#modalDeleteToken" data-remote="false" {% if not content.remote_auth_token.first() %} style="display: none;" {% endif %}>{{_('Delete')}}</div>
|
<div class="btn btn-danger" id="config_delete_kobo_token" data-value="{{ content.id }}" data-remote="false" {% if not content.remote_auth_token.first() %} style="display: none;" {% endif %}>{{_('Delete')}}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -82,8 +82,8 @@
|
|||||||
<label for="Show_detail_random">{{_('Show Random Books in Detail View')}}</label>
|
<label for="Show_detail_random">{{_('Show Random Books in Detail View')}}</label>
|
||||||
</div>
|
</div>
|
||||||
{% if ( g.user and g.user.role_admin() and not new_user ) %}
|
{% if ( g.user and g.user.role_admin() and not new_user ) %}
|
||||||
<a href="#" id="get_user_tags" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add Allowed/Denied Tags')}}</a>
|
<a href="#" id="get_user_tags" class="btn btn-default" data-id="{{content.id}}" data-toggle="modal" data-target="#restrictModal">{{_('Add Allowed/Denied Tags')}}</a>
|
||||||
<a href="#" id="get_user_column_values" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add allowed/Denied Custom Column Values')}}</a>
|
<a href="#" id="get_user_column_values" data-id="{{content.id}}" class="btn btn-default" data-toggle="modal" data-target="#restrictModal">{{_('Add allowed/Denied Custom Column Values')}}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
@ -125,19 +125,15 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
|
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" id="delete" name="delete"> {{_('Delete User')}}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" id="submit" class="btn btn-default">{{_('Save')}}</button>
|
<div id="user_submit" class="btn btn-default">{{_('Save')}}</div>
|
||||||
{% if not profile %}
|
{% if not profile %}
|
||||||
<a href="{{ url_for('admin.admin') }}" id="back" class="btn btn-default">{{_('Cancel')}}</a>
|
<a href="{{ url_for('admin.admin') }}" id="back" class="btn btn-default">{{_('Cancel')}}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
|
||||||
|
<div class="btn btn-danger" id="btndeluser" data-value="{{ content.id }}" data-remote="false" >{{_('Delete User')}}</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -157,23 +153,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="modalDeleteToken" class="modal fade" role="dialog">
|
|
||||||
<div class="modal-dialog modal-sm">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header bg-danger">
|
|
||||||
</div>
|
|
||||||
<div class="modal-body text-center">
|
|
||||||
<p>{{_('Do you really want to delete the Kobo Token?')}}</p>
|
|
||||||
<button type="button" class="btn btn-danger" id="btndeletetoken" value="{{content.id}}">{{_('Delete')}}</button>
|
|
||||||
<button type="button" class="btn btn-default" id="btncancel" data-dismiss="modal">{{_('Back')}}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block modal %}
|
{% block modal %}
|
||||||
{{ restrict_modal() }}
|
{{ restrict_modal() }}
|
||||||
|
{{ delete_confirm_modal() }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
|
||||||
|
File diff suppressed because it is too large
Load Diff
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 flask import session as flask_session
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
|
|
||||||
from flask import g
|
|
||||||
from flask_babel import gettext as _
|
|
||||||
from flask_login import AnonymousUserMixin, current_user
|
from flask_login import AnonymousUserMixin, current_user
|
||||||
from werkzeug.local import LocalProxy
|
|
||||||
try:
|
try:
|
||||||
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
|
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
|
||||||
oauth_support = True
|
oauth_support = True
|
||||||
@ -45,7 +43,7 @@ from sqlalchemy import Column, ForeignKey
|
|||||||
from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float, JSON
|
from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float, JSON
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
from sqlalchemy.orm import backref, relationship, sessionmaker, Session
|
from sqlalchemy.orm import backref, relationship, sessionmaker, Session, scoped_session
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
from . import constants
|
from . import constants
|
||||||
@ -57,73 +55,6 @@ Base = declarative_base()
|
|||||||
searched_ids = {}
|
searched_ids = {}
|
||||||
|
|
||||||
|
|
||||||
def get_sidebar_config(kwargs=None):
|
|
||||||
kwargs = kwargs or []
|
|
||||||
if 'content' in kwargs:
|
|
||||||
content = kwargs['content']
|
|
||||||
content = isinstance(content, (User, LocalProxy)) and not content.role_anonymous()
|
|
||||||
else:
|
|
||||||
content = 'conf' in kwargs
|
|
||||||
sidebar = list()
|
|
||||||
sidebar.append({"glyph": "glyphicon-book", "text": _('Recently Added'), "link": 'web.index', "id": "new",
|
|
||||||
"visibility": constants.SIDEBAR_RECENT, 'public': True, "page": "root",
|
|
||||||
"show_text": _('Show recent books'), "config_show":False})
|
|
||||||
sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot",
|
|
||||||
"visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot",
|
|
||||||
"show_text": _('Show Hot Books'), "config_show": True})
|
|
||||||
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
|
|
||||||
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
|
|
||||||
"page": "download", "show_text": _('Show Downloaded Books'),
|
|
||||||
"config_show": content})
|
|
||||||
sidebar.append(
|
|
||||||
{"glyph": "glyphicon-star", "text": _('Top Rated Books'), "link": 'web.books_list', "id": "rated",
|
|
||||||
"visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated",
|
|
||||||
"show_text": _('Show Top Rated Books'), "config_show": True})
|
|
||||||
sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read",
|
|
||||||
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous),
|
|
||||||
"page": "read", "show_text": _('Show read and unread'), "config_show": content})
|
|
||||||
sidebar.append(
|
|
||||||
{"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread",
|
|
||||||
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread",
|
|
||||||
"show_text": _('Show unread'), "config_show": False})
|
|
||||||
sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand",
|
|
||||||
"visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover",
|
|
||||||
"show_text": _('Show random books'), "config_show": True})
|
|
||||||
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
|
|
||||||
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category",
|
|
||||||
"show_text": _('Show category selection'), "config_show": True})
|
|
||||||
sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie",
|
|
||||||
"visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series",
|
|
||||||
"show_text": _('Show series selection'), "config_show": True})
|
|
||||||
sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author",
|
|
||||||
"visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author",
|
|
||||||
"show_text": _('Show author selection'), "config_show": True})
|
|
||||||
sidebar.append(
|
|
||||||
{"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher",
|
|
||||||
"visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher",
|
|
||||||
"show_text": _('Show publisher selection'), "config_show":True})
|
|
||||||
sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang",
|
|
||||||
"visibility": constants.SIDEBAR_LANGUAGE, 'public': (g.user.filter_language() == 'all'),
|
|
||||||
"page": "language",
|
|
||||||
"show_text": _('Show language selection'), "config_show": True})
|
|
||||||
sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate",
|
|
||||||
"visibility": constants.SIDEBAR_RATING, 'public': True,
|
|
||||||
"page": "rating", "show_text": _('Show ratings selection'), "config_show": True})
|
|
||||||
sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format",
|
|
||||||
"visibility": constants.SIDEBAR_FORMAT, 'public': True,
|
|
||||||
"page": "format", "show_text": _('Show file formats selection'), "config_show": True})
|
|
||||||
sidebar.append(
|
|
||||||
{"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
|
|
||||||
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived",
|
|
||||||
"show_text": _('Show archived books'), "config_show": content})
|
|
||||||
sidebar.append(
|
|
||||||
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
|
|
||||||
"visibility": constants.SIDEBAR_LIST, 'public': (not g.user.is_anonymous), "page": "list",
|
|
||||||
"show_text": _('Show Books List'), "config_show": content})
|
|
||||||
|
|
||||||
return sidebar
|
|
||||||
|
|
||||||
|
|
||||||
def store_ids(result):
|
def store_ids(result):
|
||||||
ids = list()
|
ids = list()
|
||||||
for element in result:
|
for element in result:
|
||||||
@ -521,7 +452,7 @@ def migrate_Database(session):
|
|||||||
if not engine.dialect.has_table(engine.connect(), "archived_book"):
|
if not engine.dialect.has_table(engine.connect(), "archived_book"):
|
||||||
ArchivedBook.__table__.create(bind=engine)
|
ArchivedBook.__table__.create(bind=engine)
|
||||||
if not engine.dialect.has_table(engine.connect(), "registration"):
|
if not engine.dialect.has_table(engine.connect(), "registration"):
|
||||||
ReadBook.__table__.create(bind=engine)
|
Registration.__table__.create(bind=engine)
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
conn.execute("insert into registration (domain, allow) values('%.%',1)")
|
conn.execute("insert into registration (domain, allow) values('%.%',1)")
|
||||||
session.commit()
|
session.commit()
|
||||||
@ -570,12 +501,16 @@ def migrate_Database(session):
|
|||||||
for book_shelf in session.query(BookShelf).all():
|
for book_shelf in session.query(BookShelf).all():
|
||||||
book_shelf.date_added = datetime.datetime.now()
|
book_shelf.date_added = datetime.datetime.now()
|
||||||
session.commit()
|
session.commit()
|
||||||
# Handle table exists, but no content
|
try:
|
||||||
cnt = session.query(Registration).count()
|
# Handle table exists, but no content
|
||||||
if not cnt:
|
cnt = session.query(Registration).count()
|
||||||
with engine.connect() as conn:
|
if not cnt:
|
||||||
conn.execute("insert into registration (domain, allow) values('%.%',1)")
|
with engine.connect() as conn:
|
||||||
session.commit()
|
conn.execute("insert into registration (domain, allow) values('%.%',1)")
|
||||||
|
session.commit()
|
||||||
|
except exc.OperationalError: # Database is not writeable
|
||||||
|
print('Settings database is not writeable. Exiting...')
|
||||||
|
sys.exit(2)
|
||||||
try:
|
try:
|
||||||
session.query(exists().where(BookShelf.order)).scalar()
|
session.query(exists().where(BookShelf.order)).scalar()
|
||||||
except exc.OperationalError: # Database is not compatible, some columns are missing
|
except exc.OperationalError: # Database is not compatible, some columns are missing
|
||||||
@ -660,7 +595,7 @@ def migrate_Database(session):
|
|||||||
session.commit()
|
session.commit()
|
||||||
except exc.OperationalError:
|
except exc.OperationalError:
|
||||||
print('Settings database is not writeable. Exiting...')
|
print('Settings database is not writeable. Exiting...')
|
||||||
sys.exit(1)
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
def clean_database(session):
|
def clean_database(session):
|
||||||
@ -678,13 +613,19 @@ def update_download(book_id, user_id):
|
|||||||
if not check:
|
if not check:
|
||||||
new_download = Downloads(user_id=user_id, book_id=book_id)
|
new_download = Downloads(user_id=user_id, book_id=book_id)
|
||||||
session.add(new_download)
|
session.add(new_download)
|
||||||
session.commit()
|
try:
|
||||||
|
session.commit()
|
||||||
|
except exc.OperationalError:
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
|
||||||
# Delete non exisiting downloaded books in calibre-web's own database
|
# Delete non exisiting downloaded books in calibre-web's own database
|
||||||
def delete_download(book_id):
|
def delete_download(book_id):
|
||||||
session.query(Downloads).filter(book_id == Downloads.book_id).delete()
|
session.query(Downloads).filter(book_id == Downloads.book_id).delete()
|
||||||
session.commit()
|
try:
|
||||||
|
session.commit()
|
||||||
|
except exc.OperationalError:
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
# Generate user Guest (translated text), as anonymous user, no rights
|
# Generate user Guest (translated text), as anonymous user, no rights
|
||||||
def create_anonymous_user(session):
|
def create_anonymous_user(session):
|
||||||
@ -725,7 +666,7 @@ def init_db(app_db_path):
|
|||||||
app_DB_path = app_db_path
|
app_DB_path = app_db_path
|
||||||
engine = create_engine(u'sqlite:///{0}'.format(app_db_path), echo=False)
|
engine = create_engine(u'sqlite:///{0}'.format(app_db_path), echo=False)
|
||||||
|
|
||||||
Session = sessionmaker()
|
Session = scoped_session(sessionmaker())
|
||||||
Session.configure(bind=engine)
|
Session.configure(bind=engine)
|
||||||
session = Session()
|
session = Session()
|
||||||
|
|
||||||
|
@ -66,14 +66,6 @@ except ImportError as e:
|
|||||||
log.debug('Cannot import fb2, extracting fb2 metadata will not work: %s', e)
|
log.debug('Cannot import fb2, extracting fb2 metadata will not work: %s', e)
|
||||||
use_fb2_meta = False
|
use_fb2_meta = False
|
||||||
|
|
||||||
try:
|
|
||||||
from PIL import Image as PILImage
|
|
||||||
from PIL import __version__ as PILversion
|
|
||||||
use_PIL = True
|
|
||||||
except ImportError as e:
|
|
||||||
log.debug('Cannot import Pillow, using png and webp images as cover will not work: %s', e)
|
|
||||||
use_PIL = False
|
|
||||||
|
|
||||||
|
|
||||||
def process(tmp_file_path, original_file_name, original_file_extension, rarExecutable):
|
def process(tmp_file_path, original_file_name, original_file_extension, rarExecutable):
|
||||||
meta = None
|
meta = None
|
||||||
@ -179,10 +171,6 @@ def get_versions():
|
|||||||
XVersion = 'v'+'.'.join(map(str, lxmlversion))
|
XVersion = 'v'+'.'.join(map(str, lxmlversion))
|
||||||
else:
|
else:
|
||||||
XVersion = u'not installed'
|
XVersion = u'not installed'
|
||||||
if use_PIL:
|
|
||||||
PILVersion = 'v' + PILversion
|
|
||||||
else:
|
|
||||||
PILVersion = u'not installed'
|
|
||||||
if comic.use_comic_meta:
|
if comic.use_comic_meta:
|
||||||
ComicVersion = comic.comic_version or u'installed'
|
ComicVersion = comic.comic_version or u'installed'
|
||||||
else:
|
else:
|
||||||
@ -191,7 +179,7 @@ def get_versions():
|
|||||||
'PyPdf': PVersion,
|
'PyPdf': PVersion,
|
||||||
'lxml':XVersion,
|
'lxml':XVersion,
|
||||||
'Wand': WVersion,
|
'Wand': WVersion,
|
||||||
'Pillow': PILVersion,
|
# 'Pillow': PILVersion,
|
||||||
'Comic_API': ComicVersion}
|
'Comic_API': ComicVersion}
|
||||||
|
|
||||||
|
|
||||||
|
88
cps/usermanagement.py
Normal file
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
|
from __future__ import division, print_function, unicode_literals
|
||||||
import os
|
import os
|
||||||
import base64
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import traceback
|
import chardet # dependency of requests
|
||||||
import binascii
|
|
||||||
import re
|
|
||||||
|
|
||||||
from babel.dates import format_date
|
from babel.dates import format_date
|
||||||
from babel import Locale as LC
|
from babel import Locale as LC
|
||||||
from babel.core import UnknownLocaleError
|
from babel.core import UnknownLocaleError
|
||||||
from flask import Blueprint, jsonify
|
from flask import Blueprint, jsonify
|
||||||
from flask import render_template, request, redirect, send_from_directory, make_response, g, flash, abort, url_for
|
from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for
|
||||||
from flask import session as flask_session
|
from flask import session as flask_session
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import login_user, logout_user, login_required, current_user, confirm_login
|
from flask_login import login_user, logout_user, login_required, current_user
|
||||||
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
|
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
|
||||||
from sqlalchemy.sql.expression import text, func, true, false, not_, and_, or_
|
from sqlalchemy.sql.expression import text, func, false, not_, and_
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
from werkzeug.exceptions import default_exceptions
|
|
||||||
from sqlalchemy.sql.functions import coalesce
|
from sqlalchemy.sql.functions import coalesce
|
||||||
|
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
|
|
||||||
try:
|
|
||||||
from werkzeug.exceptions import FailedDependency
|
|
||||||
except ImportError:
|
|
||||||
from werkzeug.exceptions import UnprocessableEntity as FailedDependency
|
|
||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
|
||||||
from . import constants, logger, isoLanguages, services
|
from . import constants, logger, isoLanguages, services
|
||||||
from . import lm, babel, db, ub, config, get_locale, app
|
from . import babel, db, ub, config, get_locale, app
|
||||||
from . import calibre_db
|
from . import calibre_db, shelf
|
||||||
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
|
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
|
||||||
from .helper import check_valid_domain, render_task_status, \
|
from .helper import check_valid_domain, render_task_status, \
|
||||||
get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \
|
get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \
|
||||||
send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password
|
send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
from .redirect import redirect_back
|
from .redirect import redirect_back
|
||||||
|
from .usermanagement import login_required_if_no_ano
|
||||||
|
from .render_template import render_title_template
|
||||||
|
|
||||||
feature_support = {
|
feature_support = {
|
||||||
'ldap': bool(services.ldap),
|
'ldap': bool(services.ldap),
|
||||||
@ -71,7 +65,6 @@ feature_support = {
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from .oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status
|
from .oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status
|
||||||
|
|
||||||
feature_support['oauth'] = True
|
feature_support['oauth'] = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
feature_support['oauth'] = False
|
feature_support['oauth'] = False
|
||||||
@ -82,55 +75,12 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pass # We're not using Python 3
|
pass # We're not using Python 3
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from natsort import natsorted as sort
|
from natsort import natsorted as sort
|
||||||
except ImportError:
|
except ImportError:
|
||||||
sort = sorted # Just use regular sort then, may cause issues with badly named pages in cbz/cbr files
|
sort = sorted # Just use regular sort then, may cause issues with badly named pages in cbz/cbr files
|
||||||
|
|
||||||
|
|
||||||
# custom error page
|
|
||||||
def error_http(error):
|
|
||||||
return render_template('http_error.html',
|
|
||||||
error_code="Error {0}".format(error.code),
|
|
||||||
error_name=error.name,
|
|
||||||
issue=False,
|
|
||||||
instance=config.config_calibre_web_title
|
|
||||||
), error.code
|
|
||||||
|
|
||||||
|
|
||||||
def internal_error(error):
|
|
||||||
return render_template('http_error.html',
|
|
||||||
error_code="Internal Server Error",
|
|
||||||
error_name=str(error),
|
|
||||||
issue=True,
|
|
||||||
error_stack=traceback.format_exc().split("\n"),
|
|
||||||
instance=config.config_calibre_web_title
|
|
||||||
), 500
|
|
||||||
|
|
||||||
|
|
||||||
# http error handling
|
|
||||||
for ex in default_exceptions:
|
|
||||||
if ex < 500:
|
|
||||||
app.register_error_handler(ex, error_http)
|
|
||||||
elif ex == 500:
|
|
||||||
app.register_error_handler(ex, internal_error)
|
|
||||||
|
|
||||||
|
|
||||||
if feature_support['ldap']:
|
|
||||||
# Only way of catching the LDAPException upon logging in with LDAP server down
|
|
||||||
@app.errorhandler(services.ldap.LDAPException)
|
|
||||||
def handle_exception(e):
|
|
||||||
log.debug('LDAP server not accessible while trying to login to opds feed')
|
|
||||||
return error_http(FailedDependency())
|
|
||||||
|
|
||||||
# @app.errorhandler(InvalidRequestError)
|
|
||||||
#@app.errorhandler(OperationalError)
|
|
||||||
#def handle_db_exception(e):
|
|
||||||
# db.session.rollback()
|
|
||||||
# log.error('Database request error: %s',e)
|
|
||||||
# return internal_error(InternalServerError(e))
|
|
||||||
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def add_security_headers(resp):
|
def add_security_headers(resp):
|
||||||
# resp.headers['Content-Security-Policy']= "script-src 'self' https://www.googleapis.com https://api.douban.com https://comicvine.gamespot.com;"
|
# resp.headers['Content-Security-Policy']= "script-src 'self' https://www.googleapis.com https://api.douban.com https://comicvine.gamespot.com;"
|
||||||
@ -146,104 +96,6 @@ log = logger.create()
|
|||||||
|
|
||||||
|
|
||||||
# ################################### Login logic and rights management ###############################################
|
# ################################### Login logic and rights management ###############################################
|
||||||
def _fetch_user_by_name(username):
|
|
||||||
return ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first()
|
|
||||||
|
|
||||||
|
|
||||||
@lm.user_loader
|
|
||||||
def load_user(user_id):
|
|
||||||
return ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
|
||||||
|
|
||||||
|
|
||||||
@lm.request_loader
|
|
||||||
def load_user_from_request(request):
|
|
||||||
if config.config_allow_reverse_proxy_header_login:
|
|
||||||
rp_header_name = config.config_reverse_proxy_login_header_name
|
|
||||||
if rp_header_name:
|
|
||||||
rp_header_username = request.headers.get(rp_header_name)
|
|
||||||
if rp_header_username:
|
|
||||||
user = _fetch_user_by_name(rp_header_username)
|
|
||||||
if user:
|
|
||||||
return user
|
|
||||||
|
|
||||||
auth_header = request.headers.get("Authorization")
|
|
||||||
if auth_header:
|
|
||||||
user = load_user_from_auth_header(auth_header)
|
|
||||||
if user:
|
|
||||||
return user
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def load_user_from_auth_header(header_val):
|
|
||||||
if header_val.startswith('Basic '):
|
|
||||||
header_val = header_val.replace('Basic ', '', 1)
|
|
||||||
basic_username = basic_password = ''
|
|
||||||
try:
|
|
||||||
header_val = base64.b64decode(header_val).decode('utf-8')
|
|
||||||
basic_username = header_val.split(':')[0]
|
|
||||||
basic_password = header_val.split(':')[1]
|
|
||||||
except (TypeError, UnicodeDecodeError, binascii.Error):
|
|
||||||
pass
|
|
||||||
user = _fetch_user_by_name(basic_username)
|
|
||||||
if user and config.config_login_type == constants.LOGIN_LDAP and services.ldap:
|
|
||||||
if services.ldap.bind_user(str(user.password), basic_password):
|
|
||||||
return user
|
|
||||||
if user and check_password_hash(str(user.password), basic_password):
|
|
||||||
return user
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def login_required_if_no_ano(func):
|
|
||||||
@wraps(func)
|
|
||||||
def decorated_view(*args, **kwargs):
|
|
||||||
if config.config_anonbrowse == 1:
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
return login_required(func)(*args, **kwargs)
|
|
||||||
|
|
||||||
return decorated_view
|
|
||||||
|
|
||||||
|
|
||||||
def remote_login_required(f):
|
|
||||||
@wraps(f)
|
|
||||||
def inner(*args, **kwargs):
|
|
||||||
if config.config_remote_login:
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
|
||||||
data = {'status': 'error', 'message': 'Forbidden'}
|
|
||||||
response = make_response(json.dumps(data, ensure_ascii=False))
|
|
||||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
|
||||||
return response, 403
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
return inner
|
|
||||||
|
|
||||||
|
|
||||||
def admin_required(f):
|
|
||||||
"""
|
|
||||||
Checks if current_user.role == 1
|
|
||||||
"""
|
|
||||||
|
|
||||||
@wraps(f)
|
|
||||||
def inner(*args, **kwargs):
|
|
||||||
if current_user.role_admin():
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
return inner
|
|
||||||
|
|
||||||
|
|
||||||
def unconfigured(f):
|
|
||||||
"""
|
|
||||||
Checks if calibre-web instance is not configured
|
|
||||||
"""
|
|
||||||
@wraps(f)
|
|
||||||
def inner(*args, **kwargs):
|
|
||||||
if not config.db_configured:
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
return inner
|
|
||||||
|
|
||||||
|
|
||||||
def download_required(f):
|
def download_required(f):
|
||||||
@ -265,155 +117,6 @@ def viewer_required(f):
|
|||||||
|
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
|
||||||
def upload_required(f):
|
|
||||||
@wraps(f)
|
|
||||||
def inner(*args, **kwargs):
|
|
||||||
if current_user.role_upload() or current_user.role_admin():
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
return inner
|
|
||||||
|
|
||||||
|
|
||||||
def edit_required(f):
|
|
||||||
@wraps(f)
|
|
||||||
def inner(*args, **kwargs):
|
|
||||||
if current_user.role_edit() or current_user.role_admin():
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
return inner
|
|
||||||
|
|
||||||
|
|
||||||
# ################################### Helper functions ################################################################
|
|
||||||
|
|
||||||
|
|
||||||
@web.before_app_request
|
|
||||||
def before_request():
|
|
||||||
if current_user.is_authenticated:
|
|
||||||
confirm_login()
|
|
||||||
g.constants = constants
|
|
||||||
g.user = current_user
|
|
||||||
g.allow_registration = config.config_public_reg
|
|
||||||
g.allow_anonymous = config.config_anonbrowse
|
|
||||||
g.allow_upload = config.config_uploading
|
|
||||||
g.current_theme = config.config_theme
|
|
||||||
g.config_authors_max = config.config_authors_max
|
|
||||||
g.shelves_access = ub.session.query(ub.Shelf).filter(
|
|
||||||
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
|
|
||||||
if not config.db_configured and request.endpoint not in (
|
|
||||||
'admin.basic_configuration', 'login') and '/static/' not in request.path:
|
|
||||||
return redirect(url_for('admin.basic_configuration'))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/import_ldap_users')
|
|
||||||
@login_required
|
|
||||||
@admin_required
|
|
||||||
def import_ldap_users():
|
|
||||||
showtext = {}
|
|
||||||
try:
|
|
||||||
new_users = services.ldap.get_group_members(config.config_ldap_group_name)
|
|
||||||
except (services.ldap.LDAPException, TypeError, AttributeError, KeyError) as e:
|
|
||||||
log.exception(e)
|
|
||||||
showtext['text'] = _(u'Error: %(ldaperror)s', ldaperror=e)
|
|
||||||
return json.dumps(showtext)
|
|
||||||
if not new_users:
|
|
||||||
log.debug('LDAP empty response')
|
|
||||||
showtext['text'] = _(u'Error: No user returned in response of LDAP server')
|
|
||||||
return json.dumps(showtext)
|
|
||||||
|
|
||||||
imported = 0
|
|
||||||
for username in new_users:
|
|
||||||
user = username.decode('utf-8')
|
|
||||||
if '=' in user:
|
|
||||||
# if member object field is empty take user object as filter
|
|
||||||
if config.config_ldap_member_user_object:
|
|
||||||
query_filter = config.config_ldap_member_user_object
|
|
||||||
else:
|
|
||||||
query_filter = config.config_ldap_user_object
|
|
||||||
try:
|
|
||||||
user_identifier = extract_user_identifier(user, query_filter)
|
|
||||||
except Exception as e:
|
|
||||||
log.warning(e)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
user_identifier = user
|
|
||||||
query_filter = None
|
|
||||||
try:
|
|
||||||
user_data = services.ldap.get_object_details(user=user_identifier, query_filter=query_filter)
|
|
||||||
except AttributeError as e:
|
|
||||||
log.exception(e)
|
|
||||||
continue
|
|
||||||
if user_data:
|
|
||||||
user_login_field = extract_dynamic_field_from_filter(user, config.config_ldap_user_object)
|
|
||||||
|
|
||||||
username = user_data[user_login_field][0].decode('utf-8')
|
|
||||||
# check for duplicate username
|
|
||||||
if ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first():
|
|
||||||
# if ub.session.query(ub.User).filter(ub.User.nickname == username).first():
|
|
||||||
log.warning("LDAP User %s Already in Database", user_data)
|
|
||||||
continue
|
|
||||||
|
|
||||||
kindlemail = ''
|
|
||||||
if 'mail' in user_data:
|
|
||||||
useremail = user_data['mail'][0].decode('utf-8')
|
|
||||||
if (len(user_data['mail']) > 1):
|
|
||||||
kindlemail = user_data['mail'][1].decode('utf-8')
|
|
||||||
|
|
||||||
else:
|
|
||||||
log.debug('No Mail Field Found in LDAP Response')
|
|
||||||
useremail = username + '@email.com'
|
|
||||||
# check for duplicate email
|
|
||||||
if ub.session.query(ub.User).filter(func.lower(ub.User.email) == useremail.lower()).first():
|
|
||||||
log.warning("LDAP Email %s Already in Database", user_data)
|
|
||||||
continue
|
|
||||||
content = ub.User()
|
|
||||||
content.nickname = username
|
|
||||||
content.password = '' # dummy password which will be replaced by ldap one
|
|
||||||
content.email = useremail
|
|
||||||
content.kindle_mail = kindlemail
|
|
||||||
content.role = config.config_default_role
|
|
||||||
content.sidebar_view = config.config_default_show
|
|
||||||
content.allowed_tags = config.config_allowed_tags
|
|
||||||
content.denied_tags = config.config_denied_tags
|
|
||||||
content.allowed_column_value = config.config_allowed_column_value
|
|
||||||
content.denied_column_value = config.config_denied_column_value
|
|
||||||
ub.session.add(content)
|
|
||||||
try:
|
|
||||||
ub.session.commit()
|
|
||||||
imported +=1
|
|
||||||
except Exception as e:
|
|
||||||
log.warning("Failed to create LDAP user: %s - %s", user, e)
|
|
||||||
ub.session.rollback()
|
|
||||||
showtext['text'] = _(u'Failed to Create at Least One LDAP User')
|
|
||||||
else:
|
|
||||||
log.warning("LDAP User: %s Not Found", user)
|
|
||||||
showtext['text'] = _(u'At Least One LDAP User Not Found in Database')
|
|
||||||
if not showtext:
|
|
||||||
showtext['text'] = _(u'{} User Successfully Imported'.format(imported))
|
|
||||||
return json.dumps(showtext)
|
|
||||||
|
|
||||||
|
|
||||||
def extract_user_data_from_field(user, field):
|
|
||||||
match = re.search(field + "=([\d\s\w-]+)", user, re.IGNORECASE | re.UNICODE)
|
|
||||||
if match:
|
|
||||||
return match.group(1)
|
|
||||||
else:
|
|
||||||
raise Exception("Could Not Parse LDAP User: {}".format(user))
|
|
||||||
|
|
||||||
def extract_dynamic_field_from_filter(user, filter):
|
|
||||||
match = re.search("([a-zA-Z0-9-]+)=%s", filter, re.IGNORECASE | re.UNICODE)
|
|
||||||
if match:
|
|
||||||
return match.group(1)
|
|
||||||
else:
|
|
||||||
raise Exception("Could Not Parse LDAP Userfield: {}", user)
|
|
||||||
|
|
||||||
def extract_user_identifier(user, filter):
|
|
||||||
dynamic_field = extract_dynamic_field_from_filter(user, filter)
|
|
||||||
return extract_user_data_from_field(user, dynamic_field)
|
|
||||||
|
|
||||||
|
|
||||||
# ################################### data provider functions #########################################################
|
# ################################### data provider functions #########################################################
|
||||||
|
|
||||||
|
|
||||||
@ -432,7 +135,10 @@ def bookmark(book_id, book_format):
|
|||||||
ub.Bookmark.book_id == book_id,
|
ub.Bookmark.book_id == book_id,
|
||||||
ub.Bookmark.format == book_format)).delete()
|
ub.Bookmark.format == book_format)).delete()
|
||||||
if not bookmark_key:
|
if not bookmark_key:
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
return "", 204
|
return "", 204
|
||||||
|
|
||||||
lbookmark = ub.Bookmark(user_id=current_user.id,
|
lbookmark = ub.Bookmark(user_id=current_user.id,
|
||||||
@ -440,7 +146,10 @@ def bookmark(book_id, book_format):
|
|||||||
format=book_format,
|
format=book_format,
|
||||||
bookmark_key=bookmark_key)
|
bookmark_key=bookmark_key)
|
||||||
ub.session.merge(lbookmark)
|
ub.session.merge(lbookmark)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
return "", 201
|
return "", 201
|
||||||
|
|
||||||
|
|
||||||
@ -465,7 +174,10 @@ def toggle_read(book_id):
|
|||||||
kobo_reading_state.statistics = ub.KoboStatistics()
|
kobo_reading_state.statistics = ub.KoboStatistics()
|
||||||
book.kobo_reading_state = kobo_reading_state
|
book.kobo_reading_state = kobo_reading_state
|
||||||
ub.session.merge(book)
|
ub.session.merge(book)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
calibre_db.update_title_sort(config)
|
calibre_db.update_title_sort(config)
|
||||||
@ -499,7 +211,10 @@ def toggle_archived(book_id):
|
|||||||
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)
|
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)
|
||||||
archived_book.is_archived = True
|
archived_book.is_archived = True
|
||||||
ub.session.merge(archived_book)
|
ub.session.merge(archived_book)
|
||||||
ub.session.commit()
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
ub.session.rollback()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@ -620,8 +335,6 @@ def get_matching_tags():
|
|||||||
title_input = request.args.get('book_title') or ''
|
title_input = request.args.get('book_title') or ''
|
||||||
include_tag_inputs = request.args.getlist('include_tag') or ''
|
include_tag_inputs = request.args.getlist('include_tag') or ''
|
||||||
exclude_tag_inputs = request.args.getlist('exclude_tag') or ''
|
exclude_tag_inputs = request.args.getlist('exclude_tag') or ''
|
||||||
# include_extension_inputs = request.args.getlist('include_extension') or ''
|
|
||||||
# exclude_extension_inputs = request.args.getlist('exclude_extension') or ''
|
|
||||||
q = q.filter(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + author_input + "%")),
|
q = q.filter(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + author_input + "%")),
|
||||||
func.lower(db.Books.title).ilike("%" + title_input + "%"))
|
func.lower(db.Books.title).ilike("%" + title_input + "%"))
|
||||||
if len(include_tag_inputs) > 0:
|
if len(include_tag_inputs) > 0:
|
||||||
@ -638,14 +351,6 @@ def get_matching_tags():
|
|||||||
return json_dumps
|
return json_dumps
|
||||||
|
|
||||||
|
|
||||||
# Returns the template for rendering and includes the instance name
|
|
||||||
def render_title_template(*args, **kwargs):
|
|
||||||
sidebar = ub.get_sidebar_config(kwargs)
|
|
||||||
return render_template(instance=config.config_calibre_web_title, sidebar=sidebar,
|
|
||||||
accept=constants.EXTENSIONS_UPLOAD,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def render_books_list(data, sort, book_id, page):
|
def render_books_list(data, sort, book_id, page):
|
||||||
order = [db.Books.timestamp.desc()]
|
order = [db.Books.timestamp.desc()]
|
||||||
if sort == 'stored':
|
if sort == 'stored':
|
||||||
@ -749,8 +454,6 @@ def render_hot_books(page):
|
|||||||
entries.append(downloadBook)
|
entries.append(downloadBook)
|
||||||
else:
|
else:
|
||||||
ub.delete_download(book.Downloads.book_id)
|
ub.delete_download(book.Downloads.book_id)
|
||||||
# ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete()
|
|
||||||
# ub.session.commit()
|
|
||||||
numBooks = entries.__len__()
|
numBooks = entries.__len__()
|
||||||
pagination = Pagination(page, config.config_books_per_page, numBooks)
|
pagination = Pagination(page, config.config_books_per_page, numBooks)
|
||||||
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
|
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
|
||||||
@ -905,7 +608,8 @@ def render_language_books(page, name, order):
|
|||||||
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name,
|
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name,
|
||||||
title=_(u"Language: %(name)s", name=lang_name), page="language")
|
title=_(u"Language: %(name)s", name=lang_name), page="language")
|
||||||
|
|
||||||
def render_read_books(page, are_read, as_xml=False, order=None, *args, **kwargs):
|
|
||||||
|
def render_read_books(page, are_read, as_xml=False, order=None):
|
||||||
order = order or []
|
order = order or []
|
||||||
if not config.config_read_column:
|
if not config.config_read_column:
|
||||||
if are_read:
|
if are_read:
|
||||||
@ -917,7 +621,7 @@ def render_read_books(page, are_read, as_xml=False, order=None, *args, **kwargs)
|
|||||||
db.Books,
|
db.Books,
|
||||||
db_filter,
|
db_filter,
|
||||||
order,
|
order,
|
||||||
ub.ReadBook, db.Books.id==ub.ReadBook.book_id)
|
ub.ReadBook, db.Books.id == ub.ReadBook.book_id)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
if are_read:
|
if are_read:
|
||||||
@ -1086,11 +790,12 @@ def update_table_settings():
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
except InvalidRequestError:
|
except (InvalidRequestError, OperationalError):
|
||||||
log.error("Invalid request received: %r ", request, )
|
log.error("Invalid request received: %r ", request, )
|
||||||
return "Invalid request", 400
|
return "Invalid request", 400
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@web.route("/author")
|
@web.route("/author")
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
def author_list():
|
def author_list():
|
||||||
@ -1323,6 +1028,7 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
|||||||
rating_low = term.get("ratinghigh")
|
rating_low = term.get("ratinghigh")
|
||||||
rating_high = term.get("ratinglow")
|
rating_high = term.get("ratinglow")
|
||||||
description = term.get("comment")
|
description = term.get("comment")
|
||||||
|
read_status = term.get("read_status")
|
||||||
if author_name:
|
if author_name:
|
||||||
author_name = author_name.strip().lower().replace(',', '|')
|
author_name = author_name.strip().lower().replace(',', '|')
|
||||||
if book_title:
|
if book_title:
|
||||||
@ -1340,7 +1046,7 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
|||||||
if include_tag_inputs or exclude_tag_inputs or include_series_inputs or exclude_series_inputs or \
|
if include_tag_inputs or exclude_tag_inputs or include_series_inputs or exclude_series_inputs or \
|
||||||
include_languages_inputs or exclude_languages_inputs or author_name or book_title or \
|
include_languages_inputs or exclude_languages_inputs or author_name or book_title or \
|
||||||
publisher or pub_start or pub_end or rating_low or rating_high or description or cc_present or \
|
publisher or pub_start or pub_end or rating_low or rating_high or description or cc_present or \
|
||||||
include_extension_inputs or exclude_extension_inputs:
|
include_extension_inputs or exclude_extension_inputs or read_status:
|
||||||
searchterm.extend((author_name.replace('|', ','), book_title, publisher))
|
searchterm.extend((author_name.replace('|', ','), book_title, publisher))
|
||||||
if pub_start:
|
if pub_start:
|
||||||
try:
|
try:
|
||||||
@ -1358,8 +1064,12 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
|||||||
pub_start = u""
|
pub_start = u""
|
||||||
tag_names = calibre_db.session.query(db.Tags).filter(db.Tags.id.in_(include_tag_inputs)).all()
|
tag_names = calibre_db.session.query(db.Tags).filter(db.Tags.id.in_(include_tag_inputs)).all()
|
||||||
searchterm.extend(tag.name for tag in tag_names)
|
searchterm.extend(tag.name for tag in tag_names)
|
||||||
|
tag_names = calibre_db.session.query(db.Tags).filter(db.Tags.id.in_(exclude_tag_inputs)).all()
|
||||||
|
searchterm.extend(tag.name for tag in tag_names)
|
||||||
serie_names = calibre_db.session.query(db.Series).filter(db.Series.id.in_(include_series_inputs)).all()
|
serie_names = calibre_db.session.query(db.Series).filter(db.Series.id.in_(include_series_inputs)).all()
|
||||||
searchterm.extend(serie.name for serie in serie_names)
|
searchterm.extend(serie.name for serie in serie_names)
|
||||||
|
serie_names = calibre_db.session.query(db.Series).filter(db.Series.id.in_(exclude_series_inputs)).all()
|
||||||
|
searchterm.extend(serie.name for serie in serie_names)
|
||||||
language_names = calibre_db.session.query(db.Languages).\
|
language_names = calibre_db.session.query(db.Languages).\
|
||||||
filter(db.Languages.id.in_(include_languages_inputs)).all()
|
filter(db.Languages.id.in_(include_languages_inputs)).all()
|
||||||
if language_names:
|
if language_names:
|
||||||
@ -1369,6 +1079,8 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
|||||||
searchterm.extend([_(u"Rating <= %(rating)s", rating=rating_high)])
|
searchterm.extend([_(u"Rating <= %(rating)s", rating=rating_high)])
|
||||||
if rating_low:
|
if rating_low:
|
||||||
searchterm.extend([_(u"Rating >= %(rating)s", rating=rating_low)])
|
searchterm.extend([_(u"Rating >= %(rating)s", rating=rating_low)])
|
||||||
|
if read_status:
|
||||||
|
searchterm.extend([_(u"Read Status = %(status)s", status=read_status)])
|
||||||
searchterm.extend(ext for ext in include_extension_inputs)
|
searchterm.extend(ext for ext in include_extension_inputs)
|
||||||
searchterm.extend(ext for ext in exclude_extension_inputs)
|
searchterm.extend(ext for ext in exclude_extension_inputs)
|
||||||
# handle custom columns
|
# handle custom columns
|
||||||
@ -1385,6 +1097,23 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
|
|||||||
q = q.filter(db.Books.pubdate >= pub_start)
|
q = q.filter(db.Books.pubdate >= pub_start)
|
||||||
if pub_end:
|
if pub_end:
|
||||||
q = q.filter(db.Books.pubdate <= pub_end)
|
q = q.filter(db.Books.pubdate <= pub_end)
|
||||||
|
if read_status:
|
||||||
|
if config.config_read_column:
|
||||||
|
if read_status=="True":
|
||||||
|
q = q.join(db.cc_classes[config.config_read_column], isouter=True) \
|
||||||
|
.filter(db.cc_classes[config.config_read_column].value == True)
|
||||||
|
else:
|
||||||
|
q = q.join(db.cc_classes[config.config_read_column], isouter=True) \
|
||||||
|
.filter(coalesce(db.cc_classes[config.config_read_column].value, False) != True)
|
||||||
|
else:
|
||||||
|
if read_status == "True":
|
||||||
|
q = q.join(ub.ReadBook, db.Books.id==ub.ReadBook.book_id, isouter=True)\
|
||||||
|
.filter(ub.ReadBook.user_id == int(current_user.id),
|
||||||
|
ub.ReadBook.read_status == ub.ReadBook.STATUS_FINISHED)
|
||||||
|
else:
|
||||||
|
q = q.join(ub.ReadBook, db.Books.id == ub.ReadBook.book_id, isouter=True) \
|
||||||
|
.filter(ub.ReadBook.user_id == int(current_user.id),
|
||||||
|
coalesce(ub.ReadBook.read_status, 0) != ub.ReadBook.STATUS_FINISHED)
|
||||||
if publisher:
|
if publisher:
|
||||||
q = q.filter(db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + publisher + "%")))
|
q = q.filter(db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + publisher + "%")))
|
||||||
for tag in include_tag_inputs:
|
for tag in include_tag_inputs:
|
||||||
@ -1487,8 +1216,14 @@ def serve_book(book_id, book_format, anyname):
|
|||||||
headers = Headers()
|
headers = Headers()
|
||||||
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
||||||
df = getFileFromEbooksFolder(book.path, data.name + "." + book_format)
|
df = getFileFromEbooksFolder(book.path, data.name + "." + book_format)
|
||||||
return do_gdrive_download(df, headers)
|
return do_gdrive_download(df, headers, (book_format.upper() == 'TXT'))
|
||||||
else:
|
else:
|
||||||
|
if book_format.upper() == 'TXT':
|
||||||
|
rawdata = open(os.path.join(config.config_calibre_dir, book.path, data.name + "." + book_format),
|
||||||
|
"rb").read()
|
||||||
|
result = chardet.detect(rawdata)
|
||||||
|
return make_response(
|
||||||
|
rawdata.decode(result['encoding']).encode('utf-8'))
|
||||||
return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format)
|
return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format)
|
||||||
|
|
||||||
|
|
||||||
@ -1674,89 +1409,7 @@ def logout():
|
|||||||
return redirect(url_for('web.login'))
|
return redirect(url_for('web.login'))
|
||||||
|
|
||||||
|
|
||||||
@web.route('/remote/login')
|
|
||||||
@remote_login_required
|
|
||||||
def remote_login():
|
|
||||||
auth_token = ub.RemoteAuthToken()
|
|
||||||
ub.session.add(auth_token)
|
|
||||||
ub.session.commit()
|
|
||||||
|
|
||||||
verify_url = url_for('web.verify_token', token=auth_token.auth_token, _external=true)
|
|
||||||
log.debug(u"Remot Login request with token: %s", auth_token.auth_token)
|
|
||||||
return render_title_template('remote_login.html', title=_(u"login"), token=auth_token.auth_token,
|
|
||||||
verify_url=verify_url, page="remotelogin")
|
|
||||||
|
|
||||||
|
|
||||||
@web.route('/verify/<token>')
|
|
||||||
@remote_login_required
|
|
||||||
@login_required
|
|
||||||
def verify_token(token):
|
|
||||||
auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first()
|
|
||||||
|
|
||||||
# Token not found
|
|
||||||
if auth_token is None:
|
|
||||||
flash(_(u"Token not found"), category="error")
|
|
||||||
log.error(u"Remote Login token not found")
|
|
||||||
return redirect(url_for('web.index'))
|
|
||||||
|
|
||||||
# Token expired
|
|
||||||
if datetime.now() > auth_token.expiration:
|
|
||||||
ub.session.delete(auth_token)
|
|
||||||
ub.session.commit()
|
|
||||||
|
|
||||||
flash(_(u"Token has expired"), category="error")
|
|
||||||
log.error(u"Remote Login token expired")
|
|
||||||
return redirect(url_for('web.index'))
|
|
||||||
|
|
||||||
# Update token with user information
|
|
||||||
auth_token.user_id = current_user.id
|
|
||||||
auth_token.verified = True
|
|
||||||
ub.session.commit()
|
|
||||||
|
|
||||||
flash(_(u"Success! Please return to your device"), category="success")
|
|
||||||
log.debug(u"Remote Login token for userid %s verified", auth_token.user_id)
|
|
||||||
return redirect(url_for('web.index'))
|
|
||||||
|
|
||||||
|
|
||||||
@web.route('/ajax/verify_token', methods=['POST'])
|
|
||||||
@remote_login_required
|
|
||||||
def token_verified():
|
|
||||||
token = request.form['token']
|
|
||||||
auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first()
|
|
||||||
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
# Token not found
|
|
||||||
if auth_token is None:
|
|
||||||
data['status'] = 'error'
|
|
||||||
data['message'] = _(u"Token not found")
|
|
||||||
|
|
||||||
# Token expired
|
|
||||||
elif datetime.now() > auth_token.expiration:
|
|
||||||
ub.session.delete(auth_token)
|
|
||||||
ub.session.commit()
|
|
||||||
|
|
||||||
data['status'] = 'error'
|
|
||||||
data['message'] = _(u"Token has expired")
|
|
||||||
|
|
||||||
elif not auth_token.verified:
|
|
||||||
data['status'] = 'not_verified'
|
|
||||||
|
|
||||||
else:
|
|
||||||
user = ub.session.query(ub.User).filter(ub.User.id == auth_token.user_id).first()
|
|
||||||
login_user(user)
|
|
||||||
|
|
||||||
ub.session.delete(auth_token)
|
|
||||||
ub.session.commit()
|
|
||||||
|
|
||||||
data['status'] = 'success'
|
|
||||||
log.debug(u"Remote Login for userid %s succeded", user.id)
|
|
||||||
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success")
|
|
||||||
|
|
||||||
response = make_response(json.dumps(data, ensure_ascii=False))
|
|
||||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
# ################################### Users own configuration #########################################################
|
# ################################### Users own configuration #########################################################
|
||||||
@ -1839,14 +1492,11 @@ def profile():
|
|||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Found an existing account for this e-mail address."), category="error")
|
flash(_(u"Found an existing account for this e-mail address."), category="error")
|
||||||
log.debug(u"Found an existing account for this e-mail address.")
|
log.debug(u"Found an existing account for this e-mail address.")
|
||||||
'''return render_title_template("user_edit.html",
|
except OperationalError as e:
|
||||||
content=current_user,
|
ub.session.rollback()
|
||||||
translations=translations,
|
log.error("Database error: %s", e)
|
||||||
kobo_support=kobo_support,
|
flash(_(u"Database error: %(error)s.", error=e), category="error")
|
||||||
title=_(u"%(name)s's profile", name=current_user.nickname),
|
|
||||||
page="me",
|
|
||||||
registered_oauth=local_oauth_check,
|
|
||||||
oauth_status=oauth_status)'''
|
|
||||||
return render_title_template("user_edit.html",
|
return render_title_template("user_edit.html",
|
||||||
translations=translations,
|
translations=translations,
|
||||||
profile=1,
|
profile=1,
|
||||||
@ -1900,14 +1550,6 @@ def read_book(book_id, book_format):
|
|||||||
log.debug(u"Start comic reader for %d", book_id)
|
log.debug(u"Start comic reader for %d", book_id)
|
||||||
return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book"),
|
return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book"),
|
||||||
extension=fileExt)
|
extension=fileExt)
|
||||||
# if feature_support['rar']:
|
|
||||||
# extensionList = ["cbr","cbt","cbz"]
|
|
||||||
# else:
|
|
||||||
# extensionList = ["cbt","cbz"]
|
|
||||||
# for fileext in extensionList:
|
|
||||||
# if book_format.lower() == fileext:
|
|
||||||
# return render_title_template('readcbr.html', comicfile=book_id,
|
|
||||||
# extension=fileext, title=_(u"Read a Book"), book=book)
|
|
||||||
log.debug(u"Error opening eBook. File does not exist or file is not accessible")
|
log.debug(u"Error opening eBook. File does not exist or file is not accessible")
|
||||||
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
|
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
|
||||||
return redirect(url_for("web.index"))
|
return redirect(url_for("web.index"))
|
||||||
|
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
|
uritemplate>=3.0.0,<3.1.0
|
||||||
pyasn1-modules>=0.0.8,<0.3.0
|
pyasn1-modules>=0.0.8,<0.3.0
|
||||||
pyasn1>=0.1.9,<0.5.0
|
pyasn1>=0.1.9,<0.5.0
|
||||||
PyDrive>=1.3.1,<1.4.0
|
PyDrive2>=1.3.1,<1.8.0
|
||||||
PyYAML>=3.12
|
PyYAML>=3.12
|
||||||
rsa==3.4.2,<4.1.0
|
rsa==3.4.2,<4.1.0
|
||||||
six>=1.10.0,<1.15.0
|
six>=1.10.0,<1.15.0
|
||||||
@ -26,7 +26,6 @@ SQLAlchemy-Utils>=0.33.5,<0.37.0
|
|||||||
|
|
||||||
# extracting metadata
|
# extracting metadata
|
||||||
lxml>=3.8.0,<4.6.0
|
lxml>=3.8.0,<4.6.0
|
||||||
Pillow>=4.0.0,<7.2.0
|
|
||||||
rarfile>=2.7
|
rarfile>=2.7
|
||||||
|
|
||||||
# other
|
# other
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
Babel>=1.3, <2.9
|
Babel>=1.3, <2.9
|
||||||
Flask-Babel>=0.11.1,<1.1.0
|
Flask-Babel>=0.11.1,<2.1.0
|
||||||
Flask-Login>=0.3.2,<0.5.1
|
Flask-Login>=0.3.2,<0.5.1
|
||||||
Flask-Principal>=0.3.2,<0.5.1
|
Flask-Principal>=0.3.2,<0.5.1
|
||||||
singledispatch>=3.4.0.0,<3.5.0.0
|
singledispatch>=3.4.0.0,<3.5.0.0
|
||||||
@ -11,5 +11,5 @@ pytz>=2016.10
|
|||||||
requests>=2.11.1,<2.25.0
|
requests>=2.11.1,<2.25.0
|
||||||
SQLAlchemy>=1.3.0,<1.4.0
|
SQLAlchemy>=1.3.0,<1.4.0
|
||||||
tornado>=4.1,<6.2
|
tornado>=4.1,<6.2
|
||||||
Wand>=0.4.4,<0.6.0
|
Wand>=0.4.4,<0.7.0
|
||||||
unidecode>=0.04.19,<1.2.0
|
unidecode>=0.04.19,<1.2.0
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user