diff --git a/cps.py b/cps.py index 20aa9cac..50ab0076 100755 --- a/cps.py +++ b/cps.py @@ -41,6 +41,8 @@ from cps.shelf import shelf from cps.admin import admi from cps.gdrive import gdrive from cps.editbooks import editbook +from cps.remotelogin import remotelogin +from cps.error_handler import init_errorhandler try: from cps.kobo import kobo, get_kobo_activated @@ -58,14 +60,18 @@ except ImportError: def main(): app = create_app() + + init_errorhandler() + app.register_blueprint(web) app.register_blueprint(opds) app.register_blueprint(jinjia) app.register_blueprint(about) app.register_blueprint(shelf) app.register_blueprint(admi) - if config.config_use_google_drive: - app.register_blueprint(gdrive) + app.register_blueprint(remotelogin) + # if config.config_use_google_drive: + app.register_blueprint(gdrive) app.register_blueprint(editbook) if kobo_available: app.register_blueprint(kobo) diff --git a/cps/__init__.py b/cps/__init__.py index 03945b57..1a7dc868 100644 --- a/cps/__init__.py +++ b/cps/__init__.py @@ -94,7 +94,8 @@ def create_app(): app.root_path = app.root_path.decode('utf-8') app.instance_path = app.instance_path.decode('utf-8') - cache_buster.init_cache_busting(app) + if os.environ.get('FLASK_DEBUG'): + cache_buster.init_cache_busting(app) log.info('Starting Calibre Web...') Principal(app) diff --git a/cps/about.py b/cps/about.py index f9c58738..6ea584f4 100644 --- a/cps/about.py +++ b/cps/about.py @@ -31,7 +31,7 @@ import werkzeug, flask, flask_login, flask_principal, jinja2 from flask_babel import gettext as _ from . import db, calibre_db, converter, uploader, server, isoLanguages, constants -from .web import render_title_template +from .render_template import render_title_template try: from flask_login import __version__ as flask_loginVersion except ImportError: diff --git a/cps/admin.py b/cps/admin.py index 531d855d..33939ef9 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -5,7 +5,7 @@ # andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, # falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, # ruben-herold, marblepebble, JackED42, SiphonSquirrel, -# apetresc, nanu-c, mutschler +# apetresc, nanu-c, mutschler, GammaC0de, vuolter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -26,24 +26,31 @@ import re import base64 import json import time +import operator from datetime import datetime, timedelta from babel import Locale as LC from babel.dates import format_datetime -from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory -from flask_login import login_required, current_user, logout_user +from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g +from flask_login import login_required, current_user, logout_user, confirm_login from flask_babel import gettext as _ from sqlalchemy import and_ from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError -from sqlalchemy.sql.expression import func +from sqlalchemy.sql.expression import func, or_ from . import constants, logger, helper, services +from .cli import filepicker from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash from .gdriveutils import is_gdrive_ready, gdrive_support -from .web import admin_required, render_title_template, before_request, unconfigured +from .render_template import render_title_template, get_sidebar_config from . import debug_info +try: + from functools import wraps +except ImportError: + pass # We're not using Python 3 + log = logger.create() feature_support = { @@ -72,6 +79,49 @@ feature_support['gdrive'] = gdrive_support admi = Blueprint('admin', __name__) +def admin_required(f): + """ + Checks if current_user.role == 1 + """ + + @wraps(f) + def inner(*args, **kwargs): + if current_user.role_admin(): + return f(*args, **kwargs) + abort(403) + + return inner + + +def unconfigured(f): + """ + Checks if calibre-web instance is not configured + """ + @wraps(f) + def inner(*args, **kwargs): + if not config.db_configured: + return f(*args, **kwargs) + abort(403) + + return inner + +@admi.before_app_request +def before_request(): + if current_user.is_authenticated: + confirm_login() + g.constants = constants + g.user = current_user + g.allow_registration = config.config_public_reg + g.allow_anonymous = config.config_anonbrowse + g.allow_upload = config.config_uploading + g.current_theme = config.config_theme + g.config_authors_max = config.config_authors_max + g.shelves_access = ub.session.query(ub.Shelf).filter( + or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all() + if not config.db_configured and request.endpoint not in ( + 'admin.basic_configuration', 'login', 'admin.config_pathchooser') and '/static/' not in request.path: + return redirect(url_for('admin.basic_configuration')) + @admi.route("/admin") @login_required @@ -143,7 +193,7 @@ def admin(): @admin_required def configuration(): if request.method == "POST": - return _configuration_update_helper() + return _configuration_update_helper(True) return _configuration_result() @@ -195,6 +245,21 @@ def update_view_configuration(): return view_configuration() +@admi.route("/ajax/loaddialogtexts/") +@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/", methods=['POST']) @login_required @admin_required @@ -206,7 +271,10 @@ def edit_domain(allow): vals = request.form.to_dict() answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first() answer.domain = vals['value'].replace('*', '%').replace('?', '_').lower() - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() return "" @@ -220,7 +288,10 @@ def add_domain(allow): if not check: new_domain = ub.Registration(domain=domain_name, allow=allow) ub.session.add(new_domain) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() return "" @@ -228,14 +299,23 @@ def add_domain(allow): @login_required @admin_required def delete_domain(): - domain_id = request.form.to_dict()['domainid'].replace('*', '%').replace('?', '_').lower() - ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete() - ub.session.commit() - # If last domain was deleted, add all domains by default - if not ub.session.query(ub.Registration).filter(ub.Registration.allow==1).count(): - new_domain = ub.Registration(domain="%.%",allow=1) - ub.session.add(new_domain) - ub.session.commit() + try: + domain_id = request.form.to_dict()['domainid'].replace('*', '%').replace('?', '_').lower() + ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() + # If last domain was deleted, add all domains by default + if not ub.session.query(ub.Registration).filter(ub.Registration.allow==1).count(): + new_domain = ub.Registration(domain="%.%",allow=1) + ub.session.add(new_domain) + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() + except KeyError: + pass return "" @@ -250,10 +330,11 @@ def list_domain(allow): response.headers["Content-Type"] = "application/json; charset=utf-8" return response -@admi.route("/ajax/editrestriction/", methods=['POST']) +@admi.route("/ajax/editrestriction/", defaults={"user_id":0}, methods=['POST']) +@admi.route("/ajax/editrestriction//", methods=['POST']) @login_required @admin_required -def edit_restriction(res_type): +def edit_restriction(res_type, user_id): element = request.form.to_dict() if element['id'].startswith('a'): if res_type == 0: # Tags as template @@ -267,25 +348,29 @@ def edit_restriction(res_type): config.config_allowed_column_value = ','.join(elementlist) config.save() if res_type == 2: # Tags per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user elementlist = usr.list_allowed_tags() elementlist[int(element['id'][1:])]=element['Element'] usr.allowed_tags = ','.join(elementlist) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() if res_type == 3: # CColumn per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user elementlist = usr.list_allowed_column_values() elementlist[int(element['id'][1:])]=element['Element'] usr.allowed_column_value = ','.join(elementlist) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() if element['id'].startswith('d'): if res_type == 0: # Tags as template elementlist = config.list_denied_tags() @@ -298,25 +383,29 @@ def edit_restriction(res_type): config.config_denied_column_value = ','.join(elementlist) config.save() if res_type == 2: # Tags per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user elementlist = usr.list_denied_tags() elementlist[int(element['id'][1:])]=element['Element'] usr.denied_tags = ','.join(elementlist) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() if res_type == 3: # CColumn per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user elementlist = usr.list_denied_column_values() elementlist[int(element['id'][1:])]=element['Element'] usr.denied_column_value = ','.join(elementlist) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() return "" def restriction_addition(element, list_func): @@ -335,10 +424,11 @@ def restriction_deletion(element, list_func): return ','.join(elementlist) -@admi.route("/ajax/addrestriction/", methods=['POST']) +@admi.route("/ajax/addrestriction/", defaults={"user_id":0}, methods=['POST']) +@admi.route("/ajax/addrestriction//", methods=['POST']) @login_required @admin_required -def add_restriction(res_type): +def add_restriction(res_type, user_id): element = request.form.to_dict() if res_type == 0: # Tags as template if 'submit_allow' in element: @@ -355,35 +445,46 @@ def add_restriction(res_type): config.config_denied_column_value = restriction_addition(element, config.list_allowed_column_values) config.save() if res_type == 2: # Tags per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user if 'submit_allow' in element: usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() elif 'submit_deny' in element: usr.denied_tags = restriction_addition(element, usr.list_denied_tags) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() if res_type == 3: # CustomC per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user if 'submit_allow' in element: usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() elif 'submit_deny' in element: usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() return "" -@admi.route("/ajax/deleterestriction/", methods=['POST']) +@admi.route("/ajax/deleterestriction/", defaults={"user_id":0}, methods=['POST']) +@admi.route("/ajax/deleterestriction//", methods=['POST']) @login_required @admin_required -def delete_restriction(res_type): +def delete_restriction(res_type, user_id): element = request.form.to_dict() if res_type == 0: # Tags as template if element['id'].startswith('a'): @@ -400,36 +501,46 @@ def delete_restriction(res_type): config.config_denied_column_value = restriction_deletion(element, config.list_denied_column_values) config.save() elif res_type == 2: # Tags per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user if element['id'].startswith('a'): usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() elif element['id'].startswith('d'): usr.denied_tags = restriction_deletion(element, usr.list_denied_tags) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() elif res_type == 3: # Columns per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: # select current user if admins are editing their own rights - usr = ub.session.query(ub.User).filter(ub.User.id == int(usr_id)).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() else: usr = current_user if element['id'].startswith('a'): usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() elif element['id'].startswith('d'): usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() return "" - -@admi.route("/ajax/listrestriction/") +@admi.route("/ajax/listrestriction/", defaults={"user_id":0}) +@admi.route("/ajax/listrestriction//") @login_required @admin_required -def list_restriction(res_type): +def list_restriction(res_type, user_id): if res_type == 0: # Tags as template restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) } for i,x in enumerate(config.list_denied_tags()) if x != '' ] @@ -443,9 +554,8 @@ def list_restriction(res_type): for i,x in enumerate(config.list_allowed_column_values()) if x != ''] json_dumps = restrict + allow elif res_type == 2: # Tags per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id == usr_id).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id == user_id).first() else: usr = current_user restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) } @@ -454,9 +564,8 @@ def list_restriction(res_type): for i,x in enumerate(usr.list_allowed_tags()) if x != ''] json_dumps = restrict + allow elif res_type == 3: # CustomC per user - usr_id = os.path.split(request.referrer)[-1] - if usr_id.isdigit() == True: - usr = ub.session.query(ub.User).filter(ub.User.id==usr_id).first() + if isinstance(user_id, int): + usr = ub.session.query(ub.User).filter(ub.User.id==user_id).first() else: usr = current_user restrict = [{'Element': x, 'type':_('Deny'), 'id': 'd'+str(i) } @@ -471,14 +580,108 @@ def list_restriction(res_type): response.headers["Content-Type"] = "application/json; charset=utf-8" return response +@admi.route("/basicconfig/pathchooser/") +@unconfigured +def config_pathchooser(): + if filepicker: + return pathchooser() + abort(403) -@admi.route("/config", methods=["GET", "POST"]) +@admi.route("/ajax/pathchooser/") +@login_required +@admin_required +def ajax_pathchooser(): + return pathchooser() + +def pathchooser(): + browse_for = "folder" + folder_only = request.args.get('folder', False) == "true" + file_filter = request.args.get('filter', "") + path = os.path.normpath(request.args.get('path', "")) + + if os.path.isfile(path): + oldfile = path + path = os.path.dirname(path) + else: + oldfile = "" + + abs = False + + if os.path.isdir(path): + #if os.path.isabs(path): + cwd = os.path.realpath(path) + abs = True + #else: + # cwd = os.path.relpath(path) + else: + cwd = os.getcwd() + + cwd = os.path.normpath(os.path.realpath(cwd)) + parentdir = os.path.dirname(cwd) + if not abs: + if os.path.realpath(cwd) == os.path.realpath("/"): + cwd = os.path.relpath(cwd) + else: + cwd = os.path.relpath(cwd) + os.path.sep + parentdir = os.path.relpath(parentdir) + os.path.sep + + if os.path.realpath(cwd) == os.path.realpath("/"): + parentdir = "" + + try: + folders = os.listdir(cwd) + except Exception: + folders = [] + + files = [] + # locale = get_locale() + for f in folders: + try: + data = {"name": f, "fullpath": os.path.join(cwd, f)} + data["sort"] = data["fullpath"].lower() + except Exception: + continue + + if os.path.isfile(os.path.join(cwd, f)): + if folder_only: + continue + if file_filter != "" and file_filter != f: + continue + data["type"] = "file" + data["size"] = os.path.getsize(os.path.join(cwd, f)) + + power = 0 + while (data["size"] >> 10) > 0.3: + power += 1 + data["size"] >>= 10 + units = ("", "K", "M", "G", "T") + data["size"] = str(data["size"]) + " " + units[power] + "Byte" + else: + data["type"] = "dir" + data["size"] = "" + + files.append(data) + + files = sorted(files, key=operator.itemgetter("type", "sort")) + + context = { + "cwd": cwd, + "files": files, + "parentdir": parentdir, + "type": browse_for, + "oldfile": oldfile, + "absolute": abs, + } + return json.dumps(context) + + +@admi.route("/basicconfig", methods=["GET", "POST"]) @unconfigured def basic_configuration(): logout_user() if request.method == "POST": - return _configuration_update_helper() - return _configuration_result() + return _configuration_update_helper(configured=filepicker) + return _configuration_result(configured=filepicker) def _config_int(to_save, x, func=int): @@ -633,13 +836,13 @@ def _configuration_ldap_helper(to_save, gdriveError): return reboot_required, None -def _configuration_update_helper(): +def _configuration_update_helper(configured): reboot_required = False db_change = False to_save = request.form.to_dict() gdriveError = None - to_save['config_calibre_dir'] = re.sub('[\\/]metadata\.db$', + to_save['config_calibre_dir'] = re.sub(r'[\\/]metadata\.db$', '', to_save['config_calibre_dir'], flags=re.IGNORECASE) @@ -653,11 +856,15 @@ def _configuration_update_helper(): reboot_required |= _config_string(to_save, "config_keyfile") if config.config_keyfile and not os.path.isfile(config.config_keyfile): - return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'), gdriveError) + return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'), + gdriveError, + configured) reboot_required |= _config_string(to_save, "config_certfile") if config.config_certfile and not os.path.isfile(config.config_certfile): - return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'), gdriveError) + return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'), + gdriveError, + configured) _config_checkbox_int(to_save, "config_uploading") # Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case @@ -722,10 +929,10 @@ def _configuration_update_helper(): if "config_rarfile_location" in to_save: unrar_status = helper.check_unrar(config.config_rarfile_location) if unrar_status: - return _configuration_result(unrar_status, gdriveError) + return _configuration_result(unrar_status, gdriveError, configured) except (OperationalError, InvalidRequestError): ub.session.rollback() - _configuration_result(_(u"Settings DB is not Writeable"), gdriveError) + _configuration_result(_(u"Settings DB is not Writeable"), gdriveError, configured) try: metadata_db = os.path.join(config.config_calibre_dir, "metadata.db") @@ -733,11 +940,13 @@ def _configuration_update_helper(): gdriveutils.downloadFile(None, "metadata.db", metadata_db) db_change = True except Exception as e: - return _configuration_result('%s' % e, gdriveError) + return _configuration_result('%s' % e, gdriveError, configured) if db_change: if not calibre_db.setup_db(config, ub.app_DB_path): - return _configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdriveError) + return _configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), + gdriveError, + configured) if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK): flash(_(u"DB is not Writeable"), category="warning") @@ -746,10 +955,10 @@ def _configuration_update_helper(): if reboot_required: web_server.stop(True) - return _configuration_result(None, gdriveError) + return _configuration_result(None, gdriveError, configured) -def _configuration_result(error_flash=None, gdriveError=None): +def _configuration_result(error_flash=None, gdriveError=None, configured=True): gdrive_authenticate = not is_gdrive_ready() gdrivefolders = [] if gdriveError is None: @@ -770,7 +979,7 @@ def _configuration_result(error_flash=None, gdriveError=None): return render_title_template("config_edit.html", config=config, provider=oauthblueprints, show_back_button=show_back_button, show_login_button=show_login_button, - show_authenticate_google_drive=gdrive_authenticate, + show_authenticate_google_drive=gdrive_authenticate, filepicker=configured, gdriveError=gdriveError, gdrivefolders=gdrivefolders, feature_support=feature_support, title=_(u"Basic Configuration"), page="config") @@ -816,7 +1025,10 @@ def _handle_new_user(to_save, content,languages, translations, kobo_support): content.allowed_column_value = config.config_allowed_column_value content.denied_column_value = config.config_denied_column_value ub.session.add(content) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() flash(_(u"User '%(user)s' created", user=content.nickname), category="success") return redirect(url_for('admin.admin')) except IntegrityError: @@ -832,7 +1044,10 @@ def _handle_edit_user(to_save, content,languages, translations, kobo_support): if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, ub.User.id != content.id).count(): ub.session.query(ub.User).filter(ub.User.id == content.id).delete() - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success") return redirect(url_for('admin.admin')) else: @@ -855,7 +1070,7 @@ def _handle_edit_user(to_save, content,languages, translations, kobo_support): content.role &= ~constants.ROLE_ANONYMOUS val = [int(k[5:]) for k in to_save if k.startswith('show_')] - sidebar = ub.get_sidebar_config() + sidebar = get_sidebar_config() for element in sidebar: value = element['visibility'] if value in val and not content.check_visibility(value): @@ -907,7 +1122,10 @@ def _handle_edit_user(to_save, content,languages, translations, kobo_support): if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail: content.kindle_mail = to_save["kindle_mail"] try: - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success") except IntegrityError: ub.session.rollback() @@ -1119,3 +1337,110 @@ def get_updater_status(): except Exception: status['status'] = 11 return json.dumps(status) + + +@admi.route('/import_ldap_users') +@login_required +@admin_required +def import_ldap_users(): + showtext = {} + try: + new_users = services.ldap.get_group_members(config.config_ldap_group_name) + except (services.ldap.LDAPException, TypeError, AttributeError, KeyError) as e: + log.debug_or_exception(e) + showtext['text'] = _(u'Error: %(ldaperror)s', ldaperror=e) + return json.dumps(showtext) + if not new_users: + log.debug('LDAP empty response') + showtext['text'] = _(u'Error: No user returned in response of LDAP server') + return json.dumps(showtext) + + imported = 0 + for username in new_users: + user = username.decode('utf-8') + if '=' in user: + # if member object field is empty take user object as filter + if config.config_ldap_member_user_object: + query_filter = config.config_ldap_member_user_object + else: + query_filter = config.config_ldap_user_object + try: + user_identifier = extract_user_identifier(user, query_filter) + except Exception as e: + log.warning(e) + continue + else: + user_identifier = user + query_filter = None + try: + user_data = services.ldap.get_object_details(user=user_identifier, query_filter=query_filter) + except AttributeError as e: + log.debug_or_exception(e) + continue + if user_data: + user_login_field = extract_dynamic_field_from_filter(user, config.config_ldap_user_object) + + username = user_data[user_login_field][0].decode('utf-8') + # check for duplicate username + if ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first(): + # if ub.session.query(ub.User).filter(ub.User.nickname == username).first(): + log.warning("LDAP User %s Already in Database", user_data) + continue + + kindlemail = '' + if 'mail' in user_data: + useremail = user_data['mail'][0].decode('utf-8') + if (len(user_data['mail']) > 1): + kindlemail = user_data['mail'][1].decode('utf-8') + + else: + log.debug('No Mail Field Found in LDAP Response') + useremail = username + '@email.com' + # check for duplicate email + if ub.session.query(ub.User).filter(func.lower(ub.User.email) == useremail.lower()).first(): + log.warning("LDAP Email %s Already in Database", user_data) + continue + content = ub.User() + content.nickname = username + content.password = '' # dummy password which will be replaced by ldap one + content.email = useremail + content.kindle_mail = kindlemail + content.role = config.config_default_role + content.sidebar_view = config.config_default_show + content.allowed_tags = config.config_allowed_tags + content.denied_tags = config.config_denied_tags + content.allowed_column_value = config.config_allowed_column_value + content.denied_column_value = config.config_denied_column_value + ub.session.add(content) + try: + ub.session.commit() + imported +=1 + except Exception as e: + log.warning("Failed to create LDAP user: %s - %s", user, e) + ub.session.rollback() + showtext['text'] = _(u'Failed to Create at Least One LDAP User') + else: + log.warning("LDAP User: %s Not Found", user) + showtext['text'] = _(u'At Least One LDAP User Not Found in Database') + if not showtext: + showtext['text'] = _(u'{} User Successfully Imported'.format(imported)) + return json.dumps(showtext) + + +def extract_user_data_from_field(user, field): + match = re.search(field + "=([\d\s\w-]+)", user, re.IGNORECASE | re.UNICODE) + if match: + return match.group(1) + else: + raise Exception("Could Not Parse LDAP User: {}".format(user)) + +def extract_dynamic_field_from_filter(user, filter): + match = re.search("([a-zA-Z0-9-]+)=%s", filter, re.IGNORECASE | re.UNICODE) + if match: + return match.group(1) + else: + raise Exception("Could Not Parse LDAP Userfield: {}", user) + +def extract_user_identifier(user, filter): + dynamic_field = extract_dynamic_field_from_filter(user, filter) + return extract_user_data_from_field(user, dynamic_field) diff --git a/cps/cli.py b/cps/cli.py index c94cb89d..65a4185a 100644 --- a/cps/cli.py +++ b/cps/cli.py @@ -45,6 +45,7 @@ parser.add_argument('-v', '--version', action='version', help='Shows version num version=version_info()) parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen') parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password') +parser.add_argument('-f', action='store_true', help='Enables filepicker in unconfigured mode') args = parser.parse_args() if sys.version_info < (3, 0): @@ -110,3 +111,6 @@ if ipadress: # handle and check user password argument user_password = args.s or None + +# Handles enableing of filepicker +filepicker = args.f or None diff --git a/cps/comic.py b/cps/comic.py index 7e3c7d47..a25f9a51 100644 --- a/cps/comic.py +++ b/cps/comic.py @@ -18,21 +18,21 @@ from __future__ import division, print_function, unicode_literals import os -import io from . import logger, isoLanguages from .constants import BookMeta -try: - from PIL import Image as PILImage - use_PIL = True -except ImportError as e: - use_PIL = False - log = logger.create() +try: + from wand.image import Image + use_IM = True +except (ImportError, RuntimeError) as e: + use_IM = False + + try: from comicapi.comicarchive import ComicArchive, MetaDataStyle use_comic_meta = True @@ -52,20 +52,23 @@ except (ImportError, LookupError) as e: use_rarfile = False use_comic_meta = False +NO_JPEG_EXTENSIONS = ['.png', '.webp', '.bmp'] +COVER_EXTENSIONS = ['.png', '.webp', '.bmp', '.jpg', '.jpeg'] + def _cover_processing(tmp_file_name, img, extension): - if use_PIL: + tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg') + if use_IM: # convert to jpg because calibre only supports jpg - if extension in ('.png', '.webp'): - imgc = PILImage.open(io.BytesIO(img)) - im = imgc.convert('RGB') - tmp_bytesio = io.BytesIO() - im.save(tmp_bytesio, format='JPEG') - img = tmp_bytesio.getvalue() + if extension in NO_JPEG_EXTENSIONS: + with Image(filename=tmp_file_name) as imgc: + imgc.format = 'jpeg' + imgc.transform_colorspace('rgb') + imgc.save(tmp_cover_name) + return tmp_cover_name if not img: return None - tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg') with open(tmp_cover_name, 'wb') as f: f.write(img) return tmp_cover_name @@ -80,7 +83,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable): ext = os.path.splitext(name) if len(ext) > 1: extension = ext[1].lower() - if extension in ('.jpg', '.jpeg', '.png', '.webp'): + if extension in COVER_EXTENSIONS: cover_data = archive.getPage(index) break else: @@ -90,7 +93,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable): ext = os.path.splitext(name) if len(ext) > 1: extension = ext[1].lower() - if extension in ('.jpg', '.jpeg', '.png', '.webp'): + if extension in COVER_EXTENSIONS: cover_data = cf.read(name) break elif original_file_extension.upper() == '.CBT': @@ -99,7 +102,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable): ext = os.path.splitext(name) if len(ext) > 1: extension = ext[1].lower() - if extension in ('.jpg', '.jpeg', '.png', '.webp'): + if extension in COVER_EXTENSIONS: cover_data = cf.extractfile(name).read() break elif original_file_extension.upper() == '.CBR' and use_rarfile: @@ -110,7 +113,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable): ext = os.path.splitext(name) if len(ext) > 1: extension = ext[1].lower() - if extension in ('.jpg', '.jpeg', '.png', '.webp'): + if extension in COVER_EXTENSIONS: cover_data = cf.read(name) break except Exception as e: diff --git a/cps/config_sql.py b/cps/config_sql.py index 877ad1c2..b1e487f9 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -22,6 +22,7 @@ import os import sys from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON +from sqlalchemy.exc import OperationalError from sqlalchemy.ext.declarative import declarative_base from . import constants, cli, logger, ub @@ -271,6 +272,14 @@ class _ConfigSQL(object): setattr(self, field, new_value) return True + def toDict(self): + storage = {} + for k, v in self.__dict__.items(): + if k[0] != '_' or k.endswith("password"): + storage[k] = v + return storage + + def load(self): '''Load all configuration values from the underlying storage.''' s = self._read_from_storage() # type: _Settings @@ -295,7 +304,11 @@ class _ConfigSQL(object): log.warning("Log path %s not valid, falling back to default", self.config_logfile) self.config_logfile = logfile self._session.merge(s) - self._session.commit() + try: + self._session.commit() + except OperationalError as e: + log.error('Database error: %s', e) + self._session.rollback() def save(self): '''Apply all configuration values to the underlying storage.''' @@ -309,7 +322,11 @@ class _ConfigSQL(object): log.debug("_ConfigSQL updating storage") self._session.merge(s) - self._session.commit() + try: + self._session.commit() + except OperationalError as e: + log.error('Database error: %s', e) + self._session.rollback() self.load() def invalidate(self, error=None): @@ -350,7 +367,10 @@ def _migrate_table(session, orm_class): changed = True if changed: - session.commit() + try: + session.commit() + except OperationalError: + session.rollback() def autodetect_calibre_binary(): diff --git a/cps/db.py b/cps/db.py index f648a794..f30fb609 100644 --- a/cps/db.py +++ b/cps/db.py @@ -32,9 +32,9 @@ from sqlalchemy.orm import relationship, sessionmaker, scoped_session from sqlalchemy.orm.collections import InstrumentedList from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta from sqlalchemy.pool import StaticPool -from flask_login import current_user from sqlalchemy.sql.expression import and_, true, false, text, func, or_ from sqlalchemy.ext.associationproxy import association_proxy +from flask_login import current_user from babel import Locale as LC from babel.core import UnknownLocaleError from flask_babel import gettext as _ @@ -425,18 +425,19 @@ class CalibreDB(): # instances alive once they reach the end of their respective scopes instances = WeakSet() - def __init__(self): + def __init__(self, expire_on_commit=True): """ Initialize a new CalibreDB session """ self.session = None if self._init: - self.initSession() + self.initSession(expire_on_commit) self.instances.add(self) - def initSession(self): + def initSession(self, expire_on_commit=True): self.session = self.session_factory() + self.session.expire_on_commit = expire_on_commit self.update_title_sort(self.config) @classmethod @@ -444,6 +445,8 @@ class CalibreDB(): cls.config = config cls.dispose() + # toDo: if db changed -> delete shelfs, delete download books, delete read boks, kobo sync?? + if not config.config_calibre_dir: config.invalidate() return False @@ -764,5 +767,5 @@ def lcase(s): return unidecode.unidecode(s.lower()) except Exception as e: log = logger.create() - log.exception(e) + log.debug_or_exception(e) return s.lower() diff --git a/cps/debug_info.py b/cps/debug_info.py index 75ef3bb8..8f0cdeee 100644 --- a/cps/debug_info.py +++ b/cps/debug_info.py @@ -21,7 +21,12 @@ import shutil import glob import zipfile import json -import io +from io import BytesIO +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + import os from flask import send_file @@ -32,11 +37,12 @@ from .about import collect_stats log = logger.create() def assemble_logfiles(file_name): - log_list = glob.glob(file_name + '*') - wfd = io.StringIO() + log_list = sorted(glob.glob(file_name + '*'), reverse=True) + wfd = StringIO() for f in log_list: with open(f, 'r') as fd: shutil.copyfileobj(fd, wfd) + wfd.seek(0) return send_file(wfd, as_attachment=True, attachment_filename=os.path.basename(file_name)) @@ -44,8 +50,12 @@ def assemble_logfiles(file_name): def send_debug(): file_list = glob.glob(logger.get_logfile(config.config_logfile) + '*') file_list.extend(glob.glob(logger.get_accesslogfile(config.config_access_logfile) + '*')) - memory_zip = io.BytesIO() + for element in [logger.LOG_TO_STDOUT, logger.LOG_TO_STDERR]: + if element in file_list: + file_list.remove(element) + memory_zip = BytesIO() with zipfile.ZipFile(memory_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf: + zf.writestr('settings.txt', json.dumps(config.toDict())) zf.writestr('libs.txt', json.dumps(collect_stats())) for fp in file_list: zf.write(fp, os.path.basename(fp)) diff --git a/cps/editbooks.py b/cps/editbooks.py index 7d207ed3..08ee93b1 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -37,13 +37,38 @@ from . import config, get_locale, ub, db from . import calibre_db from .services.worker import WorkerThread from .tasks.upload import TaskUpload -from .web import login_required_if_no_ano, render_title_template, edit_required, upload_required +from .render_template import render_title_template +from .usermanagement import login_required_if_no_ano + +try: + from functools import wraps +except ImportError: + pass # We're not using Python 3 editbook = Blueprint('editbook', __name__) log = logger.create() +def upload_required(f): + @wraps(f) + def inner(*args, **kwargs): + if current_user.role_upload() or current_user.role_admin(): + return f(*args, **kwargs) + abort(403) + + return inner + +def edit_required(f): + @wraps(f) + def inner(*args, **kwargs): + if current_user.role_edit() or current_user.role_admin(): + return f(*args, **kwargs) + abort(403) + + return inner + + # Modifies different Database objects, first check if elements have to be added to database, than check # if elements have to be deleted, because they are no longer used def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type): @@ -259,7 +284,7 @@ def delete_book(book_id, book_format, jsonResponse): filter(db.Data.format == book_format).delete() calibre_db.session.commit() except Exception as e: - log.exception(e) + log.debug_or_exception(e) calibre_db.session.rollback() else: # book not found @@ -287,7 +312,7 @@ def delete_book(book_id, book_format, jsonResponse): def render_edit_book(book_id): calibre_db.update_title_sort(config) cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() - book = calibre_db.get_filtered_book(book_id) + book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) if not book: flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error") return redirect(url_for("web.index")) @@ -716,7 +741,7 @@ def edit_book(book_id): flash(error, category="error") return render_edit_book(book_id) except Exception as e: - log.exception(e) + log.debug_or_exception(e) calibre_db.session.rollback() flash(_("Error editing book, please check logfile for details"), category="error") return redirect(url_for('web.show_book', book_id=book.id)) diff --git a/cps/error_handler.py b/cps/error_handler.py new file mode 100644 index 00000000..e9cb601a --- /dev/null +++ b/cps/error_handler.py @@ -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 . + +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)) diff --git a/cps/gdrive.py b/cps/gdrive.py index 90b83e64..b1896bbb 100644 --- a/cps/gdrive.py +++ b/cps/gdrive.py @@ -35,9 +35,9 @@ from flask_babel import gettext as _ from flask_login import login_required from . import logger, gdriveutils, config, ub, calibre_db -from .web import admin_required +from .admin import admin_required -gdrive = Blueprint('gdrive', __name__) +gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive') log = logger.create() try: @@ -50,7 +50,7 @@ current_milli_time = lambda: int(round(time() * 1000)) gdrive_watch_callback_token = 'target=calibreweb-watch_files' -@gdrive.route("/gdrive/authenticate") +@gdrive.route("/authenticate") @login_required @admin_required def authenticate_google_drive(): @@ -63,7 +63,7 @@ def authenticate_google_drive(): return redirect(authUrl) -@gdrive.route("/gdrive/callback") +@gdrive.route("/callback") def google_drive_callback(): auth_code = request.args.get('code') if not auth_code: @@ -77,18 +77,14 @@ def google_drive_callback(): return redirect(url_for('admin.configuration')) -@gdrive.route("/gdrive/watch/subscribe") +@gdrive.route("/watch/subscribe") @login_required @admin_required def watch_gdrive(): if not config.config_google_drive_watch_changes_response: with open(gdriveutils.CLIENT_SECRETS, 'r') as settings: filedata = json.load(settings) - if filedata['web']['redirect_uris'][0].endswith('/'): - filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-((len('/gdrive/callback')+1))] - else: - filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-(len('/gdrive/callback'))] - address = '%s/gdrive/watch/callback' % filedata['web']['redirect_uris'][0] + address = filedata['web']['redirect_uris'][0].rstrip('/').replace('/gdrive/callback', '/gdrive/watch/callback') notification_id = str(uuid4()) try: result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id, @@ -98,14 +94,15 @@ def watch_gdrive(): except HttpError as e: reason=json.loads(e.content)['error']['errors'][0] if reason['reason'] == u'push.webhookUrlUnauthorized': - flash(_(u'Callback domain is not verified, please follow steps to verify domain in google developer console'), category="error") + flash(_(u'Callback domain is not verified, ' + u'please follow steps to verify domain in google developer console'), category="error") else: flash(reason['message'], category="error") return redirect(url_for('admin.configuration')) -@gdrive.route("/gdrive/watch/revoke") +@gdrive.route("/watch/revoke") @login_required @admin_required def revoke_watch_gdrive(): @@ -121,14 +118,14 @@ def revoke_watch_gdrive(): return redirect(url_for('admin.configuration')) -@gdrive.route("/gdrive/watch/callback", methods=['GET', 'POST']) +@gdrive.route("/watch/callback", methods=['GET', 'POST']) def on_received_watch_confirmation(): if not config.config_google_drive_watch_changes_response: return '' if request.headers.get('X-Goog-Channel-Token') != gdrive_watch_callback_token \ or request.headers.get('X-Goog-Resource-State') != 'change' \ or not request.data: - return '' # redirect(url_for('admin.configuration')) + return '' log.debug('%r', request.headers) log.debug('%r', request.data) @@ -146,15 +143,18 @@ def on_received_watch_confirmation(): dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode() if not response['deleted'] and response['file']['title'] == 'metadata.db' \ and response['file']['md5Checksum'] != hashlib.md5(dbpath): - tmpDir = tempfile.gettempdir() + tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web') + if not os.path.isdir(tmp_dir): + os.mkdir(tmp_dir) + log.info('Database file updated') - copyfile(dbpath, os.path.join(tmpDir, "metadata.db_" + str(current_milli_time()))) + copyfile(dbpath, os.path.join(tmp_dir, "metadata.db_" + str(current_milli_time()))) log.info('Backing up existing and downloading updated metadata.db') - gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmpDir, "tmp_metadata.db")) + gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmp_dir, "tmp_metadata.db")) log.info('Setting up new DB') - # prevent error on windows, as os.rename does on exisiting files - move(os.path.join(tmpDir, "tmp_metadata.db"), dbpath) + # prevent error on windows, as os.rename does on existing files, also allow cross hdd move + move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath) calibre_db.reconnect_db(config, ub.app_DB_path) except Exception as e: - log.exception(e) + log.debug_or_exception(e) return '' diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py index 3e00c9af..6f78ab47 100644 --- a/cps/gdriveutils.py +++ b/cps/gdriveutils.py @@ -32,16 +32,25 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.exc import OperationalError, InvalidRequestError try: - from pydrive.auth import GoogleAuth - from pydrive.drive import GoogleDrive - from pydrive.auth import RefreshError from apiclient import errors from httplib2 import ServerNotFoundError - gdrive_support = True importError = None -except ImportError as err: - importError = err + gdrive_support = True +except ImportError as e: + importError = e gdrive_support = False +try: + from pydrive2.auth import GoogleAuth + from pydrive2.drive import GoogleDrive + from pydrive2.auth import RefreshError +except ImportError as err: + try: + from pydrive.auth import GoogleAuth + from pydrive.drive import GoogleDrive + from pydrive.auth import RefreshError + except ImportError as err: + importError = err + gdrive_support = False from . import logger, cli, config from .constants import CONFIG_DIR as _CONFIG_DIR @@ -91,7 +100,7 @@ class Singleton: except AttributeError: self._instance = self._decorated() return self._instance - except ImportError as e: + except (ImportError, NameError) as e: log.debug(e) return None @@ -190,7 +199,7 @@ def getDrive(drive=None, gauth=None): except RefreshError as e: log.error("Google Drive error: %s", e) except Exception as e: - log.exception(e) + log.debug_or_exception(e) else: # Initialize the saved creds gauth.Authorize() @@ -208,7 +217,7 @@ def listRootFolders(): drive = getDrive(Gdrive.Instance().drive) folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false" fileList = drive.ListFile({'q': folder}).GetList() - except ServerNotFoundError as e: + except (ServerNotFoundError, ssl.SSLError) as e: log.info("GDrive Error %s" % e) fileList = [] return fileList @@ -547,21 +556,24 @@ def partial(total_byte_len, part_size_limit): return s # downloads files in chunks from gdrive -def do_gdrive_download(df, headers): +def do_gdrive_download(df, headers, convert_encoding=False): total_size = int(df.metadata.get('fileSize')) download_url = df.metadata.get('downloadUrl') s = partial(total_size, 1024 * 1024) # I'm downloading BIG files, so 100M chunk size is fine for me - def stream(): + def stream(convert_encoding): for byte in s: headers = {"Range": 'bytes=%s-%s' % (byte[0], byte[1])} resp, content = df.auth.Get_Http_Object().request(download_url, headers=headers) if resp.status == 206: + if convert_encoding: + result = chardet.detect(content) + content = content.decode(result['encoding']).encode('utf-8') yield content else: log.warning('An error occurred: %s', resp) return - return Response(stream_with_context(stream()), headers=headers) + return Response(stream_with_context(stream(convert_encoding)), headers=headers) _SETTINGS_YAML_TEMPLATE = """ diff --git a/cps/helper.py b/cps/helper.py index 9845df97..506afe71 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -24,10 +24,7 @@ import io import mimetypes import re import shutil -import glob import time -import zipfile -import json import unicodedata from datetime import datetime, timedelta from tempfile import gettempdir @@ -53,13 +50,6 @@ try: except ImportError: use_unidecode = False -try: - from PIL import Image as PILImage - from PIL import UnidentifiedImageError - use_PIL = True -except ImportError: - use_PIL = False - from . import calibre_db from .tasks.convert import TaskConvert from . import logger, config, get_locale, db, ub @@ -69,9 +59,16 @@ from .subproc_wrapper import process_wait from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS from .tasks.mail import TaskEmail - log = logger.create() +try: + from wand.image import Image + from wand.exceptions import MissingDelegateError + use_IM = True +except (ImportError, RuntimeError) as e: + log.debug('Cannot import Image, generating covers from non jpg files will not work: %s', e) + use_IM = False + # Convert existing book entry to new format def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, user_id, kindle_mail=None): @@ -112,21 +109,21 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, def send_test_mail(kindle_mail, user_name): WorkerThread.add(user_name, TaskEmail(_(u'Calibre-Web test e-mail'), None, None, config.get_mail_settings(), kindle_mail, _(u"Test e-mail"), - _(u'This e-mail has been sent via Calibre-Web.'))) + _(u'This e-mail has been sent via Calibre-Web.'))) return # Send registration email or password reset email, depending on parameter resend (False means welcome email) def send_registration_mail(e_mail, user_name, default_password, resend=False): - text = "Hello %s!\r\n" % user_name + txt = "Hello %s!\r\n" % user_name if not resend: - text += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n" - text += "Please log in to your account using the following informations:\r\n" - text += "User name: %s\r\n" % user_name - text += "Password: %s\r\n" % default_password - text += "Don't forget to change your password after first login.\r\n" - text += "Sincerely\r\n\r\n" - text += "Your Calibre-Web team" + txt += "Your new account at Calibre-Web has been created. Thanks for joining us!\r\n" + txt += "Please log in to your account using the following informations:\r\n" + txt += "User name: %s\r\n" % user_name + txt += "Password: %s\r\n" % default_password + txt += "Don't forget to change your password after first login.\r\n" + txt += "Sincerely\r\n\r\n" + txt += "Your Calibre-Web team" WorkerThread.add(None, TaskEmail( subject=_(u'Get Started with Calibre-Web'), filepath=None, @@ -134,7 +131,7 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False): settings=config.get_mail_settings(), recipient=e_mail, taskMessage=_(u"Registration e-mail for user: %(name)s", name=user_name), - text=text + text=txt )) return @@ -180,7 +177,7 @@ def check_send_to_kindle(entry): 'convert': 0, 'text': _('Send %(format)s to Kindle', format='Pdf')}) if config.config_converterpath: - if 'EPUB' in formats and not 'MOBI' in formats: + if 'EPUB' in formats and 'MOBI' not in formats: bookformats.append({'format': 'Mobi', 'convert':1, 'text': _('Convert %(orig)s to %(format)s and send to Kindle', @@ -565,8 +562,7 @@ def get_book_cover_internal(book, use_generic_cover_on_failure): log.error('%s/cover.jpg not found on Google Drive', book.path) return get_cover_on_failure(use_generic_cover_on_failure) except Exception as e: - log.exception(e) - # traceback.print_exc() + log.debug_or_exception(e) return get_cover_on_failure(use_generic_cover_on_failure) else: cover_file_path = os.path.join(config.config_calibre_dir, book.path) @@ -589,16 +585,15 @@ def save_cover_from_url(url, book_path): requests.exceptions.Timeout) as ex: log.info(u'Cover Download Error %s', ex) return False, _("Error Downloading Cover") - except UnidentifiedImageError as ex: + except MissingDelegateError as ex: log.info(u'File Format Error %s', ex) return False, _("Cover Format Error") def save_cover_from_filestorage(filepath, saved_filename, img): - if hasattr(img, '_content'): - f = open(os.path.join(filepath, saved_filename), "wb") - f.write(img._content) - f.close() + if hasattr(img,"metadata"): + img.save(filename=os.path.join(filepath, saved_filename)) + img.close() else: # check if file path exists, otherwise create it, copy file to calibre path and delete temp file if not os.path.exists(filepath): @@ -619,31 +614,33 @@ def save_cover_from_filestorage(filepath, saved_filename, img): def save_cover(img, book_path): content_type = img.headers.get('content-type') - if use_PIL: - if content_type not in ('image/jpeg', 'image/png', 'image/webp'): - log.error("Only jpg/jpeg/png/webp files are supported as coverfile") - return False, _("Only jpg/jpeg/png/webp files are supported as coverfile") + if use_IM: + if content_type not in ('image/jpeg', 'image/png', 'image/webp', 'image/bmp'): + log.error("Only jpg/jpeg/png/webp/bmp files are supported as coverfile") + return False, _("Only jpg/jpeg/png/webp/bmp files are supported as coverfile") # convert to jpg because calibre only supports jpg - if content_type in ('image/png', 'image/webp'): + if content_type != 'image/jpg': if hasattr(img, 'stream'): - imgc = PILImage.open(img.stream) + imgc = Image(blob=img.stream) else: - imgc = PILImage.open(io.BytesIO(img.content)) - im = imgc.convert('RGB') - tmp_bytesio = io.BytesIO() - im.save(tmp_bytesio, format='JPEG') - img._content = tmp_bytesio.getvalue() + imgc = Image(blob=io.BytesIO(img.content)) + imgc.format = 'jpeg' + imgc.transform_colorspace("rgb") + img = imgc else: if content_type not in 'image/jpeg': log.error("Only jpg/jpeg files are supported as coverfile") return False, _("Only jpg/jpeg files are supported as coverfile") if config.config_use_google_drive: - tmpDir = gettempdir() - ret, message = save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img) + tmp_dir = os.path.join(gettempdir(), 'calibre_web') + + if not os.path.isdir(tmp_dir): + os.mkdir(tmp_dir) + ret, message = save_cover_from_filestorage(tmp_dir, "uploaded_cover.jpg", img) if ret is True: - gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), - os.path.join(tmpDir, "uploaded_cover.jpg")) + gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg').replace("\\","/"), + os.path.join(tmp_dir, "uploaded_cover.jpg")) log.info("Cover is saved on Google Drive") return True, None else: @@ -697,7 +694,7 @@ def check_unrar(unrarLocation): log.debug("unrar version %s", version) break except (OSError, UnicodeDecodeError) as err: - log.exception(err) + log.debug_or_exception(err) return _('Error excecuting UnRar') @@ -827,4 +824,3 @@ def get_download_link(book_id, book_format, client): return do_download_file(book, book_format, client, data1, headers) else: abort(404) - diff --git a/cps/kobo.py b/cps/kobo.py index a9c0f936..029592fe 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -43,6 +43,8 @@ from flask_login import current_user from werkzeug.datastructures import Headers from sqlalchemy import func from sqlalchemy.sql.expression import and_, or_ +from sqlalchemy.exc import OperationalError +from sqlalchemy.orm import load_only from sqlalchemy.exc import StatementError import requests @@ -56,6 +58,8 @@ KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB3", "EPUB"]} KOBO_STOREAPI_URL = "https://storeapi.kobo.com" KOBO_IMAGEHOST_URL = "https://kbimages1-a.akamaihd.net" +SYNC_ITEM_LIMIT = 5 + kobo = Blueprint("kobo", __name__, url_prefix="/kobo/") kobo_auth.disable_failed_auth_redirect_for_blueprint(kobo) kobo_auth.register_url_value_preprocessor(kobo) @@ -142,68 +146,80 @@ def HandleSyncRequest(): new_books_last_modified = sync_token.books_last_modified new_books_last_created = sync_token.books_last_created new_reading_state_last_modified = sync_token.reading_state_last_modified + new_archived_last_modified = datetime.datetime.min sync_results = [] # We reload the book database so that the user get's a fresh view of the library # in case of external changes (e.g: adding a book through Calibre). calibre_db.reconnect_db(config, ub.app_DB_path) - archived_books = ( - ub.session.query(ub.ArchivedBook) - .filter(ub.ArchivedBook.user_id == int(current_user.id)) - .all() - ) - - # We join-in books that have had their Archived bit recently modified in order to either: - # * Restore them to the user's device. - # * Delete them from the user's device. - # (Ideally we would use a join for this logic, however cross-database joins don't look trivial in SqlAlchemy.) - recently_restored_or_archived_books = [] - archived_book_ids = {} - new_archived_last_modified = datetime.datetime.min - for archived_book in archived_books: - if archived_book.last_modified > sync_token.archive_last_modified: - recently_restored_or_archived_books.append(archived_book.book_id) - if archived_book.is_archived: - archived_book_ids[archived_book.book_id] = True - new_archived_last_modified = max( - new_archived_last_modified, archived_book.last_modified) - - # sqlite gives unexpected results when performing the last_modified comparison without the datetime cast. - # It looks like it's treating the db.Books.last_modified field as a string and may fail - # the comparison because of the +00:00 suffix. - changed_entries = ( - calibre_db.session.query(db.Books) - .join(db.Data) - .filter(or_(func.datetime(db.Books.last_modified) > sync_token.books_last_modified, - db.Books.id.in_(recently_restored_or_archived_books))) - .filter(db.Data.format.in_(KOBO_FORMATS)) - .all() - ) + if sync_token.books_last_id > -1: + changed_entries = ( + calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived) + .join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) + .filter(db.Books.last_modified >= sync_token.books_last_modified) + .filter(db.Books.id>sync_token.books_last_id) + .filter(db.Data.format.in_(KOBO_FORMATS)) + .order_by(db.Books.last_modified) + .order_by(db.Books.id) + .limit(SYNC_ITEM_LIMIT) + ) + else: + changed_entries = ( + calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived) + .join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) + .filter(db.Books.last_modified > sync_token.books_last_modified) + .filter(db.Data.format.in_(KOBO_FORMATS)) + .order_by(db.Books.last_modified) + .order_by(db.Books.id) + .limit(SYNC_ITEM_LIMIT) + ) reading_states_in_new_entitlements = [] for book in changed_entries: - kobo_reading_state = get_or_create_reading_state(book.id) + kobo_reading_state = get_or_create_reading_state(book.Books.id) entitlement = { - "BookEntitlement": create_book_entitlement(book, archived=(book.id in archived_book_ids)), - "BookMetadata": get_metadata(book), + "BookEntitlement": create_book_entitlement(book.Books, archived=(book.is_archived == True)), + "BookMetadata": get_metadata(book.Books), } if kobo_reading_state.last_modified > sync_token.reading_state_last_modified: - entitlement["ReadingState"] = get_kobo_reading_state_response(book, kobo_reading_state) + entitlement["ReadingState"] = get_kobo_reading_state_response(book.Books, kobo_reading_state) new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified) - reading_states_in_new_entitlements.append(book.id) + reading_states_in_new_entitlements.append(book.Books.id) - if book.timestamp > sync_token.books_last_created: + if book.Books.timestamp > sync_token.books_last_created: sync_results.append({"NewEntitlement": entitlement}) else: sync_results.append({"ChangedEntitlement": entitlement}) new_books_last_modified = max( - book.last_modified, new_books_last_modified + book.Books.last_modified, new_books_last_modified ) - new_books_last_created = max(book.timestamp, new_books_last_created) + new_books_last_created = max(book.Books.timestamp, new_books_last_created) + max_change = (changed_entries + .from_self() + .filter(ub.ArchivedBook.is_archived) + .order_by(func.datetime(ub.ArchivedBook.last_modified).desc()) + .first() + ) + if max_change: + max_change = max_change.last_modified + else: + max_change = new_archived_last_modified + new_archived_last_modified = max(new_archived_last_modified, max_change) + + # no. of books returned + book_count = changed_entries.count() + + # last entry: + if book_count: + books_last_id = changed_entries.all()[-1].Books.id or -1 + else: + books_last_id = -1 + + # generate reading state data changed_reading_states = ( ub.session.query(ub.KoboReadingState) .filter(and_(func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified, @@ -225,11 +241,12 @@ def HandleSyncRequest(): sync_token.books_last_modified = new_books_last_modified sync_token.archive_last_modified = new_archived_last_modified sync_token.reading_state_last_modified = new_reading_state_last_modified + sync_token.books_last_id = books_last_id - return generate_sync_response(sync_token, sync_results) + return generate_sync_response(sync_token, sync_results, book_count) -def generate_sync_response(sync_token, sync_results): +def generate_sync_response(sync_token, sync_results, set_cont=False): extra_headers = {} if config.config_kobo_proxy: # Merge in sync results from the official Kobo store. @@ -245,6 +262,8 @@ def generate_sync_response(sync_token, sync_results): except Exception as e: log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e)) + if set_cont: + extra_headers["x-kobo-sync"] = "continue" sync_token.to_headers(extra_headers) response = make_response(jsonify(sync_results), extra_headers) @@ -443,8 +462,10 @@ def HandleTagCreate(): items_unknown_to_calibre = add_items_to_shelf(items, shelf) if items_unknown_to_calibre: log.debug("Received request to add unknown books to a collection. Silently ignoring items.") - ub.session.commit() - + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() return make_response(jsonify(str(shelf.uuid)), 201) @@ -476,7 +497,10 @@ def HandleTagUpdate(tag_id): shelf.name = name ub.session.merge(shelf) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() return make_response(' ', 200) @@ -528,7 +552,10 @@ def HandleTagAddItem(tag_id): log.debug("Received request to add an unknown book to a collection. Silently ignoring item.") ub.session.merge(shelf) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() return make_response('', 201) @@ -569,7 +596,10 @@ def HandleTagRemoveItem(tag_id): shelf.books.filter(ub.BookShelf.book_id == book.id).delete() except KeyError: items_unknown_to_calibre.append(item) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() if items_unknown_to_calibre: log.debug("Received request to remove an unknown book to a collecition. Silently ignoring item.") @@ -615,7 +645,10 @@ def sync_shelves(sync_token, sync_results): "ChangedTag": tag }) sync_token.tags_last_modified = new_tags_last_modified - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() # Creates a Kobo "Tag" object from a ub.Shelf object @@ -696,7 +729,10 @@ def HandleStateRequest(book_uuid): abort(400, description="Malformed request data is missing 'ReadingStates' key") ub.session.merge(kobo_reading_state) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() return jsonify({ "RequestResult": "Success", "UpdateResults": [update_results_response], @@ -734,7 +770,10 @@ def get_or_create_reading_state(book_id): kobo_reading_state.statistics = ub.KoboStatistics() book_read.kobo_reading_state = kobo_reading_state ub.session.add(book_read) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() return book_read.kobo_reading_state @@ -837,7 +876,10 @@ def HandleBookDeletionRequest(book_uuid): archived_book.last_modified = datetime.datetime.utcnow() ub.session.merge(archived_book) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() return ("", 204) diff --git a/cps/kobo_auth.py b/cps/kobo_auth.py index 0f6cd174..3288bb73 100644 --- a/cps/kobo_auth.py +++ b/cps/kobo_auth.py @@ -66,9 +66,10 @@ from os import urandom from flask import g, Blueprint, url_for, abort, request from flask_login import login_user, login_required from flask_babel import gettext as _ +from sqlalchemy.exc import OperationalError from . import logger, ub, lm -from .web import render_title_template +from .render_template import render_title_template try: from functools import wraps @@ -147,7 +148,10 @@ def generate_auth_token(user_id): auth_token.token_type = 1 ub.session.add(auth_token) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() return render_title_template( "generate_kobo_auth_url.html", title=_(u"Kobo Setup"), @@ -164,5 +168,8 @@ def delete_auth_token(user_id): # Invalidate any prevously generated Kobo Auth token for this user. ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\ .filter(ub.RemoteAuthToken.token_type==1).delete() - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() return "" diff --git a/cps/logger.py b/cps/logger.py index 7cc0f4d9..a9606cb1 100644 --- a/cps/logger.py +++ b/cps/logger.py @@ -41,10 +41,18 @@ logging.addLevelName(logging.WARNING, "WARN") logging.addLevelName(logging.CRITICAL, "CRIT") +class _Logger(logging.Logger): + + def debug_or_exception(self, message, *args, **kwargs): + if is_debug_enabled(): + self.exception(message, stacklevel=2, *args, **kwargs) + else: + self.error(message, stacklevel=2, *args, **kwargs) + + def get(name=None): return logging.getLogger(name) - def create(): parent_frame = inspect.stack(0)[1] if hasattr(parent_frame, 'frame'): @@ -54,7 +62,6 @@ def create(): parent_module = inspect.getmodule(parent_frame) return get(parent_module.__name__) - def is_debug_enabled(): return logging.root.level <= logging.DEBUG @@ -99,6 +106,7 @@ def setup(log_file, log_level=None): May be called multiple times. ''' log_level = log_level or DEFAULT_LOG_LEVEL + logging.setLoggerClass(_Logger) logging.getLogger(__package__).setLevel(log_level) r = logging.root diff --git a/cps/oauth_bb.py b/cps/oauth_bb.py index 4d489cdd..cafef3eb 100644 --- a/cps/oauth_bb.py +++ b/cps/oauth_bb.py @@ -30,11 +30,12 @@ from flask_babel import gettext as _ from flask_dance.consumer import oauth_authorized, oauth_error from flask_dance.contrib.github import make_github_blueprint, github from flask_dance.contrib.google import make_google_blueprint, google -from flask_login import login_user, current_user +from flask_login import login_user, current_user, login_required from sqlalchemy.orm.exc import NoResultFound +from sqlalchemy.exc import OperationalError from . import constants, logger, config, app, ub -from .web import login_required + from .oauth import OAuthBackend, backend_resultcode @@ -87,7 +88,7 @@ def register_user_with_oauth(user=None): try: ub.session.commit() except Exception as e: - log.exception(e) + log.debug_or_exception(e) ub.session.rollback() @@ -109,7 +110,10 @@ if ub.oauth_support: oauthProvider.provider_name = "google" oauthProvider.active = False ub.session.add(oauthProvider) - ub.session.commit() + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() oauth_ids = ub.session.query(ub.OAuthProvider).all() ele1 = dict(provider_name='github', @@ -203,7 +207,7 @@ if ub.oauth_support: ub.session.add(oauth_entry) ub.session.commit() except Exception as e: - log.exception(e) + log.debug_or_exception(e) ub.session.rollback() # Disable Flask-Dance's default behavior for saving the OAuth token @@ -235,7 +239,7 @@ if ub.oauth_support: flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success") return redirect(url_for('web.profile')) except Exception as e: - log.exception(e) + log.debug_or_exception(e) ub.session.rollback() else: flash(_(u"Login failed, No User Linked With OAuth Account"), category="error") @@ -282,7 +286,7 @@ if ub.oauth_support: logout_oauth_user() flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success") except Exception as e: - log.exception(e) + log.debug_or_exception(e) ub.session.rollback() flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error") except NoResultFound: diff --git a/cps/opds.py b/cps/opds.py index 05e9c68c..1ae7010c 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -33,7 +33,8 @@ from werkzeug.security import check_password_hash from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages from .helper import get_download_link, get_book_cover from .pagination import Pagination -from .web import render_read_books, download_required, load_user_from_request +from .web import render_read_books +from .usermanagement import load_user_from_request from flask_babel import gettext as _ from babel import Locale as LC from babel.core import UnknownLocaleError diff --git a/cps/remotelogin.py b/cps/remotelogin.py new file mode 100644 index 00000000..a448e5fc --- /dev/null +++ b/cps/remotelogin.py @@ -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 . + +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/') +@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 diff --git a/cps/render_template.py b/cps/render_template.py new file mode 100644 index 00000000..16fdab13 --- /dev/null +++ b/cps/render_template.py @@ -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 . + +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) diff --git a/cps/services/SyncToken.py b/cps/services/SyncToken.py index f6db960b..26eb396c 100644 --- a/cps/services/SyncToken.py +++ b/cps/services/SyncToken.py @@ -85,6 +85,7 @@ class SyncToken: "archive_last_modified": {"type": "string"}, "reading_state_last_modified": {"type": "string"}, "tags_last_modified": {"type": "string"}, + "books_last_id": {"type": "integer", "optional": True} }, } @@ -96,6 +97,7 @@ class SyncToken: archive_last_modified=datetime.min, reading_state_last_modified=datetime.min, tags_last_modified=datetime.min, + books_last_id=-1 ): self.raw_kobo_store_token = raw_kobo_store_token self.books_last_created = books_last_created @@ -103,6 +105,7 @@ class SyncToken: self.archive_last_modified = archive_last_modified self.reading_state_last_modified = reading_state_last_modified self.tags_last_modified = tags_last_modified + self.books_last_id = books_last_id @staticmethod def from_headers(headers): @@ -137,9 +140,12 @@ class SyncToken: archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified") reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified") tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified") + books_last_id = data_json["books_last_id"] except TypeError: log.error("SyncToken timestamps don't parse to a datetime.") return SyncToken(raw_kobo_store_token=raw_kobo_store_token) + except KeyError: + books_last_id = -1 return SyncToken( raw_kobo_store_token=raw_kobo_store_token, @@ -147,7 +153,8 @@ class SyncToken: books_last_modified=books_last_modified, archive_last_modified=archive_last_modified, reading_state_last_modified=reading_state_last_modified, - tags_last_modified=tags_last_modified + tags_last_modified=tags_last_modified, + books_last_id=books_last_id ) def set_kobo_store_header(self, store_headers): @@ -170,7 +177,8 @@ class SyncToken: "books_last_created": to_epoch_timestamp(self.books_last_created), "archive_last_modified": to_epoch_timestamp(self.archive_last_modified), "reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified), - "tags_last_modified": to_epoch_timestamp(self.tags_last_modified) + "tags_last_modified": to_epoch_timestamp(self.tags_last_modified), + "books_last_id":self.books_last_id }, } return b64encode_json(token) diff --git a/cps/services/worker.py b/cps/services/worker.py index c2ea594c..072674a0 100644 --- a/cps/services/worker.py +++ b/cps/services/worker.py @@ -110,7 +110,7 @@ class WorkerThread(threading.Thread): # We don't use a daemon here because we don't want the tasks to just be abruptly halted, leading to # possible file / database corruption item = self.queue.get(timeout=1) - except queue.Empty as ex: + except queue.Empty: time.sleep(1) continue @@ -161,7 +161,7 @@ class CalibreTask: self.run(*args) except Exception as e: self._handleError(str(e)) - log.exception(e) + log.debug_or_exception(e) self.end_time = datetime.now() @@ -210,7 +210,6 @@ class CalibreTask: self._progress = x def _handleError(self, error_message): - log.exception(error_message) self.stat = STAT_FAIL self.progress = 1 self.error = error_message diff --git a/cps/shelf.py b/cps/shelf.py index ea7f1eeb..6dae333c 100644 --- a/cps/shelf.py +++ b/cps/shelf.py @@ -22,6 +22,7 @@ from __future__ import division, print_function, unicode_literals from datetime import datetime +import sys from flask import Blueprint, request, flash, redirect, url_for from flask_babel import gettext as _ @@ -29,8 +30,9 @@ from flask_login import login_required, current_user from sqlalchemy.sql.expression import func from sqlalchemy.exc import OperationalError, InvalidRequestError -from . import logger, ub, calibre_db -from .web import login_required_if_no_ano, render_title_template +from . import logger, ub, calibre_db, db +from .render_template import render_title_template +from .usermanagement import login_required_if_no_ano shelf = Blueprint('shelf', __name__) @@ -138,18 +140,14 @@ def search_to_shelf(shelf_id): books_for_shelf = ub.searched_ids[current_user.id] if not books_for_shelf: - log.error("Books are already part of %s", shelf) + log.error("Books are already part of %s", shelf.name) flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error") return redirect(url_for('web.index')) - maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first() - if maxOrder[0] is None: - maxOrder = 0 - else: - maxOrder = maxOrder[0] + maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0 for book in books_for_shelf: - maxOrder = maxOrder + 1 + maxOrder += 1 shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder)) shelf.last_modified = datetime.utcnow() try: @@ -322,8 +320,11 @@ def delete_shelf_helper(cur_shelf): ub.session.delete(cur_shelf) ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete() ub.session.add(ub.ShelfArchive(uuid=cur_shelf.uuid, user_id=cur_shelf.user_id)) - ub.session.commit() - log.info("successfully deleted %s", cur_shelf) + try: + ub.session.commit() + log.info("successfully deleted %s", cur_shelf) + except OperationalError: + ub.session.rollback() @@ -333,44 +334,22 @@ def delete_shelf(shelf_id): cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() try: delete_shelf_helper(cur_shelf) - except (OperationalError, InvalidRequestError): + except InvalidRequestError: ub.session.rollback() flash(_(u"Settings DB is not Writeable"), category="error") return redirect(url_for('web.index')) - -@shelf.route("/shelf/", defaults={'shelf_type': 1}) -@shelf.route("/shelf//") +@shelf.route("/simpleshelf/") @login_required_if_no_ano -def show_shelf(shelf_type, shelf_id): - shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() +def show_simpleshelf(shelf_id): + return render_show_shelf(2, shelf_id, 1, None) - result = list() - # user is allowed to access shelf - if shelf and check_shelf_view_permissions(shelf): - page = "shelf.html" if shelf_type == 1 else 'shelfdown.html' - - books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\ - .order_by(ub.BookShelf.order.asc()).all() - for book in books_in_shelf: - cur_book = calibre_db.get_filtered_book(book.book_id) - if cur_book: - result.append(cur_book) - else: - cur_book = calibre_db.get_book(book.book_id) - if not cur_book: - log.info('Not existing book %s in %s deleted', book.book_id, shelf) - try: - ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete() - ub.session.commit() - except (OperationalError, InvalidRequestError): - ub.session.rollback() - flash(_(u"Settings DB is not Writeable"), category="error") - return render_title_template(page, entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name), - shelf=shelf, page="shelf") - else: - flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error") - return redirect(url_for("web.index")) +@shelf.route("/shelf/", defaults={"sort_param": "order", 'page': 1}) +@shelf.route("/shelf//", defaults={'page': 1}) +@shelf.route("/shelf///") +@login_required_if_no_ano +def show_shelf(shelf_id, sort_param, page): + return render_show_shelf(1, shelf_id, page, sort_param) @shelf.route("/shelf/order/", methods=["GET", "POST"]) @@ -394,22 +373,80 @@ def order_shelf(shelf_id): shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() result = list() if shelf and check_shelf_view_permissions(shelf): - books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \ - .order_by(ub.BookShelf.order.asc()).all() - for book in books_in_shelf2: - cur_book = calibre_db.get_filtered_book(book.book_id) - if cur_book: - result.append({'title': cur_book.title, - 'id': cur_book.id, - 'author': cur_book.authors, - 'series': cur_book.series, - 'series_index': cur_book.series_index}) - else: - cur_book = calibre_db.get_book(book.book_id) - result.append({'title': _('Hidden Book'), - 'id': cur_book.id, - 'author': [], - 'series': []}) + result = calibre_db.session.query(db.Books)\ + .join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id , isouter=True) \ + .add_columns(calibre_db.common_filters().label("visible")) \ + .filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all() return render_title_template('shelf_order.html', entries=result, title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name), shelf=shelf, page="shelforder") + +def change_shelf_order(shelf_id, order): + result = calibre_db.session.query(db.Books).join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id)\ + .filter(ub.BookShelf.shelf == shelf_id).order_by(*order).all() + for index, entry in enumerate(result): + book = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \ + .filter(ub.BookShelf.book_id == entry.id).first() + book.order = index + try: + ub.session.commit() + except OperationalError: + ub.session.rollback() + +def render_show_shelf(shelf_type, shelf_id, page_no, sort_param): + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() + + # check user is allowed to access shelf + if shelf and check_shelf_view_permissions(shelf): + + if shelf_type == 1: + # order = [ub.BookShelf.order.asc()] + if sort_param == 'pubnew': + change_shelf_order(shelf_id, [db.Books.pubdate.desc()]) + if sort_param == 'pubold': + change_shelf_order(shelf_id, [db.Books.pubdate]) + if sort_param == 'abc': + change_shelf_order(shelf_id, [db.Books.sort]) + if sort_param == 'zyx': + change_shelf_order(shelf_id, [db.Books.sort.desc()]) + if sort_param == 'new': + change_shelf_order(shelf_id, [db.Books.timestamp.desc()]) + if sort_param == 'old': + change_shelf_order(shelf_id, [db.Books.timestamp]) + if sort_param == 'authaz': + change_shelf_order(shelf_id, [db.Books.author_sort.asc()]) + if sort_param == 'authza': + change_shelf_order(shelf_id, [db.Books.author_sort.desc()]) + page = "shelf.html" + pagesize = 0 + else: + pagesize = sys.maxsize + page = 'shelfdown.html' + + result, __, pagination = calibre_db.fill_indexpage(page_no, pagesize, + db.Books, + ub.BookShelf.shelf == shelf_id, + [ub.BookShelf.order.asc()], + ub.BookShelf,ub.BookShelf.book_id == db.Books.id) + # delete chelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web + wrong_entries = calibre_db.session.query(ub.BookShelf)\ + .join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True)\ + .filter(db.Books.id == None).all() + for entry in wrong_entries: + log.info('Not existing book {} in {} deleted'.format(entry.book_id, shelf)) + try: + ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == entry.book_id).delete() + ub.session.commit() + except (OperationalError, InvalidRequestError): + ub.session.rollback() + flash(_(u"Settings DB is not Writeable"), category="error") + + return render_title_template(page, + entries=result, + pagination=pagination, + title=_(u"Shelf: '%(name)s'", name=shelf.name), + shelf=shelf, + page="shelf") + else: + flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error") + return redirect(url_for("web.index")) diff --git a/cps/static/css/caliBlur.css b/cps/static/css/caliBlur.css index c67fdfb2..ffa5ecca 100644 --- a/cps/static/css/caliBlur.css +++ b/cps/static/css/caliBlur.css @@ -240,7 +240,7 @@ body.blur .row-fluid .col-sm-10 { .col-sm-10 .book-meta > div.btn-toolbar:after { content: ''; - direction: block; + direction: ltr; position: fixed; top: 120px; right: 0; @@ -398,20 +398,17 @@ body.blur .row-fluid .col-sm-10 { .shelforder #sortTrue > div:hover { background-color: hsla(0, 0%, 100%, .06) !important; - cursor: move; cursor: grab; - cursor: -webkit-grab; color: #eee } .shelforder #sortTrue > div:active { cursor: grabbing; - cursor: -webkit-grabbing } .shelforder #sortTrue > div:before { content: "\EA53"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; margin-right: 30px; margin-left: 15px; vertical-align: bottom; @@ -446,7 +443,7 @@ body.blur .row-fluid .col-sm-10 { body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before { content: "\e155"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -494,7 +491,7 @@ body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before { } #have_read_cb + label:before, #have_read_cb:checked + label:before { - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-size: 16px; height: 40px; width: 60px; @@ -550,13 +547,12 @@ body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before { height: 60px; width: 50px; cursor: pointer; - margin: 0; display: inline-block; - margin-top: -4px; + margin: -4px 0 0; } #archived_cb + label:before, #archived_cb:checked + label:before { - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-size: 16px; height: 40px; width: 60px; @@ -618,7 +614,7 @@ div[aria-label="Edit/Delete book"] > .btn > span { div[aria-label="Edit/Delete book"] > .btn > span:before { content: "\EA5d"; - font-family: plex-icons; + font-family: plex-icons, serif; font-size: 20px; padding: 16px 15px; display: inline-block; @@ -641,7 +637,7 @@ div[aria-label="Edit/Delete book"] > .btn > span:hover { width: 225px; max-width: 225px; position: relative !important; - left: auto !important; + left: auto !important; top: auto !important; -webkit-transform: none !important; -ms-transform: none !important; @@ -760,7 +756,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a .home-btn { color: hsla(0, 0%, 100%, .7); - line-height: 34.29px; + line-height: 34px; margin: 0; padding: 0; position: absolute; @@ -770,7 +766,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a .home-btn > a { color: rgba(255, 255, 255, .7); - font-family: plex-icons-new; + font-family: plex-icons-new, serif; line-height: 60px; position: relative; text-align: center; @@ -800,7 +796,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.home-btn > a:hove .glyphicon-search:before { content: "\EA4F"; - font-family: plex-icons + font-family: plex-icons, serif } #nav_about:after, .profileDrop > span:after, .profileDrop > span:before { @@ -833,7 +829,7 @@ body:not(.read-frame) { overflow: hidden; margin: 0; /* scroll bar fix for firefox */ - scrollbar-color: hsla(0, 0%, 100%, .2) transparent; + scrollbar-color: hsla(0, 0%, 100%, .2) transparent; scrollbar-width: thin; } @@ -966,7 +962,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d #form-upload .form-group .btn:before { content: "\e043"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -991,7 +987,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d #form-upload .form-group .btn:after { content: "\EA13"; position: absolute; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 8px; background: #3c444a; color: hsla(0, 0%, 100%, .7); @@ -1019,7 +1015,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d text-transform: none; font-weight: 400; font-style: normal; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; line-height: 1; @@ -1075,7 +1071,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > d body > div.navbar.navbar-default.navbar-static-top > div > form > div > span > button:before { content: "\EA32"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; color: #eee; background: #555; font-size: 10px; @@ -1097,7 +1093,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form > div > span > b body > div.navbar.navbar-default.navbar-static-top > div > form:before { content: "\EA4F"; display: block; - font-family: plex-icons; + font-family: plex-icons, serif; position: absolute; color: #eee; font-weight: 400; @@ -1120,7 +1116,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > form:before { body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > div > span.input-group-btn:before { content: "\EA4F"; display: block; - font-family: plex-icons; + font-family: plex-icons, serif; position: absolute; left: -298px; top: 8px; @@ -1193,7 +1189,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.c body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul > li > #top_admin > .glyphicon-dashboard::before { content: "\EA31"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 20px } @@ -1272,7 +1268,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover { user-select: none } -.navigation li, .navigation li:not(ul>li) { +.navigation li, .navigation li:not(ul > li) { border-radius: 0 4px 4px 0 } @@ -1352,32 +1348,32 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover { #nav_hot .glyphicon-fire::before { content: "\1F525"; - font-family: glyphicons regular + font-family: glyphicons regular, serif } .glyphicon-star:before { content: "\EA10"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } #nav_rand .glyphicon-random::before { content: "\EA44"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } .glyphicon-list::before { content: "\EA4D"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } #nav_about .glyphicon-info-sign::before { content: "\EA26"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } #nav_cat .glyphicon-inbox::before, .glyphicon-tags::before { content: "\E067"; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; margin-left: 2px } @@ -1423,7 +1419,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover { .navigation .create-shelf a:before { content: "\EA13"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 100%; padding-right: 10px; vertical-align: middle @@ -1473,7 +1469,7 @@ body > div.container-fluid > div > div.col-sm-10 > div > form > a:hover { #books > .cover > a:before, #books_rand > .cover > a:before, .book.isotope-item > .cover > a:before, body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 > div.col-sm-12 > div.col-sm-2 > a:before { content: "\e352"; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; background: var(--color-secondary); border-radius: 50%; font-weight: 400; @@ -1521,8 +1517,8 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form top: 0; left: 0; opacity: 0; - background: -webkit-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%); - background: -o-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%); + background: -webkit-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%); + background: -o-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%); background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%); z-index: -9 } @@ -1562,8 +1558,8 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form top: 0; left: 0; opacity: 0; - background: -webkit-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%); - background: -o-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%); + background: -webkit-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%); + background: -o-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%); background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%) } @@ -1739,7 +1735,7 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 { body.me > div.container-fluid > div.row-fluid > div.col-sm-10:before { content: ''; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; font-size: 6vw; @@ -1947,7 +1943,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.pagination .page-next > a body > div.container-fluid > div > div.col-sm-10 > div.pagination .page-previous > a { top: 0; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-weight: 100; -webkit-font-smoothing: antialiased; line-height: 60px; @@ -2026,7 +2022,7 @@ body.authorlist > div.container-fluid > div > div.col-sm-10 > div.container > di body.serieslist > div.container-fluid > div > div.col-sm-10:before { content: "\e044"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -2130,7 +2126,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container body.catlist > div.container-fluid > div.row-fluid > div.col-sm-10:before { content: "\E067"; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; font-style: normal; font-weight: 400; line-height: 1; @@ -2150,7 +2146,7 @@ body.catlist > div.container-fluid > div.row-fluid > div.col-sm-10:before { body.authorlist > div.container-fluid > div.row-fluid > div.col-sm-10:before, body.langlist > div.container-fluid > div > div.col-sm-10:before { - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -2249,7 +2245,7 @@ body.langlist > div.container-fluid > div > div.col-sm-10 > div.container:before body.advsearch > div.container-fluid > div > div.col-sm-10 > div.col-md-10.col-lg-6 { padding: 15px 10px 15px 40px; -} +} @media screen and (max-width: 992px) { body.advsearch > div.container-fluid > div > div.col-sm-10 > div.col-md-10.col-lg-6 { @@ -2491,7 +2487,6 @@ body > div.container-fluid > div > div.col-sm-10 > div.col-sm-8 > form > .btn.bt } textarea { - resize: none; resize: vertical } @@ -2837,7 +2832,7 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-8 > form body.advanced_search > div.container-fluid > div > div.col-sm-10 > div.col-sm-8:before { content: "\EA4F"; - font-family: plex-icons; + font-family: plex-icons, serif; font-style: normal; font-weight: 400; line-height: 1; @@ -3159,7 +3154,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div. #add-to-shelf > span.glyphicon.glyphicon-list:before { content: "\EA59"; - font-family: plex-icons; + font-family: plex-icons, serif; font-size: 18px } @@ -3171,7 +3166,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div. #read-in-browser > span.glyphicon-eye-open:before, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.glyphicon-eye-open:before { content: "\e352"; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; font-size: 18px; padding-right: 5px } @@ -3183,7 +3178,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div. #btnGroupDrop1 > span.glyphicon-download:before { font-size: 20px; content: "\ea66"; - font-family: plex-icons + font-family: plex-icons, serif } .col-sm-10 .book-meta > div.btn-toolbar { @@ -3287,7 +3282,6 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd -webkit-box-shadow: 0 4px 10px rgba(0, 0, 0, .35); box-shadow: 0 4px 10px rgba(0, 0, 0, .35); -o-transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4); - transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4); transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4), -webkit-transform .15s cubic-bezier(.6, .4, .2, 1.4); -webkit-transform-origin: center top; -ms-transform-origin: center top; @@ -3416,7 +3410,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover > .btn-primary:l .book-meta > div.more-stuff > .btn-toolbar > .btn-group[aria-label="Remove from shelves"] > a > .glyphicon-remove:before { content: "\ea64"; - font-family: plex-icons + font-family: plex-icons, serif } body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > .col-sm-6 { @@ -3530,7 +3524,7 @@ body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-b body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [data-target="#DeleteShelfDialog"]:before { content: "\EA6D"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; position: absolute; color: hsla(0, 0%, 100%, .7); font-size: 20px; @@ -3560,7 +3554,7 @@ body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-b body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [href*=edit]:before { content: "\EA5d"; - font-family: plex-icons; + font-family: plex-icons, serif; position: absolute; color: hsla(0, 0%, 100%, .7); font-size: 20px; @@ -3590,7 +3584,7 @@ body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-b body.shelf > div.container-fluid > div > div.col-sm-10 > div.discover > .shelf-btn-group > [href*=order]:before { content: "\E409"; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; position: absolute; color: hsla(0, 0%, 100%, .7); font-size: 20px; @@ -3727,7 +3721,7 @@ body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-header > a .plexBack > a { color: rgba(255, 255, 255, .7); - font-family: plex-icons-new; + font-family: plex-icons-new, serif; -webkit-font-variant-ligatures: normal; font-variant-ligatures: normal; line-height: 60px; @@ -3839,11 +3833,9 @@ body.login > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.l -webkit-transform: translateY(-50%); -ms-transform: translateY(-50%); transform: translateY(-50%); - border-style: solid; vertical-align: middle; -webkit-transition: border .2s, -webkit-transform .4s; -o-transition: border .2s, transform .4s; - transition: border .2s, transform .4s; transition: border .2s, transform .4s, -webkit-transform .4s; margin: 9px 6px } @@ -3862,11 +3854,9 @@ body.login > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.l -webkit-transform: translateY(-50%); -ms-transform: translateY(-50%); transform: translateY(-50%); - border-style: solid; vertical-align: middle; -webkit-transition: border .2s, -webkit-transform .4s; -o-transition: border .2s, transform .4s; - transition: border .2s, transform .4s; transition: border .2s, transform .4s, -webkit-transform .4s; margin: 12px 6px } @@ -3946,7 +3936,7 @@ body.author img.bg-blur[src=undefined] { body.author:not(.authorlist) .undefined-img:before { content: "\e008"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -4095,7 +4085,7 @@ body.shelf.modal-open > .container-fluid { font-size: 18px; color: #999; display: inline-block; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; font-style: normal; font-weight: 400 } @@ -4196,7 +4186,7 @@ body.shelf.modal-open > .container-fluid { #remove-from-shelves > .btn > span:before { content: "\EA52"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; color: transparent; padding-left: 5px } @@ -4208,7 +4198,7 @@ body.shelf.modal-open > .container-fluid { #remove-from-shelves > a:first-of-type:before { content: "\EA4D"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; position: absolute; color: hsla(0, 0%, 100%, .45); font-style: normal; @@ -4248,7 +4238,7 @@ body.shelf.modal-open > .container-fluid { content: "\E208"; padding-right: 10px; display: block; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; font-style: normal; font-weight: 400; position: absolute; @@ -4259,7 +4249,6 @@ body.shelf.modal-open > .container-fluid { opacity: .5; -webkit-transition: -webkit-transform .3s ease-out; -o-transition: transform .3s ease-out; - transition: transform .3s ease-out; transition: transform .3s ease-out, -webkit-transform .3s ease-out; -webkit-transform: translate(0, -60px); -ms-transform: translate(0, -60px); @@ -4319,7 +4308,7 @@ body.advanced_search > div.container-fluid > div > div.col-sm-10 > div.col-sm-8 .glyphicon-remove:before { content: "\EA52"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-weight: 400 } @@ -4405,7 +4394,7 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div body:not(.blur) #nav_new:before { content: "\EA4F"; - font-family: plex-icons; + font-family: plex-icons, serif; font-style: normal; font-weight: 400; line-height: 1; @@ -4431,7 +4420,7 @@ body.advanced_search > div.container-fluid > div.row-fluid > div.col-sm-10 > div color: hsla(0, 0%, 100%, .7); cursor: pointer; display: block; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 20px; font-stretch: 100%; font-style: normal; @@ -4527,12 +4516,12 @@ body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > d } body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row > div.col .table > tbody > tr > td, body.admin > div.container-fluid > div > div.col-sm-10 > div.container-fluid > div.row > div.col .table > tbody > tr > th, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > .table > tbody > tr > td, body.admin > div.container-fluid > div > div.col-sm-10 > div.discover > .table > tbody > tr > th { - border: collapse + border: collapse; } body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10::before, body.newuser.admin > div.container-fluid > div.row-fluid > div.col-sm-10::before { content: ''; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; font-size: 6vw; @@ -4636,7 +4625,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. content: "\e352"; display: inline-block; position: absolute; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; background: var(--color-secondary); color: #fff; border-radius: 50%; @@ -4674,8 +4663,8 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. top: 0; left: 0; opacity: 0; - background: -webkit-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%); - background: -o-radial-gradient(50% 50%, farthest-corner, rgba(50, 50, 50, .5) 50%, #323232 100%); + background: -webkit-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%); + background: -o-radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%); background: radial-gradient(farthest-corner at 50% 50%, rgba(50, 50, 50, .5) 50%, #323232 100%) } @@ -4727,7 +4716,7 @@ body.admin td > a:hover { .glyphicon-ok::before { content: "\EA55"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-weight: 400 } @@ -4796,7 +4785,7 @@ body:not(.blur):not(.login):not(.me):not(.author):not(.editbook):not(.upload):no background-position: center center, center center, center center !important; background-size: auto, auto, cover !important; -webkit-background-size: auto, auto, cover !important; - -moz-background-size: autom, auto, cover !important; + -moz-background-size: auto, auto, cover !important; -o-background-size: auto, auto, cover !important; width: 100%; height: 60px; @@ -4862,7 +4851,6 @@ body.read:not(.blur) a[href*=readbooks] { .tooltip.in { opacity: 1; -o-transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4); - transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4); transition: opacity .15s ease-out, transform .15s cubic-bezier(.6, .4, .2, 1.4), -webkit-transform .15s cubic-bezier(.6, .4, .2, 1.4); -webkit-transform: translate(0) scale(1); -ms-transform: translate(0) scale(1); @@ -4962,7 +4950,7 @@ body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-3 > div.text-center > #delete:before, body.upload > div.container-fluid > div.row-fluid > div.col-sm-10 > form > div.col-sm-3 > div.text-center > #delete:before { content: "\EA6D"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 18px; color: hsla(0, 0%, 100%, .7) } @@ -5047,7 +5035,7 @@ body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.boot body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.bootstrap-table > div.fixed-table-container > div.fixed-table-body > #table > thead > tr > th > .th-inner.asc:after { content: "\EA58"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-weight: 400; right: 20px; position: absolute @@ -5055,7 +5043,7 @@ body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.boot body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.bootstrap-table > div.fixed-table-container > div.fixed-table-body > #table > thead > tr > th > .th-inner.desc:after { content: "\EA57"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-weight: 400; right: 20px; position: absolute @@ -5118,7 +5106,7 @@ body.tasks > div.container-fluid > div > div.col-sm-10 > div.discover > div.boot .epub-back:before { content: "\EA1C"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-weight: 400; color: #4f4f4f; position: absolute; @@ -5281,7 +5269,7 @@ body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-3 > div.text-center > #delete:before, body.upload > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm-3 > div.text-center > #delete:before { content: "\EA6D"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 18px; color: hsla(0, 0%, 100%, .7); vertical-align: super @@ -5441,7 +5429,7 @@ body.editbook > div.container-fluid > div.row-fluid > div.col-sm-10 > div.col-sm #main-nav + #scnd-nav .create-shelf a:before { content: "\EA13"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 100%; padding-right: 10px; vertical-align: middle @@ -5486,7 +5474,7 @@ body.admin.modal-open .navbar { content: "\E208"; padding-right: 10px; display: block; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; font-style: normal; font-weight: 400; position: absolute; @@ -5497,7 +5485,6 @@ body.admin.modal-open .navbar { opacity: .5; -webkit-transition: -webkit-transform .3s ease-out; -o-transition: transform .3s ease-out; - transition: transform .3s ease-out; transition: transform .3s ease-out, -webkit-transform .3s ease-out; -webkit-transform: translate(0, -60px); -ms-transform: translate(0, -60px); @@ -5551,22 +5538,22 @@ body.admin.modal-open .navbar { #RestartDialog > .modal-dialog > .modal-content > .modal-header:before { content: "\EA4F"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } #ShutdownDialog > .modal-dialog > .modal-content > .modal-header:before { content: "\E064"; - font-family: glyphicons regular + font-family: glyphicons regular, serif } #StatusDialog > .modal-dialog > .modal-content > .modal-header:before { content: "\EA15"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } #deleteModal > .modal-dialog > .modal-content > .modal-header:before { content: "\EA6D"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } #RestartDialog > .modal-dialog > .modal-content > .modal-header:after { @@ -5957,7 +5944,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. .home-btn { height: 48px; - line-height: 28.29px; + line-height: 28px; right: 10px; left: auto } @@ -5969,7 +5956,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. .plexBack { height: 48px; - line-height: 28.29px; + line-height: 28px; left: 48px; display: none } @@ -6048,7 +6035,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. body > div.navbar.navbar-default.navbar-static-top > div > form.search-focus > div > span.input-group-btn:before { content: "\EA33"; display: block; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; position: fixed; left: 0; top: 0; @@ -6200,7 +6187,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. #form-upload .form-group .btn:before { content: "\e043"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; line-height: 1; -webkit-font-smoothing: antialiased; color: #fff; @@ -6218,7 +6205,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. #form-upload .form-group .btn:after { content: "\EA13"; position: absolute; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 8px; background: #3c444a; color: #fff; @@ -6271,7 +6258,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. } #top_admin, #top_tasks { - padding: 11.5px 15px; + padding: 12px 15px; font-size: 13px; line-height: 1.71428571; overflow: hidden @@ -6280,7 +6267,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. #top_admin > .glyphicon, #top_tasks > .glyphicon-tasks { position: relative; top: 0; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; line-height: 1; border-radius: 0; background: 0 0; @@ -6299,7 +6286,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. #top_tasks > .glyphicon-tasks::before, body > div.navbar.navbar-default.navbar-static-top > div > div.navbar-collapse.collapse > ul > li > #top_admin > .glyphicon-dashboard::before { text-transform: none; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; @@ -6624,7 +6611,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. .author > .container-fluid > .row-fluid > .col-sm-10 > h2:after { content: "\e008"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -6829,7 +6816,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. color: hsla(0, 0%, 100%, .7); cursor: pointer; display: block; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; font-size: 20px; font-stretch: 100%; font-style: normal; @@ -6996,11 +6983,9 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. -webkit-transform: translateY(-50%); -ms-transform: translateY(-50%); transform: translateY(-50%); - border-style: solid; vertical-align: middle; -webkit-transition: border .2s, -webkit-transform .4s; -o-transition: border .2s, transform .4s; - transition: border .2s, transform .4s; transition: border .2s, transform .4s, -webkit-transform .4s; margin: 12px 6px } @@ -7019,18 +7004,16 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. -webkit-transform: translateY(-50%); -ms-transform: translateY(-50%); transform: translateY(-50%); - border-style: solid; vertical-align: middle; -webkit-transition: border .2s, -webkit-transform .4s; -o-transition: border .2s, transform .4s; - transition: border .2s, transform .4s; transition: border .2s, transform .4s, -webkit-transform .4s; margin: 9px 6px } body.author:not(.authorlist) .blur-wrapper:before, body.author > .container-fluid > .row-fluid > .col-sm-10 > h2:after { content: "\e008"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-weight: 400; z-index: 9; line-height: 1; @@ -7361,7 +7344,6 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. transform: translate3d(0, 0, 0); -webkit-transition: -webkit-transform .5s; -o-transition: transform .5s; - transition: transform .5s; transition: transform .5s, -webkit-transform .5s; z-index: 99 } @@ -7376,7 +7358,6 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. transform: translate3d(-240px, 0, 0); -webkit-transition: -webkit-transform .5s; -o-transition: transform .5s; - transition: transform .5s; transition: transform .5s, -webkit-transform .5s; top: 0; margin: 0; @@ -7415,7 +7396,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. text-align: center; min-width: 40px; pointer-events: none; - color: # + // color: # } .col-xs-12 > .row > .col-xs-10 { @@ -7526,7 +7507,7 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. body.publisherlist > div.container-fluid > div > div.col-sm-10:before { content: "\e241"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -7546,7 +7527,7 @@ body.publisherlist > div.container-fluid > div > div.col-sm-10:before { body.ratingslist > div.container-fluid > div > div.col-sm-10:before { content: "\e007"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -7572,7 +7553,7 @@ body.ratingslist > div.container-fluid > div > div.col-sm-10:before { body.formatslist > div.container-fluid > div > div.col-sm-10:before { content: "\e022"; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings', serif; font-style: normal; font-weight: 400; line-height: 1; @@ -7747,7 +7728,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .editabl body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphicon-trash:before { content: "\EA6D"; - font-family: plex-icons-new + font-family: plex-icons-new, serif } #DeleteDomain { @@ -7770,7 +7751,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic content: "\E208"; padding-right: 10px; display: block; - font-family: Glyphicons Regular; + font-family: Glyphicons Regular, serif; font-style: normal; font-weight: 400; position: absolute; @@ -7781,7 +7762,6 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic opacity: .5; -webkit-transition: -webkit-transform .3s ease-out; -o-transition: transform .3s ease-out; - transition: transform .3s ease-out; transition: transform .3s ease-out, -webkit-transform .3s ease-out; -webkit-transform: translate(0, -60px); -ms-transform: translate(0, -60px); @@ -7820,7 +7800,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic #DeleteDomain > .modal-dialog > .modal-content > .modal-header:before { content: "\EA6D"; - font-family: plex-icons-new; + font-family: plex-icons-new, serif; padding-right: 10px; font-size: 18px; color: #999; diff --git a/cps/static/css/caliBlur_override.css b/cps/static/css/caliBlur_override.css index 7f940212..00ba3cca 100644 --- a/cps/static/css/caliBlur_override.css +++ b/cps/static/css/caliBlur_override.css @@ -1,11 +1,11 @@ body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{ display: none; } - .cover .badge{ position: absolute; top: 0; left: 0; + color: #fff; background-color: #cc7b19; border-radius: 0; padding: 0 8px; @@ -15,3 +15,8 @@ body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{ .cover{ box-shadow: 0 0 4px rgba(0,0,0,.6); } + +.cover .read{ + padding: 0 0px; + line-height: 15px; +} diff --git a/cps/static/css/libs/bootstrap-select.min.css b/cps/static/css/libs/bootstrap-select.min.css new file mode 100644 index 00000000..59708ed5 --- /dev/null +++ b/cps/static/css/libs/bootstrap-select.min.css @@ -0,0 +1,6 @@ +/*! + * 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) + */@-webkit-keyframes bs-notify-fadeOut{0%{opacity:.9}100%{opacity:0}}@-o-keyframes bs-notify-fadeOut{0%{opacity:.9}100%{opacity:0}}@keyframes bs-notify-fadeOut{0%{opacity:.9}100%{opacity:0}}.bootstrap-select>select.bs-select-hidden,select.bs-select-hidden,select.selectpicker{display:none!important}.bootstrap-select{width:220px\0;vertical-align:middle}.bootstrap-select>.dropdown-toggle{position:relative;width:100%;text-align:right;white-space:nowrap;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.bootstrap-select>.dropdown-toggle:after{margin-top:-1px}.bootstrap-select>.dropdown-toggle.bs-placeholder,.bootstrap-select>.dropdown-toggle.bs-placeholder:active,.bootstrap-select>.dropdown-toggle.bs-placeholder:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder:hover{color:#999}.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:hover{color:rgba(255,255,255,.5)}.bootstrap-select>select{position:absolute!important;bottom:0;left:50%;display:block!important;width:.5px!important;height:100%!important;padding:0!important;opacity:0!important;border:none;z-index:0!important}.bootstrap-select>select.mobile-device{top:0;left:0;display:block!important;width:100%!important;z-index:2!important}.bootstrap-select.is-invalid .dropdown-toggle,.error .bootstrap-select .dropdown-toggle,.has-error .bootstrap-select .dropdown-toggle,.was-validated .bootstrap-select select:invalid+.dropdown-toggle{border-color:#b94a48}.bootstrap-select.is-valid .dropdown-toggle,.was-validated .bootstrap-select select:valid+.dropdown-toggle{border-color:#28a745}.bootstrap-select.fit-width{width:auto!important}.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn){width:220px}.bootstrap-select .dropdown-toggle:focus,.bootstrap-select>select.mobile-device:focus+.dropdown-toggle{outline:thin dotted #333!important;outline:5px auto -webkit-focus-ring-color!important;outline-offset:-2px}.bootstrap-select.form-control{margin-bottom:0;padding:0;border:none;height:auto}:not(.input-group)>.bootstrap-select.form-control:not([class*=col-]){width:100%}.bootstrap-select.form-control.input-group-btn{float:none;z-index:auto}.form-inline .bootstrap-select,.form-inline .bootstrap-select.form-control:not([class*=col-]){width:auto}.bootstrap-select:not(.input-group-btn),.bootstrap-select[class*=col-]{float:none;display:inline-block;margin-left:0}.bootstrap-select.dropdown-menu-right,.bootstrap-select[class*=col-].dropdown-menu-right,.row .bootstrap-select[class*=col-].dropdown-menu-right{float:right}.form-group .bootstrap-select,.form-horizontal .bootstrap-select,.form-inline .bootstrap-select{margin-bottom:0}.form-group-lg .bootstrap-select.form-control,.form-group-sm .bootstrap-select.form-control{padding:0}.form-group-lg .bootstrap-select.form-control .dropdown-toggle,.form-group-sm .bootstrap-select.form-control .dropdown-toggle{height:100%;font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-lg .dropdown-toggle,.bootstrap-select.form-control-sm .dropdown-toggle{font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-sm .dropdown-toggle{padding:.25rem .5rem}.bootstrap-select.form-control-lg .dropdown-toggle{padding:.5rem 1rem}.form-inline .bootstrap-select .form-control{width:100%}.bootstrap-select.disabled,.bootstrap-select>.disabled{cursor:not-allowed}.bootstrap-select.disabled:focus,.bootstrap-select>.disabled:focus{outline:0!important}.bootstrap-select.bs-container{position:absolute;top:0;left:0;height:0!important;padding:0!important}.bootstrap-select.bs-container .dropdown-menu{z-index:1060}.bootstrap-select .dropdown-toggle .filter-option{position:static;top:0;left:0;float:left;height:100%;width:100%;text-align:left;overflow:hidden;-webkit-box-flex:0;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto}.bs3.bootstrap-select .dropdown-toggle .filter-option{padding-right:inherit}.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option{position:absolute;padding-top:inherit;padding-bottom:inherit;padding-left:inherit;float:none}.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option .filter-option-inner{padding-right:inherit}.bootstrap-select .dropdown-toggle .filter-option-inner-inner{overflow:hidden}.bootstrap-select .dropdown-toggle .filter-expand{width:0!important;float:left;opacity:0!important;overflow:hidden}.bootstrap-select .dropdown-toggle .caret{position:absolute;top:50%;right:12px;margin-top:-2px;vertical-align:middle}.input-group .bootstrap-select.form-control .dropdown-toggle{border-radius:inherit}.bootstrap-select[class*=col-] .dropdown-toggle{width:100%}.bootstrap-select .dropdown-menu{min-width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .dropdown-menu>.inner:focus{outline:0!important}.bootstrap-select .dropdown-menu.inner{position:static;float:none;border:0;padding:0;margin:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.bootstrap-select .dropdown-menu li{position:relative}.bootstrap-select .dropdown-menu li.active small{color:rgba(255,255,255,.5)!important}.bootstrap-select .dropdown-menu li.disabled a{cursor:not-allowed}.bootstrap-select .dropdown-menu li a{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.bootstrap-select .dropdown-menu li a.opt{position:relative;padding-left:2.25em}.bootstrap-select .dropdown-menu li a span.check-mark{display:none}.bootstrap-select .dropdown-menu li a span.text{display:inline-block}.bootstrap-select .dropdown-menu li small{padding-left:.5em}.bootstrap-select .dropdown-menu .notify{position:absolute;bottom:5px;width:96%;margin:0 2%;min-height:26px;padding:3px 5px;background:#f5f5f5;border:1px solid #e3e3e3;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05);pointer-events:none;opacity:.9;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .dropdown-menu .notify.fadeOut{-webkit-animation:.3s linear 750ms forwards bs-notify-fadeOut;-o-animation:.3s linear 750ms forwards bs-notify-fadeOut;animation:.3s linear 750ms forwards bs-notify-fadeOut}.bootstrap-select .no-results{padding:3px;background:#f5f5f5;margin:0 5px;white-space:nowrap}.bootstrap-select.fit-width .dropdown-toggle .filter-option{position:static;display:inline;padding:0}.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner,.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner-inner{display:inline}.bootstrap-select.fit-width .dropdown-toggle .bs-caret:before{content:'\00a0'}.bootstrap-select.fit-width .dropdown-toggle .caret{position:static;top:auto;margin-top:-1px}.bootstrap-select.show-tick .dropdown-menu .selected span.check-mark{position:absolute;display:inline-block;right:15px;top:5px}.bootstrap-select.show-tick .dropdown-menu li a span.text{margin-right:34px}.bootstrap-select .bs-ok-default:after{content:'';display:block;width:.5em;height:1em;border-style:solid;border-width:0 .26em .26em 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle{z-index:1061}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:before{content:'';border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid rgba(204,204,204,.2);position:absolute;bottom:-4px;left:9px;display:none}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:after{content:'';border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;bottom:-4px;left:10px;display:none}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:before{bottom:auto;top:-4px;border-top:7px solid rgba(204,204,204,.2);border-bottom:0}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:after{bottom:auto;top:-4px;border-top:6px solid #fff;border-bottom:0}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:before{right:12px;left:auto}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:after{right:13px;left:auto}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:after,.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:before,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:after,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:before{display:block}.bs-actionsbox,.bs-donebutton,.bs-searchbox{padding:4px 8px}.bs-actionsbox{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-actionsbox .btn-group button{width:50%}.bs-donebutton{float:left;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-donebutton .btn-group button{width:100%}.bs-searchbox+.bs-actionsbox{padding:0 8px 4px}.bs-searchbox .form-control{margin-bottom:0;width:100%;float:none} \ No newline at end of file diff --git a/cps/static/css/main.css b/cps/static/css/main.css index 08506bc2..2831a0fd 100644 --- a/cps/static/css/main.css +++ b/cps/static/css/main.css @@ -25,10 +25,9 @@ body { overflow: hidden; -webkit-transition: -webkit-transform 0.4s, width 0.2s; -moz-transition: -webkit-transform 0.4s, width 0.2s; - -ms-transition: -webkit-transform 0.4s, width 0.2s; + transition: -webkit-transform 0.4s, width 0.2s; -moz-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1); -webkit-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1); - -ms-box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1); box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.1); } @@ -45,7 +44,7 @@ body { text-align: center; -webkit-transition: opacity 0.5s; -moz-transition: opacity 0.5s; - -ms-transition: opacity 0.5s; + transition: opacity 0.5s; z-index: 10; } @@ -79,7 +78,6 @@ body { color: rgba(0, 0, 0, 0.6); -moz-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8); -webkit-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8); - -ms-box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8); box-shadow: inset 0 0 6px rgba(155, 155, 155, 0.8); } @@ -121,7 +119,6 @@ body { font-weight: bold; cursor: pointer; -webkit-user-select: none; - -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; @@ -147,7 +144,7 @@ body { height: 100%; -webkit-transition: -webkit-transform 0.5s; -moz-transition: -moz-transform 0.5s; - -ms-transition: -moz-transform 0.5s; + transition: -moz-transform 0.5s; overflow: hidden; } @@ -183,7 +180,6 @@ body { height: 14px; -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); - -ms-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); } @@ -232,7 +228,6 @@ body { input::-webkit-input-placeholder { color: #454545; } input:-moz-placeholder { color: #454545; } -input:-ms-placeholder { color: #454545; } #divider { position: absolute; @@ -268,18 +263,18 @@ input:-ms-placeholder { color: #454545; } width: 25%; height: 100%; visibility: hidden; - -webkit-transition: visibility 0 ease 0.5s; - -moz-transition: visibility 0 ease 0.5s; - -ms-transition: visibility 0 ease 0.5s; + -webkit-transition: visibility 0s ease 0.5s; + -moz-transition: visibility 0s ease 0.5s; + transition: visibility 0s ease 0.5s; } #sidebar.open #tocView, #sidebar.open #bookmarksView { overflow-y: auto; visibility: visible; - -webkit-transition: visibility 0 ease 0; - -moz-transition: visibility 0 ease 0; - -ms-transition: visibility 0 ease 0; + -webkit-transition: visibility 0s ease 0s; + -moz-transition: visibility 0s ease 0s; + transition: visibility 0s ease 0s; } #sidebar.open #tocView { @@ -495,9 +490,8 @@ input:-ms-placeholder { color: #454545; } position: fixed; top: 50%; left: 50%; - width: 50%; + // width: 50%; width: 630px; - height: auto; z-index: 2000; visibility: hidden; @@ -518,7 +512,6 @@ input:-ms-placeholder { color: #454545; } background: rgba(255, 255, 255, 0.8); -webkit-transition: all 0.3s; -moz-transition: all 0.3s; - -ms-transition: all 0.3s; transition: all 0.3s; } @@ -588,7 +581,6 @@ input:-ms-placeholder { color: #454545; } opacity: 0; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; - -ms-transition: all 0.3s; transition: all 0.3s; } @@ -601,7 +593,7 @@ input:-ms-placeholder { color: #454545; } } .md-content > .closer { - font-size: 18px; + //font-size: 18px; position: absolute; right: 0; top: 0; @@ -663,7 +655,7 @@ input:-ms-placeholder { color: #454545; } -ms-transform: translate(0, 0); -webkit-transition: -webkit-transform .3s; -moz-transition: -moz-transform .3s; - -ms-transition: -moz-transform .3s; + transition: -moz-transform .3s; } #main.closed { @@ -778,7 +770,7 @@ and (orientation : landscape) } [class^="icon-"]:before, [class*=" icon-"]:before { - font-family: "fontello"; + font-family: "fontello", serif; font-style: normal; font-weight: normal; speak: none; diff --git a/cps/static/css/style.css b/cps/static/css/style.css index 47b0c4cb..8c99aaa0 100644 --- a/cps/static/css/style.css +++ b/cps/static/css/style.css @@ -116,6 +116,7 @@ a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d display: block; max-width: 100%; height: auto; + max-height: 100%; } .container-fluid .discover{ margin-bottom: 50px; } @@ -132,12 +133,19 @@ a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d position: relative; } -.container-fluid .book .cover img { +.container-fluid .book .cover span.img { + bottom: 0; + height: 100%; + position: absolute; +} + +.container-fluid .book .cover span img { + position: relative; + top: 0; + left: 0; + height: 100%; border: 1px solid #fff; box-sizing: border-box; - height: 100%; - bottom: 0; - position: absolute; -webkit-box-shadow: 0 5px 8px -6px #777; -moz-box-shadow: 0 5px 8px -6px #777; box-shadow: 0 5px 8px -6px #777; @@ -206,11 +214,22 @@ span.glyphicon.glyphicon-tags { .navbar-default .navbar-toggle .icon-bar {background-color: #000; } .navbar-default .navbar-toggle {border-color: #000; } .cover { margin-bottom: 10px; } + .cover .badge{ position: absolute; top: 2px; left: 2px; - background-color: #777; + color: #000; + border-radius: 10px; + background-color: #fff; +} +.cover .read{ + left: auto; + right: 2px; + width: 17px; + height: 17px; + display: inline-block; + padding: 2px; } .cover-height { max-height: 100px;} @@ -241,7 +260,7 @@ span.glyphicon.glyphicon-tags { .button-link {color: #fff; } .btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open .dropdown-toggle.btn-primary{ background-color: #1C5484; } .btn-primary.disabled, .btn-primary[disabled], fieldset[disabled] .btn-primary, .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled:active, .btn-primary[disabled]:active, fieldset[disabled] .btn-primary:active, .btn-primary.disabled.active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary.active { background-color: #89B9E2; } -.btn-toolbar>.btn+.btn, .btn-toolbar>.btn-group+.btn, .btn-toolbar>.btn+.btn-group, .btn-toolbar>.btn-group+.btn-group { margin-left: 0px; } +.btn-toolbar>.btn+.btn, .btn-toolbar>.btn-group+.btn, .btn-toolbar>.btn+.btn-group, .btn-toolbar>.btn-group+.btn-group { margin-left: 0; } .panel-body {background-color: #f5f5f5; } .spinner {margin: 0 41%; } .spinner2 {margin: 0 41%; } @@ -311,11 +330,11 @@ input.pill:not(:checked) + label .glyphicon { display: none; } .editable-input { display:inline-block; } .editable-cancel { - margin-bottom: 0px !important; + margin-bottom: 0 !important; margin-left: 7px !important; } -.editable-submit { margin-bottom: 0px !important; } +.editable-submit { margin-bottom: 0 !important; } .filterheader { margin-bottom: 20px; } .errorlink { margin-top: 20px; } .emailconfig { margin-top: 10px; } @@ -326,7 +345,7 @@ input.pill:not(:checked) + label .glyphicon { display: none; } } div.log { - font-family: Courier New; + font-family: Courier New, serif; font-size: 12px; box-sizing: border-box; height: 700px; diff --git a/cps/static/js/archive/archive.js b/cps/static/js/archive/archive.js index 06c05624..cb76321f 100644 --- a/cps/static/js/archive/archive.js +++ b/cps/static/js/archive/archive.js @@ -411,19 +411,6 @@ bitjs.archive = bitjs.archive || {}; return "unrar.js"; }; - /** - * Unrarrer5 - * @extends {bitjs.archive.Unarchiver} - * @constructor - */ - bitjs.archive.Unrarrer5 = function(arrayBuffer, optPathToBitJS) { - bitjs.base(this, arrayBuffer, optPathToBitJS); - }; - bitjs.inherits(bitjs.archive.Unrarrer5, bitjs.archive.Unarchiver); - bitjs.archive.Unrarrer5.prototype.getScriptFileName = function() { - return "unrar5.js"; - }; - /** * Untarrer * @extends {bitjs.archive.Unarchiver} diff --git a/cps/static/js/archive/unrar.js b/cps/static/js/archive/unrar.js index 3e2a45af..fadb791e 100644 --- a/cps/static/js/archive/unrar.js +++ b/cps/static/js/archive/unrar.js @@ -14,10 +14,10 @@ /* global VM_FIXEDGLOBALSIZE, VM_GLOBALMEMSIZE, MAXWINMASK, VM_GLOBALMEMADDR, MAXWINSIZE */ // This file expects to be invoked as a Worker (see onmessage below). -/*importScripts("../io/bitstream.js"); +importScripts("../io/bitstream.js"); importScripts("../io/bytebuffer.js"); importScripts("archive.js"); -importScripts("rarvm.js");*/ +importScripts("rarvm.js"); // Progress variables. var currentFilename = ""; @@ -29,21 +29,19 @@ var totalFilesInArchive = 0; // Helper functions. var info = function(str) { - console.log(str); - // postMessage(new bitjs.archive.UnarchiveInfoEvent(str)); + postMessage(new bitjs.archive.UnarchiveInfoEvent(str)); }; var err = function(str) { - console.log(str); - // postMessage(new bitjs.archive.UnarchiveErrorEvent(str)); + postMessage(new bitjs.archive.UnarchiveErrorEvent(str)); }; var postProgress = function() { - /*postMessage(new bitjs.archive.UnarchiveProgressEvent( + postMessage(new bitjs.archive.UnarchiveProgressEvent( currentFilename, currentFileNumber, currentBytesUnarchivedInFile, currentBytesUnarchived, totalUncompressedBytesInArchive, - totalFilesInArchive));*/ + totalFilesInArchive)); }; // shows a byte value as its hex representation @@ -1300,7 +1298,7 @@ var unrar = function(arrayBuffer) { totalUncompressedBytesInArchive = 0; totalFilesInArchive = 0; - //postMessage(new bitjs.archive.UnarchiveStartEvent()); + postMessage(new bitjs.archive.UnarchiveStartEvent()); var bstream = new bitjs.io.BitStream(arrayBuffer, false /* rtl */); var header = new RarVolumeHeader(bstream); @@ -1350,7 +1348,7 @@ var unrar = function(arrayBuffer) { localfile.unrar(); if (localfile.isValid) { - // postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile)); + postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile)); postProgress(); } } @@ -1360,7 +1358,7 @@ var unrar = function(arrayBuffer) { } else { err("Invalid RAR file"); } - // postMessage(new bitjs.archive.UnarchiveFinishEvent()); + postMessage(new bitjs.archive.UnarchiveFinishEvent()); }; // event.data.file has the ArrayBuffer. diff --git a/cps/static/js/edit_books.js b/cps/static/js/edit_books.js index 35515aa1..5f9154fc 100644 --- a/cps/static/js/edit_books.js +++ b/cps/static/js/edit_books.js @@ -249,18 +249,26 @@ promisePublishers.done(function() { ); }); -$("#search").on("change input.typeahead:selected", function() { +$("#search").on("change input.typeahead:selected", function(event) { + if (event.target.type == "search" && event.target.tagName == "INPUT") { + return; + } var form = $("form").serialize(); $.getJSON( getPath() + "/get_matching_tags", form, function( data ) { $(".tags_click").each(function() { - if ($.inArray(parseInt($(this).children("input").first().val(), 10), data.tags) === -1 ) { - if (!($(this).hasClass("active"))) { - $(this).addClass("disabled"); + if ($.inArray(parseInt($(this).val(), 10), data.tags) === -1) { + if(!$(this).prop("selected")) { + $(this).prop("disabled", true); } } else { - $(this).removeClass("disabled"); + $(this).prop("disabled", false); } }); + $("#include_tag option:selected").each(function () { + $("#exclude_tag").find("[value="+$(this).val()+"]").prop("disabled", true); + }); + $('#include_tag').selectpicker("refresh"); + $('#exclude_tag').selectpicker("refresh"); }); }); diff --git a/cps/static/js/filter_list.js b/cps/static/js/filter_list.js index 676ff47b..679c5359 100644 --- a/cps/static/js/filter_list.js +++ b/cps/static/js/filter_list.js @@ -19,16 +19,9 @@ var direction = 0; // Descending order var sort = 0; // Show sorted entries $("#sort_name").click(function() { - var class_name = $("h1").attr('Class') + "_sort_name"; + var className = $("h1").attr("Class") + "_sort_name"; var obj = {}; - obj[class_name] = sort; - /*$.ajax({ - method:"post", - contentType: "application/json; charset=utf-8", - dataType: "json", - url: window.location.pathname + "/../../ajax/view", - data: JSON.stringify({obj}), - });*/ + obj[className] = sort; var count = 0; var index = 0; diff --git a/cps/static/js/kthoom.js b/cps/static/js/kthoom.js index bbb3fead..33a2ac0e 100644 --- a/cps/static/js/kthoom.js +++ b/cps/static/js/kthoom.js @@ -162,15 +162,10 @@ function initProgressClick() { function loadFromArrayBuffer(ab) { var start = (new Date).getTime(); var h = new Uint8Array(ab, 0, 10); - unrar5(ab); var pathToBitJS = "../../static/js/archive/"; var lastCompletion = 0; - /*if (h[0] === 0x52 && h[1] === 0x61 && h[2] === 0x72 && h[3] === 0x21) { //Rar! - if (h[7] === 0x01) { - unarchiver = new bitjs.archive.Unrarrer(ab, pathToBitJS); - } else { - unarchiver = new bitjs.archive.Unrarrer5(ab, pathToBitJS); - } + if (h[0] === 0x52 && h[1] === 0x61 && h[2] === 0x72 && h[3] === 0x21) { //Rar! + unarchiver = new bitjs.archive.Unrarrer(ab, pathToBitJS); } else if (h[0] === 80 && h[1] === 75) { //PK (Zip) unarchiver = new bitjs.archive.Unzipper(ab, pathToBitJS); } else if (h[0] === 255 && h[1] === 216) { // JPEG @@ -234,7 +229,7 @@ function loadFromArrayBuffer(ab) { unarchiver.start(); } else { alert("Some error"); - }*/ + } } function scrollTocToActive() { diff --git a/cps/static/js/libs/bootstrap-select.min.js b/cps/static/js/libs/bootstrap-select.min.js new file mode 100644 index 00000000..92e3a32e --- /dev/null +++ b/cps/static/js/libs/bootstrap-select.min.js @@ -0,0 +1,9 @@ +/*! + * 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){!function(z){"use strict";var d=["sanitize","whiteList","sanitizeFn"],r=["background","cite","href","itemtype","longdesc","poster","src","xlink:href"],e={"*":["class","dir","id","lang","role","tabindex","style",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},l=/^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi,a=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;function v(e,t){var i=e.nodeName.toLowerCase();if(-1!==z.inArray(i,t))return-1===z.inArray(i,r)||Boolean(e.nodeValue.match(l)||e.nodeValue.match(a));for(var s=z(t).filter(function(e,t){return t instanceof RegExp}),n=0,o=s.length;n]+>/g,"")),s&&(a=w(a)),a=a.toUpperCase(),o="contains"===i?0<=a.indexOf(t):a.startsWith(t)))break}return o}function L(e){return parseInt(e,10)||0}z.fn.triggerNative=function(e){var t,i=this[0];i.dispatchEvent?(u?t=new Event(e,{bubbles:!0}):(t=document.createEvent("Event")).initEvent(e,!0,!1),i.dispatchEvent(t)):i.fireEvent?((t=document.createEventObject()).eventType=e,i.fireEvent("on"+e,t)):this.trigger(e)};var f={"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a","\xe3":"a","\xe4":"a","\xe5":"a","\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y","\xfd":"y","\xff":"y","\xc6":"Ae","\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss","\u0100":"A","\u0102":"A","\u0104":"A","\u0101":"a","\u0103":"a","\u0105":"a","\u0106":"C","\u0108":"C","\u010a":"C","\u010c":"C","\u0107":"c","\u0109":"c","\u010b":"c","\u010d":"c","\u010e":"D","\u0110":"D","\u010f":"d","\u0111":"d","\u0112":"E","\u0114":"E","\u0116":"E","\u0118":"E","\u011a":"E","\u0113":"e","\u0115":"e","\u0117":"e","\u0119":"e","\u011b":"e","\u011c":"G","\u011e":"G","\u0120":"G","\u0122":"G","\u011d":"g","\u011f":"g","\u0121":"g","\u0123":"g","\u0124":"H","\u0126":"H","\u0125":"h","\u0127":"h","\u0128":"I","\u012a":"I","\u012c":"I","\u012e":"I","\u0130":"I","\u0129":"i","\u012b":"i","\u012d":"i","\u012f":"i","\u0131":"i","\u0134":"J","\u0135":"j","\u0136":"K","\u0137":"k","\u0138":"k","\u0139":"L","\u013b":"L","\u013d":"L","\u013f":"L","\u0141":"L","\u013a":"l","\u013c":"l","\u013e":"l","\u0140":"l","\u0142":"l","\u0143":"N","\u0145":"N","\u0147":"N","\u014a":"N","\u0144":"n","\u0146":"n","\u0148":"n","\u014b":"n","\u014c":"O","\u014e":"O","\u0150":"O","\u014d":"o","\u014f":"o","\u0151":"o","\u0154":"R","\u0156":"R","\u0158":"R","\u0155":"r","\u0157":"r","\u0159":"r","\u015a":"S","\u015c":"S","\u015e":"S","\u0160":"S","\u015b":"s","\u015d":"s","\u015f":"s","\u0161":"s","\u0162":"T","\u0164":"T","\u0166":"T","\u0163":"t","\u0165":"t","\u0167":"t","\u0168":"U","\u016a":"U","\u016c":"U","\u016e":"U","\u0170":"U","\u0172":"U","\u0169":"u","\u016b":"u","\u016d":"u","\u016f":"u","\u0171":"u","\u0173":"u","\u0174":"W","\u0175":"w","\u0176":"Y","\u0177":"y","\u0178":"Y","\u0179":"Z","\u017b":"Z","\u017d":"Z","\u017a":"z","\u017c":"z","\u017e":"z","\u0132":"IJ","\u0133":"ij","\u0152":"Oe","\u0153":"oe","\u0149":"'n","\u017f":"s"},m=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,g=RegExp("[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\u1ab0-\\u1aff\\u1dc0-\\u1dff]","g");function b(e){return f[e]}function w(e){return(e=e.toString())&&e.replace(m,b).replace(g,"")}var I,x,y,$,S=(I={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},x="(?:"+Object.keys(I).join("|")+")",y=RegExp(x),$=RegExp(x,"g"),function(e){return e=null==e?"":""+e,y.test(e)?e.replace($,E):e});function E(e){return I[e]}var C={32:" ",48:"0",49:"1",50:"2",51:"3",52:"4",53:"5",54:"6",55:"7",56:"8",57:"9",59:";",65:"A",66:"B",67:"C",68:"D",69:"E",70:"F",71:"G",72:"H",73:"I",74:"J",75:"K",76:"L",77:"M",78:"N",79:"O",80:"P",81:"Q",82:"R",83:"S",84:"T",85:"U",86:"V",87:"W",88:"X",89:"Y",90:"Z",96:"0",97:"1",98:"2",99:"3",100:"4",101:"5",102:"6",103:"7",104:"8",105:"9"},N=27,D=13,H=32,W=9,B=38,M=40,R={success:!1,major:"3"};try{R.full=(z.fn.dropdown.Constructor.VERSION||"").split(" ")[0].split("."),R.major=R.full[0],R.success=!0}catch(e){}var U=0,j=".bs.select",V={DISABLED:"disabled",DIVIDER:"divider",SHOW:"open",DROPUP:"dropup",MENU:"dropdown-menu",MENURIGHT:"dropdown-menu-right",MENULEFT:"dropdown-menu-left",BUTTONCLASS:"btn-default",POPOVERHEADER:"popover-title",ICONBASE:"glyphicon",TICKICON:"glyphicon-ok"},F={MENU:"."+V.MENU},_={span:document.createElement("span"),i:document.createElement("i"),subtext:document.createElement("small"),a:document.createElement("a"),li:document.createElement("li"),whitespace:document.createTextNode("\xa0"),fragment:document.createDocumentFragment()};_.a.setAttribute("role","option"),"4"===R.major&&(_.a.className="dropdown-item"),_.subtext.className="text-muted",_.text=_.span.cloneNode(!1),_.text.className="text",_.checkMark=_.span.cloneNode(!1);var G=new RegExp(B+"|"+M),q=new RegExp("^"+W+"$|"+N),K={li:function(e,t,i){var s=_.li.cloneNode(!1);return e&&(1===e.nodeType||11===e.nodeType?s.appendChild(e):s.innerHTML=e),void 0!==t&&""!==t&&(s.className=t),null!=i&&s.classList.add("optgroup-"+i),s},a:function(e,t,i){var s=_.a.cloneNode(!0);return e&&(11===e.nodeType?s.appendChild(e):s.insertAdjacentHTML("beforeend",e)),void 0!==t&&""!==t&&s.classList.add.apply(s.classList,t.split(" ")),i&&s.setAttribute("style",i),s},text:function(e,t){var i,s,n=_.text.cloneNode(!1);if(e.content)n.innerHTML=e.content;else{if(n.textContent=e.text,e.icon){var o=_.whitespace.cloneNode(!1);(s=(!0===t?_.i:_.span).cloneNode(!1)).className=this.options.iconBase+" "+e.icon,_.fragment.appendChild(s),_.fragment.appendChild(o)}e.subtext&&((i=_.subtext.cloneNode(!1)).textContent=e.subtext,n.appendChild(i))}if(!0===t)for(;0'},maxOptions:!1,mobile:!1,selectOnTab:!1,dropdownAlignRight:!1,windowPadding:0,virtualScroll:600,display:!1,sanitize:!0,sanitizeFn:null,whiteList:e},Y.prototype={constructor:Y,init:function(){var i=this,e=this.$element.attr("id");U++,this.selectId="bs-select-"+U,this.$element[0].classList.add("bs-select-hidden"),this.multiple=this.$element.prop("multiple"),this.autofocus=this.$element.prop("autofocus"),this.$element[0].classList.contains("show-tick")&&(this.options.showTick=!0),this.$newElement=this.createDropdown(),this.buildData(),this.$element.after(this.$newElement).prependTo(this.$newElement),this.$button=this.$newElement.children("button"),this.$menu=this.$newElement.children(F.MENU),this.$menuInner=this.$menu.children(".inner"),this.$searchbox=this.$menu.find("input"),this.$element[0].classList.remove("bs-select-hidden"),!0===this.options.dropdownAlignRight&&this.$menu[0].classList.add(V.MENURIGHT),void 0!==e&&this.$button.attr("data-id",e),this.checkDisabled(),this.clickListener(),this.options.liveSearch?(this.liveSearchListener(),this.focusedParent=this.$searchbox[0]):this.focusedParent=this.$menuInner[0],this.setStyle(),this.render(),this.setWidth(),this.options.container?this.selectPosition():this.$element.on("hide"+j,function(){if(i.isVirtual()){var e=i.$menuInner[0],t=e.firstChild.cloneNode(!1);e.replaceChild(t,e.firstChild),e.scrollTop=0}}),this.$menu.data("this",this),this.$newElement.data("this",this),this.options.mobile&&this.mobile(),this.$newElement.on({"hide.bs.dropdown":function(e){i.$element.trigger("hide"+j,e)},"hidden.bs.dropdown":function(e){i.$element.trigger("hidden"+j,e)},"show.bs.dropdown":function(e){i.$element.trigger("show"+j,e)},"shown.bs.dropdown":function(e){i.$element.trigger("shown"+j,e)}}),i.$element[0].hasAttribute("required")&&this.$element.on("invalid"+j,function(){i.$button[0].classList.add("bs-invalid"),i.$element.on("shown"+j+".invalid",function(){i.$element.val(i.$element.val()).off("shown"+j+".invalid")}).on("rendered"+j,function(){this.validity.valid&&i.$button[0].classList.remove("bs-invalid"),i.$element.off("rendered"+j)}),i.$button.on("blur"+j,function(){i.$element.trigger("focus").trigger("blur"),i.$button.off("blur"+j)})}),setTimeout(function(){i.buildList(),i.$element.trigger("loaded"+j)})},createDropdown:function(){var e=this.multiple||this.options.showTick?" show-tick":"",t=this.multiple?' aria-multiselectable="true"':"",i="",s=this.autofocus?" autofocus":"";R.major<4&&this.$element.parent().hasClass("input-group")&&(i=" input-group-btn");var n,o="",r="",l="",a="";return this.options.header&&(o='
'+this.options.header+"
"),this.options.liveSearch&&(r=''),this.multiple&&this.options.actionsBox&&(l='
"),this.multiple&&this.options.doneButton&&(a='
"),n='",z(n)},setPositionData:function(){this.selectpicker.view.canHighlight=[];for(var e=this.selectpicker.view.size=0;e=this.options.virtualScroll||!0===this.options.virtualScroll},createView:function(A,e,t){var L,N,D=this,i=0,H=[];if(this.selectpicker.isSearching=A,this.selectpicker.current=A?this.selectpicker.search:this.selectpicker.main,this.setPositionData(),e)if(t)i=this.$menuInner[0].scrollTop;else if(!D.multiple){var s=D.$element[0],n=(s.options[s.selectedIndex]||{}).liIndex;if("number"==typeof n&&!1!==D.options.size){var o=D.selectpicker.main.data[n],r=o&&o.position;r&&(i=r-(D.sizeInfo.menuInnerHeight+D.sizeInfo.liHeight)/2)}}function l(e,t){var i,s,n,o,r,l,a,c,d=D.selectpicker.current.elements.length,h=[],p=!0,u=D.isVirtual();D.selectpicker.view.scrollTop=e,i=Math.ceil(D.sizeInfo.menuInnerHeight/D.sizeInfo.liHeight*1.5),s=Math.round(d/i)||1;for(var f=0;fd-1?0:D.selectpicker.current.data[d-1].position-D.selectpicker.current.data[D.selectpicker.view.position1-1].position,b.firstChild.style.marginTop=v+"px",b.firstChild.style.marginBottom=g+"px"):(b.firstChild.style.marginTop=0,b.firstChild.style.marginBottom=0),b.firstChild.appendChild(w),!0===u&&D.sizeInfo.hasScrollBar){var C=b.firstChild.offsetWidth;if(t&&CD.sizeInfo.selectWidth)b.firstChild.style.minWidth=D.sizeInfo.menuInnerInnerWidth+"px";else if(C>D.sizeInfo.menuInnerInnerWidth){D.$menu[0].style.minWidth=0;var O=b.firstChild.offsetWidth;O>D.sizeInfo.menuInnerInnerWidth&&(D.sizeInfo.menuInnerInnerWidth=O,b.firstChild.style.minWidth=D.sizeInfo.menuInnerInnerWidth+"px"),D.$menu[0].style.minWidth=""}}}if(D.prevActiveIndex=D.activeIndex,D.options.liveSearch){if(A&&t){var z,T=0;D.selectpicker.view.canHighlight[T]||(T=1+D.selectpicker.view.canHighlight.slice(1).indexOf(!0)),z=D.selectpicker.view.visibleElements[T],D.defocusItem(D.selectpicker.view.currentActive),D.activeIndex=(D.selectpicker.current.data[T]||{}).index,D.focusItem(z)}}else D.$menuInner.trigger("focus")}l(i,!0),this.$menuInner.off("scroll.createView").on("scroll.createView",function(e,t){D.noScroll||l(this.scrollTop,t),D.noScroll=!1}),z(window).off("resize"+j+"."+this.selectId+".createView").on("resize"+j+"."+this.selectId+".createView",function(){D.$newElement.hasClass(V.SHOW)&&l(D.$menuInner[0].scrollTop)})},focusItem:function(e,t,i){if(e){t=t||this.selectpicker.main.data[this.activeIndex];var s=e.firstChild;s&&(s.setAttribute("aria-setsize",this.selectpicker.view.size),s.setAttribute("aria-posinset",t.posinset),!0!==i&&(this.focusedParent.setAttribute("aria-activedescendant",s.id),e.classList.add("active"),s.classList.add("active")))}},defocusItem:function(e){e&&(e.classList.remove("active"),e.firstChild&&e.firstChild.classList.remove("active"))},setPlaceholder:function(){var e=!1;if(this.options.title&&!this.multiple){this.selectpicker.view.titleOption||(this.selectpicker.view.titleOption=document.createElement("option")),e=!0;var t=this.$element[0],i=!1,s=!this.selectpicker.view.titleOption.parentNode;if(s)this.selectpicker.view.titleOption.className="bs-title-option",this.selectpicker.view.titleOption.value="",i=void 0===z(t.options[t.selectedIndex]).attr("selected")&&void 0===this.$element.data("selected");!s&&0===this.selectpicker.view.titleOption.index||t.insertBefore(this.selectpicker.view.titleOption,t.firstChild),i&&(t.selectedIndex=0)}return e},buildData:function(){var p=':not([hidden]):not([data-hidden="true"])',u=[],f=0,e=this.setPlaceholder()?1:0;this.options.hideDisabled&&(p+=":not(:disabled)");var t=this.$element[0].querySelectorAll("select > *"+p);function m(e){var t=u[u.length-1];t&&"divider"===t.type&&(t.optID||e.optID)||((e=e||{}).type="divider",u.push(e))}function v(e,t){if((t=t||{}).divider="true"===e.getAttribute("data-divider"),t.divider)m({optID:t.optID});else{var i=u.length,s=e.style.cssText,n=s?S(s):"",o=(e.className||"")+(t.optgroupClass||"");t.optID&&(o="opt "+o),t.optionClass=o.trim(),t.inlineStyle=n,t.text=e.textContent,t.content=e.getAttribute("data-content"),t.tokens=e.getAttribute("data-tokens"),t.subtext=e.getAttribute("data-subtext"),t.icon=e.getAttribute("data-icon"),e.liIndex=i,t.display=t.content||t.text,t.type="option",t.index=i,t.option=e,t.selected=!!e.selected,t.disabled=t.disabled||!!e.disabled,u.push(t)}}function i(e,t){var i=t[e],s=t[e-1],n=t[e+1],o=i.querySelectorAll("option"+p);if(o.length){var r,l,a={display:S(i.label),subtext:i.getAttribute("data-subtext"),icon:i.getAttribute("data-icon"),type:"optgroup-label",optgroupClass:" "+(i.className||"")};f++,s&&m({optID:f}),a.optID=f,u.push(a);for(var c=0,d=o.length;c li")},render:function(){var e,t=this,i=this.$element[0],s=this.setPlaceholder()&&0===i.selectedIndex,n=O(i,this.options.hideDisabled),o=n.length,r=this.$button[0],l=r.querySelector(".filter-option-inner-inner"),a=document.createTextNode(this.options.multipleSeparator),c=_.fragment.cloneNode(!1),d=!1;if(r.classList.toggle("bs-placeholder",t.multiple?!o:!T(i,n)),this.tabIndex(),"static"===this.options.selectedTextFormat)c=K.text.call(this,{text:this.options.title},!0);else if(!1===(this.multiple&&-1!==this.options.selectedTextFormat.indexOf("count")&&1")).length&&o>e[1]||1===e.length&&2<=o))){if(!s){for(var h=0;h option"+m+", optgroup"+m+" option"+m).length,g="function"==typeof this.options.countSelectedText?this.options.countSelectedText(o,v):this.options.countSelectedText;c=K.text.call(this,{text:g.replace("{0}",o.toString()).replace("{1}",v.toString())},!0)}if(null==this.options.title&&(this.options.title=this.$element.attr("title")),c.childNodes.length||(c=K.text.call(this,{text:void 0!==this.options.title?this.options.title:this.options.noneSelectedText},!0)),r.title=c.textContent.replace(/<[^>]*>?/g,"").trim(),this.options.sanitize&&d&&P([c],t.options.whiteList,t.options.sanitizeFn),l.innerHTML="",l.appendChild(c),R.major<4&&this.$newElement[0].classList.contains("bs3-has-addon")){var b=r.querySelector(".filter-expand"),w=l.cloneNode(!0);w.className="filter-expand",b?r.replaceChild(w,b):r.appendChild(w)}this.$element.trigger("rendered"+j)},setStyle:function(e,t){var i,s=this.$button[0],n=this.$newElement[0],o=this.options.style.trim();this.$element.attr("class")&&this.$newElement.addClass(this.$element.attr("class").replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi,"")),R.major<4&&(n.classList.add("bs3"),n.parentNode.classList.contains("input-group")&&(n.previousElementSibling||n.nextElementSibling)&&(n.previousElementSibling||n.nextElementSibling).classList.contains("input-group-addon")&&n.classList.add("bs3-has-addon")),i=e?e.trim():o,"add"==t?i&&s.classList.add.apply(s.classList,i.split(" ")):"remove"==t?i&&s.classList.remove.apply(s.classList,i.split(" ")):(o&&s.classList.remove.apply(s.classList,o.split(" ")),i&&s.classList.add.apply(s.classList,i.split(" ")))},liHeight:function(e){if(e||!1!==this.options.size&&!Object.keys(this.sizeInfo).length){var t=document.createElement("div"),i=document.createElement("div"),s=document.createElement("div"),n=document.createElement("ul"),o=document.createElement("li"),r=document.createElement("li"),l=document.createElement("li"),a=document.createElement("a"),c=document.createElement("span"),d=this.options.header&&0this.sizeInfo.menuExtras.vert&&l+this.sizeInfo.menuExtras.vert+50>this.sizeInfo.selectOffsetBot,!0===this.selectpicker.isSearching&&(a=this.selectpicker.dropup),this.$newElement.toggleClass(V.DROPUP,a),this.selectpicker.dropup=a),"auto"===this.options.size)n=3this.options.size){for(var b=0;bthis.sizeInfo.menuInnerHeight&&(this.sizeInfo.hasScrollBar=!0,this.sizeInfo.totalMenuWidth=this.sizeInfo.menuWidth+this.sizeInfo.scrollBarWidth),"auto"===this.options.dropdownAlignRight&&this.$menu.toggleClass(V.MENURIGHT,this.sizeInfo.selectOffsetLeft>this.sizeInfo.selectOffsetRight&&this.sizeInfo.selectOffsetRightthis.options.size&&i.off("resize"+j+"."+this.selectId+".setMenuSize scroll"+j+"."+this.selectId+".setMenuSize")}this.createView(!1,!0,e)},setWidth:function(){var i=this;"auto"===this.options.width?requestAnimationFrame(function(){i.$menu.css("min-width","0"),i.$element.on("loaded"+j,function(){i.liHeight(),i.setMenuSize();var e=i.$newElement.clone().appendTo("body"),t=e.css("width","auto").children("button").outerWidth();e.remove(),i.sizeInfo.selectWidth=Math.max(i.sizeInfo.totalMenuWidth,t),i.$newElement.css("width",i.sizeInfo.selectWidth+"px")})}):"fit"===this.options.width?(this.$menu.css("min-width",""),this.$newElement.css("width","").addClass("fit-width")):this.options.width?(this.$menu.css("min-width",""),this.$newElement.css("width",this.options.width)):(this.$menu.css("min-width",""),this.$newElement.css("width","")),this.$newElement.hasClass("fit-width")&&"fit"!==this.options.width&&this.$newElement[0].classList.remove("fit-width")},selectPosition:function(){this.$bsContainer=z('
');function e(e){var t={},i=r.options.display||!!z.fn.dropdown.Constructor.Default&&z.fn.dropdown.Constructor.Default.display;r.$bsContainer.addClass(e.attr("class").replace(/form-control|fit-width/gi,"")).toggleClass(V.DROPUP,e.hasClass(V.DROPUP)),s=e.offset(),l.is("body")?n={top:0,left:0}:((n=l.offset()).top+=parseInt(l.css("borderTopWidth"))-l.scrollTop(),n.left+=parseInt(l.css("borderLeftWidth"))-l.scrollLeft()),o=e.hasClass(V.DROPUP)?0:e[0].offsetHeight,(R.major<4||"static"===i)&&(t.top=s.top-n.top+o,t.left=s.left-n.left),t.width=e[0].offsetWidth,r.$bsContainer.css(t)}var s,n,o,r=this,l=z(this.options.container);this.$button.on("click.bs.dropdown.data-api",function(){r.isDisabled()||(e(r.$newElement),r.$bsContainer.appendTo(r.options.container).toggleClass(V.SHOW,!r.$button.hasClass(V.SHOW)).append(r.$menu))}),z(window).off("resize"+j+"."+this.selectId+" scroll"+j+"."+this.selectId).on("resize"+j+"."+this.selectId+" scroll"+j+"."+this.selectId,function(){r.$newElement.hasClass(V.SHOW)&&e(r.$newElement)}),this.$element.on("hide"+j,function(){r.$menu.data("height",r.$menu.height()),r.$bsContainer.detach()})},setOptionStatus:function(e){var t=this;if(t.noScroll=!1,t.selectpicker.view.visibleElements&&t.selectpicker.view.visibleElements.length)for(var i=0;i
');y[2]&&($=$.replace("{var}",y[2][1"+$+"")),d=!1,C.$element.trigger("maxReached"+j)),g&&w&&(E.append(z("
"+S+"
")),d=!1,C.$element.trigger("maxReachedGrp"+j)),setTimeout(function(){C.setSelected(r,!1)},10),E[0].classList.add("fadeOut"),setTimeout(function(){E.remove()},1050)}}}else c&&(c.selected=!1),h.selected=!0,C.setSelected(r,!0);!C.multiple||C.multiple&&1===C.options.maxOptions?C.$button.trigger("focus"):C.options.liveSearch&&C.$searchbox.trigger("focus"),d&&(!C.multiple&&a===s.selectedIndex||(A=[h.index,p.prop("selected"),l],C.$element.triggerNative("change")))}}),this.$menu.on("click","li."+V.DISABLED+" a, ."+V.POPOVERHEADER+", ."+V.POPOVERHEADER+" :not(.close)",function(e){e.currentTarget==this&&(e.preventDefault(),e.stopPropagation(),C.options.liveSearch&&!z(e.target).hasClass("close")?C.$searchbox.trigger("focus"):C.$button.trigger("focus"))}),this.$menuInner.on("click",".divider, .dropdown-header",function(e){e.preventDefault(),e.stopPropagation(),C.options.liveSearch?C.$searchbox.trigger("focus"):C.$button.trigger("focus")}),this.$menu.on("click","."+V.POPOVERHEADER+" .close",function(){C.$button.trigger("click")}),this.$searchbox.on("click",function(e){e.stopPropagation()}),this.$menu.on("click",".actions-btn",function(e){C.options.liveSearch?C.$searchbox.trigger("focus"):C.$button.trigger("focus"),e.preventDefault(),e.stopPropagation(),z(this).hasClass("bs-select-all")?C.selectAll():C.deselectAll()}),this.$element.on("change"+j,function(){C.render(),C.$element.trigger("changed"+j,A),A=null}).on("focus"+j,function(){C.options.mobile||C.$button.trigger("focus")})},liveSearchListener:function(){var u=this,f=document.createElement("li");this.$button.on("click.bs.dropdown.data-api",function(){u.$searchbox.val()&&u.$searchbox.val("")}),this.$searchbox.on("click.bs.dropdown.data-api focus.bs.dropdown.data-api touchend.bs.dropdown.data-api",function(e){e.stopPropagation()}),this.$searchbox.on("input propertychange",function(){var e=u.$searchbox.val();if(u.selectpicker.search.elements=[],u.selectpicker.search.data=[],e){var t=[],i=e.toUpperCase(),s={},n=[],o=u._searchStyle(),r=u.options.liveSearchNormalize;r&&(i=w(i));for(var l=0;l=a.selectpicker.view.canHighlight.length&&(t=0),a.selectpicker.view.canHighlight[t+f]||(t=t+1+a.selectpicker.view.canHighlight.slice(t+f+1).indexOf(!0))),e.preventDefault();var m=f+t;e.which===B?0===f&&t===c.length-1?(a.$menuInner[0].scrollTop=a.$menuInner[0].scrollHeight,m=a.selectpicker.current.elements.length-1):d=(o=(n=a.selectpicker.current.data[m]).position-n.height)u+a.sizeInfo.menuInnerHeight),s=a.selectpicker.main.elements[v],a.activeIndex=b[x],a.focusItem(s),s&&s.firstChild.focus(),d&&(a.$menuInner[0].scrollTop=o),r.trigger("focus")}}i&&(e.which===H&&!a.selectpicker.keydown.keyHistory||e.which===D||e.which===W&&a.options.selectOnTab)&&(e.which!==H&&e.preventDefault(),a.options.liveSearch&&e.which===H||(a.$menuInner.find(".active a").trigger("click",!0),r.trigger("focus"),a.options.liveSearch||(e.preventDefault(),z(document).data("spaceSelect",!0))))}},mobile:function(){this.$element[0].classList.add("mobile-device")},refresh:function(){var e=z.extend({},this.options,this.$element.data());this.options=e,this.checkDisabled(),this.setStyle(),this.render(),this.buildData(),this.buildList(),this.setWidth(),this.setSize(!0),this.$element.trigger("refreshed"+j)},hide:function(){this.$newElement.hide()},show:function(){this.$newElement.show()},remove:function(){this.$newElement.remove(),this.$element.remove()},destroy:function(){this.$newElement.before(this.$element).remove(),this.$bsContainer?this.$bsContainer.remove():this.$menu.remove(),this.$element.off(j).removeData("selectpicker").removeClass("bs-select-hidden selectpicker"),z(window).off(j+"."+this.selectId)}};var J=z.fn.selectpicker;z.fn.selectpicker=Z,z.fn.selectpicker.Constructor=Y,z.fn.selectpicker.noConflict=function(){return z.fn.selectpicker=J,this};var Q=z.fn.dropdown.Constructor._dataApiKeydownHandler||z.fn.dropdown.Constructor.prototype.keydown;z(document).off("keydown.bs.dropdown.data-api").on("keydown.bs.dropdown.data-api",':not(.bootstrap-select) > [data-toggle="dropdown"]',Q).on("keydown.bs.dropdown.data-api",":not(.bootstrap-select) > .dropdown-menu",Q).on("keydown"+j,'.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input',Y.prototype.keydown).on("focusin.modal",'.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input',function(e){e.stopPropagation()}),z(window).on("load"+j+".data-api",function(){z(".selectpicker").each(function(){var e=z(this);Z.call(e,e.data())})})}(e)}); +//# sourceMappingURL=bootstrap-select.min.js.map \ No newline at end of file diff --git a/cps/static/js/libs/bootstrap-select/defaults-cs.min.js b/cps/static/js/libs/bootstrap-select/defaults-cs.min.js new file mode 100644 index 00000000..be309a10 --- /dev/null +++ b/cps/static/js/libs/bootstrap-select/defaults-cs.min.js @@ -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"}}); \ No newline at end of file diff --git a/cps/static/js/libs/bootstrap-select/defaults-de.min.js b/cps/static/js/libs/bootstrap-select/defaults-de.min.js new file mode 100644 index 00000000..e625440b --- /dev/null +++ b/cps/static/js/libs/bootstrap-select/defaults-de.min.js @@ -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:", "}}); \ No newline at end of file diff --git a/cps/static/js/libs/bootstrap-select/defaults-es.min.js b/cps/static/js/libs/bootstrap-select/defaults-es.min.js new file mode 100644 index 00000000..25efec39 --- /dev/null +++ b/cps/static/js/libs/bootstrap-select/defaults-es.min.js @@ -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"}}); \ No newline at end of file diff --git a/cps/static/js/libs/bootstrap-select/defaults-fi.min.js b/cps/static/js/libs/bootstrap-select/defaults-fi.min.js new file mode 100644 index 00000000..bee14048 --- /dev/null +++ b/cps/static/js/libs/bootstrap-select/defaults-fi.min.js @@ -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:", "}}); \ No newline at end of file diff --git a/cps/static/js/libs/bootstrap-select/defaults-fr.min.js b/cps/static/js/libs/bootstrap-select/defaults-fr.min.js new file mode 100644 index 00000000..d8931590 --- /dev/null +++ b/cps/static/js/libs/bootstrap-select/defaults-fr.min.js @@ -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 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 = ""; + } else { + var type = ""; + } + $("" + type + "" + entry.name + "" + + entry.size + "").appendTo($("#file_table")); + }); + }, + timeout: 2000 + }); + } + $(".discover .row").isotope({ // options itemSelector : ".book", @@ -402,18 +480,98 @@ $(function() { $("#config_delete_kobo_token").show(); }); - $("#btndeletetoken").click(function() { - //get data-id attribute of the clicked element - var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src; - var path = src.substring(0, src.lastIndexOf("/")); - // var domainId = $(this).value("domainId"); - $.ajax({ - method:"get", - url: path + "/../../kobo_auth/deleteauthtoken/" + this.value, - }); - $("#modalDeleteToken").modal("hide"); - $("#config_delete_kobo_token").hide(); + $("#config_delete_kobo_token").click(function() { + ConfirmDialog( + $(this).attr('id'), + $(this).data('value'), + function (value) { + var pathname = document.getElementsByTagName("script"); + var src = pathname[pathname.length - 1].src; + var path = src.substring(0, src.lastIndexOf("/")); + $.ajax({ + method: "get", + url: path + "/../../kobo_auth/deleteauthtoken/" + value, + }); + $("#config_delete_kobo_token").hide(); + } + ); + }); + $("#toggle_order_shelf").click(function() { + $("#new").toggleClass("disabled"); + $("#old").toggleClass("disabled"); + $("#asc").toggleClass("disabled"); + $("#desc").toggleClass("disabled"); + $("#auth_az").toggleClass("disabled"); + $("#auth_za").toggleClass("disabled"); + $("#pub_new").toggleClass("disabled"); + $("#pub_old").toggleClass("disabled"); + var alternative_text = $("#toggle_order_shelf").data('alt-text'); + $("#toggle_order_shelf")[0].attributes['data-alt-text'].value = $("#toggle_order_shelf").html(); + $("#toggle_order_shelf").html(alternative_text); + }); + + $("#btndeluser").click(function() { + ConfirmDialog( + $(this).attr('id'), + $(this).data('value'), + function(value){ + var subform = $('#user_submit').closest("form"); + subform.submit(function(eventObj) { + $(this).append(''); + 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() { diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 62f7e220..efe0fad4 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -45,14 +45,13 @@ $(function() { if (selections.length < 1) { $("#delete_selection").addClass("disabled"); $("#delete_selection").attr("aria-disabled", true); - } - else{ + } else { $("#delete_selection").removeClass("disabled"); $("#delete_selection").attr("aria-disabled", false); } }); $("#delete_selection").click(function() { - $("#books-table").bootstrapTable('uncheckAll'); + $("#books-table").bootstrapTable("uncheckAll"); }); $("#merge_confirm").click(function() { @@ -63,8 +62,8 @@ $(function() { url: window.location.pathname + "/../../ajax/mergebooks", data: JSON.stringify({"Merge_books":selections}), success: function success() { - $('#books-table').bootstrapTable('refresh'); - $("#books-table").bootstrapTable('uncheckAll'); + $("#books-table").bootstrapTable("refresh"); + $("#books-table").bootstrapTable("uncheckAll"); } }); }); @@ -76,11 +75,11 @@ $(function() { dataType: "json", url: window.location.pathname + "/../../ajax/simulatemerge", data: JSON.stringify({"Merge_books":selections}), - success: function success(book_titles) { - $.each(book_titles.from, function(i, item) { + success: function success(booTitles) { + $.each(booTitles.from, function(i, item) { $("- " + item + "").appendTo("#merge_from"); }); - $('#merge_to').text("- " + book_titles.to); + $("#merge_to").text("- " + booTitles.to); } }); @@ -126,34 +125,35 @@ $(function() { formatNoMatches: function () { return ""; }, + // eslint-disable-next-line no-unused-vars onEditableSave: function (field, row, oldvalue, $el) { - if (field === 'title' || field === 'authors') { - $.ajax({ - method:"get", - dataType: "json", - url: window.location.pathname + "/../../ajax/sort_value/" + field + '/' + row.id, - success: function success(data) { - var key = Object.keys(data)[0] - $("#books-table").bootstrapTable('updateCellByUniqueId', { - id: row.id, - field: key, - value: data[key] - }); - console.log(data); - } - }); - } + if (field === "title" || field === "authors") { + $.ajax({ + method:"get", + dataType: "json", + url: window.location.pathname + "/../../ajax/sort_value/" + field + "/" + row.id, + success: function success(data) { + var key = Object.keys(data)[0]; + $("#books-table").bootstrapTable("updateCellByUniqueId", { + id: row.id, + field: key, + value: data[key] + }); + // console.log(data); + } + }); + } }, + // eslint-disable-next-line no-unused-vars onColumnSwitch: function (field, checked) { - var visible = $("#books-table").bootstrapTable('getVisibleColumns'); - var hidden = $("#books-table").bootstrapTable('getHiddenColumns'); - var visibility =[] - var st = "" + var visible = $("#books-table").bootstrapTable("getVisibleColumns"); + var hidden = $("#books-table").bootstrapTable("getHiddenColumns"); + var st = ""; visible.forEach(function(item) { - st += "\""+ item.field + "\":\"" +"true"+ "\"," + st += "\"" + item.field + "\":\"" + "true" + "\","; }); hidden.forEach(function(item) { - st += "\""+ item.field + "\":\"" +"false"+ "\"," + st += "\"" + item.field + "\":\"" + "false" + "\","; }); st = st.slice(0, -1); $.ajax({ @@ -208,15 +208,13 @@ $(function() { }, striped: false }); - $("#btndeletedomain").click(function() { - //get data-id attribute of the clicked element - var domainId = $(this).data("domainId"); + + function domain_handle(domainId) { $.ajax({ method:"post", url: window.location.pathname + "/../../ajax/deletedomain", data: {"domainid":domainId} }); - $("#DeleteDomain").modal("hide"); $.ajax({ method:"get", url: window.location.pathname + "/../../ajax/domainlist/1", @@ -235,12 +233,16 @@ $(function() { $("#domain-deny-table").bootstrapTable("load", data); } }); + } + $("#domain-allow-table").on("click-cell.bs.table", function (field, value, row, $element) { + if (value === 2) { + ConfirmDialog("btndeletedomain", $element.id, domain_handle); + } }); - //triggered when modal is about to be shown - $("#DeleteDomain").on("show.bs.modal", function(e) { - //get data-id attribute of the clicked element and store in button - var domainId = $(e.relatedTarget).data("domain-id"); - $(e.currentTarget).find("#btndeletedomain").data("domainId", domainId); + $("#domain-deny-table").on("click-cell.bs.table", function (field, value, row, $element) { + if (value === 2) { + ConfirmDialog("btndeletedomain", $element.id, domain_handle); + } }); $("#restrictModal").on("hidden.bs.modal", function () { @@ -253,14 +255,14 @@ $(function() { $("#h3").addClass("hidden"); $("#h4").addClass("hidden"); }); - function startTable(type) { + function startTable(type, user_id) { var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src; var path = src.substring(0, src.lastIndexOf("/")); $("#restrict-elements-table").bootstrapTable({ formatNoMatches: function () { return ""; }, - url: path + "/../../ajax/listrestriction/" + type, + url: path + "/../../ajax/listrestriction/" + type + "/" + user_id, rowStyle: function(row) { // console.log('Reihe :' + row + " Index :" + index); if (row.id.charAt(0) === "a") { @@ -274,13 +276,13 @@ $(function() { $.ajax ({ type: "Post", data: "id=" + row.id + "&type=" + row.type + "&Element=" + encodeURIComponent(row.Element), - url: path + "/../../ajax/deleterestriction/" + type, + url: path + "/../../ajax/deleterestriction/" + type + "/" + user_id, async: true, timeout: 900, success:function() { $.ajax({ method:"get", - url: path + "/../../ajax/listrestriction/" + type, + url: path + "/../../ajax/listrestriction/" + type + "/" + user_id, async: true, timeout: 900, success:function(data) { @@ -296,7 +298,7 @@ $(function() { $("#restrict-elements-table").removeClass("table-hover"); $("#restrict-elements-table").on("editable-save.bs.table", function (e, field, row) { $.ajax({ - url: path + "/../../ajax/editrestriction/" + type, + url: path + "/../../ajax/editrestriction/" + type + "/" + user_id, type: "Post", data: row }); @@ -304,13 +306,13 @@ $(function() { $("[id^=submit_]").click(function() { $(this)[0].blur(); $.ajax({ - url: path + "/../../ajax/addrestriction/" + type, + url: path + "/../../ajax/addrestriction/" + type + "/" + user_id, type: "Post", data: $(this).closest("form").serialize() + "&" + $(this)[0].name + "=", success: function () { $.ajax ({ method:"get", - url: path + "/../../ajax/listrestriction/" + type, + url: path + "/../../ajax/listrestriction/" + type + "/" + user_id, async: true, timeout: 900, success:function(data) { @@ -323,21 +325,21 @@ $(function() { }); } $("#get_column_values").on("click", function() { - startTable(1); + startTable(1, 0); $("#h2").removeClass("hidden"); }); $("#get_tags").on("click", function() { - startTable(0); + startTable(0, 0); $("#h1").removeClass("hidden"); }); $("#get_user_column_values").on("click", function() { - startTable(3); + startTable(3, $(this).data('id')); $("#h4").removeClass("hidden"); }); $("#get_user_tags").on("click", function() { - startTable(2); + startTable(2, $(this).data('id')); $(this)[0].blur(); $("#h3").removeClass("hidden"); }); @@ -347,7 +349,7 @@ $(function() { /* Function for deleting domain restrictions */ function TableActions (value, row) { return [ - "", "", "" diff --git a/cps/tasks/convert.py b/cps/tasks/convert.py index 659c6e9c..75542b38 100644 --- a/cps/tasks/convert.py +++ b/cps/tasks/convert.py @@ -9,7 +9,7 @@ from shutil import copyfile from sqlalchemy.exc import SQLAlchemyError from cps.services.worker import CalibreTask -from cps import calibre_db, db +from cps import db from cps import logger, config from cps.subproc_wrapper import process_open from flask_babel import gettext as _ @@ -33,8 +33,9 @@ class TaskConvert(CalibreTask): def run(self, worker_thread): self.worker_thread = worker_thread if config.config_use_google_drive: - cur_book = calibre_db.get_book(self.bookid) - data = calibre_db.get_book_format(self.bookid, self.settings['old_book_format']) + worker_db = db.CalibreDB(expire_on_commit=False) + cur_book = worker_db.get_book(self.bookid) + data = worker_db.get_book_format(self.bookid, self.settings['old_book_format']) df = gdriveutils.getFileFromEbooksFolder(cur_book.path, data.name + "." + self.settings['old_book_format'].lower()) if df: @@ -44,10 +45,12 @@ class TaskConvert(CalibreTask): if not os.path.exists(os.path.join(config.config_calibre_dir, cur_book.path)): os.makedirs(os.path.join(config.config_calibre_dir, cur_book.path)) df.GetContentFile(datafile) + worker_db.session.close() else: error_message = _(u"%(format)s not found on Google Drive: %(fn)s", format=self.settings['old_book_format'], fn=data.name + "." + self.settings['old_book_format'].lower()) + worker_db.session.close() return error_message filename = self._convert_ebook_format() @@ -71,21 +74,23 @@ class TaskConvert(CalibreTask): def _convert_ebook_format(self): error_message = None - local_session = db.CalibreDB().session + local_db = db.CalibreDB(expire_on_commit=False) file_path = self.file_path book_id = self.bookid format_old_ext = u'.' + self.settings['old_book_format'].lower() format_new_ext = u'.' + self.settings['new_book_format'].lower() - # check to see if destination format already exists - + # check to see if destination format already exists - or if book is in database # if it does - mark the conversion task as complete and return a success # this will allow send to kindle workflow to continue to work - if os.path.isfile(file_path + format_new_ext): + if os.path.isfile(file_path + format_new_ext) or\ + local_db.get_book_format(self.bookid, self.settings['new_book_format']): log.info("Book id %d already converted to %s", book_id, format_new_ext) - cur_book = calibre_db.get_book(book_id) + cur_book = local_db.get_book(book_id) self.results['path'] = file_path self.results['title'] = cur_book.title self._handleSuccess() + local_db.session.close() return os.path.basename(file_path + format_new_ext) else: log.info("Book id %d - target format of %s does not exist. Moving forward with convert.", @@ -105,18 +110,18 @@ class TaskConvert(CalibreTask): check, error_message = self._convert_calibre(file_path, format_old_ext, format_new_ext) if check == 0: - cur_book = calibre_db.get_book(book_id) + cur_book = local_db.get_book(book_id) if os.path.isfile(file_path + format_new_ext): - # self.db_queue.join() new_format = db.Data(name=cur_book.data[0].name, book_format=self.settings['new_book_format'].upper(), book=book_id, uncompressed_size=os.path.getsize(file_path + format_new_ext)) try: - local_session.merge(new_format) - local_session.commit() + local_db.session.merge(new_format) + local_db.session.commit() except SQLAlchemyError as e: - local_session.rollback() + local_db.session.rollback() log.error("Database error: %s", e) + local_db.session.close() return self.results['path'] = cur_book.path self.results['title'] = cur_book.title @@ -125,6 +130,7 @@ class TaskConvert(CalibreTask): return os.path.basename(file_path + format_new_ext) else: error_message = _('%(format)s format not found on disk', format=format_new_ext.upper()) + local_db.session.close() log.info("ebook converter failed with error while converting book") if not error_message: error_message = _('Ebook converter failed with unknown error') diff --git a/cps/tasks/mail.py b/cps/tasks/mail.py index ac3ec424..8424f084 100644 --- a/cps/tasks/mail.py +++ b/cps/tasks/mail.py @@ -167,7 +167,7 @@ class TaskEmail(CalibreTask): smtplib.stderr = org_smtpstderr except (MemoryError) as e: - log.exception(e) + log.debug_or_exception(e) self._handleError(u'MemoryError sending email: ' + str(e)) # return None except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e: @@ -178,7 +178,7 @@ class TaskEmail(CalibreTask): elif hasattr(e, "args"): text = '\n'.join(e.args) else: - log.exception(e) + log.debug_or_exception(e) text = '' self._handleError(u'Smtplib Error sending email: ' + text) # return None @@ -225,7 +225,7 @@ class TaskEmail(CalibreTask): data = file_.read() file_.close() except IOError as e: - log.exception(e) + log.debug_or_exception(e) log.error(u'The requested file could not be read. Maybe wrong permissions?') return None diff --git a/cps/templates/author.html b/cps/templates/author.html index 7887aa4a..4e32db80 100644 --- a/cps/templates/author.html +++ b/cps/templates/author.html @@ -36,7 +36,10 @@
diff --git a/cps/templates/book_edit.html b/cps/templates/book_edit.html index 003b33f9..c0fc141e 100644 --- a/cps/templates/book_edit.html +++ b/cps/templates/book_edit.html @@ -197,7 +197,8 @@ {% endblock %} {% block modal %} -{{ delete_book(book.id) }} +{{ delete_book() }} +{{ delete_confirm_modal() }}