diff --git a/cps.py b/cps.py index e4b1ede8..ab9896ce 100755 --- a/cps.py +++ b/cps.py @@ -17,18 +17,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import absolute_import, division, print_function, unicode_literals import sys import os # Insert local directories into path -if sys.version_info < (3, 0): - sys.path.append(os.path.dirname(os.path.abspath(__file__.decode('utf-8')))) - sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__.decode('utf-8'))), 'vendor')) -else: - sys.path.append(os.path.dirname(os.path.abspath(__file__))) - sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vendor')) +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vendor')) from cps import create_app @@ -49,7 +44,7 @@ try: from cps.kobo import kobo, get_kobo_activated from cps.kobo_auth import kobo_auth kobo_available = get_kobo_activated() -except ImportError: +except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator) kobo_available = False try: diff --git a/cps/MyLoginManager.py b/cps/MyLoginManager.py new file mode 100644 index 00000000..7c916bd5 --- /dev/null +++ b/cps/MyLoginManager.py @@ -0,0 +1,34 @@ +# -*- 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, 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 +# 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_login import LoginManager +from flask import session + + +class MyLoginManager(LoginManager): + def _session_protection_failed(self): + sess = session._get_current_object() + ident = self._session_identifier_generator() + if(sess and not (len(sess) == 1 and sess.get('csrf_token', None))) and ident != sess.get('_id', None): + return super(). _session_protection_failed() + return False diff --git a/cps/__init__.py b/cps/__init__.py index 517358c5..a6305f0a 100644 --- a/cps/__init__.py +++ b/cps/__init__.py @@ -20,7 +20,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import sys import os import mimetypes @@ -29,7 +28,7 @@ from babel import Locale as LC from babel import negotiate_locale from babel.core import UnknownLocaleError from flask import Flask, request, g -from flask_login import LoginManager +from .MyLoginManager import MyLoginManager from flask_babel import Babel from flask_principal import Principal @@ -43,6 +42,12 @@ try: except ImportError: lxml_present = False +try: + from flask_wtf.csrf import CSRFProtect + wtf_present = True +except ImportError: + wtf_present = False + mimetypes.init() mimetypes.add_type('application/xhtml+xml', '.xhtml') mimetypes.add_type('application/epub+zip', '.epub') @@ -70,11 +75,17 @@ app.config.update( ) -lm = LoginManager() +lm = MyLoginManager() lm.login_view = 'web.login' lm.anonymous_user = ub.Anonymous lm.session_protection = 'strong' +if wtf_present: + csrf = CSRFProtect() + csrf.init_app(app) +else: + csrf = None + ub.init_db(cli.settingspath) # pylint: disable=no-member config = config_sql.load_configuration(ub.session) @@ -105,12 +116,12 @@ def create_app(): log.info('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***') print('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***') sys.exit(6) + if not wtf_present: + log.info('*** "flask-WTF" is needed for calibre-web to run. Please install it using pip: "pip install flask-WTF" ***') + print('*** "flask-WTF" is needed for calibre-web to run. Please install it using pip: "pip install flask-WTF" ***') + # sys.exit(7) + app.wsgi_app = ReverseProxied(app.wsgi_app) - # For python2 convert path to unicode - if sys.version_info < (3, 0): - app.static_folder = app.static_folder.decode('utf-8') - app.root_path = app.root_path.decode('utf-8') - app.instance_path = app.instance_path.decode('utf-8') if os.environ.get('FLASK_DEBUG'): cache_buster.init_cache_busting(app) diff --git a/cps/about.py b/cps/about.py index 66c0ef40..f3c8b95f 100644 --- a/cps/about.py +++ b/cps/about.py @@ -20,7 +20,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import sys import platform import sqlite3 @@ -29,6 +28,10 @@ from collections import OrderedDict import babel, pytz, requests, sqlalchemy import werkzeug, flask, flask_login, flask_principal, jinja2 from flask_babel import gettext as _ +try: + from flask_wtf import __version__ as flaskwtf_version +except ImportError: + flaskwtf_version = _(u'not installed') from . import db, calibre_db, converter, uploader, server, isoLanguages, constants from .render_template import render_title_template @@ -75,6 +78,7 @@ _VERSIONS = OrderedDict( Flask=flask.__version__, Flask_Login=flask_loginVersion, Flask_Principal=flask_principal.__version__, + Flask_WTF=flaskwtf_version, Werkzeug=werkzeug.__version__, Babel=babel.__version__, Jinja2=jinja2.__version__, @@ -84,14 +88,14 @@ _VERSIONS = OrderedDict( SQLite=sqlite3.sqlite_version, iso639=isoLanguages.__version__, pytz=pytz.__version__, - Unidecode = unidecode_version, - Scholarly = scholarly_version, - Flask_SimpleLDAP = u'installed' if bool(services.ldap) else None, - python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None, - Goodreads = u'installed' if bool(services.goodreads_support) else None, - jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else None, - flask_dance = flask_danceVersion, - greenlet = greenlet_Version + Unidecode=unidecode_version, + Scholarly=scholarly_version, + Flask_SimpleLDAP=u'installed' if bool(services.ldap) else None, + python_LDAP=services.ldapVersion if bool(services.ldapVersion) else None, + Goodreads=u'installed' if bool(services.goodreads_support) else None, + jsonschema=services.SyncToken.__version__ if bool(services.SyncToken) else None, + flask_dance=flask_danceVersion, + greenlet=greenlet_Version ) _VERSIONS.update(uploader.get_versions()) diff --git a/cps/admin.py b/cps/admin.py index 0a0b362d..2f377187 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -20,7 +20,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import os import re import base64 @@ -41,7 +40,7 @@ from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError from sqlalchemy.sql.expression import func, or_, text from . import constants, logger, helper, services -from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils +from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils, kobo_sync_status from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \ valid_email, check_username from .gdriveutils import is_gdrive_ready, gdrive_support @@ -146,7 +145,7 @@ def shutdown(): else: showtext['text'] = _(u'Performing shutdown of server, please close window') # stop gevent/tornado server - web_server.stop(task==0) + web_server.stop(task == 0) return json.dumps(showtext) if task == 2: @@ -236,8 +235,12 @@ def view_configuration(): .filter(and_(db.Custom_Columns.datatype == 'bool', db.Custom_Columns.mark_for_delete == 0)).all() restrict_columns = calibre_db.session.query(db.Custom_Columns)\ .filter(and_(db.Custom_Columns.datatype == 'text', db.Custom_Columns.mark_for_delete == 0)).all() + languages = calibre_db.speaking_language() + translations = [LC('en')] + babel.list_translations() return render_title_template("config_view_edit.html", conf=config, readColumns=read_column, restrictColumns=restrict_columns, + languages=languages, + translations=translations, title=_(u"UI Configuration"), page="uiconfig") @admi.route("/admin/usertable") @@ -515,16 +518,12 @@ def check_valid_restricted_column(column): return True - @admi.route("/admin/viewconfig", methods=["POST"]) @login_required @admin_required def update_view_configuration(): to_save = request.form.to_dict() - # _config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y) - # _config_int = lambda x: config.set_from_dictionary(to_save, x, int) - _config_string(to_save, "config_calibre_web_title") _config_string(to_save, "config_columns_to_ignore") if _config_string(to_save, "config_title_regex"): @@ -546,6 +545,8 @@ def update_view_configuration(): _config_int(to_save, "config_random_books") _config_int(to_save, "config_books_per_page") _config_int(to_save, "config_authors_max") + _config_string(to_save, "config_default_language") + _config_string(to_save, "config_default_locale") config.config_default_role = constants.selected_roles(to_save) @@ -1431,8 +1432,13 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support): else: content.sidebar_view &= ~constants.DETAIL_RANDOM + old_state = content.kobo_only_shelves_sync content.kobo_only_shelves_sync = int(to_save.get("kobo_only_shelves_sync") == "on") or 0 - + # 1 -> 0: nothing has to be done + # 0 -> 1: all synced books have to be added to archived books, + currently synced shelfs + # which don't have to be synced have to be removed (added to Shelf archive) + if old_state == 0 and content.kobo_only_shelves_sync == 1: + kobo_sync_status.update_on_sync_shelfs(content.id) if to_save.get("default_language"): content.default_language = to_save["default_language"] if to_save.get("locale"): @@ -1488,6 +1494,8 @@ def new_user(): else: content.role = config.config_default_role content.sidebar_view = config.config_default_show + content.locale = config.config_default_locale + content.default_language = config.config_default_language return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, languages=languages, title=_(u"Add new user"), page="newuser", kobo_support=kobo_support, registered_oauth=oauth_check) @@ -1740,6 +1748,8 @@ def ldap_import_create_user(user, user_data): content.password = '' # dummy password which will be replaced by ldap one content.email = useremail content.kindle_mail = kindlemail + content.default_language = config.config_default_language + content.locale = config.config_default_locale content.role = config.config_default_role content.sidebar_view = config.config_default_show content.allowed_tags = config.config_allowed_tags diff --git a/cps/cache_buster.py b/cps/cache_buster.py index 8c521fe1..9619d605 100644 --- a/cps/cache_buster.py +++ b/cps/cache_buster.py @@ -19,7 +19,6 @@ # Inspired by https://github.com/ChrisTM/Flask-CacheBust # Uses query strings so CSS font files are found without having to resort to absolute URLs -from __future__ import division, print_function, unicode_literals import os import hashlib diff --git a/cps/cli.py b/cps/cli.py index 3bb08c1f..3685e8e2 100644 --- a/cps/cli.py +++ b/cps/cli.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import sys import os import argparse @@ -48,19 +47,6 @@ parser.add_argument('-s', metavar='user:pass', help='Sets specific username to n parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version') args = parser.parse_args() -if sys.version_info < (3, 0): - if args.p: - args.p = args.p.decode('utf-8') - if args.g: - args.g = args.g.decode('utf-8') - if args.k: - args.k = args.k.decode('utf-8') - if args.c: - args.c = args.c.decode('utf-8') - if args.s: - args.s = args.s.decode('utf-8') - - settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db") gdpath = args.g or os.path.join(_CONFIG_DIR, "gdrive.db") diff --git a/cps/comic.py b/cps/comic.py index 462c11f0..b094c60f 100644 --- a/cps/comic.py +++ b/cps/comic.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import os from . import logger, isoLanguages diff --git a/cps/config_sql.py b/cps/config_sql.py index 88107f9b..0b45059d 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - -from __future__ import division, print_function, unicode_literals import os import sys import json @@ -91,6 +89,8 @@ class _Settings(_Base): config_default_role = Column(SmallInteger, default=0) config_default_show = Column(SmallInteger, default=constants.ADMIN_USER_SIDEBAR) + config_default_language = Column(String(3), default="all") + config_default_locale = Column(String(2), default="en") config_columns_to_ignore = Column(String) config_denied_tags = Column(String, default="") @@ -361,10 +361,6 @@ def _migrate_table(session, orm_class): session.query(column).first() except OperationalError as err: log.debug("%s: %s", column_name, err.args[0]) - if column.default is not None: - if sys.version_info < (3, 0): - if isinstance(column.default.arg, unicode): - column.default.arg = column.default.arg.encode('utf-8') if column.default is None: column_default = "" else: diff --git a/cps/constants.py b/cps/constants.py index a0daa515..367bc29d 100644 --- a/cps/constants.py +++ b/cps/constants.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import sys import os from collections import namedtuple @@ -31,12 +30,7 @@ HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file UPDATER_AVAILABLE = True # Base dir is parent of current file, necessary if called from different folder -if sys.version_info < (3, 0): - BASE_DIR = os.path.abspath(os.path.join( - os.path.dirname(os.path.abspath(__file__)),os.pardir)).decode('utf-8') -else: - BASE_DIR = os.path.abspath(os.path.join( - os.path.dirname(os.path.abspath(__file__)),os.pardir)) +BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),os.pardir)) STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static') TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates') TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations') @@ -157,7 +151,7 @@ def selected_roles(dictionary): BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' 'series_id, languages, publisher') -STABLE_VERSION = {'version': '0.6.13 Beta'} +STABLE_VERSION = {'version': '0.6.14 Beta'} NIGHTLY_VERSION = {} NIGHTLY_VERSION[0] = '$Format:%H$' diff --git a/cps/converter.py b/cps/converter.py index 6b0f22e4..fcbabbfc 100644 --- a/cps/converter.py +++ b/cps/converter.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import os import re from flask_babel import gettext as _ diff --git a/cps/db.py b/cps/db.py index 71f5031b..d8709335 100644 --- a/cps/db.py +++ b/cps/db.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import sys import os import re @@ -387,8 +386,6 @@ class Custom_Columns(Base): def get_display_dict(self): display_dict = ast.literal_eval(self.display) - if sys.version_info < (3, 0): - display_dict['enum_values'] = [x.decode('unicode_escape') for x in display_dict['enum_values']] return display_dict @@ -740,6 +737,7 @@ class CalibreDB(): self.session.connection().connection.connection.create_function("lower", 1, lcase) entries = self.session.query(database).filter(tag_filter). \ filter(func.lower(database.name).ilike("%" + query + "%")).all() + # json_dumps = json.dumps([dict(name=escape(r.name.replace(*replace))) for r in entries]) json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries]) return json_dumps @@ -805,11 +803,16 @@ class CalibreDB(): .filter(self.common_filters()) \ .group_by(text('books_languages_link.lang_code')).all() for lang in languages: - try: - cur_l = LC.parse(lang.lang_code) - lang.name = cur_l.get_language_name(get_locale()) - except UnknownLocaleError: - lang.name = _(isoLanguages.get(part3=lang.lang_code).name) + lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code) + #try: + # if lang.lang_code.lower() == "und": + # lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code) + # # lang.name = _("Undetermined") + # else: + # cur_l = LC.parse(lang.lang_code) + # lang.name = cur_l.get_language_name(get_locale()) + #except UnknownLocaleError: + # lang.name = _(isoLanguages.get(part3=lang.lang_code).name) return languages def update_title_sort(self, config, conn=None): diff --git a/cps/editbooks.py b/cps/editbooks.py index dd506fa6..d3e1f01e 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -20,7 +20,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import os from datetime import datetime import json @@ -40,14 +39,12 @@ try: except ImportError: have_scholar = False -from babel import Locale as LC -from babel.core import UnknownLocaleError from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response from flask_babel import gettext as _ from flask_login import current_user, login_required from sqlalchemy.exc import OperationalError, IntegrityError from sqlite3 import OperationalError as sqliteOperationalError -from . import constants, logger, isoLanguages, gdriveutils, uploader, helper +from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status from . import config, get_locale, ub, db from . import calibre_db from .services.worker import WorkerThread @@ -826,6 +823,8 @@ def edit_book(book_id): if modif_date: book.last_modified = datetime.utcnow() + kobo_sync_status.remove_synced_book(edited_books_id) + calibre_db.session.merge(book) calibre_db.session.commit() if config.config_use_google_drive: @@ -1131,10 +1130,11 @@ def edit_list_book(param): else: lang_names = list() for lang in book.languages: - try: - lang_names.append(LC.parse(lang.lang_code).get_language_name(get_locale())) - except UnknownLocaleError: - lang_names.append(_(isoLanguages.get(part3=lang.lang_code).name)) + lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code)) + #try: + # lang_names.append(LC.parse(lang.lang_code).get_language_name(get_locale())) + #except UnknownLocaleError: + # lang_names.append(_(isoLanguages.get(part3=lang.lang_code).name)) ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}), mimetype='application/json') elif param =='author_sort': diff --git a/cps/epub.py b/cps/epub.py index 998dbfa6..cbbdcbbd 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import os import zipfile from lxml import etree diff --git a/cps/fb2.py b/cps/fb2.py index af4a29a7..21586736 100644 --- a/cps/fb2.py +++ b/cps/fb2.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals from lxml import etree from .constants import BookMeta diff --git a/cps/gdrive.py b/cps/gdrive.py index c0764015..267607de 100644 --- a/cps/gdrive.py +++ b/cps/gdrive.py @@ -20,9 +20,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import os -import sys import hashlib import json import tempfile @@ -34,7 +32,7 @@ from flask import Blueprint, flash, request, redirect, url_for, abort from flask_babel import gettext as _ from flask_login import login_required -from . import logger, gdriveutils, config, ub, calibre_db +from . import logger, gdriveutils, config, ub, calibre_db, csrf from .admin import admin_required gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive') @@ -118,6 +116,7 @@ def revoke_watch_gdrive(): return redirect(url_for('admin.db_configuration')) +@csrf.exempt @gdrive.route("/watch/callback", methods=['GET', 'POST']) def on_received_watch_confirmation(): if not config.config_google_drive_watch_changes_response: @@ -137,10 +136,7 @@ def on_received_watch_confirmation(): response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id']) log.debug('%r', response) if response: - if sys.version_info < (3, 0): - dbpath = os.path.join(config.config_calibre_dir, "metadata.db") - else: - dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode() + dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode() if not response['deleted'] and response['file']['title'] == 'metadata.db' \ and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web') diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py index c63b0393..d3277814 100644 --- a/cps/gdriveutils.py +++ b/cps/gdriveutils.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import os import json import shutil diff --git a/cps/helper.py b/cps/helper.py index f7b72a8d..df7019e6 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import sys import os import io @@ -490,10 +489,7 @@ def reset_password(user_id): def generate_random_password(): s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?" passlen = 8 - if sys.version_info < (3, 0): - return "".join(s[ord(c) % len(s)] for c in os.urandom(passlen)) - else: - return "".join(s[c % len(s)] for c in os.urandom(passlen)) + return "".join(s[c % len(s)] for c in os.urandom(passlen)) def uniq(inpt): @@ -707,8 +703,6 @@ def check_unrar(unrarLocation): return _('Unrar binary file not found') try: - if sys.version_info < (3, 0): - unrarLocation = unrarLocation.encode(sys.getfilesystemencoding()) unrarLocation = [unrarLocation] value = process_wait(unrarLocation, pattern='UNRAR (.*) freeware') if value: diff --git a/cps/isoLanguages.py b/cps/isoLanguages.py index 35d9f0a7..6c66a583 100644 --- a/cps/isoLanguages.py +++ b/cps/isoLanguages.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals - from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES diff --git a/cps/iso_language_names.py b/cps/iso_language_names.py index 73e6f326..9966ecd9 100644 --- a/cps/iso_language_names.py +++ b/cps/iso_language_names.py @@ -6,8 +6,6 @@ # pylint: disable=too-many-lines,bad-continuation -from __future__ import unicode_literals - # This file is autogenerated, do NOT add, change, or delete ANY string # If you need help or assistance for adding a new language, please contact the project team @@ -972,6 +970,8 @@ LANGUAGE_NAMES = { "gor": "Gorontalo", "got": "Γοτθικά", "grb": "Grebo", + "grc": "Ελληνικά; Αρχαία (to 1453)", + "ell": "Ελληνικά; Μοντέρνα (1453-)", "grn": "Guarani", "guj": "Gujarati", "gwi": "Gwichʼin", @@ -7853,4 +7853,4 @@ LANGUAGE_NAMES = { "zxx": "No linguistic content", "zza": "Zaza" } -} \ No newline at end of file +} diff --git a/cps/jinjia.py b/cps/jinjia.py index 554bc791..06e99141 100644 --- a/cps/jinjia.py +++ b/cps/jinjia.py @@ -22,7 +22,6 @@ # custom jinja filters -from __future__ import division, print_function, unicode_literals import datetime import mimetypes from uuid import uuid4 diff --git a/cps/kobo.py b/cps/kobo.py index 6952a692..92abf295 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -19,7 +19,6 @@ import base64 import datetime -import sys import os import uuid from time import gmtime, strftime @@ -47,7 +46,8 @@ from sqlalchemy.exc import StatementError from sqlalchemy.sql import select import requests -from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub + +from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status from .constants import sqlalchemy_version2 from .helper import get_download_link from .services import SyncToken as SyncToken @@ -170,9 +170,12 @@ def HandleSyncRequest(): ub.ArchivedBook.is_archived) changed_entries = (changed_entries .join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) - .filter(or_(db.Books.last_modified > sync_token.books_last_modified, - ub.BookShelf.date_added > sync_token.books_last_modified)) - .filter(db.Data.format.in_(KOBO_FORMATS)).filter(calibre_db.common_filters()) + .join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True) + .filter(or_(ub.KoboSyncedBooks.user_id != current_user.id, + ub.KoboSyncedBooks.book_id == None)) + .filter(ub.BookShelf.date_added > sync_token.books_last_modified) + .filter(db.Data.format.in_(KOBO_FORMATS)) + .filter(calibre_db.common_filters(allow_show_archived=True)) .order_by(db.Books.id) .order_by(ub.ArchivedBook.last_modified) .join(ub.BookShelf, db.Books.id == ub.BookShelf.book_id) @@ -189,16 +192,16 @@ def HandleSyncRequest(): ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived) changed_entries = (changed_entries - .join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) - .filter(db.Books.last_modified > sync_token.books_last_modified) - .filter(calibre_db.common_filters()) - .filter(db.Data.format.in_(KOBO_FORMATS)) - .order_by(db.Books.last_modified) - .order_by(db.Books.id) + .join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) + .join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True) + .filter(or_(ub.KoboSyncedBooks.user_id != current_user.id, + ub.KoboSyncedBooks.book_id == None)) + .filter(calibre_db.common_filters()) + .filter(db.Data.format.in_(KOBO_FORMATS)) + .order_by(db.Books.last_modified) + .order_by(db.Books.id) ) - if sync_token.books_last_id > -1: - changed_entries = changed_entries.filter(db.Books.id > sync_token.books_last_id) reading_states_in_new_entitlements = [] if sqlalchemy_version2: @@ -206,6 +209,7 @@ def HandleSyncRequest(): else: books = changed_entries.limit(SYNC_ITEM_LIMIT) for book in books: + kobo_sync_status.add_synced_books(book.Books.id) formats = [data.format for data in book.Books.data] if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats: helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name) @@ -263,11 +267,10 @@ def HandleSyncRequest(): entries = calibre_db.session.execute(changed_entries).all() book_count = len(entries) else: - entries = changed_entries.all() book_count = changed_entries.count() # last entry: - books_last_id = entries[-1].Books.id or -1 if book_count else -1 - + cont_sync = bool(book_count) + log.debug("Remaining books to Sync: {}".format(book_count)) # generate reading state data changed_reading_states = ub.session.query(ub.KoboReadingState) @@ -278,18 +281,18 @@ def HandleSyncRequest(): .filter(current_user.id == ub.Shelf.user_id)\ .filter(ub.Shelf.kobo_sync, or_( - func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified, + ub.KoboReadingState.last_modified > sync_token.reading_state_last_modified, func.datetime(ub.BookShelf.date_added) > sync_token.books_last_modified )).distinct() else: changed_reading_states = changed_reading_states.filter( - func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified) + ub.KoboReadingState.last_modified > sync_token.reading_state_last_modified) changed_reading_states = changed_reading_states.filter( and_(ub.KoboReadingState.user_id == current_user.id, ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements))) - - for kobo_reading_state in changed_reading_states.all(): + cont_sync |= bool(changed_reading_states.count() > SYNC_ITEM_LIMIT) + for kobo_reading_state in changed_reading_states.limit(SYNC_ITEM_LIMIT).all(): book = calibre_db.session.query(db.Books).filter(db.Books.id == kobo_reading_state.book_id).one_or_none() if book: sync_results.append({ @@ -305,9 +308,9 @@ 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 + # sync_token.books_last_id = books_last_id - return generate_sync_response(sync_token, sync_results, book_count) + return generate_sync_response(sync_token, sync_results, cont_sync) def generate_sync_response(sync_token, sync_results, set_cont=False): @@ -330,7 +333,7 @@ def generate_sync_response(sync_token, sync_results, set_cont=False): extra_headers["x-kobo-sync"] = "continue" sync_token.to_headers(extra_headers) - log.debug("Kobo Sync Content: {}".format(sync_results)) + # log.debug("Kobo Sync Content: {}".format(sync_results)) response = make_response(jsonify(sync_results), extra_headers) return response @@ -483,10 +486,7 @@ def get_metadata(book): metadata.update(get_author(book)) if get_series(book): - if sys.version_info < (3, 0): - name = get_series(book).encode("utf-8") - else: - name = get_series(book) + name = get_series(book) metadata["Series"] = { "Name": get_series(book), "Number": get_seriesindex(book), # ToDo Check int() ? @@ -497,7 +497,7 @@ def get_metadata(book): return metadata - +@csrf.exempt @kobo.route("/v1/library/tags", methods=["POST", "DELETE"]) @requires_kobo_auth # Creates a Shelf with the given items, and returns the shelf's uuid. @@ -532,6 +532,7 @@ def HandleTagCreate(): return make_response(jsonify(str(shelf.uuid)), 201) +@csrf.exempt @kobo.route("/v1/library/tags/", methods=["DELETE", "PUT"]) @requires_kobo_auth def HandleTagUpdate(tag_id): @@ -587,6 +588,7 @@ def add_items_to_shelf(items, shelf): return items_unknown_to_calibre +@csrf.exempt @kobo.route("/v1/library/tags//items", methods=["POST"]) @requires_kobo_auth def HandleTagAddItem(tag_id): @@ -616,6 +618,7 @@ def HandleTagAddItem(tag_id): return make_response('', 201) +@csrf.exempt @kobo.route("/v1/library/tags//items/delete", methods=["POST"]) @requires_kobo_auth def HandleTagRemoveItem(tag_id): @@ -678,6 +681,9 @@ def sync_shelves(sync_token, sync_results, only_kobo_shelves=False): } } }) + ub.session.delete(shelf) + ub.session_commit() + extra_filters = [] if only_kobo_shelves: @@ -757,7 +763,7 @@ def create_kobo_tag(shelf): ) return {"Tag": tag} - +@csrf.exempt @kobo.route("/v1/library//state", methods=["GET", "PUT"]) @requires_kobo_auth def HandleStateRequest(book_uuid): @@ -932,6 +938,7 @@ def TopLevelEndpoint(): return make_response(jsonify({})) +@csrf.exempt @kobo.route("/v1/library/", methods=["DELETE"]) @requires_kobo_auth def HandleBookDeletionRequest(book_uuid): @@ -958,6 +965,7 @@ def HandleBookDeletionRequest(book_uuid): # TODO: Implement the following routes +@csrf.exempt @kobo.route("/v1/library/", methods=["DELETE", "GET"]) def HandleUnimplementedRequest(dummy=None): log.debug("Unimplemented Library Request received: %s", request.base_url) @@ -965,6 +973,7 @@ def HandleUnimplementedRequest(dummy=None): # TODO: Implement the following routes +@csrf.exempt @kobo.route("/v1/user/loyalty/", methods=["GET", "POST"]) @kobo.route("/v1/user/profile", methods=["GET", "POST"]) @kobo.route("/v1/user/wishlist", methods=["GET", "POST"]) @@ -975,6 +984,7 @@ def HandleUserRequest(dummy=None): return redirect_or_proxy_request() +@csrf.exempt @kobo.route("/v1/products//prices", methods=["GET", "POST"]) @kobo.route("/v1/products//recommendations", methods=["GET", "POST"]) @kobo.route("/v1/products//nextread", methods=["GET", "POST"]) @@ -1008,6 +1018,7 @@ def make_calibre_web_auth_response(): ) +@csrf.exempt @kobo.route("/v1/auth/device", methods=["POST"]) @requires_kobo_auth def HandleAuthRequest(): diff --git a/cps/kobo_sync_status.py b/cps/kobo_sync_status.py new file mode 100644 index 00000000..f4a66604 --- /dev/null +++ b/cps/kobo_sync_status.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2021 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_login import current_user +from . import ub +import datetime +from sqlalchemy.sql.expression import or_ + + +def add_synced_books(book_id): + synced_book = ub.KoboSyncedBooks() + synced_book.user_id = current_user.id + synced_book.book_id = book_id + ub.session.add(synced_book) + ub.session_commit() + + +def remove_synced_book(book_id): + ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).delete() + ub.session_commit() + +def add_archived_books(book_id): + archived_book = ( + ub.session.query(ub.ArchivedBook) + .filter(ub.ArchivedBook.book_id == book_id) + .first() + ) + if not archived_book: + archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id) + archived_book.is_archived = True + archived_book.last_modified = datetime.datetime.utcnow() + + ub.session.merge(archived_book) + ub.session_commit() + + + +# select all books which are synced by the current user and do not belong to a synced shelf and them to archive +# select all shelfs from current user which are synced and do not belong to the "only sync" shelfs +def update_on_sync_shelfs(content_id): + books_to_archive = (ub.session.query(ub.KoboSyncedBooks) + .join(ub.BookShelf, ub.KoboSyncedBooks.book_id == ub.BookShelf.book_id, isouter=True) + .join(ub.Shelf, ub.Shelf.user_id == content_id, isouter=True) + .filter(or_(ub.Shelf.kobo_sync == 0, ub.Shelf.kobo_sync == None)) + .filter(ub.KoboSyncedBooks.user_id == content_id).all()) + for b in books_to_archive: + add_archived_books(b.book_id) + ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == b.book_id).filter(ub.KoboSyncedBooks.user_id == content_id).delete() + ub.session_commit() + + shelfs_to_archive = ub.session.query(ub.Shelf).filter(ub.Shelf.user_id == content_id).filter( + ub.Shelf.kobo_sync == 0).all() + for a in shelfs_to_archive: + ub.session.add(ub.ShelfArchive(uuid=a.uuid, user_id=content_id)) + ub.session_commit() diff --git a/cps/logger.py b/cps/logger.py index e2747f53..5847188b 100644 --- a/cps/logger.py +++ b/cps/logger.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import os import sys import inspect diff --git a/cps/oauth.py b/cps/oauth.py index a8995180..f8e5c1fd 100644 --- a/cps/oauth.py +++ b/cps/oauth.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -from __future__ import division, print_function, unicode_literals from flask import session try: diff --git a/cps/oauth_bb.py b/cps/oauth_bb.py index c8cc2e3e..d9efd41e 100644 --- a/cps/oauth_bb.py +++ b/cps/oauth_bb.py @@ -20,7 +20,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -from __future__ import division, print_function, unicode_literals import json from functools import wraps diff --git a/cps/opds.py b/cps/opds.py index e444302a..92c51d1b 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -20,7 +20,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import sys import datetime from functools import wraps @@ -433,16 +432,17 @@ def feed_languagesindex(): if current_user.filter_language() == u"all": languages = calibre_db.speaking_language() else: - try: - cur_l = LC.parse(current_user.filter_language()) - except UnknownLocaleError: - cur_l = None + #try: + # cur_l = LC.parse(current_user.filter_language()) + #except UnknownLocaleError: + # cur_l = None languages = calibre_db.session.query(db.Languages).filter( db.Languages.lang_code == current_user.filter_language()).all() - if cur_l: - languages[0].name = cur_l.get_language_name(get_locale()) - else: - languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name) + languages[0].name = isoLanguages.get_language_name(get_locale(), languages[0].lang_code) + #if cur_l: + # languages[0].name = cur_l.get_language_name(get_locale()) + #else: + # languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name) pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(languages)) return render_xml_template('feed.xml', listelements=languages, folder='opds.feed_languages', pagination=pagination) @@ -536,11 +536,10 @@ def feed_search(term): def check_auth(username, password): - if sys.version_info.major == 3: - try: - username = username.encode('windows-1252') - except UnicodeEncodeError: - username = username.encode('utf-8') + try: + username = username.encode('windows-1252') + except UnicodeEncodeError: + username = username.encode('utf-8') user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.decode('utf-8').lower()).first() if bool(user and check_password_hash(str(user.password), password)): diff --git a/cps/pagination.py b/cps/pagination.py index c29717c2..7a9bfb70 100644 --- a/cps/pagination.py +++ b/cps/pagination.py @@ -20,7 +20,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals from math import ceil diff --git a/cps/redirect.py b/cps/redirect.py index 593afd0c..d491b353 100644 --- a/cps/redirect.py +++ b/cps/redirect.py @@ -27,7 +27,6 @@ # http://flask.pocoo.org/snippets/62/ -from __future__ import division, print_function, unicode_literals try: from urllib.parse import urlparse, urljoin except ImportError: diff --git a/cps/reverseproxy.py b/cps/reverseproxy.py index 3bcbd3b7..4acb8e45 100644 --- a/cps/reverseproxy.py +++ b/cps/reverseproxy.py @@ -36,8 +36,6 @@ # # Inspired by http://flask.pocoo.org/snippets/35/ -from __future__ import division, print_function, unicode_literals - class ReverseProxied(object): """Wrap the application in this middleware and configure the diff --git a/cps/search_metadata.py b/cps/search_metadata.py index 72e77cdd..e837fe21 100644 --- a/cps/search_metadata.py +++ b/cps/search_metadata.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import os import json import importlib diff --git a/cps/server.py b/cps/server.py index 9b79f77d..e261c50a 100644 --- a/cps/server.py +++ b/cps/server.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import sys import os import errno diff --git a/cps/services/SyncToken.py b/cps/services/SyncToken.py index cc67542c..692aaa24 100644 --- a/cps/services/SyncToken.py +++ b/cps/services/SyncToken.py @@ -35,10 +35,7 @@ log = logger.create() def b64encode_json(json_data): - if sys.version_info < (3, 0): - return b64encode(json.dumps(json_data)) - else: - return b64encode(json.dumps(json_data).encode()) + return b64encode(json.dumps(json_data).encode()) # Python3 has a timestamp() method we could be calling, however it's not avaiable in python2. @@ -85,8 +82,8 @@ class SyncToken: "books_last_created": {"type": "string"}, "archive_last_modified": {"type": "string"}, "reading_state_last_modified": {"type": "string"}, - "tags_last_modified": {"type": "string"}, - "books_last_id": {"type": "integer", "optional": True} + "tags_last_modified": {"type": "string"} + # "books_last_id": {"type": "integer", "optional": True} }, } @@ -97,8 +94,8 @@ class SyncToken: books_last_modified=datetime.min, archive_last_modified=datetime.min, reading_state_last_modified=datetime.min, - tags_last_modified=datetime.min, - books_last_id=-1 + tags_last_modified=datetime.min + # books_last_id=-1 ): # nosec self.raw_kobo_store_token = raw_kobo_store_token self.books_last_created = books_last_created @@ -106,7 +103,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 + # self.books_last_id = books_last_id @staticmethod def from_headers(headers): @@ -141,12 +138,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"] + # 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 + #except KeyError: + # books_last_id = -1 return SyncToken( raw_kobo_store_token=raw_kobo_store_token, @@ -155,7 +152,7 @@ class SyncToken: archive_last_modified=archive_last_modified, reading_state_last_modified=reading_state_last_modified, tags_last_modified=tags_last_modified, - books_last_id=books_last_id + #books_last_id=books_last_id ) def set_kobo_store_header(self, store_headers): @@ -179,16 +176,16 @@ class SyncToken: "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), - "books_last_id":self.books_last_id + #"books_last_id":self.books_last_id }, } return b64encode_json(token) def __str__(self): - return "{},{},{},{},{},{},{}".format(self.raw_kobo_store_token, + return "{},{},{},{},{},{}".format(self.raw_kobo_store_token, self.books_last_created, self.books_last_modified, self.archive_last_modified, self.reading_state_last_modified, - self.tags_last_modified, - self.books_last_id) + self.tags_last_modified) + #self.books_last_id) diff --git a/cps/services/__init__.py b/cps/services/__init__.py index e6e5954c..32a9d485 100644 --- a/cps/services/__init__.py +++ b/cps/services/__init__.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals - from .. import logger diff --git a/cps/services/gmail.py b/cps/services/gmail.py index baada1f8..ff36b308 100644 --- a/cps/services/gmail.py +++ b/cps/services/gmail.py @@ -1,4 +1,21 @@ -from __future__ import print_function +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2021 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 os.path from google_auth_oauthlib.flow import InstalledAppFlow from google.auth.transport.requests import Request diff --git a/cps/services/goodreads_support.py b/cps/services/goodreads_support.py index 9312bc0f..74e6eba9 100644 --- a/cps/services/goodreads_support.py +++ b/cps/services/goodreads_support.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import time from functools import reduce diff --git a/cps/services/simpleldap.py b/cps/services/simpleldap.py index 4125bdab..1ca7e5bf 100644 --- a/cps/services/simpleldap.py +++ b/cps/services/simpleldap.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import base64 from flask_simpleldap import LDAP, LDAPException diff --git a/cps/services/worker.py b/cps/services/worker.py index 238c8dbf..5952c705 100644 --- a/cps/services/worker.py +++ b/cps/services/worker.py @@ -1,5 +1,21 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2020 pwr +# +# 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 __future__ import division, print_function, unicode_literals import threading import abc import uuid diff --git a/cps/shelf.py b/cps/shelf.py index d232e850..09fe8dd4 100644 --- a/cps/shelf.py +++ b/cps/shelf.py @@ -20,8 +20,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals - import sys from datetime import datetime diff --git a/cps/static/js/libs/djvu_html5/Djvu_html5.css b/cps/static/css/libs/Djvu_html5.css similarity index 93% rename from cps/static/js/libs/djvu_html5/Djvu_html5.css rename to cps/static/css/libs/Djvu_html5.css index 77e37c97..a7ed799e 100644 --- a/cps/static/js/libs/djvu_html5/Djvu_html5.css +++ b/cps/static/css/libs/Djvu_html5.css @@ -35,7 +35,7 @@ body { float: left; width: 40px; height: 40px; - background-image: url("img/toolbar-buttons.png"); + background-image: url("../../js/libs/djvu_html5/img/toolbar-buttons.png"); background-repeat: no-repeat; background-size: 500% 300%; } @@ -185,10 +185,10 @@ _:-ms-lang(x), .textLayer { width: 128px; height: 128px; margin: -72px 0 0 -64px; - background-image: url("img/status.png"); + background-image: url("../../js/libs/djvu_html5/img/status.png"); background-repeat: no-repeat; } .blankImage { - background-image: url("img/blank.jpg"); -} \ No newline at end of file + background-image: url("../../js/libs/djvu_html5/img/blank.jpg"); +} diff --git a/cps/static/css/libs/typeahead.css b/cps/static/css/libs/typeahead.css index fde250d6..fcc17a5b 100644 --- a/cps/static/css/libs/typeahead.css +++ b/cps/static/css/libs/typeahead.css @@ -145,7 +145,7 @@ fieldset[disabled] .twitter-typeahead .tt-input { cursor: not-allowed; background-color: #eeeeee !important; } -.tt-dropdown-menu { +.tt-menu { position: absolute; top: 100%; left: 0; @@ -166,7 +166,7 @@ fieldset[disabled] .twitter-typeahead .tt-input { *border-right-width: 2px; *border-bottom-width: 2px; } -.tt-dropdown-menu .tt-suggestion { +.tt-menu .tt-suggestion { display: block; padding: 3px 20px; clear: both; @@ -175,15 +175,15 @@ fieldset[disabled] .twitter-typeahead .tt-input { color: #333333; white-space: nowrap; } -.tt-dropdown-menu .tt-suggestion.tt-cursor { +.tt-menu .tt-suggestion.tt-cursor { text-decoration: none; outline: 0; background-color: #f5f5f5; color: #262626; } -.tt-dropdown-menu .tt-suggestion.tt-cursor a { +.tt-menu .tt-suggestion.tt-cursor a { color: #262626; } -.tt-dropdown-menu .tt-suggestion p { +.tt-menu .tt-suggestion p { margin: 0; } diff --git a/cps/static/css/text.css b/cps/static/css/text.css new file mode 100644 index 00000000..74204680 --- /dev/null +++ b/cps/static/css/text.css @@ -0,0 +1,44 @@ +body { + background: white; +} + +#readmain { + position: absolute; + width: 100%; + height: 100%; +} + +#area { + width: 80%; + height: 80%; + margin: 5% auto; + max-width: 1250px; +} + +#area iframe { + border: none; +} + +xmp, pre, plaintext { + display: block; + font-family: -moz-fixed; + white-space: pre; + margin: 1em 0; +} + +#area{ + overflow:hidden; +} + +pre { + white-space: pre-wrap; + word-wrap: break-word; + font-family: -moz-fixed; + column-count:2; + -webkit-columns:2; + -moz-columns:2; + column-gap:20px; + -moz-column-gap:20px; + -webkit-column-gap:20px; + position:relative; +} diff --git a/cps/static/js/caliBlur.js b/cps/static/js/caliBlur.js index ce230730..3203c255 100644 --- a/cps/static/js/caliBlur.js +++ b/cps/static/js/caliBlur.js @@ -150,11 +150,16 @@ if ($("body.book").length > 0) { var splitText = $(this).text().split(':'); var label = splitText.shift().trim(); var value = splitText.join(':').trim(); + var class_value = "" // Preserve Links if ($(this).find('a').length) { value = $(this).find('a').first().removeClass(); } - $(this).html('' + label + '').find('span').last().append(value); + // Preserve glyphicons + if ($(this).find('span').length) { + class_value = $(this).find('span').first().attr('class'); + } + $(this).html('' + label + '').find('span').last().append(value); }); $(".book-meta h2:first").clone() diff --git a/cps/static/js/details.js b/cps/static/js/details.js index 3487b901..9caf9470 100644 --- a/cps/static/js/details.js +++ b/cps/static/js/details.js @@ -22,8 +22,10 @@ $(function() { }); $("#have_read_cb").on("change", function() { - $.post({ + $.ajax({ url: this.closest("form").action, + method:"post", + data: $(this).closest("form").serialize(), error: function(response) { var data = [{type:"danger", message:response.responseText}] $("#flash_success").remove(); diff --git a/cps/static/js/edit_books.js b/cps/static/js/edit_books.js index 00c971d3..88057162 100644 --- a/cps/static/js/edit_books.js +++ b/cps/static/js/edit_books.js @@ -23,7 +23,6 @@ if ($(".tiny_editor").length) { $(".datepicker").datepicker({ format: "yyyy-mm-dd", - language: language }).on("change", function () { // Show localized date over top of the standard YYYY-MM-DD date var pubDate; @@ -47,90 +46,20 @@ $(".datepicker_delete").click(function() { Takes a prefix, query typeahead callback, Bloodhound typeahead adapter and returns the completions it gets from the bloodhound engine prefixed. */ -function prefixedSource(prefix, query, cb, bhAdapter) { - bhAdapter(query, function(retArray) { +function prefixedSource(prefix, query, cb, source) { + function async(retArray) { + retArray = retArray || []; var matches = []; for (var i = 0; i < retArray.length; i++) { var obj = {name : prefix + retArray[i].name}; matches.push(obj); } cb(matches); - }); + } + source.search(query, cb, async); } -var authors = new Bloodhound({ - name: "authors", - datumTokenizer: function datumTokenizer(datum) { - return [datum.name]; - }, - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: getPath() + "/get_authors_json?q=%QUERY" - } -}); - -var series = new Bloodhound({ - name: "series", - datumTokenizer: function datumTokenizer(datum) { - return [datum.name]; - }, - queryTokenizer: function queryTokenizer(query) { - return [query]; - }, - remote: { - url: getPath() + "/get_series_json?q=", - replace: function replace(url, query) { - return url + encodeURIComponent(query); - } - } -}); - - -var tags = new Bloodhound({ - name: "tags", - datumTokenizer: function datumTokenizer(datum) { - return [datum.name]; - }, - queryTokenizer: function queryTokenizer(query) { - var tokens = query.split(","); - tokens = [tokens[tokens.length - 1].trim()]; - return tokens; - }, - remote: { - url: getPath() + "/get_tags_json?q=%QUERY" - } -}); - -var languages = new Bloodhound({ - name: "languages", - datumTokenizer: function datumTokenizer(datum) { - return [datum.name]; - }, - queryTokenizer: function queryTokenizer(query) { - return [query]; - }, - remote: { - url: getPath() + "/get_languages_json?q=", - replace: function replace(url, query) { - return url + encodeURIComponent(query); - } - } -}); - -var publishers = new Bloodhound({ - name: "publisher", - datumTokenizer: function datumTokenizer(datum) { - return [datum.name]; - }, - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: getPath() + "/get_publishers_json?q=%QUERY" - } -}); - function sourceSplit(query, cb, split, source) { - var bhAdapter = source.ttAdapter(); - var tokens = query.split(split); var currentSource = tokens[tokens.length - 1].trim(); @@ -145,84 +74,148 @@ function sourceSplit(query, cb, split, source) { for (var i = 0; i < tokens.length; i++) { prefix += tokens[i].trim() + newSplit; } - prefixedSource(prefix, currentSource, cb, bhAdapter); + prefixedSource(prefix, currentSource, cb, source); } -var promiseAuthors = authors.initialize(); -promiseAuthors.done(function() { - $("#bookAuthor").typeahead( - { - highlight: true, minLength: 1, - hint: true - }, { - name: "authors", - displayKey: "name", - source: function source(query, cb) { - return sourceSplit(query, cb, "&", authors); //sourceSplit //("&") - } - } - ); +var authors = new Bloodhound({ + name: "authors", + identify: function(obj) { return obj.name; }, + datumTokenizer: function datumTokenizer(datum) { + return [datum.name]; + }, + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: getPath() + "/get_authors_json?q=%QUERY", + wildcard: '%QUERY', + }, }); -var promiseSeries = series.initialize(); -promiseSeries.done(function() { - $("#series").typeahead( - { - highlight: true, minLength: 0, - hint: true - }, { - name: "series", - displayKey: "name", - source: series.ttAdapter() +$(".form-group #bookAuthor").typeahead( + { + highlight: true, + minLength: 1, + hint: true + }, { + name: "authors", + display: 'name', + source: function source(query, cb, asyncResults) { + return sourceSplit(query, cb, "&", authors); } - ); + } +); + + +var series = new Bloodhound({ + name: "series", + datumTokenizer: function datumTokenizer(datum) { + return [datum.name]; + }, + // queryTokenizer: Bloodhound.tokenizers.whitespace, + queryTokenizer: function queryTokenizer(query) { + return [query]; + }, + remote: { + url: getPath() + "/get_series_json?q=%QUERY", + wildcard: '%QUERY', + /*replace: function replace(url, query) { + return url + encodeURIComponent(query); + }*/ + } +}); +$(".form-group #series").typeahead( + { + highlight: true, + minLength: 0, + hint: true + }, { + name: "series", + displayKey: "name", + source: series + } +); + +var tags = new Bloodhound({ + name: "tags", + datumTokenizer: function datumTokenizer(datum) { + return [datum.name]; + }, + queryTokenizer: function queryTokenizer(query) { + var tokens = query.split(","); + tokens = [tokens[tokens.length - 1].trim()]; + return tokens; + }, + remote: { + url: getPath() + "/get_tags_json?q=%QUERY", + wildcard: '%QUERY' + } }); -var promiseTags = tags.initialize(); -promiseTags.done(function() { - $("#tags").typeahead( - { - highlight: true, minLength: 0, - hint: true - }, { - name: "tags", - displayKey: "name", - source: function source(query, cb) { - return sourceSplit(query, cb, ",", tags); - } +$(".form-group #tags").typeahead( + { + highlight: true, + minLength: 0, + hint: true + }, { + name: "tags", + display: "name", + source: function source(query, cb, asyncResults) { + return sourceSplit(query, cb, ",", tags); } - ); + } +); + +var languages = new Bloodhound({ + name: "languages", + datumTokenizer: function datumTokenizer(datum) { + return [datum.name]; + }, + queryTokenizer: function queryTokenizer(query) { + return [query]; + }, + remote: { + url: getPath() + "/get_languages_json?q=%QUERY", + wildcard: '%QUERY' + /*replace: function replace(url, query) { + return url + encodeURIComponent(query); + }*/ + } }); -var promiseLanguages = languages.initialize(); -promiseLanguages.done(function() { - $("#languages").typeahead( - { - highlight: true, minLength: 0, - hint: true - }, { - name: "languages", - displayKey: "name", - source: function source(query, cb) { - return sourceSplit(query, cb, ",", languages); //(",") - } +$(".form-group #languages").typeahead( + { + highlight: true, minLength: 0, + hint: true + }, { + name: "languages", + display: "name", + source: function source(query, cb, asyncResults) { + return sourceSplit(query, cb, ",", languages); } - ); + } +); + +var publishers = new Bloodhound({ + name: "publisher", + datumTokenizer: function datumTokenizer(datum) { + return [datum.name]; + }, + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: getPath() + "/get_publishers_json?q=%QUERY", + wildcard: '%QUERY' + } }); -var promisePublishers = publishers.initialize(); -promisePublishers.done(function() { - $("#publisher").typeahead( - { - highlight: true, minLength: 0, - hint: true - }, { - name: "publishers", - displayKey: "name", - source: publishers.ttAdapter() - } - ); -}); +$(".form-group #publisher").typeahead( + { + highlight: true, minLength: 0, + hint: true + }, { + name: "publishers", + displayKey: "name", + source: publishers + } +); $("#search").on("change input.typeahead:selected", function(event) { if (event.target.type === "search" && event.target.tagName === "INPUT") { diff --git a/cps/static/js/libs/typeahead.bundle.js b/cps/static/js/libs/typeahead.bundle.js index 8164a3c2..bfb5d010 100644 --- a/cps/static/js/libs/typeahead.bundle.js +++ b/cps/static/js/libs/typeahead.bundle.js @@ -1,10 +1,21 @@ /*! - * typeahead.js 0.10.5 - * https://github.com/twitter/typeahead.js - * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT + * typeahead.js 1.3.1 master from 10.10.2021 + * https://github.com/corejavascript/typeahead.js + * Copyright 2013-2020 Twitter, Inc. and other contributors; Licensed MIT */ -(function($) { + +(function(root, factory) { + if (typeof define === "function" && define.amd) { + define([ "jquery" ], function(a0) { + return root["Bloodhound"] = factory(a0); + }); + } else if (typeof module === "object" && module.exports) { + module.exports = factory(require("jquery")); + } else { + root["Bloodhound"] = factory(root["jQuery"]); + } +})(this, function($) { var _ = function() { "use strict"; return { @@ -29,6 +40,12 @@ isUndefined: function(obj) { return typeof obj === "undefined"; }, + isElement: function(obj) { + return !!(obj && obj.nodeType === 1); + }, + isJQuery: function(obj) { + return obj instanceof $; + }, toStr: function toStr(s) { return _.isUndefined(s) || s === null ? "" : s + ""; }, @@ -66,12 +83,18 @@ return !!result; }, mixin: $.extend, - getUniqueId: function() { + identity: function(x) { + return x; + }, + clone: function(obj) { + return $.extend(true, {}, obj); + }, + getIdGenerator: function() { var counter = 0; return function() { return counter++; }; - }(), + }, templatify: function templatify(obj) { return $.isFunction(obj) ? obj : template; function template() { @@ -123,18 +146,30 @@ return result; }; }, + stringify: function(val) { + return _.isString(val) ? val : JSON.stringify(val); + }, + guid: function() { + function _p8(s) { + var p = (Math.random().toString(16) + "000000000").substr(2, 8); + return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p; + } + return "tt-" + _p8() + _p8(true) + _p8(true) + _p8(); + }, noop: function() {} }; }(); - var VERSION = "0.10.5"; + var VERSION = "1.3.1"; var tokenizers = function() { "use strict"; return { nonword: nonword, whitespace: whitespace, + ngram: ngram, obj: { nonword: getObjTokenizer(nonword), - whitespace: getObjTokenizer(whitespace) + whitespace: getObjTokenizer(whitespace), + ngram: getObjTokenizer(ngram) } }; function whitespace(str) { @@ -145,12 +180,25 @@ str = _.toStr(str); return str ? str.split(/\W+/) : []; } + function ngram(str) { + str = _.toStr(str); + var tokens = [], word = ""; + _.each(str.split(""), function(char) { + if (char.match(/\s+/)) { + word = ""; + } else { + tokens.push(word + char); + word += char; + } + }); + return tokens; + } function getObjTokenizer(tokenizer) { - return function setKey() { - var args = [].slice.call(arguments, 0); + return function setKey(keys) { + keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0); return function tokenize(o) { var tokens = []; - _.each(args, function(k) { + _.each(keys, function(k) { tokens = tokens.concat(tokenizer(_.toStr(o[k]))); }); return tokens; @@ -173,6 +221,7 @@ if (this.size >= this.maxSize) { this.list.remove(tailItem); delete this.hash[tailItem.key]; + this.size--; } if (node = this.hash[key]) { node.val = val; @@ -227,73 +276,72 @@ }(); var PersistentStorage = function() { "use strict"; - var ls, methods; + var LOCAL_STORAGE; try { - ls = window.localStorage; - ls.setItem("~~~", "!"); - ls.removeItem("~~~"); + LOCAL_STORAGE = window.localStorage; + LOCAL_STORAGE.setItem("~~~", "!"); + LOCAL_STORAGE.removeItem("~~~"); } catch (err) { - ls = null; + LOCAL_STORAGE = null; } - function PersistentStorage(namespace) { + function PersistentStorage(namespace, override) { this.prefix = [ "__", namespace, "__" ].join(""); this.ttlKey = "__ttl__"; this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix)); + this.ls = override || LOCAL_STORAGE; + !this.ls && this._noop(); } - if (ls && window.JSON) { - methods = { - _prefix: function(key) { - return this.prefix + key; - }, - _ttlKey: function(key) { - return this._prefix(key) + this.ttlKey; - }, - get: function(key) { - if (this.isExpired(key)) { - this.remove(key); + _.mixin(PersistentStorage.prototype, { + _prefix: function(key) { + return this.prefix + key; + }, + _ttlKey: function(key) { + return this._prefix(key) + this.ttlKey; + }, + _noop: function() { + this.get = this.set = this.remove = this.clear = this.isExpired = _.noop; + }, + _safeSet: function(key, val) { + try { + this.ls.setItem(key, val); + } catch (err) { + if (err.name === "QuotaExceededError") { + this.clear(); + this._noop(); } - return decode(ls.getItem(this._prefix(key))); - }, - set: function(key, val, ttl) { - if (_.isNumber(ttl)) { - ls.setItem(this._ttlKey(key), encode(now() + ttl)); - } else { - ls.removeItem(this._ttlKey(key)); - } - return ls.setItem(this._prefix(key), encode(val)); - }, - remove: function(key) { - ls.removeItem(this._ttlKey(key)); - ls.removeItem(this._prefix(key)); - return this; - }, - clear: function() { - var i, key, keys = [], len = ls.length; - for (i = 0; i < len; i++) { - if ((key = ls.key(i)).match(this.keyMatcher)) { - keys.push(key.replace(this.keyMatcher, "")); - } - } - for (i = keys.length; i--; ) { - this.remove(keys[i]); - } - return this; - }, - isExpired: function(key) { - var ttl = decode(ls.getItem(this._ttlKey(key))); - return _.isNumber(ttl) && now() > ttl ? true : false; } - }; - } else { - methods = { - get: _.noop, - set: _.noop, - remove: _.noop, - clear: _.noop, - isExpired: _.noop - }; - } - _.mixin(PersistentStorage.prototype, methods); + }, + get: function(key) { + if (this.isExpired(key)) { + this.remove(key); + } + return decode(this.ls.getItem(this._prefix(key))); + }, + set: function(key, val, ttl) { + if (_.isNumber(ttl)) { + this._safeSet(this._ttlKey(key), encode(now() + ttl)); + } else { + this.ls.removeItem(this._ttlKey(key)); + } + return this._safeSet(this._prefix(key), encode(val)); + }, + remove: function(key) { + this.ls.removeItem(this._ttlKey(key)); + this.ls.removeItem(this._prefix(key)); + return this; + }, + clear: function() { + var i, keys = gatherMatchingKeys(this.keyMatcher); + for (i = keys.length; i--; ) { + this.remove(keys[i]); + } + return this; + }, + isExpired: function(key) { + var ttl = decode(this.ls.getItem(this._ttlKey(key))); + return _.isNumber(ttl) && now() > ttl ? true : false; + } + }); return PersistentStorage; function now() { return new Date().getTime(); @@ -302,105 +350,104 @@ return JSON.stringify(_.isUndefined(val) ? null : val); } function decode(val) { - return JSON.parse(val); + return $.parseJSON(val); + } + function gatherMatchingKeys(keyMatcher) { + var i, key, keys = [], len = LOCAL_STORAGE.length; + for (i = 0; i < len; i++) { + if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) { + keys.push(key.replace(keyMatcher, "")); + } + } + return keys; } }(); var Transport = function() { "use strict"; - var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10); + var pendingRequestsCount = 0, pendingRequests = {}, sharedCache = new LruCache(10); function Transport(o) { o = o || {}; + this.maxPendingRequests = o.maxPendingRequests || 6; this.cancelled = false; - this.lastUrl = null; - this._send = o.transport ? callbackToDeferred(o.transport) : $.ajax; - this._get = o.rateLimiter ? o.rateLimiter(this._get) : this._get; + this.lastReq = null; + this._send = o.transport; + this._get = o.limiter ? o.limiter(this._get) : this._get; this._cache = o.cache === false ? new LruCache(0) : sharedCache; } Transport.setMaxPendingRequests = function setMaxPendingRequests(num) { - maxPendingRequests = num; + this.maxPendingRequests = num; }; Transport.resetCache = function resetCache() { sharedCache.reset(); }; _.mixin(Transport.prototype, { - _get: function(url, o, cb) { - var that = this, jqXhr; - if (this.cancelled || url !== this.lastUrl) { + _fingerprint: function fingerprint(o) { + o = o || {}; + return o.url + o.type + $.param(o.data || {}); + }, + _get: function(o, cb) { + var that = this, fingerprint, jqXhr; + fingerprint = this._fingerprint(o); + if (this.cancelled || fingerprint !== this.lastReq) { return; } - if (jqXhr = pendingRequests[url]) { + if (jqXhr = pendingRequests[fingerprint]) { jqXhr.done(done).fail(fail); - } else if (pendingRequestsCount < maxPendingRequests) { + } else if (pendingRequestsCount < this.maxPendingRequests) { pendingRequestsCount++; - pendingRequests[url] = this._send(url, o).done(done).fail(fail).always(always); + pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always); } else { this.onDeckRequestArgs = [].slice.call(arguments, 0); } function done(resp) { - cb && cb(null, resp); - that._cache.set(url, resp); + cb(null, resp); + that._cache.set(fingerprint, resp); } function fail() { - cb && cb(true); + cb(true); } function always() { pendingRequestsCount--; - delete pendingRequests[url]; + delete pendingRequests[fingerprint]; if (that.onDeckRequestArgs) { that._get.apply(that, that.onDeckRequestArgs); that.onDeckRequestArgs = null; } } }, - get: function(url, o, cb) { - var resp; - if (_.isFunction(o)) { - cb = o; - o = {}; - } + get: function(o, cb) { + var resp, fingerprint; + cb = cb || $.noop; + o = _.isString(o) ? { + url: o + } : o || {}; + fingerprint = this._fingerprint(o); this.cancelled = false; - this.lastUrl = url; - if (resp = this._cache.get(url)) { - _.defer(function() { - cb && cb(null, resp); - }); + this.lastReq = fingerprint; + if (resp = this._cache.get(fingerprint)) { + cb(null, resp); } else { - this._get(url, o, cb); + this._get(o, cb); } - return !!resp; }, cancel: function() { this.cancelled = true; } }); return Transport; - function callbackToDeferred(fn) { - return function customSendWrapper(url, o) { - var deferred = $.Deferred(); - fn(url, o, onSuccess, onError); - return deferred; - function onSuccess(resp) { - _.defer(function() { - deferred.resolve(resp); - }); - } - function onError(err) { - _.defer(function() { - deferred.reject(err); - }); - } - }; - } }(); - var SearchIndex = function() { + var SearchIndex = window.SearchIndex = function() { "use strict"; + var CHILDREN = "c", IDS = "i"; function SearchIndex(o) { o = o || {}; if (!o.datumTokenizer || !o.queryTokenizer) { $.error("datumTokenizer and queryTokenizer are both required"); } + this.identify = o.identify || _.stringify; this.datumTokenizer = o.datumTokenizer; this.queryTokenizer = o.queryTokenizer; + this.matchAnyQueryToken = o.matchAnyQueryToken; this.reset(); } _.mixin(SearchIndex.prototype, { @@ -413,46 +460,61 @@ data = _.isArray(data) ? data : [ data ]; _.each(data, function(datum) { var id, tokens; - id = that.datums.push(datum) - 1; + that.datums[id = that.identify(datum)] = datum; tokens = normalizeTokens(that.datumTokenizer(datum)); _.each(tokens, function(token) { var node, chars, ch; node = that.trie; chars = token.split(""); while (ch = chars.shift()) { - node = node.children[ch] || (node.children[ch] = newNode()); - node.ids.push(id); + node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode()); + node[IDS].push(id); } }); }); }, - get: function get(query) { + get: function get(ids) { + var that = this; + return _.map(ids, function(id) { + return that.datums[id]; + }); + }, + search: function search(query) { var that = this, tokens, matches; tokens = normalizeTokens(this.queryTokenizer(query)); _.each(tokens, function(token) { var node, chars, ch, ids; - if (matches && matches.length === 0) { + if (matches && matches.length === 0 && !that.matchAnyQueryToken) { return false; } node = that.trie; chars = token.split(""); while (node && (ch = chars.shift())) { - node = node.children[ch]; + node = node[CHILDREN][ch]; } if (node && chars.length === 0) { - ids = node.ids.slice(0); + ids = node[IDS].slice(0); matches = matches ? getIntersection(matches, ids) : ids; } else { - matches = []; - return false; + if (!that.matchAnyQueryToken) { + matches = []; + return false; + } } }); return matches ? _.map(unique(matches), function(id) { return that.datums[id]; }) : []; }, + all: function all() { + var values = []; + for (var key in this.datums) { + values.push(this.datums[key]); + } + return values; + }, reset: function reset() { - this.datums = []; + this.datums = {}; this.trie = newNode(); }, serialize: function serialize() { @@ -473,10 +535,10 @@ return tokens; } function newNode() { - return { - ids: [], - children: {} - }; + var node = {}; + node[IDS] = []; + node[CHILDREN] = {}; + return node; } function unique(array) { var seen = {}, uniques = []; @@ -490,8 +552,8 @@ } function getIntersection(arrayA, arrayB) { var ai = 0, bi = 0, intersection = []; - arrayA = arrayA.sort(compare); - arrayB = arrayB.sort(compare); + arrayA = arrayA.sort(); + arrayB = arrayB.sort(); var lenArrayA = arrayA.length, lenArrayB = arrayB.length; while (ai < lenArrayA && bi < lenArrayB) { if (arrayA[ai] < arrayB[bi]) { @@ -505,169 +567,331 @@ } } return intersection; - function compare(a, b) { - return a - b; - } } }(); - var oParser = function() { + var Prefetch = function() { "use strict"; - return { - local: getLocal, - prefetch: getPrefetch, - remote: getRemote - }; - function getLocal(o) { - return o.local || null; - } - function getPrefetch(o) { - var prefetch, defaults; - defaults = { - url: null, - thumbprint: "", - ttl: 24 * 60 * 60 * 1e3, - filter: null, - ajax: {} - }; - if (prefetch = o.prefetch || null) { - prefetch = _.isString(prefetch) ? { - url: prefetch - } : prefetch; - prefetch = _.mixin(defaults, prefetch); - prefetch.thumbprint = VERSION + prefetch.thumbprint; - prefetch.ajax.type = prefetch.ajax.type || "GET"; - prefetch.ajax.dataType = prefetch.ajax.dataType || "json"; - !prefetch.url && $.error("prefetch requires url to be set"); - } - return prefetch; - } - function getRemote(o) { - var remote, defaults; - defaults = { - url: null, - cache: true, - wildcard: "%QUERY", - replace: null, - rateLimitBy: "debounce", - rateLimitWait: 300, - send: null, - filter: null, - ajax: {} - }; - if (remote = o.remote || null) { - remote = _.isString(remote) ? { - url: remote - } : remote; - remote = _.mixin(defaults, remote); - remote.rateLimiter = /^throttle$/i.test(remote.rateLimitBy) ? byThrottle(remote.rateLimitWait) : byDebounce(remote.rateLimitWait); - remote.ajax.type = remote.ajax.type || "GET"; - remote.ajax.dataType = remote.ajax.dataType || "json"; - delete remote.rateLimitBy; - delete remote.rateLimitWait; - !remote.url && $.error("remote requires url to be set"); - } - return remote; - function byDebounce(wait) { - return function(fn) { - return _.debounce(fn, wait); - }; - } - function byThrottle(wait) { - return function(fn) { - return _.throttle(fn, wait); - }; - } - } - }(); - (function(root) { - "use strict"; - var old, keys; - old = root.Bloodhound; + var keys; keys = { data: "data", protocol: "protocol", thumbprint: "thumbprint" }; - root.Bloodhound = Bloodhound; - function Bloodhound(o) { - if (!o || !o.local && !o.prefetch && !o.remote) { - $.error("one of local, prefetch, or remote is required"); + function Prefetch(o) { + this.url = o.url; + this.ttl = o.ttl; + this.cache = o.cache; + this.prepare = o.prepare; + this.transform = o.transform; + this.transport = o.transport; + this.thumbprint = o.thumbprint; + this.storage = new PersistentStorage(o.cacheKey); + } + _.mixin(Prefetch.prototype, { + _settings: function settings() { + return { + url: this.url, + type: "GET", + dataType: "json" + }; + }, + store: function store(data) { + if (!this.cache) { + return; + } + this.storage.set(keys.data, data, this.ttl); + this.storage.set(keys.protocol, location.protocol, this.ttl); + this.storage.set(keys.thumbprint, this.thumbprint, this.ttl); + }, + fromCache: function fromCache() { + var stored = {}, isExpired; + if (!this.cache) { + return null; + } + stored.data = this.storage.get(keys.data); + stored.protocol = this.storage.get(keys.protocol); + stored.thumbprint = this.storage.get(keys.thumbprint); + isExpired = stored.thumbprint !== this.thumbprint || stored.protocol !== location.protocol; + return stored.data && !isExpired ? stored.data : null; + }, + fromNetwork: function(cb) { + var that = this, settings; + if (!cb) { + return; + } + settings = this.prepare(this._settings()); + this.transport(settings).fail(onError).done(onResponse); + function onError() { + cb(true); + } + function onResponse(resp) { + cb(null, that.transform(resp)); + } + }, + clear: function clear() { + this.storage.clear(); + return this; } - this.limit = o.limit || 5; - this.sorter = getSorter(o.sorter); - this.dupDetector = o.dupDetector || ignoreDuplicates; - this.local = oParser.local(o); - this.prefetch = oParser.prefetch(o); - this.remote = oParser.remote(o); - this.cacheKey = this.prefetch ? this.prefetch.cacheKey || this.prefetch.url : null; + }); + return Prefetch; + }(); + var Remote = function() { + "use strict"; + function Remote(o) { + this.url = o.url; + this.prepare = o.prepare; + this.transform = o.transform; + this.indexResponse = o.indexResponse; + this.transport = new Transport({ + cache: o.cache, + limiter: o.limiter, + transport: o.transport, + maxPendingRequests: o.maxPendingRequests + }); + } + _.mixin(Remote.prototype, { + _settings: function settings() { + return { + url: this.url, + type: "GET", + dataType: "json" + }; + }, + get: function get(query, cb) { + var that = this, settings; + if (!cb) { + return; + } + query = query || ""; + settings = this.prepare(query, this._settings()); + return this.transport.get(settings, onResponse); + function onResponse(err, resp) { + err ? cb([]) : cb(that.transform(resp)); + } + }, + cancelLastRequest: function cancelLastRequest() { + this.transport.cancel(); + } + }); + return Remote; + }(); + var oParser = function() { + "use strict"; + return function parse(o) { + var defaults, sorter; + defaults = { + initialize: true, + identify: _.stringify, + datumTokenizer: null, + queryTokenizer: null, + matchAnyQueryToken: false, + sufficient: 5, + indexRemote: false, + sorter: null, + local: [], + prefetch: null, + remote: null + }; + o = _.mixin(defaults, o || {}); + !o.datumTokenizer && $.error("datumTokenizer is required"); + !o.queryTokenizer && $.error("queryTokenizer is required"); + sorter = o.sorter; + o.sorter = sorter ? function(x) { + return x.sort(sorter); + } : _.identity; + o.local = _.isFunction(o.local) ? o.local() : o.local; + o.prefetch = parsePrefetch(o.prefetch); + o.remote = parseRemote(o.remote); + return o; + }; + function parsePrefetch(o) { + var defaults; + if (!o) { + return null; + } + defaults = { + url: null, + ttl: 24 * 60 * 60 * 1e3, + cache: true, + cacheKey: null, + thumbprint: "", + prepare: _.identity, + transform: _.identity, + transport: null + }; + o = _.isString(o) ? { + url: o + } : o; + o = _.mixin(defaults, o); + !o.url && $.error("prefetch requires url to be set"); + o.transform = o.filter || o.transform; + o.cacheKey = o.cacheKey || o.url; + o.thumbprint = VERSION + o.thumbprint; + o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; + return o; + } + function parseRemote(o) { + var defaults; + if (!o) { + return; + } + defaults = { + url: null, + cache: true, + prepare: null, + replace: null, + wildcard: null, + limiter: null, + rateLimitBy: "debounce", + rateLimitWait: 300, + transform: _.identity, + transport: null + }; + o = _.isString(o) ? { + url: o + } : o; + o = _.mixin(defaults, o); + !o.url && $.error("remote requires url to be set"); + o.transform = o.filter || o.transform; + o.prepare = toRemotePrepare(o); + o.limiter = toLimiter(o); + o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; + delete o.replace; + delete o.wildcard; + delete o.rateLimitBy; + delete o.rateLimitWait; + return o; + } + function toRemotePrepare(o) { + var prepare, replace, wildcard; + prepare = o.prepare; + replace = o.replace; + wildcard = o.wildcard; + if (prepare) { + return prepare; + } + if (replace) { + prepare = prepareByReplace; + } else if (o.wildcard) { + prepare = prepareByWildcard; + } else { + prepare = identityPrepare; + } + return prepare; + function prepareByReplace(query, settings) { + settings.url = replace(settings.url, query); + return settings; + } + function prepareByWildcard(query, settings) { + settings.url = settings.url.replace(wildcard, encodeURIComponent(query)); + return settings; + } + function identityPrepare(query, settings) { + return settings; + } + } + function toLimiter(o) { + var limiter, method, wait; + limiter = o.limiter; + method = o.rateLimitBy; + wait = o.rateLimitWait; + if (!limiter) { + limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait); + } + return limiter; + function debounce(wait) { + return function debounce(fn) { + return _.debounce(fn, wait); + }; + } + function throttle(wait) { + return function throttle(fn) { + return _.throttle(fn, wait); + }; + } + } + function callbackToDeferred(fn) { + return function wrapper(o) { + var deferred = $.Deferred(); + fn(o, onSuccess, onError); + return deferred; + function onSuccess(resp) { + _.defer(function() { + deferred.resolve(resp); + }); + } + function onError(err) { + _.defer(function() { + deferred.reject(err); + }); + } + }; + } + }(); + var Bloodhound = function() { + "use strict"; + var old; + old = window && window.Bloodhound; + function Bloodhound(o) { + o = oParser(o); + this.sorter = o.sorter; + this.identify = o.identify; + this.sufficient = o.sufficient; + this.indexRemote = o.indexRemote; + this.local = o.local; + this.remote = o.remote ? new Remote(o.remote) : null; + this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null; this.index = new SearchIndex({ + identify: this.identify, datumTokenizer: o.datumTokenizer, queryTokenizer: o.queryTokenizer }); - this.storage = this.cacheKey ? new PersistentStorage(this.cacheKey) : null; + o.initialize !== false && this.initialize(); } Bloodhound.noConflict = function noConflict() { - root.Bloodhound = old; + window && (window.Bloodhound = old); return Bloodhound; }; Bloodhound.tokenizers = tokenizers; _.mixin(Bloodhound.prototype, { - _loadPrefetch: function loadPrefetch(o) { - var that = this, serialized, deferred; - if (serialized = this._readFromStorage(o.thumbprint)) { + __ttAdapter: function ttAdapter() { + var that = this; + return this.remote ? withAsync : withoutAsync; + function withAsync(query, sync, async) { + return that.search(query, sync, async); + } + function withoutAsync(query, sync) { + return that.search(query, sync); + } + }, + _loadPrefetch: function loadPrefetch() { + var that = this, deferred, serialized; + deferred = $.Deferred(); + if (!this.prefetch) { + deferred.resolve(); + } else if (serialized = this.prefetch.fromCache()) { this.index.bootstrap(serialized); - deferred = $.Deferred().resolve(); + deferred.resolve(); } else { - deferred = $.ajax(o.url, o.ajax).done(handlePrefetchResponse); + this.prefetch.fromNetwork(done); } - return deferred; - function handlePrefetchResponse(resp) { - that.clear(); - that.add(o.filter ? o.filter(resp) : resp); - that._saveToStorage(that.index.serialize(), o.thumbprint, o.ttl); + return deferred.promise(); + function done(err, data) { + if (err) { + return deferred.reject(); + } + that.add(data); + that.prefetch.store(that.index.serialize()); + deferred.resolve(); } }, - _getFromRemote: function getFromRemote(query, cb) { - var that = this, url, uriEncodedQuery; - if (!this.transport) { - return; - } - query = query || ""; - uriEncodedQuery = encodeURIComponent(query); - url = this.remote.replace ? this.remote.replace(this.remote.url, query) : this.remote.url.replace(this.remote.wildcard, uriEncodedQuery); - return this.transport.get(url, this.remote.ajax, handleRemoteResponse); - function handleRemoteResponse(err, resp) { - err ? cb([]) : cb(that.remote.filter ? that.remote.filter(resp) : resp); - } - }, - _cancelLastRemoteRequest: function cancelLastRemoteRequest() { - this.transport && this.transport.cancel(); - }, - _saveToStorage: function saveToStorage(data, thumbprint, ttl) { - if (this.storage) { - this.storage.set(keys.data, data, ttl); - this.storage.set(keys.protocol, location.protocol, ttl); - this.storage.set(keys.thumbprint, thumbprint, ttl); - } - }, - _readFromStorage: function readFromStorage(thumbprint) { - var stored = {}, isExpired; - if (this.storage) { - stored.data = this.storage.get(keys.data); - stored.protocol = this.storage.get(keys.protocol); - stored.thumbprint = this.storage.get(keys.thumbprint); - } - isExpired = stored.thumbprint !== thumbprint || stored.protocol !== location.protocol; - return stored.data && !isExpired ? stored.data : null; - }, _initialize: function initialize() { - var that = this, local = this.local, deferred; - deferred = this.prefetch ? this._loadPrefetch(this.prefetch) : $.Deferred().resolve(); - local && deferred.done(addLocalToIndex); - this.transport = this.remote ? new Transport(this.remote) : null; - return this.initPromise = deferred.promise(); + var that = this, deferred; + this.clear(); + (this.initPromise = this._loadPrefetch()).done(addLocalToIndex); + return this.initPromise; function addLocalToIndex() { - that.add(_.isFunction(local) ? local() : local); + that.add(that.local); } }, initialize: function initialize(force) { @@ -675,129 +899,318 @@ }, add: function add(data) { this.index.add(data); + return this; }, - get: function get(query, cb) { - var that = this, matches = [], cacheHit = false; - matches = this.index.get(query); - matches = this.sorter(matches).slice(0, this.limit); - matches.length < this.limit ? cacheHit = this._getFromRemote(query, returnRemoteMatches) : this._cancelLastRemoteRequest(); - if (!cacheHit) { - (matches.length > 0 || !this.transport) && cb && cb(matches); + get: function get(ids) { + ids = _.isArray(ids) ? ids : [].slice.call(arguments); + return this.index.get(ids); + }, + search: function search(query, sync, async) { + var that = this, local; + sync = sync || _.noop; + async = async || _.noop; + local = this.sorter(this.index.search(query)); + sync(this.remote ? local.slice() : local); + if (this.remote && local.length < this.sufficient) { + this.remote.get(query, processRemote); + } else if (this.remote) { + this.remote.cancelLastRequest(); } - function returnRemoteMatches(remoteMatches) { - var matchesWithBackfill = matches.slice(0); - _.each(remoteMatches, function(remoteMatch) { - var isDuplicate; - isDuplicate = _.some(matchesWithBackfill, function(match) { - return that.dupDetector(remoteMatch, match); - }); - !isDuplicate && matchesWithBackfill.push(remoteMatch); - return matchesWithBackfill.length < that.limit; + return this; + function processRemote(remote) { + var nonDuplicates = []; + _.each(remote, function(r) { + !_.some(local, function(l) { + return that.identify(r) === that.identify(l); + }) && nonDuplicates.push(r); }); - cb && cb(that.sorter(matchesWithBackfill)); + that.indexRemote && that.add(nonDuplicates); + async(nonDuplicates); } }, + all: function all() { + return this.index.all(); + }, clear: function clear() { this.index.reset(); + return this; }, clearPrefetchCache: function clearPrefetchCache() { - this.storage && this.storage.clear(); + this.prefetch && this.prefetch.clear(); + return this; }, clearRemoteCache: function clearRemoteCache() { - this.transport && Transport.resetCache(); + Transport.resetCache(); + return this; }, ttAdapter: function ttAdapter() { - return _.bind(this.get, this); + return this.__ttAdapter(); } }); return Bloodhound; - function getSorter(sortFn) { - return _.isFunction(sortFn) ? sort : noSort; - function sort(array) { - return array.sort(sortFn); - } - function noSort(array) { - return array; - } - } - function ignoreDuplicates() { - return false; - } - })(this); - var html = function() { + }(); + return Bloodhound; +}); + +(function(root, factory) { + if (typeof define === "function" && define.amd) { + define([ "jquery" ], function(a0) { + return factory(a0); + }); + } else if (typeof module === "object" && module.exports) { + module.exports = factory(require("jquery")); + } else { + factory(root["jQuery"]); + } +})(this, function($) { + var _ = function() { + "use strict"; return { - wrapper: '', - dropdown: '', - dataset: '
', - suggestions: '', - suggestion: '
' + isMsie: function() { + return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; + }, + isBlankString: function(str) { + return !str || /^\s*$/.test(str); + }, + escapeRegExChars: function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }, + isString: function(obj) { + return typeof obj === "string"; + }, + isNumber: function(obj) { + return typeof obj === "number"; + }, + isArray: $.isArray, + isFunction: $.isFunction, + isObject: $.isPlainObject, + isUndefined: function(obj) { + return typeof obj === "undefined"; + }, + isElement: function(obj) { + return !!(obj && obj.nodeType === 1); + }, + isJQuery: function(obj) { + return obj instanceof $; + }, + toStr: function toStr(s) { + return _.isUndefined(s) || s === null ? "" : s + ""; + }, + bind: $.proxy, + each: function(collection, cb) { + $.each(collection, reverseArgs); + function reverseArgs(index, value) { + return cb(value, index); + } + }, + map: $.map, + filter: $.grep, + every: function(obj, test) { + var result = true; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (!(result = test.call(null, val, key, obj))) { + return false; + } + }); + return !!result; + }, + some: function(obj, test) { + var result = false; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (result = test.call(null, val, key, obj)) { + return false; + } + }); + return !!result; + }, + mixin: $.extend, + identity: function(x) { + return x; + }, + clone: function(obj) { + return $.extend(true, {}, obj); + }, + getIdGenerator: function() { + var counter = 0; + return function() { + return counter++; + }; + }, + templatify: function templatify(obj) { + return $.isFunction(obj) ? obj : template; + function template() { + return String(obj); + } + }, + defer: function(fn) { + setTimeout(fn, 0); + }, + debounce: function(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments, later, callNow; + later = function() { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + } + }; + callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + } + return result; + }; + }, + throttle: function(func, wait) { + var context, args, timeout, result, previous, later; + previous = 0; + later = function() { + previous = new Date(); + timeout = null; + result = func.apply(context, args); + }; + return function() { + var now = new Date(), remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }, + stringify: function(val) { + return _.isString(val) ? val : JSON.stringify(val); + }, + guid: function() { + function _p8(s) { + var p = (Math.random().toString(16) + "000000000").substr(2, 8); + return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p; + } + return "tt-" + _p8() + _p8(true) + _p8(true) + _p8(); + }, + noop: function() {} }; }(); - var css = function() { + var WWW = function() { "use strict"; - var css = { - wrapper: { - position: "relative", - display: "inline-block" - }, - hint: { - position: "absolute", - top: "0", - left: "0", - borderColor: "transparent", - boxShadow: "none", - opacity: "1" - }, - input: { - position: "relative", - verticalAlign: "top", - backgroundColor: "transparent" - }, - inputWithNoHint: { - position: "relative", - verticalAlign: "top" - }, - dropdown: { - position: "absolute", - top: "100%", - left: "0", - zIndex: "100", - display: "none" - }, - suggestions: { - display: "block" - }, - suggestion: { - whiteSpace: "nowrap", - cursor: "pointer" - }, - suggestionChild: { - whiteSpace: "normal" - }, - ltr: { - left: "0", - right: "auto" - }, - rtl: { - left: "auto", - right: " 0" - } + var defaultClassNames = { + wrapper: "twitter-typeahead", + input: "tt-input", + hint: "tt-hint", + menu: "tt-menu", + dataset: "tt-dataset", + suggestion: "tt-suggestion", + selectable: "tt-selectable", + empty: "tt-empty", + open: "tt-open", + cursor: "tt-cursor", + highlight: "tt-highlight" }; - if (_.isMsie()) { - _.mixin(css.input, { - backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" - }); + return build; + function build(o) { + var www, classes; + classes = _.mixin({}, defaultClassNames, o); + www = { + css: buildCss(), + classes: classes, + html: buildHtml(classes), + selectors: buildSelectors(classes) + }; + return { + css: www.css, + html: www.html, + classes: www.classes, + selectors: www.selectors, + mixin: function(o) { + _.mixin(o, www); + } + }; } - if (_.isMsie() && _.isMsie() <= 7) { - _.mixin(css.input, { - marginTop: "-1px" - }); + function buildHtml(c) { + return { + wrapper: '', + menu: '
' + }; + } + function buildSelectors(classes) { + var selectors = {}; + _.each(classes, function(v, k) { + selectors[k] = "." + v; + }); + return selectors; + } + function buildCss() { + var css = { + wrapper: { + position: "relative", + display: "inline-block" + }, + hint: { + position: "absolute", + top: "0", + left: "0", + borderColor: "transparent", + boxShadow: "none", + opacity: "1" + }, + input: { + position: "relative", + verticalAlign: "top", + backgroundColor: "transparent" + }, + inputWithNoHint: { + position: "relative", + verticalAlign: "top" + }, + menu: { + position: "absolute", + top: "100%", + left: "0", + zIndex: "100", + display: "none" + }, + ltr: { + left: "0", + right: "auto" + }, + rtl: { + left: "auto", + right: " 0" + } + }; + if (_.isMsie()) { + _.mixin(css.input, { + backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" + }); + } + return css; } - return css; }(); var EventBus = function() { "use strict"; - var namespace = "typeahead:"; + var namespace, deprecationMap; + namespace = "typeahead:"; + deprecationMap = { + render: "rendered", + cursorchange: "cursorchanged", + select: "selected", + autocomplete: "autocompleted" + }; function EventBus(o) { if (!o || !o.el) { $.error("EventBus initialized without el"); @@ -805,9 +1218,23 @@ this.$el = $(o.el); } _.mixin(EventBus.prototype, { + _trigger: function(type, args) { + var $e = $.Event(namespace + type); + this.$el.trigger.call(this.$el, $e, args || []); + return $e; + }, + before: function(type) { + var args, $e; + args = [].slice.call(arguments, 1); + $e = this._trigger("before" + type, args); + return $e.isDefaultPrevented(); + }, trigger: function(type) { - var args = [].slice.call(arguments, 1); - this.$el.trigger(namespace + type, args); + var deprecatedType; + this._trigger(type, [].slice.call(arguments, 1)); + if (deprecatedType = deprecationMap[type]) { + this._trigger(deprecatedType, [].slice.call(arguments, 1)); + } } }); return EventBus; @@ -910,7 +1337,36 @@ tagName: "strong", className: null, wordsOnly: false, - caseSensitive: false + caseSensitive: false, + diacriticInsensitive: false + }; + var accented = { + A: "[AaªÀ-Åà-åĀ-ąǍǎȀ-ȃȦȧᴬᵃḀḁẚẠ-ảₐ℀℁℻⒜Ⓐⓐ㍱-㍴㎀-㎄㎈㎉㎩-㎯㏂㏊㏟㏿Aa]", + B: "[BbᴮᵇḂ-ḇℬ⒝Ⓑⓑ㍴㎅-㎇㏃㏈㏔㏝Bb]", + C: "[CcÇçĆ-čᶜ℀ℂ℃℅℆ℭⅭⅽ⒞Ⓒⓒ㍶㎈㎉㎝㎠㎤㏄-㏇Cc]", + D: "[DdĎďDŽ-džDZ-dzᴰᵈḊ-ḓⅅⅆⅮⅾ⒟Ⓓⓓ㋏㍲㍷-㍹㎗㎭-㎯㏅㏈Dd]", + E: "[EeÈ-Ëè-ëĒ-ěȄ-ȇȨȩᴱᵉḘ-ḛẸ-ẽₑ℡ℯℰⅇ⒠Ⓔⓔ㉐㋍㋎Ee]", + F: "[FfᶠḞḟ℉ℱ℻⒡Ⓕⓕ㎊-㎌㎙ff-fflFf]", + G: "[GgĜ-ģǦǧǴǵᴳᵍḠḡℊ⒢Ⓖⓖ㋌㋍㎇㎍-㎏㎓㎬㏆㏉㏒㏿Gg]", + H: "[HhĤĥȞȟʰᴴḢ-ḫẖℋ-ℎ⒣Ⓗⓗ㋌㍱㎐-㎔㏊㏋㏗Hh]", + I: "[IiÌ-Ïì-ïĨ-İIJijǏǐȈ-ȋᴵᵢḬḭỈ-ịⁱℐℑℹⅈⅠ-ⅣⅥ-ⅨⅪⅫⅰ-ⅳⅵ-ⅸⅺⅻ⒤Ⓘⓘ㍺㏌㏕fiffiIi]", + J: "[JjIJ-ĵLJ-njǰʲᴶⅉ⒥ⒿⓙⱼJj]", + K: "[KkĶķǨǩᴷᵏḰ-ḵK⒦Ⓚⓚ㎄㎅㎉㎏㎑㎘㎞㎢㎦㎪㎸㎾㏀㏆㏍-㏏Kk]", + L: "[LlĹ-ŀLJ-ljˡᴸḶḷḺ-ḽℒℓ℡Ⅼⅼ⒧Ⓛⓛ㋏㎈㎉㏐-㏓㏕㏖㏿flfflLl]", + M: "[MmᴹᵐḾ-ṃ℠™ℳⅯⅿ⒨Ⓜⓜ㍷-㍹㎃㎆㎎㎒㎖㎙-㎨㎫㎳㎷㎹㎽㎿㏁㏂㏎㏐㏔-㏖㏘㏙㏞㏟Mm]", + N: "[NnÑñŃ-ʼnNJ-njǸǹᴺṄ-ṋⁿℕ№⒩Ⓝⓝ㎁㎋㎚㎱㎵㎻㏌㏑Nn]", + O: "[OoºÒ-Öò-öŌ-őƠơǑǒǪǫȌ-ȏȮȯᴼᵒỌ-ỏₒ℅№ℴ⒪Ⓞⓞ㍵㏇㏒㏖Oo]", + P: "[PpᴾᵖṔ-ṗℙ⒫Ⓟⓟ㉐㍱㍶㎀㎊㎩-㎬㎰㎴㎺㏋㏗-㏚Pp]", + Q: "[Qqℚ⒬Ⓠⓠ㏃Qq]", + R: "[RrŔ-řȐ-ȓʳᴿᵣṘ-ṛṞṟ₨ℛ-ℝ⒭Ⓡⓡ㋍㍴㎭-㎯㏚㏛Rr]", + S: "[SsŚ-šſȘșˢṠ-ṣ₨℁℠⒮Ⓢⓢ㎧㎨㎮-㎳㏛㏜stSs]", + T: "[TtŢ-ťȚțᵀᵗṪ-ṱẗ℡™⒯Ⓣⓣ㉐㋏㎔㏏ſtstTt]", + U: "[UuÙ-Üù-üŨ-ųƯưǓǔȔ-ȗᵁᵘᵤṲ-ṷỤ-ủ℆⒰Ⓤⓤ㍳㍺Uu]", + V: "[VvᵛᵥṼ-ṿⅣ-Ⅷⅳ-ⅷ⒱Ⓥⓥⱽ㋎㍵㎴-㎹㏜㏞Vv]", + W: "[WwŴŵʷᵂẀ-ẉẘ⒲Ⓦⓦ㎺-㎿㏝Ww]", + X: "[XxˣẊ-ẍₓ℻Ⅸ-Ⅻⅸ-ⅻ⒳Ⓧⓧ㏓Xx]", + Y: "[YyÝýÿŶ-ŸȲȳʸẎẏẙỲ-ỹ⒴Ⓨⓨ㏉Yy]", + Z: "[ZzŹ-žDZ-dzᶻẐ-ẕℤℨ⒵Ⓩⓩ㎐-㎔Zz]" }; return function hightlight(o) { var regex; @@ -919,7 +1375,7 @@ return; } o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; - regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly); + regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly, o.diacriticInsensitive); traverse(o.node, hightlightTextNode); function hightlightTextNode(textNode) { var match, patternNode, wrapperNode; @@ -945,10 +1401,17 @@ } } }; - function getRegex(patterns, caseSensitive, wordsOnly) { + function accent_replacer(chr) { + return accented[chr.toUpperCase()] || chr; + } + function getRegex(patterns, caseSensitive, wordsOnly, diacriticInsensitive) { var escapedPatterns = [], regexStr; for (var i = 0, len = patterns.length; i < len; i++) { - escapedPatterns.push(_.escapeRegExChars(patterns[i])); + var escapedWord = _.escapeRegExChars(patterns[i]); + if (diacriticInsensitive) { + escapedWord = escapedWord.replace(/\S/g, accent_replacer); + } + escapedPatterns.push(escapedWord); } regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); @@ -966,36 +1429,38 @@ 38: "up", 40: "down" }; - function Input(o) { - var that = this, onBlur, onFocus, onKeydown, onInput; + function Input(o, www) { + var id; o = o || {}; if (!o.input) { $.error("input is missing"); } - onBlur = _.bind(this._onBlur, this); - onFocus = _.bind(this._onFocus, this); - onKeydown = _.bind(this._onKeydown, this); - onInput = _.bind(this._onInput, this); + www.mixin(this); this.$hint = $(o.hint); - this.$input = $(o.input).on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); + this.$input = $(o.input); + this.$menu = $(o.menu); + id = this.$input.attr("id") || _.guid(); + this.$menu.attr("id", id + "_listbox"); + this.$hint.attr({ + "aria-hidden": true + }); + this.$input.attr({ + "aria-owns": id + "_listbox", + role: "combobox", + "aria-autocomplete": "list", + "aria-expanded": false + }); + this.query = this.$input.val(); + this.queryWhenFocused = this.hasFocus() ? this.query : null; + this.$overflowHelper = buildOverflowHelper(this.$input); + this._checkLanguageDirection(); if (this.$hint.length === 0) { this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; } - if (!_.isMsie()) { - this.$input.on("input.tt", onInput); - } else { - this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { - if (specialKeyCodeMap[$e.which || $e.keyCode]) { - return; - } - _.defer(_.bind(that._onInput, that, $e)); - }); - } - this.query = this.$input.val(); - this.$overflowHelper = buildOverflowHelper(this.$input); + this.onSync("cursorchange", this._updateDescendent); } Input.normalizeQuery = function(str) { - return (str || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); + return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " "); }; _.mixin(Input.prototype, EventEmitter, { _onBlur: function onBlur() { @@ -1003,6 +1468,7 @@ this.trigger("blurred"); }, _onFocus: function onFocus() { + this.queryWhenFocused = this.query; this.trigger("focused"); }, _onKeydown: function onKeydown($e) { @@ -1013,17 +1479,13 @@ } }, _onInput: function onInput() { - this._checkInputValue(); + this._setQuery(this.getInputValue()); + this.clearHintIfInvalid(); + this._checkLanguageDirection(); }, _managePreventDefault: function managePreventDefault(keyName, $e) { - var preventDefault, hintValue, inputValue; + var preventDefault; switch (keyName) { - case "tab": - hintValue = this.getHint(); - inputValue = this.getInputValue(); - preventDefault = hintValue && hintValue !== inputValue && !withModifier($e); - break; - case "up": case "down": preventDefault = !withModifier($e); @@ -1046,39 +1508,76 @@ } return trigger; }, - _checkInputValue: function checkInputValue() { - var inputValue, areEquivalent, hasDifferentWhitespace; - inputValue = this.getInputValue(); - areEquivalent = areQueriesEquivalent(inputValue, this.query); - hasDifferentWhitespace = areEquivalent ? this.query.length !== inputValue.length : false; - this.query = inputValue; - if (!areEquivalent) { + _checkLanguageDirection: function checkLanguageDirection() { + var dir = (this.$input.css("direction") || "ltr").toLowerCase(); + if (this.dir !== dir) { + this.dir = dir; + this.$hint.attr("dir", dir); + this.trigger("langDirChanged", dir); + } + }, + _setQuery: function setQuery(val, silent) { + var areEquivalent, hasDifferentWhitespace; + areEquivalent = areQueriesEquivalent(val, this.query); + hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false; + this.query = val; + if (!silent && !areEquivalent) { this.trigger("queryChanged", this.query); - } else if (hasDifferentWhitespace) { + } else if (!silent && hasDifferentWhitespace) { this.trigger("whitespaceChanged", this.query); } }, + _updateDescendent: function updateDescendent(event, id) { + this.$input.attr("aria-activedescendant", id); + }, + bind: function() { + var that = this, onBlur, onFocus, onKeydown, onInput; + onBlur = _.bind(this._onBlur, this); + onFocus = _.bind(this._onFocus, this); + onKeydown = _.bind(this._onKeydown, this); + onInput = _.bind(this._onInput, this); + this.$input.on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); + if (!_.isMsie() || _.isMsie() > 9) { + this.$input.on("input.tt", onInput); + } else { + this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { + if (specialKeyCodeMap[$e.which || $e.keyCode]) { + return; + } + _.defer(_.bind(that._onInput, that, $e)); + }); + } + return this; + }, focus: function focus() { this.$input.focus(); }, blur: function blur() { this.$input.blur(); }, - getQuery: function getQuery() { - return this.query; + getLangDir: function getLangDir() { + return this.dir; }, - setQuery: function setQuery(query) { - this.query = query; + getQuery: function getQuery() { + return this.query || ""; + }, + setQuery: function setQuery(val, silent) { + this.setInputValue(val); + this._setQuery(val, silent); + }, + hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() { + return this.query !== this.queryWhenFocused; }, getInputValue: function getInputValue() { return this.$input.val(); }, - setInputValue: function setInputValue(value, silent) { + setInputValue: function setInputValue(value) { this.$input.val(value); - silent ? this.clearHint() : this._checkInputValue(); + this.clearHintIfInvalid(); + this._checkLanguageDirection(); }, resetInputValue: function resetInputValue() { - this.setInputValue(this.query, true); + this.setInputValue(this.query); }, getHint: function getHint() { return this.$hint.val(); @@ -1097,8 +1596,8 @@ isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow(); !isValid && this.clearHint(); }, - getLanguageDirection: function getLanguageDirection() { - return (this.$input.css("direction") || "ltr").toLowerCase(); + hasFocus: function hasFocus() { + return this.$input.is(":focus"); }, hasOverflow: function hasOverflow() { var constraint = this.$input.width() - 2; @@ -1121,7 +1620,11 @@ destroy: function destroy() { this.$hint.off(".tt"); this.$input.off(".tt"); - this.$hint = this.$input = this.$overflowHelper = null; + this.$overflowHelper.remove(); + this.$hint = this.$input = this.$overflowHelper = $("
"); + }, + setAriaExpanded: function setAriaExpanded(value) { + this.$input.attr("aria-expanded", value); } }); return Input; @@ -1151,118 +1654,193 @@ }(); var Dataset = function() { "use strict"; - var datasetKey = "ttDataset", valueKey = "ttValue", datumKey = "ttDatum"; - function Dataset(o) { + var keys, nameGenerator; + keys = { + dataset: "tt-selectable-dataset", + val: "tt-selectable-display", + obj: "tt-selectable-object" + }; + nameGenerator = _.getIdGenerator(); + function Dataset(o, www) { o = o || {}; o.templates = o.templates || {}; + o.templates.notFound = o.templates.notFound || o.templates.empty; if (!o.source) { $.error("missing source"); } + if (!o.node) { + $.error("missing node"); + } if (o.name && !isValidName(o.name)) { $.error("invalid dataset name: " + o.name); } - this.query = null; + www.mixin(this); this.highlight = !!o.highlight; - this.name = o.name || _.getUniqueId(); - this.source = o.source; + this.name = _.toStr(o.name || nameGenerator()); + this.limit = o.limit || 5; this.displayFn = getDisplayFn(o.display || o.displayKey); this.templates = getTemplates(o.templates, this.displayFn); - this.$el = $(html.dataset.replace("%CLASS%", this.name)); + this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source; + this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async; + this._resetLastSuggestion(); + this.$el = $(o.node).attr("role", "presentation").addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name); } - Dataset.extractDatasetName = function extractDatasetName(el) { - return $(el).data(datasetKey); - }; - Dataset.extractValue = function extractDatum(el) { - return $(el).data(valueKey); - }; - Dataset.extractDatum = function extractDatum(el) { - return $(el).data(datumKey); + Dataset.extractData = function extractData(el) { + var $el = $(el); + if ($el.data(keys.obj)) { + return { + dataset: $el.data(keys.dataset) || "", + val: $el.data(keys.val) || "", + obj: $el.data(keys.obj) || null + }; + } + return null; }; _.mixin(Dataset.prototype, EventEmitter, { - _render: function render(query, suggestions) { - if (!this.$el) { - return; - } - var that = this, hasSuggestions; - this.$el.empty(); - hasSuggestions = suggestions && suggestions.length; - if (!hasSuggestions && this.templates.empty) { - this.$el.html(getEmptyHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null); - } else if (hasSuggestions) { - this.$el.html(getSuggestionsHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null); - } - this.trigger("rendered"); - function getEmptyHtml() { - return that.templates.empty({ - query: query, - isEmpty: true - }); - } - function getSuggestionsHtml() { - var $suggestions, nodes; - $suggestions = $(html.suggestions).css(css.suggestions); - nodes = _.map(suggestions, getSuggestionNode); - $suggestions.append.apply($suggestions, nodes); - that.highlight && highlight({ - className: "tt-highlight", - node: $suggestions[0], - pattern: query - }); - return $suggestions; - function getSuggestionNode(suggestion) { - var $el; - $el = $(html.suggestion).append(that.templates.suggestion(suggestion)).data(datasetKey, that.name).data(valueKey, that.displayFn(suggestion)).data(datumKey, suggestion); - $el.children().each(function() { - $(this).css(css.suggestionChild); - }); - return $el; - } - } - function getHeaderHtml() { - return that.templates.header({ - query: query, - isEmpty: !hasSuggestions - }); - } - function getFooterHtml() { - return that.templates.footer({ - query: query, - isEmpty: !hasSuggestions - }); + _overwrite: function overwrite(query, suggestions) { + suggestions = suggestions || []; + if (suggestions.length) { + this._renderSuggestions(query, suggestions); + } else if (this.async && this.templates.pending) { + this._renderPending(query); + } else if (!this.async && this.templates.notFound) { + this._renderNotFound(query); + } else { + this._empty(); } + this.trigger("rendered", suggestions, false, this.name); }, - getRoot: function getRoot() { - return this.$el; + _append: function append(query, suggestions) { + suggestions = suggestions || []; + if (suggestions.length && this.$lastSuggestion.length) { + this._appendSuggestions(query, suggestions); + } else if (suggestions.length) { + this._renderSuggestions(query, suggestions); + } else if (!this.$lastSuggestion.length && this.templates.notFound) { + this._renderNotFound(query); + } + this.trigger("rendered", suggestions, true, this.name); + }, + _renderSuggestions: function renderSuggestions(query, suggestions) { + var $fragment; + $fragment = this._getSuggestionsFragment(query, suggestions); + this.$lastSuggestion = $fragment.children().last(); + this.$el.html($fragment).prepend(this._getHeader(query, suggestions)).append(this._getFooter(query, suggestions)); + }, + _appendSuggestions: function appendSuggestions(query, suggestions) { + var $fragment, $lastSuggestion; + $fragment = this._getSuggestionsFragment(query, suggestions); + $lastSuggestion = $fragment.children().last(); + this.$lastSuggestion.after($fragment); + this.$lastSuggestion = $lastSuggestion; + }, + _renderPending: function renderPending(query) { + var template = this.templates.pending; + this._resetLastSuggestion(); + template && this.$el.html(template({ + query: query, + dataset: this.name + })); + }, + _renderNotFound: function renderNotFound(query) { + var template = this.templates.notFound; + this._resetLastSuggestion(); + template && this.$el.html(template({ + query: query, + dataset: this.name + })); + }, + _empty: function empty() { + this.$el.empty(); + this._resetLastSuggestion(); + }, + _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) { + var that = this, fragment; + fragment = document.createDocumentFragment(); + _.each(suggestions, function getSuggestionNode(suggestion) { + var $el, context; + context = that._injectQuery(query, suggestion); + $el = $(that.templates.suggestion(context)).data(keys.dataset, that.name).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable); + fragment.appendChild($el[0]); + }); + this.highlight && highlight({ + className: this.classes.highlight, + node: fragment, + pattern: query + }); + return $(fragment); + }, + _getFooter: function getFooter(query, suggestions) { + return this.templates.footer ? this.templates.footer({ + query: query, + suggestions: suggestions, + dataset: this.name + }) : null; + }, + _getHeader: function getHeader(query, suggestions) { + return this.templates.header ? this.templates.header({ + query: query, + suggestions: suggestions, + dataset: this.name + }) : null; + }, + _resetLastSuggestion: function resetLastSuggestion() { + this.$lastSuggestion = $(); + }, + _injectQuery: function injectQuery(query, obj) { + return _.isObject(obj) ? _.mixin({ + _query: query + }, obj) : obj; }, update: function update(query) { - var that = this; - this.query = query; - this.canceled = false; - this.source(query, render); - function render(suggestions) { - if (!that.canceled && query === that.query) { - that._render(query, suggestions); + var that = this, canceled = false, syncCalled = false, rendered = 0; + this.cancel(); + this.cancel = function cancel() { + canceled = true; + that.cancel = $.noop; + that.async && that.trigger("asyncCanceled", query, that.name); + }; + this.source(query, sync, async); + !syncCalled && sync([]); + function sync(suggestions) { + if (syncCalled && !async) { + return; + } + syncCalled = true; + suggestions = (suggestions || []).slice(0, that.limit); + rendered = suggestions.length; + that._overwrite(query, suggestions); + if (rendered < that.limit && that.async) { + that.trigger("asyncRequested", query, that.name); + } + } + function async(suggestions) { + suggestions = suggestions || []; + if (!canceled && rendered < that.limit) { + that.cancel = $.noop; + var idx = Math.abs(rendered - that.limit); + rendered += idx; + that._append(query, suggestions.slice(0, idx)); + that.async && that.trigger("asyncReceived", query, that.name); } } }, - cancel: function cancel() { - this.canceled = true; - }, + cancel: $.noop, clear: function clear() { + this._empty(); this.cancel(); - this.$el.empty(); - this.trigger("rendered"); + this.trigger("cleared"); }, isEmpty: function isEmpty() { return this.$el.is(":empty"); }, destroy: function destroy() { - this.$el = null; + this.$el = $("
"); } }); return Dataset; function getDisplayFn(display) { - display = display || "value"; + display = display || _.stringify; return _.isFunction(display) ? display : displayFn; function displayFn(obj) { return obj[display]; @@ -1270,404 +1848,763 @@ } function getTemplates(templates, displayFn) { return { - empty: templates.empty && _.templatify(templates.empty), + notFound: templates.notFound && _.templatify(templates.notFound), + pending: templates.pending && _.templatify(templates.pending), header: templates.header && _.templatify(templates.header), footer: templates.footer && _.templatify(templates.footer), - suggestion: templates.suggestion || suggestionTemplate + suggestion: templates.suggestion ? userSuggestionTemplate : suggestionTemplate }; + function userSuggestionTemplate(context) { + var template = templates.suggestion; + return $(template(context)).attr("id", _.guid()); + } function suggestionTemplate(context) { - return "

" + displayFn(context) + "

"; + return $('
').attr("id", _.guid()).text(displayFn(context)); } } function isValidName(str) { return /^[_a-zA-Z0-9-]+$/.test(str); } }(); - var Dropdown = function() { + var Menu = function() { "use strict"; - function Dropdown(o) { - var that = this, onSuggestionClick, onSuggestionMouseEnter, onSuggestionMouseLeave; + function Menu(o, www) { + var that = this; o = o || {}; - if (!o.menu) { - $.error("menu is required"); + if (!o.node) { + $.error("node is required"); } - this.isOpen = false; - this.isEmpty = true; + www.mixin(this); + this.$node = $(o.node); + this.query = null; this.datasets = _.map(o.datasets, initializeDataset); - onSuggestionClick = _.bind(this._onSuggestionClick, this); - onSuggestionMouseEnter = _.bind(this._onSuggestionMouseEnter, this); - onSuggestionMouseLeave = _.bind(this._onSuggestionMouseLeave, this); - this.$menu = $(o.menu).on("click.tt", ".tt-suggestion", onSuggestionClick).on("mouseenter.tt", ".tt-suggestion", onSuggestionMouseEnter).on("mouseleave.tt", ".tt-suggestion", onSuggestionMouseLeave); - _.each(this.datasets, function(dataset) { - that.$menu.append(dataset.getRoot()); - dataset.onSync("rendered", that._onRendered, that); - }); + function initializeDataset(oDataset) { + var node = that.$node.find(oDataset.node).first(); + oDataset.node = node.length ? node : $("
").appendTo(that.$node); + return new Dataset(oDataset, www); + } } - _.mixin(Dropdown.prototype, EventEmitter, { - _onSuggestionClick: function onSuggestionClick($e) { - this.trigger("suggestionClicked", $($e.currentTarget)); + _.mixin(Menu.prototype, EventEmitter, { + _onSelectableClick: function onSelectableClick($e) { + this.trigger("selectableClicked", $($e.currentTarget)); }, - _onSuggestionMouseEnter: function onSuggestionMouseEnter($e) { - this._removeCursor(); - this._setCursor($($e.currentTarget), true); + _onRendered: function onRendered(type, dataset, suggestions, async) { + this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); + this.trigger("datasetRendered", dataset, suggestions, async); }, - _onSuggestionMouseLeave: function onSuggestionMouseLeave() { - this._removeCursor(); + _onCleared: function onCleared() { + this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); + this.trigger("datasetCleared"); }, - _onRendered: function onRendered() { - this.isEmpty = _.every(this.datasets, isDatasetEmpty); - this.isEmpty ? this._hide() : this.isOpen && this._show(); - this.trigger("datasetRendered"); - function isDatasetEmpty(dataset) { - return dataset.isEmpty(); - } + _propagate: function propagate() { + this.trigger.apply(this, arguments); }, - _hide: function() { - this.$menu.hide(); + _allDatasetsEmpty: function allDatasetsEmpty() { + return _.every(this.datasets, _.bind(function isDatasetEmpty(dataset) { + var isEmpty = dataset.isEmpty(); + this.$node.attr("aria-expanded", !isEmpty); + return isEmpty; + }, this)); }, - _show: function() { - this.$menu.css("display", "block"); + _getSelectables: function getSelectables() { + return this.$node.find(this.selectors.selectable); }, - _getSuggestions: function getSuggestions() { - return this.$menu.find(".tt-suggestion"); - }, - _getCursor: function getCursor() { - return this.$menu.find(".tt-cursor").first(); - }, - _setCursor: function setCursor($el, silent) { - $el.first().addClass("tt-cursor"); - !silent && this.trigger("cursorMoved"); - }, - _removeCursor: function removeCursor() { - this._getCursor().removeClass("tt-cursor"); - }, - _moveCursor: function moveCursor(increment) { - var $suggestions, $oldCursor, newCursorIndex, $newCursor; - if (!this.isOpen) { - return; - } - $oldCursor = this._getCursor(); - $suggestions = this._getSuggestions(); - this._removeCursor(); - newCursorIndex = $suggestions.index($oldCursor) + increment; - newCursorIndex = (newCursorIndex + 1) % ($suggestions.length + 1) - 1; - if (newCursorIndex === -1) { - this.trigger("cursorRemoved"); - return; - } else if (newCursorIndex < -1) { - newCursorIndex = $suggestions.length - 1; - } - this._setCursor($newCursor = $suggestions.eq(newCursorIndex)); - this._ensureVisible($newCursor); + _removeCursor: function _removeCursor() { + var $selectable = this.getActiveSelectable(); + $selectable && $selectable.removeClass(this.classes.cursor); }, _ensureVisible: function ensureVisible($el) { - var elTop, elBottom, menuScrollTop, menuHeight; + var elTop, elBottom, nodeScrollTop, nodeHeight; elTop = $el.position().top; elBottom = elTop + $el.outerHeight(true); - menuScrollTop = this.$menu.scrollTop(); - menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10); + nodeScrollTop = this.$node.scrollTop(); + nodeHeight = this.$node.height() + parseInt(this.$node.css("paddingTop"), 10) + parseInt(this.$node.css("paddingBottom"), 10); if (elTop < 0) { - this.$menu.scrollTop(menuScrollTop + elTop); - } else if (menuHeight < elBottom) { - this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight)); + this.$node.scrollTop(nodeScrollTop + elTop); + } else if (nodeHeight < elBottom) { + this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight)); } }, - close: function close() { - if (this.isOpen) { - this.isOpen = false; - this._removeCursor(); - this._hide(); - this.trigger("closed"); - } + bind: function() { + var that = this, onSelectableClick; + onSelectableClick = _.bind(this._onSelectableClick, this); + this.$node.on("click.tt", this.selectors.selectable, onSelectableClick); + this.$node.on("mouseover", this.selectors.selectable, function() { + that.setCursor($(this)); + }); + this.$node.on("mouseleave", function() { + that._removeCursor(); + }); + _.each(this.datasets, function(dataset) { + dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that); + }); + return this; + }, + isOpen: function isOpen() { + return this.$node.hasClass(this.classes.open); }, open: function open() { - if (!this.isOpen) { - this.isOpen = true; - !this.isEmpty && this._show(); - this.trigger("opened"); - } + this.$node.scrollTop(0); + this.$node.addClass(this.classes.open); + }, + close: function close() { + this.$node.attr("aria-expanded", false); + this.$node.removeClass(this.classes.open); + this._removeCursor(); }, setLanguageDirection: function setLanguageDirection(dir) { - this.$menu.css(dir === "ltr" ? css.ltr : css.rtl); + this.$node.attr("dir", dir); }, - moveCursorUp: function moveCursorUp() { - this._moveCursor(-1); + selectableRelativeToCursor: function selectableRelativeToCursor(delta) { + var $selectables, $oldCursor, oldIndex, newIndex; + $oldCursor = this.getActiveSelectable(); + $selectables = this._getSelectables(); + oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1; + newIndex = oldIndex + delta; + newIndex = (newIndex + 1) % ($selectables.length + 1) - 1; + newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex; + return newIndex === -1 ? null : $selectables.eq(newIndex); }, - moveCursorDown: function moveCursorDown() { - this._moveCursor(+1); - }, - getDatumForSuggestion: function getDatumForSuggestion($el) { - var datum = null; - if ($el.length) { - datum = { - raw: Dataset.extractDatum($el), - value: Dataset.extractValue($el), - datasetName: Dataset.extractDatasetName($el) - }; + setCursor: function setCursor($selectable) { + this._removeCursor(); + if ($selectable = $selectable && $selectable.first()) { + $selectable.addClass(this.classes.cursor); + this._ensureVisible($selectable); } - return datum; }, - getDatumForCursor: function getDatumForCursor() { - return this.getDatumForSuggestion(this._getCursor().first()); + getSelectableData: function getSelectableData($el) { + return $el && $el.length ? Dataset.extractData($el) : null; }, - getDatumForTopSuggestion: function getDatumForTopSuggestion() { - return this.getDatumForSuggestion(this._getSuggestions().first()); + getActiveSelectable: function getActiveSelectable() { + var $selectable = this._getSelectables().filter(this.selectors.cursor).first(); + return $selectable.length ? $selectable : null; + }, + getTopSelectable: function getTopSelectable() { + var $selectable = this._getSelectables().first(); + return $selectable.length ? $selectable : null; }, update: function update(query) { - _.each(this.datasets, updateDataset); + var isValidUpdate = query !== this.query; + if (isValidUpdate) { + this.query = query; + _.each(this.datasets, updateDataset); + } + return isValidUpdate; function updateDataset(dataset) { dataset.update(query); } }, empty: function empty() { _.each(this.datasets, clearDataset); - this.isEmpty = true; + this.query = null; + this.$node.addClass(this.classes.empty); function clearDataset(dataset) { dataset.clear(); } }, - isVisible: function isVisible() { - return this.isOpen && !this.isEmpty; - }, destroy: function destroy() { - this.$menu.off(".tt"); - this.$menu = null; + this.$node.off(".tt"); + this.$node = $("
"); _.each(this.datasets, destroyDataset); function destroyDataset(dataset) { dataset.destroy(); } } }); - return Dropdown; - function initializeDataset(oDataset) { - return new Dataset(oDataset); + return Menu; + }(); + var Status = function() { + "use strict"; + function Status(options) { + this.$el = $("", { + role: "status", + "aria-live": "polite" + }).css({ + position: "absolute", + padding: "0", + border: "0", + height: "1px", + width: "1px", + "margin-bottom": "-1px", + "margin-right": "-1px", + overflow: "hidden", + clip: "rect(0 0 0 0)", + "white-space": "nowrap" + }); + options.$input.after(this.$el); + _.each(options.menu.datasets, _.bind(function(dataset) { + if (dataset.onSync) { + dataset.onSync("rendered", _.bind(this.update, this)); + dataset.onSync("cleared", _.bind(this.cleared, this)); + } + }, this)); } + _.mixin(Status.prototype, { + update: function update(event, suggestions) { + var length = suggestions.length; + var words; + if (length === 1) { + words = { + result: "result", + is: "is" + }; + } else { + words = { + result: "results", + is: "are" + }; + } + this.$el.text(length + " " + words.result + " " + words.is + " available, use up and down arrow keys to navigate."); + }, + cleared: function() { + this.$el.text(""); + } + }); + return Status; + }(); + var DefaultMenu = function() { + "use strict"; + var s = Menu.prototype; + function DefaultMenu() { + Menu.apply(this, [].slice.call(arguments, 0)); + } + _.mixin(DefaultMenu.prototype, Menu.prototype, { + open: function open() { + !this._allDatasetsEmpty() && this._show(); + return s.open.apply(this, [].slice.call(arguments, 0)); + }, + close: function close() { + this._hide(); + return s.close.apply(this, [].slice.call(arguments, 0)); + }, + _onRendered: function onRendered() { + if (this._allDatasetsEmpty()) { + this._hide(); + } else { + this.isOpen() && this._show(); + } + return s._onRendered.apply(this, [].slice.call(arguments, 0)); + }, + _onCleared: function onCleared() { + if (this._allDatasetsEmpty()) { + this._hide(); + } else { + this.isOpen() && this._show(); + } + return s._onCleared.apply(this, [].slice.call(arguments, 0)); + }, + setLanguageDirection: function setLanguageDirection(dir) { + this.$node.css(dir === "ltr" ? this.css.ltr : this.css.rtl); + return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0)); + }, + _hide: function hide() { + this.$node.hide(); + }, + _show: function show() { + this.$node.css("display", "block"); + } + }); + return DefaultMenu; }(); var Typeahead = function() { "use strict"; - var attrsKey = "ttAttrs"; - function Typeahead(o) { - var $menu, $input, $hint; + function Typeahead(o, www) { + var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed, onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged, onWhitespaceChanged; o = o || {}; if (!o.input) { $.error("missing input"); } - this.isActivated = false; - this.autoselect = !!o.autoselect; + if (!o.menu) { + $.error("missing menu"); + } + if (!o.eventBus) { + $.error("missing event bus"); + } + www.mixin(this); + this.eventBus = o.eventBus; this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; - this.$node = buildDom(o.input, o.withHint); - $menu = this.$node.find(".tt-dropdown-menu"); - $input = this.$node.find(".tt-input"); - $hint = this.$node.find(".tt-hint"); - $input.on("blur.tt", function($e) { - var active, isActive, hasActive; - active = document.activeElement; - isActive = $menu.is(active); - hasActive = $menu.has(active).length > 0; - if (_.isMsie() && (isActive || hasActive)) { - $e.preventDefault(); - $e.stopImmediatePropagation(); - _.defer(function() { - $input.focus(); - }); - } - }); - $menu.on("mousedown.tt", function($e) { - $e.preventDefault(); - }); - this.eventBus = o.eventBus || new EventBus({ - el: $input - }); - this.dropdown = new Dropdown({ - menu: $menu, - datasets: o.datasets - }).onSync("suggestionClicked", this._onSuggestionClicked, this).onSync("cursorMoved", this._onCursorMoved, this).onSync("cursorRemoved", this._onCursorRemoved, this).onSync("opened", this._onOpened, this).onSync("closed", this._onClosed, this).onAsync("datasetRendered", this._onDatasetRendered, this); - this.input = new Input({ - input: $input, - hint: $hint - }).onSync("focused", this._onFocused, this).onSync("blurred", this._onBlurred, this).onSync("enterKeyed", this._onEnterKeyed, this).onSync("tabKeyed", this._onTabKeyed, this).onSync("escKeyed", this._onEscKeyed, this).onSync("upKeyed", this._onUpKeyed, this).onSync("downKeyed", this._onDownKeyed, this).onSync("leftKeyed", this._onLeftKeyed, this).onSync("rightKeyed", this._onRightKeyed, this).onSync("queryChanged", this._onQueryChanged, this).onSync("whitespaceChanged", this._onWhitespaceChanged, this); - this._setLanguageDirection(); + this.input = o.input; + this.menu = o.menu; + this.enabled = true; + this.autoselect = !!o.autoselect; + this.active = false; + this.input.hasFocus() && this.activate(); + this.dir = this.input.getLangDir(); + this._hacks(); + this.menu.bind().onSync("selectableClicked", this._onSelectableClicked, this).onSync("asyncRequested", this._onAsyncRequested, this).onSync("asyncCanceled", this._onAsyncCanceled, this).onSync("asyncReceived", this._onAsyncReceived, this).onSync("datasetRendered", this._onDatasetRendered, this).onSync("datasetCleared", this._onDatasetCleared, this); + onFocused = c(this, "activate", "open", "_onFocused"); + onBlurred = c(this, "deactivate", "_onBlurred"); + onEnterKeyed = c(this, "isActive", "isOpen", "_onEnterKeyed"); + onTabKeyed = c(this, "isActive", "isOpen", "_onTabKeyed"); + onEscKeyed = c(this, "isActive", "_onEscKeyed"); + onUpKeyed = c(this, "isActive", "open", "_onUpKeyed"); + onDownKeyed = c(this, "isActive", "open", "_onDownKeyed"); + onLeftKeyed = c(this, "isActive", "isOpen", "_onLeftKeyed"); + onRightKeyed = c(this, "isActive", "isOpen", "_onRightKeyed"); + onQueryChanged = c(this, "_openIfActive", "_onQueryChanged"); + onWhitespaceChanged = c(this, "_openIfActive", "_onWhitespaceChanged"); + this.input.bind().onSync("focused", onFocused, this).onSync("blurred", onBlurred, this).onSync("enterKeyed", onEnterKeyed, this).onSync("tabKeyed", onTabKeyed, this).onSync("escKeyed", onEscKeyed, this).onSync("upKeyed", onUpKeyed, this).onSync("downKeyed", onDownKeyed, this).onSync("leftKeyed", onLeftKeyed, this).onSync("rightKeyed", onRightKeyed, this).onSync("queryChanged", onQueryChanged, this).onSync("whitespaceChanged", onWhitespaceChanged, this).onSync("langDirChanged", this._onLangDirChanged, this); } _.mixin(Typeahead.prototype, { - _onSuggestionClicked: function onSuggestionClicked(type, $el) { - var datum; - if (datum = this.dropdown.getDatumForSuggestion($el)) { - this._select(datum); + _hacks: function hacks() { + var $input, $menu; + $input = this.input.$input || $("
"); + $menu = this.menu.$node || $("
"); + $input.on("blur.tt", function($e) { + var active, isActive, hasActive; + active = document.activeElement; + isActive = $menu.is(active); + hasActive = $menu.has(active).length > 0; + if (_.isMsie() && (isActive || hasActive)) { + $e.preventDefault(); + $e.stopImmediatePropagation(); + _.defer(function() { + $input.focus(); + }); + } + }); + $menu.on("mousedown.tt", function($e) { + $e.preventDefault(); + }); + }, + _onSelectableClicked: function onSelectableClicked(type, $el) { + this.select($el); + }, + _onDatasetCleared: function onDatasetCleared() { + this._updateHint(); + }, + _onDatasetRendered: function onDatasetRendered(type, suggestions, async, dataset) { + this._updateHint(); + if (this.autoselect) { + var cursorClass = this.selectors.cursor.substr(1); + this.menu.$node.find(this.selectors.suggestion).first().addClass(cursorClass); } + this.eventBus.trigger("render", suggestions, async, dataset); }, - _onCursorMoved: function onCursorMoved() { - var datum = this.dropdown.getDatumForCursor(); - this.input.setInputValue(datum.value, true); - this.eventBus.trigger("cursorchanged", datum.raw, datum.datasetName); + _onAsyncRequested: function onAsyncRequested(type, dataset, query) { + this.eventBus.trigger("asyncrequest", query, dataset); }, - _onCursorRemoved: function onCursorRemoved() { - this.input.resetInputValue(); - this._updateHint(); + _onAsyncCanceled: function onAsyncCanceled(type, dataset, query) { + this.eventBus.trigger("asynccancel", query, dataset); }, - _onDatasetRendered: function onDatasetRendered() { - this._updateHint(); - }, - _onOpened: function onOpened() { - this._updateHint(); - this.eventBus.trigger("opened"); - }, - _onClosed: function onClosed() { - this.input.clearHint(); - this.eventBus.trigger("closed"); + _onAsyncReceived: function onAsyncReceived(type, dataset, query) { + this.eventBus.trigger("asyncreceive", query, dataset); }, _onFocused: function onFocused() { - this.isActivated = true; - this.dropdown.open(); + this._minLengthMet() && this.menu.update(this.input.getQuery()); }, _onBlurred: function onBlurred() { - this.isActivated = false; - this.dropdown.empty(); - this.dropdown.close(); + if (this.input.hasQueryChangedSinceLastFocus()) { + this.eventBus.trigger("change", this.input.getQuery()); + } }, _onEnterKeyed: function onEnterKeyed(type, $e) { - var cursorDatum, topSuggestionDatum; - cursorDatum = this.dropdown.getDatumForCursor(); - topSuggestionDatum = this.dropdown.getDatumForTopSuggestion(); - if (cursorDatum) { - this._select(cursorDatum); - $e.preventDefault(); - } else if (this.autoselect && topSuggestionDatum) { - this._select(topSuggestionDatum); - $e.preventDefault(); + var $selectable; + if ($selectable = this.menu.getActiveSelectable()) { + if (this.select($selectable)) { + $e.preventDefault(); + $e.stopPropagation(); + } + } else if (this.autoselect) { + if (this.select(this.menu.getTopSelectable())) { + $e.preventDefault(); + $e.stopPropagation(); + } } }, _onTabKeyed: function onTabKeyed(type, $e) { - var datum; - if (datum = this.dropdown.getDatumForCursor()) { - this._select(datum); - $e.preventDefault(); - } else { - this._autocomplete(true); + var $selectable; + if ($selectable = this.menu.getActiveSelectable()) { + this.select($selectable) && $e.preventDefault(); + } else if (this.autoselect) { + if ($selectable = this.menu.getTopSelectable()) { + this.autocomplete($selectable) && $e.preventDefault(); + } } }, _onEscKeyed: function onEscKeyed() { - this.dropdown.close(); - this.input.resetInputValue(); + this.close(); }, _onUpKeyed: function onUpKeyed() { - var query = this.input.getQuery(); - this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorUp(); - this.dropdown.open(); + this.moveCursor(-1); }, _onDownKeyed: function onDownKeyed() { - var query = this.input.getQuery(); - this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorDown(); - this.dropdown.open(); + this.moveCursor(+1); }, _onLeftKeyed: function onLeftKeyed() { - this.dir === "rtl" && this._autocomplete(); + if (this.dir === "rtl" && this.input.isCursorAtEnd()) { + this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable()); + } }, _onRightKeyed: function onRightKeyed() { - this.dir === "ltr" && this._autocomplete(); + if (this.dir === "ltr" && this.input.isCursorAtEnd()) { + this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable()); + } }, _onQueryChanged: function onQueryChanged(e, query) { - this.input.clearHintIfInvalid(); - query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.empty(); - this.dropdown.open(); - this._setLanguageDirection(); + this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty(); }, _onWhitespaceChanged: function onWhitespaceChanged() { this._updateHint(); - this.dropdown.open(); }, - _setLanguageDirection: function setLanguageDirection() { - var dir; - if (this.dir !== (dir = this.input.getLanguageDirection())) { + _onLangDirChanged: function onLangDirChanged(e, dir) { + if (this.dir !== dir) { this.dir = dir; - this.$node.css("direction", dir); - this.dropdown.setLanguageDirection(dir); + this.menu.setLanguageDirection(dir); } }, + _openIfActive: function openIfActive() { + this.isActive() && this.open(); + }, + _minLengthMet: function minLengthMet(query) { + query = _.isString(query) ? query : this.input.getQuery() || ""; + return query.length >= this.minLength; + }, _updateHint: function updateHint() { - var datum, val, query, escapedQuery, frontMatchRegEx, match; - datum = this.dropdown.getDatumForTopSuggestion(); - if (datum && this.dropdown.isVisible() && !this.input.hasOverflow()) { - val = this.input.getInputValue(); + var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match; + $selectable = this.menu.getTopSelectable(); + data = this.menu.getSelectableData($selectable); + val = this.input.getInputValue(); + if (data && !_.isBlankString(val) && !this.input.hasOverflow()) { query = Input.normalizeQuery(val); escapedQuery = _.escapeRegExChars(query); frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i"); - match = frontMatchRegEx.exec(datum.value); - match ? this.input.setHint(val + match[1]) : this.input.clearHint(); + match = frontMatchRegEx.exec(data.val); + match && this.input.setHint(val + match[1]); } else { this.input.clearHint(); } }, - _autocomplete: function autocomplete(laxCursor) { - var hint, query, isCursorAtEnd, datum; - hint = this.input.getHint(); - query = this.input.getQuery(); - isCursorAtEnd = laxCursor || this.input.isCursorAtEnd(); - if (hint && query !== hint && isCursorAtEnd) { - datum = this.dropdown.getDatumForTopSuggestion(); - datum && this.input.setInputValue(datum.value); - this.eventBus.trigger("autocompleted", datum.raw, datum.datasetName); + isEnabled: function isEnabled() { + return this.enabled; + }, + enable: function enable() { + this.enabled = true; + }, + disable: function disable() { + this.enabled = false; + }, + isActive: function isActive() { + return this.active; + }, + activate: function activate() { + if (this.isActive()) { + return true; + } else if (!this.isEnabled() || this.eventBus.before("active")) { + return false; + } else { + this.active = true; + this.eventBus.trigger("active"); + return true; } }, - _select: function select(datum) { - this.input.setQuery(datum.value); - this.input.setInputValue(datum.value, true); - this._setLanguageDirection(); - this.eventBus.trigger("selected", datum.raw, datum.datasetName); - this.dropdown.close(); - _.defer(_.bind(this.dropdown.empty, this.dropdown)); + deactivate: function deactivate() { + if (!this.isActive()) { + return true; + } else if (this.eventBus.before("idle")) { + return false; + } else { + this.active = false; + this.close(); + this.eventBus.trigger("idle"); + return true; + } + }, + isOpen: function isOpen() { + return this.menu.isOpen(); }, open: function open() { - this.dropdown.open(); + if (!this.isOpen() && !this.eventBus.before("open")) { + this.input.setAriaExpanded(true); + this.menu.open(); + this._updateHint(); + this.eventBus.trigger("open"); + } + return this.isOpen(); }, close: function close() { - this.dropdown.close(); + if (this.isOpen() && !this.eventBus.before("close")) { + this.input.setAriaExpanded(false); + this.menu.close(); + this.input.clearHint(); + this.input.resetInputValue(); + this.eventBus.trigger("close"); + } + return !this.isOpen(); }, setVal: function setVal(val) { - val = _.toStr(val); - if (this.isActivated) { - this.input.setInputValue(val); - } else { - this.input.setQuery(val); - this.input.setInputValue(val, true); - } - this._setLanguageDirection(); + this.input.setQuery(_.toStr(val)); }, getVal: function getVal() { return this.input.getQuery(); }, + select: function select($selectable) { + var data = this.menu.getSelectableData($selectable); + if (data && !this.eventBus.before("select", data.obj, data.dataset)) { + this.input.setQuery(data.val, true); + this.eventBus.trigger("select", data.obj, data.dataset); + this.close(); + return true; + } + return false; + }, + autocomplete: function autocomplete($selectable) { + var query, data, isValid; + query = this.input.getQuery(); + data = this.menu.getSelectableData($selectable); + isValid = data && query !== data.val; + if (isValid && !this.eventBus.before("autocomplete", data.obj, data.dataset)) { + this.input.setQuery(data.val); + this.eventBus.trigger("autocomplete", data.obj, data.dataset); + return true; + } + return false; + }, + moveCursor: function moveCursor(delta) { + var query, $candidate, data, suggestion, datasetName, cancelMove, id; + query = this.input.getQuery(); + $candidate = this.menu.selectableRelativeToCursor(delta); + data = this.menu.getSelectableData($candidate); + suggestion = data ? data.obj : null; + datasetName = data ? data.dataset : null; + id = $candidate ? $candidate.attr("id") : null; + this.input.trigger("cursorchange", id); + cancelMove = this._minLengthMet() && this.menu.update(query); + if (!cancelMove && !this.eventBus.before("cursorchange", suggestion, datasetName)) { + this.menu.setCursor($candidate); + if (data) { + if (typeof data.val === "string") { + this.input.setInputValue(data.val); + } + } else { + this.input.resetInputValue(); + this._updateHint(); + } + this.eventBus.trigger("cursorchange", suggestion, datasetName); + return true; + } + return false; + }, destroy: function destroy() { this.input.destroy(); - this.dropdown.destroy(); - destroyDomStructure(this.$node); - this.$node = null; + this.menu.destroy(); } }); return Typeahead; - function buildDom(input, withHint) { - var $input, $wrapper, $dropdown, $hint; - $input = $(input); - $wrapper = $(html.wrapper).css(css.wrapper); - $dropdown = $(html.dropdown).css(css.dropdown); - $hint = $input.clone().css(css.hint).css(getBackgroundStyles($input)); - $hint.val("").removeData().addClass("tt-hint").removeAttr("id name placeholder required").prop("readonly", true).attr({ - autocomplete: "off", + function c(ctx) { + var methods = [].slice.call(arguments, 1); + return function() { + var args = [].slice.call(arguments); + _.each(methods, function(method) { + return ctx[method].apply(ctx, args); + }); + }; + } + }(); + (function() { + "use strict"; + var old, keys, methods; + old = $.fn.typeahead; + keys = { + www: "tt-www", + attrs: "tt-attrs", + typeahead: "tt-typeahead" + }; + methods = { + initialize: function initialize(o, datasets) { + var www; + datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); + o = o || {}; + www = WWW(o.classNames); + return this.each(attach); + function attach() { + var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, status, typeahead, MenuConstructor; + _.each(datasets, function(d) { + d.highlight = !!o.highlight; + }); + $input = $(this); + $wrapper = $(www.html.wrapper); + $hint = $elOrNull(o.hint); + $menu = $elOrNull(o.menu); + defaultHint = o.hint !== false && !$hint; + defaultMenu = o.menu !== false && !$menu; + defaultHint && ($hint = buildHintFromInput($input, www)); + defaultMenu && ($menu = $(www.html.menu).css(www.css.menu)); + $hint && $hint.val(""); + $input = prepInput($input, www); + if (defaultHint || defaultMenu) { + $wrapper.css(www.css.wrapper); + $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint); + $input.wrap($wrapper).parent().prepend(defaultHint ? $hint : null).append(defaultMenu ? $menu : null); + } + MenuConstructor = defaultMenu ? DefaultMenu : Menu; + eventBus = new EventBus({ + el: $input + }); + input = new Input({ + hint: $hint, + input: $input, + menu: $menu + }, www); + menu = new MenuConstructor({ + node: $menu, + datasets: datasets + }, www); + status = new Status({ + $input: $input, + menu: menu + }); + typeahead = new Typeahead({ + input: input, + menu: menu, + eventBus: eventBus, + minLength: o.minLength, + autoselect: o.autoselect + }, www); + $input.data(keys.www, www); + $input.data(keys.typeahead, typeahead); + } + }, + isEnabled: function isEnabled() { + var enabled; + ttEach(this.first(), function(t) { + enabled = t.isEnabled(); + }); + return enabled; + }, + enable: function enable() { + ttEach(this, function(t) { + t.enable(); + }); + return this; + }, + disable: function disable() { + ttEach(this, function(t) { + t.disable(); + }); + return this; + }, + isActive: function isActive() { + var active; + ttEach(this.first(), function(t) { + active = t.isActive(); + }); + return active; + }, + activate: function activate() { + ttEach(this, function(t) { + t.activate(); + }); + return this; + }, + deactivate: function deactivate() { + ttEach(this, function(t) { + t.deactivate(); + }); + return this; + }, + isOpen: function isOpen() { + var open; + ttEach(this.first(), function(t) { + open = t.isOpen(); + }); + return open; + }, + open: function open() { + ttEach(this, function(t) { + t.open(); + }); + return this; + }, + close: function close() { + ttEach(this, function(t) { + t.close(); + }); + return this; + }, + select: function select(el) { + var success = false, $el = $(el); + ttEach(this.first(), function(t) { + success = t.select($el); + }); + return success; + }, + autocomplete: function autocomplete(el) { + var success = false, $el = $(el); + ttEach(this.first(), function(t) { + success = t.autocomplete($el); + }); + return success; + }, + moveCursor: function moveCursoe(delta) { + var success = false; + ttEach(this.first(), function(t) { + success = t.moveCursor(delta); + }); + return success; + }, + val: function val(newVal) { + var query; + if (!arguments.length) { + ttEach(this.first(), function(t) { + query = t.getVal(); + }); + return query; + } else { + ttEach(this, function(t) { + t.setVal(_.toStr(newVal)); + }); + return this; + } + }, + destroy: function destroy() { + ttEach(this, function(typeahead, $input) { + revert($input); + typeahead.destroy(); + }); + return this; + } + }; + $.fn.typeahead = function(method) { + if (methods[method]) { + return methods[method].apply(this, [].slice.call(arguments, 1)); + } else { + return methods.initialize.apply(this, arguments); + } + }; + $.fn.typeahead.noConflict = function noConflict() { + $.fn.typeahead = old; + return this; + }; + function ttEach($els, fn) { + $els.each(function() { + var $input = $(this), typeahead; + (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input); + }); + } + function buildHintFromInput($input, www) { + return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop({ + readonly: true, + required: false + }).removeAttr("id name placeholder").removeClass("required").attr({ spellcheck: "false", tabindex: -1 }); - $input.data(attrsKey, { + } + function prepInput($input, www) { + $input.data(keys.attrs, { dir: $input.attr("dir"), autocomplete: $input.attr("autocomplete"), spellcheck: $input.attr("spellcheck"), style: $input.attr("style") }); - $input.addClass("tt-input").attr({ - autocomplete: "off", + $input.addClass(www.classes.input).attr({ spellcheck: false - }).css(withHint ? css.input : css.inputWithNoHint); + }); try { !$input.attr("dir") && $input.attr("dir", "auto"); } catch (e) {} - return $input.wrap($wrapper).parent().prepend(withHint ? $hint : null).append($dropdown); + return $input; } function getBackgroundStyles($el) { return { @@ -1681,102 +2618,24 @@ backgroundSize: $el.css("background-size") }; } - function destroyDomStructure($node) { - var $input = $node.find(".tt-input"); - _.each($input.data(attrsKey), function(val, key) { + function revert($input) { + var www, $wrapper; + www = $input.data(keys.www); + $wrapper = $input.parent().filter(www.selectors.wrapper); + _.each($input.data(keys.attrs), function(val, key) { _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); }); - $input.detach().removeData(attrsKey).removeClass("tt-input").insertAfter($node); - $node.remove(); + $input.removeData(keys.typeahead).removeData(keys.www).removeData(keys.attr).removeClass(www.classes.input); + if ($wrapper.length) { + $input.detach().insertAfter($wrapper); + $wrapper.remove(); + } + } + function $elOrNull(obj) { + var isValid, $el; + isValid = _.isJQuery(obj) || _.isElement(obj); + $el = isValid ? $(obj).first() : []; + return $el.length ? $el : null; } - }(); - (function() { - "use strict"; - var old, typeaheadKey, methods; - old = $.fn.typeahead; - typeaheadKey = "ttTypeahead"; - methods = { - initialize: function initialize(o, datasets) { - datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); - o = o || {}; - return this.each(attach); - function attach() { - var $input = $(this), eventBus, typeahead; - _.each(datasets, function(d) { - d.highlight = !!o.highlight; - }); - typeahead = new Typeahead({ - input: $input, - eventBus: eventBus = new EventBus({ - el: $input - }), - withHint: _.isUndefined(o.hint) ? true : !!o.hint, - minLength: o.minLength, - autoselect: o.autoselect, - datasets: datasets - }); - $input.data(typeaheadKey, typeahead); - } - }, - open: function open() { - return this.each(openTypeahead); - function openTypeahead() { - var $input = $(this), typeahead; - if (typeahead = $input.data(typeaheadKey)) { - typeahead.open(); - } - } - }, - close: function close() { - return this.each(closeTypeahead); - function closeTypeahead() { - var $input = $(this), typeahead; - if (typeahead = $input.data(typeaheadKey)) { - typeahead.close(); - } - } - }, - val: function val(newVal) { - return !arguments.length ? getVal(this.first()) : this.each(setVal); - function setVal() { - var $input = $(this), typeahead; - if (typeahead = $input.data(typeaheadKey)) { - typeahead.setVal(newVal); - } - } - function getVal($input) { - var typeahead, query; - if (typeahead = $input.data(typeaheadKey)) { - query = typeahead.getVal(); - } - return query; - } - }, - destroy: function destroy() { - return this.each(unattach); - function unattach() { - var $input = $(this), typeahead; - if (typeahead = $input.data(typeaheadKey)) { - typeahead.destroy(); - $input.removeData(typeaheadKey); - } - } - } - }; - $.fn.typeahead = function(method) { - var tts; - if (methods[method] && method !== "initialize") { - tts = this.filter(function() { - return !!$(this).data(typeaheadKey); - }); - return methods[method].apply(tts, [].slice.call(arguments, 1)); - } else { - return methods.initialize.apply(this, arguments); - } - }; - $.fn.typeahead.noConflict = function noConflict() { - $.fn.typeahead = old; - return this; - }; })(); -})(window.jQuery); \ No newline at end of file +}); diff --git a/cps/static/js/main.js b/cps/static/js/main.js index 988e3b9f..48c66767 100644 --- a/cps/static/js/main.js +++ b/cps/static/js/main.js @@ -15,7 +15,6 @@ * along with this program. If not, see . */ - function getPath() { var jsFileLocation = $("script[src*=jquery]").attr("src"); // the js file path return jsFileLocation.substr(0, jsFileLocation.search("/static/js/libs/jquery.min.js")); // the js folder path @@ -113,6 +112,14 @@ $("#btn-upload").change(function() { $("#form-upload").submit(); }); +$("#form-upload").uploadprogress({ + redirect_url: getPath() + "/", //"{{ url_for('web.index')}}", + uploadedMsg: $("#form-upload").data("message"), //"{{_('Upload done, processing, please wait...')}}", + modalTitle: $("#form-upload").data("title"), //"{{_('Uploading...')}}", + modalFooter: $("#form-upload").data("footer"), //"{{_('Close')}}", + modalTitleFailed: $("#form-upload").data("failed") //"{{_('Error')}}" +}); + $(document).ready(function() { var inp = $('#query').first() if (inp.length) { @@ -224,6 +231,16 @@ $(function() { var preFilters = $.Callbacks(); $.ajaxPrefilter(preFilters.fire); + // equip all post requests with csrf_token + var csrftoken = $("input[name='csrf_token']").val(); + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { + xhr.setRequestHeader("X-CSRFToken", csrftoken) + } + } + }); + function restartTimer() { $("#spinner").addClass("hidden"); $("#RestartDialog").modal("hide"); @@ -246,7 +263,7 @@ $(function() { function updateTimer() { $.ajax({ dataType: "json", - url: window.location.pathname + "/../../get_updater_status", + url: getPath() + "/get_updater_status", success: function success(data) { $("#DialogContent").html(updateText[data.status]); if (data.status > 6) { @@ -445,8 +462,8 @@ $(function() { $.ajax({ type: "POST", dataType: "json", - data: { start: "True"}, - url: window.location.pathname + "/../../get_updater_status", + data: { start: "True" }, + url: getPath() + "/get_updater_status", success: function success(data) { updateText = data.text; $("#DialogContent").html(updateText[data.status]); @@ -577,7 +594,7 @@ $(function() { method:"post", dataType: "json", url: window.location.pathname + "/../../ajax/simulatedbchange", - data: {config_calibre_dir: $("#config_calibre_dir").val()}, + data: {config_calibre_dir: $("#config_calibre_dir").val(), csrf_token: $("input[name='csrf_token']").val()}, success: function success(data) { if ( data.change ) { if ( data.valid ) { @@ -650,10 +667,10 @@ $(function() { 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"); + $("#file_confirm").data("link", target.data("link")); + $("#file_confirm").data("folderonly", (typeof folder === 'undefined') ? false : true); + $("#file_confirm").data("filefilter", (typeof filter === 'undefined') ? "" : filter); + $("#file_confirm").data("newfile", target.data("newfile")); fillFileTable(path,"dir", folder, filter); }); @@ -667,7 +684,7 @@ $(function() { var folder = $(file_confirm).data("folderonly"); var filter = $(file_confirm).data("filefilter"); var newfile = $(file_confirm).data("newfile"); - if (newfile !== 'undefined') { + if (newfile !== "") { $("#element_selected").text(path + $("#new_file".text())); } else { $("#element_selected").text(path); @@ -713,7 +730,7 @@ $(function() { method:"post", contentType: "application/json; charset=utf-8", dataType: "json", - url: window.location.pathname + "/../ajax/view", + url: getPath() + "/ajax/view", data: "{\"series\": {\"series_view\": \""+ view +"\"}}", success: function success() { location.reload(); diff --git a/cps/static/js/reading/djvu_reader.js b/cps/static/js/reading/djvu_reader.js new file mode 100644 index 00000000..c93d705a --- /dev/null +++ b/cps/static/js/reading/djvu_reader.js @@ -0,0 +1,21 @@ +/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) + * Copyright (C) 2021 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 . + */ + +var DJVU_CONTEXT = { + background: "#666", + uiHideDelay: 1500, +}; diff --git a/cps/static/js/reading/txt_reader.js b/cps/static/js/reading/txt_reader.js new file mode 100644 index 00000000..9cfb51f5 --- /dev/null +++ b/cps/static/js/reading/txt_reader.js @@ -0,0 +1,86 @@ +/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) + * Copyright (C) 2021 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 . + */ + +$(document).ready(function() { + //to int + $("#area").width($("#area").width()); + $("#content").width($("#content").width()); + //bind text + $("#content").load($("#readmain").data('load'), function(textStr) { + $(this).height($(this).parent().height()*0.95); + $(this).text(textStr); + }); + //keybind + $(document).keydown(function(event){ + if(event.keyCode == 37){ + prevPage(); + } + if(event.keyCode == 39){ + nextPage(); + } + }); + //click + $( "#left" ).click(function() { + prevPage(); + }); + $( "#right" ).click(function() { + nextPage(); + }); + $("#readmain").swipe( { + swipeRight:function() { + prevPage(); + }, + swipeLeft:function() { + nextPage(); + }, + }); + + //bind mouse + $(window).bind('DOMMouseScroll mousewheel', function(event) { + var delta = 0; + if (event.originalEvent.wheelDelta) { + delta = event.originalEvent.wheelDelta; + } else if (event.originalEvent.detail) { + delta = event.originalEvent.detail*-1; + } + if (delta >= 0) { + prevPage(); + } else { + nextPage(); + } + }); + + //page animate + var origwidth = $("#content")[0].getBoundingClientRect().width; + var gap = 20; + function prevPage() { + if($("#content").offset().left > 0) { + return; + } + leftoff = $("#content").offset().left; + leftoff = leftoff+origwidth+gap; + $("#content").offset({left:leftoff}); + } + function nextPage() { + leftoff = $("#content").offset().left; + leftoff = leftoff-origwidth-gap; + if (leftoff + $("#content")[0].scrollWidth < 0) { + return; + } + $("#content").offset({left:leftoff}); + } +}); diff --git a/cps/static/js/remote_login.js b/cps/static/js/remote_login.js new file mode 100644 index 00000000..f0d364dc --- /dev/null +++ b/cps/static/js/remote_login.js @@ -0,0 +1,36 @@ +/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) + * Copyright (C) 2017-2021 jkrehm, 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 . + */ + +(function () { + // Poll the server to check if the user has authenticated + var t = setInterval(function () { + $.post(getPath() + "/ajax/verify_token", { token: $("#verify_url").data("token") }) + .done(function(response) { + if (response.status === 'success') { + // Wait a tick so cookies are updated + setTimeout(function () { + window.location.href = getPath() + '/'; + }, 0); + } + }) + .fail(function (xhr) { + clearInterval(t); + var response = JSON.parse(xhr.responseText); + alert(response.message); + }); + }, 5000); +})() diff --git a/cps/static/js/shelforder.js b/cps/static/js/shelforder.js index 62b7e9f3..5cbe7109 100644 --- a/cps/static/js/shelforder.js +++ b/cps/static/js/shelforder.js @@ -35,6 +35,7 @@ function sendData(path) { var form = document.createElement("form"); form.setAttribute("method", "post"); form.setAttribute("action", path); + // form.setAttribute("csrf_token", ); for (counter = 0;counter < maxElements;counter++) { tmp[counter] = elements[counter].getAttribute("id"); @@ -44,6 +45,10 @@ function sendData(path) { hiddenField.setAttribute("value", String(counter + 1)); form.appendChild(hiddenField); } + $("") + .attr("name", "csrf_token").val($("input[name='csrf_token']").val()) + .appendTo(form); + document.body.appendChild(form); form.submit(); } diff --git a/cps/subproc_wrapper.py b/cps/subproc_wrapper.py index 3cc4a070..187b2cb2 100644 --- a/cps/subproc_wrapper.py +++ b/cps/subproc_wrapper.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import sys import os import subprocess @@ -33,13 +32,8 @@ def process_open(command, quotes=(), env=None, sout=subprocess.PIPE, serr=subpro if key in quotes: command[key] = '"' + element + '"' exc_command = " ".join(command) - if sys.version_info < (3, 0): - exc_command = exc_command.encode(sys.getfilesystemencoding()) else: - if sys.version_info < (3, 0): - exc_command = [x.encode(sys.getfilesystemencoding()) for x in command] - else: - exc_command = [x for x in command] + exc_command = [x for x in command] return subprocess.Popen(exc_command, shell=False, stdout=sout, stderr=serr, universal_newlines=newlines, env=env) # nosec diff --git a/cps/tasks/convert.py b/cps/tasks/convert.py index 56cc7076..08fb1644 100644 --- a/cps/tasks/convert.py +++ b/cps/tasks/convert.py @@ -1,4 +1,21 @@ -from __future__ import division, print_function, unicode_literals +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2020 pwr +# +# 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 sys import os import re @@ -161,8 +178,6 @@ class TaskConvert(CalibreTask): while True: nextline = p.stdout.readlines() nextline = [x.strip('\n') for x in nextline if x != '\n'] - if sys.version_info < (3, 0): - nextline = [x.decode('utf-8') for x in nextline] for line in nextline: log.debug(line) if p.poll() is not None: @@ -207,10 +222,6 @@ class TaskConvert(CalibreTask): while p.poll() is None: nextline = p.stdout.readline() - if os.name == 'nt' and sys.version_info < (3, 0): - nextline = nextline.decode('windows-1252') - elif os.name == 'posix' and sys.version_info < (3, 0): - nextline = nextline.decode('utf-8') log.debug(nextline.strip('\r\n')) # parse progress string from calibre-converter progress = re.search(r"(\d+)%\s.*", nextline) @@ -224,8 +235,6 @@ class TaskConvert(CalibreTask): calibre_traceback = p.stderr.readlines() error_message = "" for ele in calibre_traceback: - if sys.version_info < (3, 0): - ele = ele.decode('utf-8') log.debug(ele.strip('\n')) if not ele.startswith('Traceback') and not ele.startswith(' File'): error_message = _("Calibre failed with error: %(error)s", error=ele.strip('\n')) diff --git a/cps/tasks/mail.py b/cps/tasks/mail.py index 292114d5..2e95ee98 100644 --- a/cps/tasks/mail.py +++ b/cps/tasks/mail.py @@ -1,11 +1,27 @@ -from __future__ import division, print_function, unicode_literals +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2020 pwr +# +# 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 sys import os import smtplib import threading import socket import mimetypes -import base64 try: from StringIO import StringIO @@ -162,17 +178,11 @@ class TaskEmail(CalibreTask): log.debug_or_exception(ex) self._handleError(u'Error sending e-mail: {}'.format(ex)) - def send_standard_email(self, msg): use_ssl = int(self.settings.get('mail_use_ssl', 0)) timeout = 600 # set timeout to 5mins - # redirect output to logfile on python2 on python3 debugoutput is caught with overwritten - # _print_debug function - if sys.version_info < (3, 0): - org_smtpstderr = smtplib.stderr - smtplib.stderr = logger.StderrLogger('worker.smtp') - + # on python3 debugoutput is caught with overwritten _print_debug function log.debug("Start sending e-mail") if use_ssl == 2: self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"], @@ -198,9 +208,6 @@ class TaskEmail(CalibreTask): self._handleSuccess() log.debug("E-mail send successfully") - if sys.version_info < (3, 0): - smtplib.stderr = org_smtpstderr - def send_gmail_email(self, message): return gmail.send_messsage(self.settings.get('mail_gmail_token', None), message) @@ -218,7 +225,6 @@ class TaskEmail(CalibreTask): self.asyncSMTP = None self._progress = x - @classmethod def _get_attachment(cls, bookpath, filename): """Get file as MIMEBase message""" diff --git a/cps/tasks/upload.py b/cps/tasks/upload.py index d7ef34c2..6a341cdd 100644 --- a/cps/tasks/upload.py +++ b/cps/tasks/upload.py @@ -1,4 +1,20 @@ -from __future__ import division, print_function, unicode_literals +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2020 pwr +# +# 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 datetime import datetime from cps.services.worker import CalibreTask, STAT_FINISH_SUCCESS diff --git a/cps/templates/admin.html b/cps/templates/admin.html index 81ef955b..9a941594 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -186,6 +186,7 @@ {% if feature_support['updater'] %} +
{{_('Check for Update')}}
diff --git a/cps/templates/author.html b/cps/templates/author.html index b011bae8..d82b2ebd 100644 --- a/cps/templates/author.html +++ b/cps/templates/author.html @@ -36,8 +36,8 @@
- - + + {% if entry.id in read_book_ids %}{% endif %} diff --git a/cps/templates/book_edit.html b/cps/templates/book_edit.html index 78a5cdc1..21ae98e6 100644 --- a/cps/templates/book_edit.html +++ b/cps/templates/book_edit.html @@ -23,20 +23,21 @@ {% if source_formats|length > 0 and conversion_formats|length > 0 %}

{{_('Convert book format:')}}

+
@@ -48,6 +49,7 @@
+
@@ -56,11 +58,11 @@
- -
+
+
diff --git a/cps/templates/book_table.html b/cps/templates/book_table.html index bbcd0ed6..d7cee112 100644 --- a/cps/templates/book_table.html +++ b/cps/templates/book_table.html @@ -20,6 +20,7 @@ {% endblock %} {% block body %}

{{_(title)}}

+
{{_('Merge selected books')}}
diff --git a/cps/templates/config_db.html b/cps/templates/config_db.html index c5027abf..0090bd95 100644 --- a/cps/templates/config_db.html +++ b/cps/templates/config_db.html @@ -8,6 +8,7 @@

{{title}}

+
diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index e062fae5..8cd0034e 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -8,6 +8,7 @@

{{title}}

+
diff --git a/cps/templates/config_view_edit.html b/cps/templates/config_view_edit.html index 2ea1c53c..ebfb5362 100644 --- a/cps/templates/config_view_edit.html +++ b/cps/templates/config_view_edit.html @@ -6,8 +6,9 @@ {% block body %}

{{title}}

- -
+ + +

@@ -114,6 +115,23 @@

+
+ + +
+
+ + +
diff --git a/cps/templates/detail.html b/cps/templates/detail.html index 4a3d8f23..6dd1c72a 100644 --- a/cps/templates/detail.html +++ b/cps/templates/detail.html @@ -214,6 +214,7 @@

+

+

{% if entry.has_cover is defined %} - - {{ entry.title }} + + {{ entry.title }} {% if entry.id in read_book_ids %}{% endif %} diff --git a/cps/templates/email_edit.html b/cps/templates/email_edit.html index 884d9689..698aa6ad 100644 --- a/cps/templates/email_edit.html +++ b/cps/templates/email_edit.html @@ -7,6 +7,7 @@

{{title}}

+ {% if feature_support['gmail'] %}
@@ -72,6 +73,7 @@

{{_('Allowed Domains (Whitelist)')}}

+
@@ -98,11 +100,12 @@ -
- - -
- + +
+ + +
+
diff --git a/cps/templates/grid.html b/cps/templates/grid.html index 88421c06..5de80caf 100644 --- a/cps/templates/grid.html +++ b/cps/templates/grid.html @@ -27,8 +27,8 @@
- - {{ entry[0].name }} + + {{ entry[0].name }} {{entry.count}} diff --git a/cps/templates/http_error.html b/cps/templates/http_error.html index eb628a5f..8abd0c16 100644 --- a/cps/templates/http_error.html +++ b/cps/templates/http_error.html @@ -1,5 +1,5 @@ - + {{ instance }} | HTTP Error ({{ error_code }}) diff --git a/cps/templates/index.html b/cps/templates/index.html index dabcd8da..df276638 100644 --- a/cps/templates/index.html +++ b/cps/templates/index.html @@ -8,8 +8,8 @@
- - {{ entry.title }} + + {{ entry.title }} {% if entry.id in read_book_ids %}{% endif %} @@ -85,8 +85,8 @@
- - {{ entry.title }} + + {{ entry.title }} {% if entry.id in read_book_ids %}{% endif %} diff --git a/cps/templates/layout.html b/cps/templates/layout.html index 017b8791..09b3f507 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -61,7 +61,8 @@ {% if g.user.role_upload() or g.user.role_admin()%} {% if g.allow_upload %}
  • -
  • diff --git a/cps/templates/login.html b/cps/templates/login.html index 7ae56d1b..0d5232e3 100644 --- a/cps/templates/login.html +++ b/cps/templates/login.html @@ -4,6 +4,7 @@

    {{_('Login')}}

    +
    diff --git a/cps/templates/readdjvu.html b/cps/templates/readdjvu.html index 9771b7c9..3504e394 100644 --- a/cps/templates/readdjvu.html +++ b/cps/templates/readdjvu.html @@ -1,35 +1,19 @@ - - - - + + + {{_('DJVU Reader')}} | {{title}} + + + + + - - -{{_('DJVU Reader')}} | {{title}} - - - - - - - - - -
    - - - + + + + + +
    + diff --git a/cps/templates/readtxt.html b/cps/templates/readtxt.html index ea294948..b157fe95 100644 --- a/cps/templates/readtxt.html +++ b/cps/templates/readtxt.html @@ -5,139 +5,26 @@ {{_('txt Reader')}} | {{title}} - + - + + + + + - - - - - - + -
    -
    -
    - -
    - - +
    +
    +
    + +
    diff --git a/cps/templates/register.html b/cps/templates/register.html index db8644fb..307a209b 100644 --- a/cps/templates/register.html +++ b/cps/templates/register.html @@ -3,6 +3,7 @@

    {{_('Register New Account')}}

    + {% if not config.config_register_email %}
    diff --git a/cps/templates/remote_login.html b/cps/templates/remote_login.html index bfa1b115..22097ef2 100644 --- a/cps/templates/remote_login.html +++ b/cps/templates/remote_login.html @@ -1,10 +1,11 @@ {% extends "layout.html" %} {% block body %}
    +

    {{_('Magic Link - Authorise New Device')}}

    {{_('On another device, login and visit:')}} -

    {{verify_url}} +

    {{verify_url}}

    {{_('Once verified, you will automatically be logged in on this device.')}} @@ -16,26 +17,5 @@ {% endblock %} {% block js %} - + {% endblock %} diff --git a/cps/templates/search.html b/cps/templates/search.html index b63819be..e88c2974 100644 --- a/cps/templates/search.html +++ b/cps/templates/search.html @@ -43,8 +43,8 @@

    {% if entry.has_cover is defined %} - - {{ entry.title }} + + {{ entry.title }} {% if entry.id in read_book_ids %}{% endif %} diff --git a/cps/templates/search_form.html b/cps/templates/search_form.html index f5483139..fa57c85b 100644 --- a/cps/templates/search_form.html +++ b/cps/templates/search_form.html @@ -3,6 +3,7 @@

    {{title}}

    +
    @@ -228,10 +229,6 @@ {% endblock %} {% block js %} - - {% if not g.user.locale == 'en' %} diff --git a/cps/templates/shelf.html b/cps/templates/shelf.html index 7ee96f7d..df05f0ad 100644 --- a/cps/templates/shelf.html +++ b/cps/templates/shelf.html @@ -7,6 +7,7 @@ {% endif %} {% if g.user.is_authenticated %} {% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %} +
    {{ _('Delete this Shelf') }}
    {{ _('Edit Shelf Properties') }} {% if entries.__len__() %} @@ -30,8 +31,8 @@
    - - {{ entry.title }} + + {{ entry.title }} {% if entry.id in read_book_ids %}{% endif %} diff --git a/cps/templates/shelf_edit.html b/cps/templates/shelf_edit.html index b3c81731..2882d08a 100644 --- a/cps/templates/shelf_edit.html +++ b/cps/templates/shelf_edit.html @@ -3,6 +3,7 @@

    {{title}}

    +
    diff --git a/cps/templates/shelf_order.html b/cps/templates/shelf_order.html index fc53a69a..ee90fd47 100644 --- a/cps/templates/shelf_order.html +++ b/cps/templates/shelf_order.html @@ -3,15 +3,16 @@

    {{title}}

    {{_('Drag to Rearrange Order')}}
    +
    {% for entry in entries %}
    diff --git a/cps/templates/tasks.html b/cps/templates/tasks.html index c13ddff9..40120335 100644 --- a/cps/templates/tasks.html +++ b/cps/templates/tasks.html @@ -26,8 +26,5 @@ {% block js %} - {% endblock %} diff --git a/cps/templates/user_edit.html b/cps/templates/user_edit.html index b4429faf..a250c246 100644 --- a/cps/templates/user_edit.html +++ b/cps/templates/user_edit.html @@ -3,6 +3,7 @@

    {{title}}

    +
    {% if new_user or ( g.user and content.name != "Guest" and g.user.role_admin() ) %}
    @@ -32,7 +33,7 @@
    @@ -41,7 +42,7 @@
    {{_('Remove Selections')}}
    @@ -183,6 +184,9 @@ +{% if not g.user.locale == 'en' %} + +{% endif %} {% endblock %} diff --git a/cps/translations/cs/LC_MESSAGES/messages.mo b/cps/translations/cs/LC_MESSAGES/messages.mo index 10ddd380..aa1ca7c7 100644 Binary files a/cps/translations/cs/LC_MESSAGES/messages.mo and b/cps/translations/cs/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/cs/LC_MESSAGES/messages.po b/cps/translations/cs/LC_MESSAGES/messages.po index 0d29c804..d8dc1416 100644 --- a/cps/translations/cs/LC_MESSAGES/messages.po +++ b/cps/translations/cs/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-Web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2020-06-09 21:11+0100\n" "Last-Translator: Lukas Heroudek \n" "Language: cs_CZ\n" @@ -439,24 +439,29 @@ msgstr "Všeobecná chyba" msgid "Update File Could Not be Saved in Temp Dir" msgstr "Aktualizační soubor nemohl být uložen do Temp Dir" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +#, fuzzy +msgid "Failed to extract at least One LDAP User" +msgstr "Nepodařilo se vytvořit nejméně jednoho uživatele LDAP" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "Nepodařilo se vytvořit nejméně jednoho uživatele LDAP" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "Chyba: %(ldaperror)s" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "Chyba: Žádná reakce od uživatele LDAP serveru" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "Nejméně jeden uživatel LDAP nenalezen v databázi" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "" diff --git a/cps/translations/de/LC_MESSAGES/messages.mo b/cps/translations/de/LC_MESSAGES/messages.mo index faa817d0..a048fc1f 100644 Binary files a/cps/translations/de/LC_MESSAGES/messages.mo and b/cps/translations/de/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/de/LC_MESSAGES/messages.po b/cps/translations/de/LC_MESSAGES/messages.po index e26cce8e..4e845366 100644 --- a/cps/translations/de/LC_MESSAGES/messages.po +++ b/cps/translations/de/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-Web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2021-08-01 17:24+0200\n" "Last-Translator: Ozzie Isaacs\n" "Language: de\n" @@ -432,24 +432,29 @@ msgstr "Allgemeiner Fehler" msgid "Update File Could Not be Saved in Temp Dir" msgstr "Updatedatei konnte nicht in Temporärem Ordner gespeichert werden" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +#, fuzzy +msgid "Failed to extract at least One LDAP User" +msgstr "Mindestens ein LDAP Benutzer konnte nicht erzeugt werden" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "Mindestens ein LDAP Benutzer konnte nicht erzeugt werden" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "Fehler: %(ldaperror)s" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "Fehler: Keine Benutzerinformationen von LDAP Server empfangen" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "Mindestens ein LDAP Benutzer wurde nicht in der Datenbank gefudnen" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "{} Benutzer erfolgreich importiert" @@ -3041,7 +3046,7 @@ msgstr "Kindle E-Mail" #: cps/templates/user_table.html:136 msgid "Locale" -msgstr "Anuzeigesprache" +msgstr "Anzeigesprache" #: cps/templates/user_table.html:137 msgid "Visible Book Languages" diff --git a/cps/translations/el/LC_MESSAGES/messages.mo b/cps/translations/el/LC_MESSAGES/messages.mo index 3c2665b5..b0649634 100644 Binary files a/cps/translations/el/LC_MESSAGES/messages.mo and b/cps/translations/el/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/el/LC_MESSAGES/messages.po b/cps/translations/el/LC_MESSAGES/messages.po index 0adebb63..88e1f30d 100644 --- a/cps/translations/el/LC_MESSAGES/messages.po +++ b/cps/translations/el/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-Web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Depountis Georgios\n" "Language: el\n" @@ -439,24 +439,29 @@ msgstr "Γενικό σφάλμα" msgid "Update File Could Not be Saved in Temp Dir" msgstr "Το Αρχείο Ενημέρωσης Δεν Μπόρεσε Να Αποθηκευτεί σε" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +#, fuzzy +msgid "Failed to extract at least One LDAP User" +msgstr "Αποτυχία Δημιουργίας Τουλάχιστον Ενός Χρήστη LDAP" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "Αποτυχία Δημιουργίας Τουλάχιστον Ενός Χρήστη LDAP" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "Σφάλμα: %(ldaperror)s" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "Σφάλμα: Δεν επιστράφηκε χρήστης σε απάντηση του διακομιστή LDAP" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "Τουλάχιστον Ένας Χρήστης LDAP Δεν Βρέθηκε Στη Βάση Δεδομένων" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "" diff --git a/cps/translations/es/LC_MESSAGES/messages.mo b/cps/translations/es/LC_MESSAGES/messages.mo index 8ffe675f..5e497627 100644 Binary files a/cps/translations/es/LC_MESSAGES/messages.mo and b/cps/translations/es/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/es/LC_MESSAGES/messages.po b/cps/translations/es/LC_MESSAGES/messages.po index 73a7bde2..68346317 100644 --- a/cps/translations/es/LC_MESSAGES/messages.po +++ b/cps/translations/es/LC_MESSAGES/messages.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-Web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2020-05-25 17:22+0200\n" "Last-Translator: minakmostoles \n" "Language: es\n" @@ -443,24 +443,29 @@ msgstr "Error general" msgid "Update File Could Not be Saved in Temp Dir" msgstr "La actualización del archivo no pudo guardarse en el directorio temporal (Temp Dir)" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +#, fuzzy +msgid "Failed to extract at least One LDAP User" +msgstr "Error al crear al menos un usuario LDAP" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "Error al crear al menos un usuario LDAP" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "Error: %(ldaperror)s" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "Error: el servidor LDAP no ha devuelto ningún usuario" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "Al menos, un usuario LDAP no se ha encontrado en la base de datos" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "{} Usuario importado con éxito" diff --git a/cps/translations/fi/LC_MESSAGES/messages.mo b/cps/translations/fi/LC_MESSAGES/messages.mo index 52e7454c..f95f1c28 100644 Binary files a/cps/translations/fi/LC_MESSAGES/messages.mo and b/cps/translations/fi/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/fi/LC_MESSAGES/messages.po b/cps/translations/fi/LC_MESSAGES/messages.po index a0c579a6..fe73c86c 100644 --- a/cps/translations/fi/LC_MESSAGES/messages.po +++ b/cps/translations/fi/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-Web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2020-01-12 13:56+0100\n" "Last-Translator: Samuli Valavuo \n" "Language: fi\n" @@ -439,24 +439,28 @@ msgstr "Yleinen virhe" msgid "Update File Could Not be Saved in Temp Dir" msgstr "" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +msgid "Failed to extract at least One LDAP User" +msgstr "" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "" diff --git a/cps/translations/fr/LC_MESSAGES/messages.mo b/cps/translations/fr/LC_MESSAGES/messages.mo index e0fc8696..6f3342df 100644 Binary files a/cps/translations/fr/LC_MESSAGES/messages.mo and b/cps/translations/fr/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/fr/LC_MESSAGES/messages.po b/cps/translations/fr/LC_MESSAGES/messages.po index ea01b7ac..79e03453 100644 --- a/cps/translations/fr/LC_MESSAGES/messages.po +++ b/cps/translations/fr/LC_MESSAGES/messages.po @@ -22,7 +22,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-Web\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2020-06-07 06:47+0200\n" "Last-Translator: \n" "Language: fr\n" @@ -455,24 +455,29 @@ msgstr "Erreur générale" msgid "Update File Could Not be Saved in Temp Dir" msgstr "Le fichier de mise à jour ne peut pas être sauvegardé dans le répertoire temporaire" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +#, fuzzy +msgid "Failed to extract at least One LDAP User" +msgstr "Impossible de créer au moins un utilisateur LDAP" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "Impossible de créer au moins un utilisateur LDAP" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "Erreur : %(ldaperror)s" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "Erreur : Aucun utilisateur renvoyé dans la réponse LDAP du serveur" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "Au moins un utilisateur LDAP n'a pas été trouvé dans la base de données" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "{} utilisateur importé avec succès" diff --git a/cps/translations/hu/LC_MESSAGES/messages.mo b/cps/translations/hu/LC_MESSAGES/messages.mo index 0913ca9a..405ca01a 100644 Binary files a/cps/translations/hu/LC_MESSAGES/messages.mo and b/cps/translations/hu/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/hu/LC_MESSAGES/messages.po b/cps/translations/hu/LC_MESSAGES/messages.po index d60dd4ae..39eac74e 100644 --- a/cps/translations/hu/LC_MESSAGES/messages.po +++ b/cps/translations/hu/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2019-04-06 23:36+0200\n" "Last-Translator: \n" "Language: hu\n" @@ -439,24 +439,28 @@ msgstr "Általános hiba" msgid "Update File Could Not be Saved in Temp Dir" msgstr "" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +msgid "Failed to extract at least One LDAP User" +msgstr "" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "" diff --git a/cps/translations/it/LC_MESSAGES/messages.mo b/cps/translations/it/LC_MESSAGES/messages.mo index 18d2fe48..b811ef23 100644 Binary files a/cps/translations/it/LC_MESSAGES/messages.mo and b/cps/translations/it/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/it/LC_MESSAGES/messages.po b/cps/translations/it/LC_MESSAGES/messages.po index f0ebab0b..a320d6b7 100644 --- a/cps/translations/it/LC_MESSAGES/messages.po +++ b/cps/translations/it/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-Web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2017-04-04 15:09+0200\n" "Last-Translator: ElQuimm \n" "Language: it\n" @@ -436,24 +436,29 @@ msgstr "Errore generale" msgid "Update File Could Not be Saved in Temp Dir" msgstr "Il file di aggiornamento non può essere salvato nella cartella temporanea" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +#, fuzzy +msgid "Failed to extract at least One LDAP User" +msgstr "Fallita la creazione di almeno un utente LDAP" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "Fallita la creazione di almeno un utente LDAP" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "Errore: %(ldaperror)s" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "Errore: nessun utente restituito in risposta dal server LDAP" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "Almeno un utente LDAP non è stato trovato nel database" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "{} utente importato con successo" diff --git a/cps/translations/ja/LC_MESSAGES/messages.mo b/cps/translations/ja/LC_MESSAGES/messages.mo index c941c6ba..aecda494 100644 Binary files a/cps/translations/ja/LC_MESSAGES/messages.mo and b/cps/translations/ja/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/ja/LC_MESSAGES/messages.po b/cps/translations/ja/LC_MESSAGES/messages.po index 59613304..e0a4db65 100644 --- a/cps/translations/ja/LC_MESSAGES/messages.po +++ b/cps/translations/ja/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-Web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2018-02-07 02:20-0500\n" "Last-Translator: white \n" "Language: ja\n" @@ -434,24 +434,28 @@ msgstr "エラー発生" msgid "Update File Could Not be Saved in Temp Dir" msgstr "" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +msgid "Failed to extract at least One LDAP User" +msgstr "" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "" diff --git a/cps/translations/km/LC_MESSAGES/messages.mo b/cps/translations/km/LC_MESSAGES/messages.mo index 13a8f6cf..2d5e66a6 100644 Binary files a/cps/translations/km/LC_MESSAGES/messages.mo and b/cps/translations/km/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/km/LC_MESSAGES/messages.po b/cps/translations/km/LC_MESSAGES/messages.po index 89d210a0..97e896ba 100644 --- a/cps/translations/km/LC_MESSAGES/messages.po +++ b/cps/translations/km/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-Web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2018-08-27 17:06+0700\n" "Last-Translator: \n" "Language: km_KH\n" @@ -439,24 +439,28 @@ msgstr "" msgid "Update File Could Not be Saved in Temp Dir" msgstr "" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +msgid "Failed to extract at least One LDAP User" +msgstr "" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "" diff --git a/cps/translations/nl/LC_MESSAGES/messages.mo b/cps/translations/nl/LC_MESSAGES/messages.mo index 076fa740..d678b934 100644 Binary files a/cps/translations/nl/LC_MESSAGES/messages.mo and b/cps/translations/nl/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/nl/LC_MESSAGES/messages.po b/cps/translations/nl/LC_MESSAGES/messages.po index 808374cb..b4503ef6 100644 --- a/cps/translations/nl/LC_MESSAGES/messages.po +++ b/cps/translations/nl/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-Web (GPLV3)\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2020-12-12 08:20+0100\n" "Last-Translator: Marcel Maas \n" "Language: nl\n" @@ -37,7 +37,7 @@ msgstr "De server is herstart, vernieuw de pagina" #: cps/admin.py:147 msgid "Performing shutdown of server, please close window" -msgstr "Bezig met afsluiten van de server, sluit het venster" +msgstr "Bezig met het afsluiten van de server, sluit het venster" #: cps/admin.py:155 msgid "Reconnect successful" @@ -60,7 +60,7 @@ msgstr "Systeembeheer" #: cps/admin.py:208 msgid "Basic Configuration" -msgstr "Basis configuratie" +msgstr "Basisconfiguratie" #: cps/admin.py:241 msgid "UI Configuration" @@ -123,11 +123,11 @@ msgstr "Ongeldige waarde" #: cps/admin.py:462 msgid "Guest's Locale is determined automatically and can't be set" -msgstr "Gast's locatie is automatisch bepaald en kan niet handmatig worden ingesteld" +msgstr "Gasts locale is automatisch bepaald en kan niet handmatig worden ingesteld" #: cps/admin.py:466 msgid "No Valid Locale Given" -msgstr "Geen geldige locatie is opgegeven" +msgstr "Geen geldige locale is opgegeven" #: cps/admin.py:477 msgid "No Valid Book Language Given" @@ -139,11 +139,11 @@ msgstr "Parameter is niet gevonden" #: cps/admin.py:534 msgid "Invalid Read Column" -msgstr "Ongeldig gelezen kolom" +msgstr "Ongeldige gelezen kolom" #: cps/admin.py:540 msgid "Invalid Restricted Column" -msgstr "Ongeldig beperkt kolom" +msgstr "Ongeldige beperkte kolom" #: cps/admin.py:559 cps/admin.py:1291 msgid "Calibre-Web configuration updated" @@ -151,7 +151,7 @@ msgstr "Calibre-Web-configuratie bijgewerkt" #: cps/admin.py:571 msgid "Do you really want to delete the Kobo Token?" -msgstr "Wil je werkelijk je Kobo Token verwijderen?" +msgstr "Wil je je Kobo Token echt verwijderen?" #: cps/admin.py:573 msgid "Do you really want to delete this domain?" @@ -168,34 +168,34 @@ msgstr "Weet je zeker dat je deze boekenplank wilt verwijderen?" #: cps/admin.py:579 #, fuzzy msgid "Are you sure you want to change locales of selected user(s)?" -msgstr "Weet je zeker dat je deze boekenplank wilt verwijderen?" +msgstr "Weet je zeker dat je de locales van de geselecteerde gebruiker(s) wil veranderen?" #: cps/admin.py:581 msgid "Are you sure you want to change visible book languages for selected user(s)?" -msgstr "Weet je zeker dat je de zichtbaarheidstalen wil veranderen voor de geselecteerde gebruiker(s)?" +msgstr "Weet je zeker dat je de zichtbare talen voor de geselecteerde gebruiker(s) wil veranderen?" #: cps/admin.py:583 msgid "Are you sure you want to change the selected role for the selected user(s)?" -msgstr "Weet je zeker dat je de geselecteerde rol wil veranderen voor de geselecteerde gebruiker(s)?" +msgstr "Weet je zeker dat je de geselecteerde rol van de geselecteerde gebruiker(s) wil veranderen?" #: cps/admin.py:585 #, fuzzy msgid "Are you sure you want to change the selected restrictions for the selected user(s)?" -msgstr "Weet je zeker dat je deze boekenplank wilt verwijderen?" +msgstr "Weet je zeker dat je de geselecteerde beperkingen voor de geselecteerde gebruikers(s) wil verwijderen?" #: cps/admin.py:587 msgid "Are you sure you want to change the selected visibility restrictions for the selected user(s)?" -msgstr "Weet je zeker dat je de geselecteerde zichtbaarheidsbeperkingen wilt verwijderen" +msgstr "Weet je zeker dat je de geselecteerde zichtbaarheidsbeperkingen voor de geselecteerde gebruiker(s) wil veranderen?" #: cps/admin.py:589 #, fuzzy msgid "Are you sure you want to change shelf sync behavior for the selected user(s)?" -msgstr "Weet je zeker dat je de geselecteerde rol wil veranderen voor de geselecteerde gebruiker(s)?" +msgstr "Weet je zeker dat je de synchronisatiegedrag van boekenplanken voor de geselecteerde gebruiker(s) wil veranderen?" #: cps/admin.py:591 #, fuzzy msgid "Are you sure you want to change Calibre library location?" -msgstr "Weet je zeker dat je Calibre-Web wilt stoppen?" +msgstr "Weet je zeker dat je de locatie van de Calibre-bibliotheek wil veranderen?" #: cps/admin.py:740 msgid "Tag not found" @@ -231,16 +231,15 @@ msgstr "De locatie vam het toegangslog is onjuist, voer een geldige locatie in" #: cps/admin.py:1095 msgid "Please Enter a LDAP Provider, Port, DN and User Object Identifier" -msgstr "Voer alsjeblieft een LDAP leverancier, Port, DN en User Object Identifier in" +msgstr "Voer alsjeblieft een LDAP Provider, Port, DN en User Object Identifier in" #: cps/admin.py:1101 -#, fuzzy msgid "Please Enter a LDAP Service Account and Password" -msgstr "Geef een geldige gebruikersnaam op om je wachtwoord te herstellen" +msgstr "Voer een geldig LDAP Service Account en wachtwoord in" #: cps/admin.py:1104 msgid "Please Enter a LDAP Service Account" -msgstr "" +msgstr "Voer een LDAP Service Account in" #: cps/admin.py:1109 #, python-format @@ -249,7 +248,7 @@ msgstr "LDAP Groep Object Filter Moet Een \"%s\" Formaat Identificiatie hebben" #: cps/admin.py:1111 msgid "LDAP Group Object Filter Has Unmatched Parenthesis" -msgstr "LDAP Groep Object Filter Heeft Een Ongelijk Haakje" +msgstr "LDAP Groep Object Filter heeft een niet-gebalanceerd haakje" #: cps/admin.py:1115 #, python-format @@ -258,7 +257,7 @@ msgstr "LDAP Gebruiker Object Filter moet \"%s\" Formaat Identificatie hebben" #: cps/admin.py:1117 msgid "LDAP User Object Filter Has Unmatched Parenthesis" -msgstr "LDAP Gebruiker Filter heeft een ongelijk haakje" +msgstr "LDAP Gebruiker Filter heeft een niet-gebalanceerd haakje" #: cps/admin.py:1124 #, python-format @@ -267,7 +266,7 @@ msgstr "LDAP Lid Gebruiker Filter moet een \"%s\" Formaat Identificatie hebben" #: cps/admin.py:1126 msgid "LDAP Member User Filter Has Unmatched Parenthesis" -msgstr "LDAP Lid Gebruiker Filter heeft een ongelijk haakje" +msgstr "LDAP Lid Gebruiker Filter heeft een niet-gebalanceerd haakje" #: cps/admin.py:1133 msgid "LDAP CACertificate, Certificate or Key Location is not Valid, Please Enter Correct Path" @@ -277,7 +276,7 @@ msgstr "LDAP CACertficaat, Certificaat of Sleutel Locatie is ongeldig. Voer alsj #: cps/admin.py:1536 cps/shelf.py:102 cps/shelf.py:162 cps/shelf.py:205 #: cps/shelf.py:272 cps/shelf.py:333 cps/shelf.py:368 cps/shelf.py:443 msgid "Settings DB is not Writeable" -msgstr "Instellingen niet opgeslagen" +msgstr "DB-instellingen niet opgeslagen" #: cps/admin.py:1186 msgid "DB Location is not Valid, Please Enter Correct Path" @@ -289,16 +288,16 @@ msgstr "Kan niet schrijven naar database" #: cps/admin.py:1204 msgid "Keyfile Location is not Valid, Please Enter Correct Path" -msgstr "SSL-sleutellocatie is niet geldig, voer een geldige locatie in" +msgstr "SSL-sleutellocatie is niet geldig, voer een geldig pad in" #: cps/admin.py:1208 msgid "Certfile Location is not Valid, Please Enter Correct Path" -msgstr "SSL-certificaatlocatie is niet geldig, voer een geldige locatie in" +msgstr "SSL-certificaatlocatie is niet geldig, voer een geldig pad in" #: cps/admin.py:1320 #, fuzzy msgid "Database Configuration" -msgstr "Geavanceerde opties" +msgstr "Databaseconfiguratie" #: cps/admin.py:1336 cps/web.py:1479 msgid "Please fill out all fields!" @@ -441,24 +440,29 @@ msgstr "Algemene fout" msgid "Update File Could Not be Saved in Temp Dir" msgstr "Geüpload bestand kon niet opgeslagen worden in de tijdelijke map" -#: cps/admin.py:1746 -msgid "Failed to Create at Least One LDAP User" +#: cps/admin.py:1709 +#, fuzzy +msgid "Failed to extract at least One LDAP User" msgstr "Mislukt om minstens een LDAP gebruiker aan te maken" -#: cps/admin.py:1759 +#: cps/admin.py:1752 +msgid "Failed to Create at Least One LDAP User" +msgstr "Het is niet gelukt tenminste een LDAP gebruiker aan te maken" + +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "Fout: %(ldaperror)s" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "Fout: No user returned in response of LDAP server" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "Minstens een LDAP Gebruiker is niet gevonden in de Database" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "{} Gebruiker succesvol geïmporteerd" @@ -495,7 +499,7 @@ msgstr "metagegevens bewerken" #: cps/editbooks.py:458 #, python-format msgid "%(seriesindex)s is not a valid number, skipping" -msgstr "" +msgstr "%(seriesindex)s is geen geldig nummer, sla het over" #: cps/editbooks.py:493 #, python-format @@ -638,11 +642,11 @@ msgstr "%(format)s versturen naar Kindle" #: cps/helper.py:220 cps/tasks/convert.py:73 #, fuzzy, python-format msgid "%(book)s send to Kindle" -msgstr "Versturen naar Kindle" +msgstr "%(book)s verzonden naar Kindle" #: cps/helper.py:225 msgid "The requested file could not be read. Maybe wrong permissions?" -msgstr "Het opgevraagde bestand kan niet worden uitgelezen. Ben je hiertoe gemachtigd?" +msgstr "Het opgevraagde bestand kan niet worden gelezen. Ben je hiertoe gemachtigd?" #: cps/helper.py:322 #, python-format @@ -657,12 +661,12 @@ msgstr "Verwijderen van boek %(id)s mislukt: %(message)s" #: cps/helper.py:339 #, fuzzy, python-format msgid "Deleting book %(id)s from database only, book path in database not valid: %(path)s" -msgstr "Verwijderen van boek %(id)s, boek pad is ongeldig: %(path)s" +msgstr "Verwijder boek %(id)s alleen uit database, boek pad is ongeldig: %(path)s" #: cps/helper.py:394 #, python-format msgid "Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s" -msgstr "Kan de naam '%(src)s' niet wijzigen in '%(dest)s': %(error)s" +msgstr "Kan de titel '%(src)s' niet wijzigen in '%(dest)s': %(error)s" #: cps/helper.py:409 #, python-format @@ -698,7 +702,7 @@ msgstr "Fout bij downloaden omslag" #: cps/helper.py:610 msgid "Cover Format Error" -msgstr "Onjuist omslag formaat" +msgstr "Onjuist omslagformaat" #: cps/helper.py:620 msgid "Failed to create path for cover" @@ -710,7 +714,7 @@ msgstr "Omslag-bestand is geen afbeelding of kon niet opgeslagen worden" #: cps/helper.py:647 msgid "Only jpg/jpeg/png/webp/bmp files are supported as coverfile" -msgstr "Alleen jpg/jpeg/png/webp/bmp bestanden worden ondersteund als voorblad" +msgstr "Alleen jpg/jpeg/png/webp/bmp bestanden worden ondersteund als omslag" #: cps/helper.py:660 msgid "Only jpg/jpeg files are supported as coverfile" @@ -1034,7 +1038,7 @@ msgstr "Boekenplank maken" #: cps/shelf.py:238 #, fuzzy msgid "Sorry you are not allowed to edit this shelf" -msgstr "Je bent niet gemachtigd om boeken te verwijderen van boekenplank: %(sname)s" +msgstr "Je bent niet gemachtigd deze boekenplank aan te passen" #: cps/shelf.py:240 msgid "Edit a shelf" @@ -1067,7 +1071,7 @@ msgstr "Een persoonlijke boekenplank met de naam '%(title)s' bestaat al." #: cps/shelf.py:378 #, python-format msgid "Change order of Shelf: '%(name)s'" -msgstr "Volgorde bewerken van boekenplank: '%(name)s'" +msgstr "Volgorde van boekenplank veranderen: '%(name)s'" #: cps/shelf.py:448 #, python-format @@ -1096,7 +1100,7 @@ msgstr "De update-informatie kan niet worden opgehaald" #: cps/updater.py:427 msgid "Click on the button below to update to the latest stable version." -msgstr "Klik op onderstaande knop om de laatste stabiele versie te installeren." +msgstr "Klik op de onderstaande knop om de laatste stabiele versie te installeren." #: cps/updater.py:436 cps/updater.py:450 cps/updater.py:461 #, python-format @@ -1207,7 +1211,7 @@ msgstr "Lees Status = %(status)s" #: cps/web.py:1354 msgid "Error on search for custom columns, please restart Calibre-Web" -msgstr "" +msgstr "Fout tijdens het zoeken van aangepaste kolommen, start Calibre-Web opnieuw op" #: cps/web.py:1449 #, python-format @@ -1275,7 +1279,7 @@ msgstr "Je bent ingelogd als: '%(nickname)s'" #: cps/web.py:1633 cps/web.py:1682 #, python-format msgid "%(name)s's profile" -msgstr "%(name)s's profiel" +msgstr "%(name)ss profiel" #: cps/web.py:1649 msgid "Profile updated" @@ -1321,7 +1325,7 @@ msgstr "Calibre mislukt met foutmelding: %(error)s" #: cps/templates/admin.html:9 msgid "Users" -msgstr "Gebruikerslijst" +msgstr "Gebruikers" #: cps/templates/admin.html:13 cps/templates/login.html:8 #: cps/templates/login.html:9 cps/templates/register.html:8 @@ -1471,11 +1475,11 @@ msgstr "Reverse proxy header naam" #: cps/templates/admin.html:153 #, fuzzy msgid "Edit Calibre Database Configuration" -msgstr "Bewerk basis configuratie" +msgstr "Bewerk Calibre databaseconfiguratie" #: cps/templates/admin.html:154 msgid "Edit Basic Configuration" -msgstr "Bewerk basis configuratie" +msgstr "Bewerk basisconfiguratie" #: cps/templates/admin.html:155 msgid "Edit UI Configuration" @@ -1783,7 +1787,7 @@ msgstr "Geselecteerde boeken verwijderen" #: cps/templates/book_table.html:29 msgid "Exchange author and title" -msgstr "" +msgstr "Auteur en titel omwisselen" #: cps/templates/book_table.html:35 msgid "Update Title Sort automatically" @@ -1804,7 +1808,7 @@ msgstr "Titel" #: cps/templates/book_table.html:52 msgid "Enter Title Sort" -msgstr "Voer Titel sorteer volgorde in" +msgstr "Voer Titel sorteervolgorde in" #: cps/templates/book_table.html:52 msgid "Title Sort" @@ -1812,7 +1816,7 @@ msgstr "Titel sorteren" #: cps/templates/book_table.html:53 msgid "Enter Author Sort" -msgstr "Voer Auteur sorteer volgorde in" +msgstr "Voer Auteur sorteervolgorde in" #: cps/templates/book_table.html:53 msgid "Author Sort" @@ -1844,7 +1848,7 @@ msgstr "Voer publicatiedatum in" #: cps/templates/book_table.html:60 msgid "Enter Publishers" -msgstr "Voer Uitgevers in" +msgstr "Voer uitgevers in" #: cps/templates/book_table.html:75 cps/templates/modal_dialogs.html:46 msgid "Are you really sure?" @@ -1852,11 +1856,11 @@ msgstr "Weet je het zeker?" #: cps/templates/book_table.html:79 msgid "Books with Title will be merged from:" -msgstr "Boeken met de Titel zullen worden samengevoegd van:" +msgstr "Boeken met de titel zullen worden samengevoegd van:" #: cps/templates/book_table.html:83 msgid "Into Book with Title:" -msgstr "In boek met Titel:" +msgstr "In boek met titel:" #: cps/templates/book_table.html:88 msgid "Merge" @@ -1889,7 +1893,7 @@ msgstr "Intrekken" #: cps/templates/config_db.html:67 #, fuzzy msgid "New db location is invalid, please enter valid path" -msgstr "Database niet gevonden, voer de juiste locatie in" +msgstr "Database niet gevonden, voer een geldig pad in" #: cps/templates/config_edit.html:17 msgid "Server Configuration" @@ -1945,7 +1949,7 @@ msgstr "Uploaden inschakelen" #: cps/templates/config_edit.html:103 msgid "Allowed Upload Fileformats" -msgstr "Toegelaten upload formaten" +msgstr "Toegelaten upload bestandsformaten" #: cps/templates/config_edit.html:109 msgid "Enable Anonymous Browsing" @@ -1981,7 +1985,7 @@ msgstr "Gebruik Goodreads" #: cps/templates/config_edit.html:145 msgid "Create an API Key" -msgstr "API-sleutel verkrijgen" +msgstr "Maak API-sleutel aan" #: cps/templates/config_edit.html:149 msgid "Goodreads API Key" @@ -2242,7 +2246,7 @@ msgstr "Voeg toegestane/geweigerde tags toe" #: cps/templates/config_view_edit.html:144 msgid "Add Allowed/Denied custom column values" -msgstr "Voeg toegestane/geweigerde aangepaste kolom waarden toe" +msgstr "Voeg toegestane/geweigerde aangepaste kolomwaarden toe" #: cps/templates/detail.html:60 cps/templates/detail.html:69 msgid "Read in Browser" @@ -2255,7 +2259,7 @@ msgstr "Luisteren in webbrowser" #: cps/templates/detail.html:124 #, fuzzy, python-format msgid "Book %(index)s of %(range)s" -msgstr "" +msgstr "Boek %(index)s van %(range)s" #: cps/templates/detail.html:173 msgid "Published" @@ -2392,7 +2396,7 @@ msgstr "Terug naar startpagina" #: cps/templates/http_error.html:50 msgid "Logout User" -msgstr "" +msgstr "Gebruiker uitloggen" #: cps/templates/index.html:72 cps/templates/search.html:33 #: cps/templates/shelf.html:20 @@ -2479,7 +2483,7 @@ msgstr "Boekenplanken" #: cps/templates/index.xml:123 msgid "Books organized in shelves" -msgstr "Boeken onderdeel van boekenplanken" +msgstr "Boeken georganiseerd op boekenplanken" #: cps/templates/layout.html:29 msgid "Home" @@ -2645,7 +2649,7 @@ msgstr "Selecteer" #: cps/templates/modal_dialogs.html:134 #, fuzzy msgid "Ok" -msgstr "Boek" +msgstr "Oké" #: cps/templates/osd.xml:5 msgid "Calibre-Web eBook Catalog" @@ -2663,7 +2667,7 @@ msgstr "Tekstindeling automatisch aanpassen als het zijpaneel geopend is." #: cps/templates/readcbr.html:7 #, fuzzy msgid "Comic Reader" -msgstr "PDF lezer" +msgstr "Comic Reader" #: cps/templates/readcbr.html:89 msgid "Keyboard Shortcuts" @@ -2763,23 +2767,23 @@ msgstr "Rechts-naar-links" #: cps/templates/readcbr.html:165 msgid "Reset to Top" -msgstr "" +msgstr "Terugzetten naar bovenkant" #: cps/templates/readcbr.html:166 msgid "Remember Position" -msgstr "" +msgstr "Positie onthouden" #: cps/templates/readcbr.html:171 msgid "Scrollbar" -msgstr "" +msgstr "Schuifbalk" #: cps/templates/readcbr.html:174 msgid "Show" -msgstr "" +msgstr "Toon" #: cps/templates/readcbr.html:175 msgid "Hide" -msgstr "" +msgstr "Verberg" #: cps/templates/readdjvu.html:10 #, fuzzy @@ -2818,7 +2822,7 @@ msgstr "Meld je aan op een ander apparaat en ga naar:" #: cps/templates/remote_login.html:10 msgid "Once verified, you will automatically be logged in on this device." -msgstr "Daarna wordt je automatisch op dit apparaat ingelogd." +msgstr "Na controle wordt je automatisch op dit apparaat ingelogd." #: cps/templates/remote_login.html:13 msgid "This verification link will expire in 10 minutes." @@ -2887,11 +2891,11 @@ msgstr "Van:" #: cps/templates/search_form.html:189 msgid "To:" -msgstr "Naar:" +msgstr "Tot:" #: cps/templates/shelf.html:10 msgid "Delete this Shelf" -msgstr "Boekenplank verwijderen" +msgstr "Deze boekenplank verwijderen" #: cps/templates/shelf.html:11 msgid "Edit Shelf Properties" @@ -2899,7 +2903,7 @@ msgstr "Bewerk boekenplank eigenschappen" #: cps/templates/shelf.html:13 msgid "Arrange books manually" -msgstr "Regel boeken handmatig" +msgstr "Boeken handmatig rangschikken" #: cps/templates/shelf.html:14 msgid "Disable Change order" @@ -2915,11 +2919,11 @@ msgstr "Boekenplank wordt permanent verwijderd voor iedereen" #: cps/templates/shelf_edit.html:13 msgid "Share with Everyone" -msgstr "Zichtbaar voor iedereen" +msgstr "Delen met iedereen" #: cps/templates/shelf_edit.html:20 msgid "Sync this shelf with Kobo device" -msgstr "" +msgstr "Synchroniseer deze boekenplank met een Kobo-apparaat" #: cps/templates/shelf_order.html:5 msgid "Drag to Rearrange Order" @@ -2951,7 +2955,7 @@ msgstr "Boekenreeksen in deze bibliotheek" #: cps/templates/stats.html:29 msgid "System Statistics" -msgstr "Systeem informatie" +msgstr "Systeeminformatie" #: cps/templates/stats.html:33 msgid "Program Library" @@ -2959,7 +2963,7 @@ msgstr "Programmabibliotheek" #: cps/templates/stats.html:34 msgid "Installed Version" -msgstr "Versie" +msgstr "Geïnstalleerde Versie" #: cps/templates/tasks.html:12 msgid "User" @@ -3019,19 +3023,19 @@ msgstr "Kobo Sync Token" #: cps/templates/user_edit.html:65 msgid "Create/View" -msgstr "Aanmaken/Bekijk" +msgstr "Aanmaken/Bekijken" #: cps/templates/user_edit.html:84 msgid "Add allowed/Denied Custom Column Values" -msgstr "Voeg toegestane/geweigerde aangepaste kolom waarden toe" +msgstr "Voeg toegestane/geweigerde aangepaste kolomwaarden toe" #: cps/templates/user_edit.html:129 msgid "Sync only books in selected shelves with Kobo" -msgstr "" +msgstr "Synchroniseer alleen boeken op geselecteerde boekenplanken met Kobo" #: cps/templates/user_edit.html:139 cps/templates/user_table.html:168 msgid "Delete User" -msgstr "Deze gebruiker verwijderen" +msgstr "Gebruiker verwijderen" #: cps/templates/user_edit.html:151 msgid "Generate Kobo Auth URL" @@ -3042,99 +3046,87 @@ msgid "Select..." msgstr "Selecteer..." #: cps/templates/user_table.html:130 -#, fuzzy msgid "Edit User" -msgstr "Systeembeheerder" +msgstr "Gebruiker Bewerken" #: cps/templates/user_table.html:133 -#, fuzzy msgid "Enter Username" -msgstr "Kies een gebruikersnaam" +msgstr "Voer gebruikersnaam in" #: cps/templates/user_table.html:134 -#, fuzzy msgid "Enter E-mail Address" -msgstr "Je e-mailadres" +msgstr "Voer e-mailadres in" #: cps/templates/user_table.html:135 -#, fuzzy msgid "Enter Kindle E-mail Address" -msgstr "Kindle-e-mailadres" +msgstr "Voer Kindle-e-mailadres in" #: cps/templates/user_table.html:135 -#, fuzzy msgid "Kindle E-mail" -msgstr "Test-e-mail" +msgstr "Kindle-e-mail" #: cps/templates/user_table.html:136 -#, fuzzy msgid "Locale" -msgstr "Schaal" +msgstr "Locale" #: cps/templates/user_table.html:137 msgid "Visible Book Languages" msgstr "Zichtbare Boek Talen" #: cps/templates/user_table.html:138 -#, fuzzy msgid "Edit Allowed Tags" -msgstr "Selecteer toegestane/geweigerde tags" +msgstr "Toegestaande labels bewerken" #: cps/templates/user_table.html:138 msgid "Allowed Tags" -msgstr "Toegestaande Tags" +msgstr "Toegestaande Labels" #: cps/templates/user_table.html:139 -#, fuzzy msgid "Edit Denied Tags" -msgstr "Selecteer toegestane/geweigerde tags" +msgstr "Verboden labels bewerken" #: cps/templates/user_table.html:139 msgid "Denied Tags" -msgstr "Verboden Tags" +msgstr "Verboden Labels" #: cps/templates/user_table.html:140 -#, fuzzy msgid "Edit Allowed Column Values" -msgstr "Voeg toegestane/geweigerde aangepaste kolom waarden toe" +msgstr "Toegestane kolomwaarden bewerken" #: cps/templates/user_table.html:140 -#, fuzzy msgid "Allowed Column Values" -msgstr "Voeg toegestane/geweigerde aangepaste kolom waarden toe" +msgstr "Toegestane kolomwaarden" #: cps/templates/user_table.html:141 #, fuzzy msgid "Edit Denied Column Values" -msgstr "Voeg toegestane/geweigerde aangepaste kolom waarden toe" +msgstr "Geweigerde kolomwaarden bewerken" #: cps/templates/user_table.html:141 #, fuzzy msgid "Denied Columns Values" -msgstr "Voeg toegestane/geweigerde aangepaste kolom waarden toe" +msgstr "Geweigerde kolomwaarden" #: cps/templates/user_table.html:143 #, fuzzy msgid "Change Password" -msgstr "Wachtwoord wijzigen toestaan" +msgstr "Wachtwoord wijzigen" #: cps/templates/user_table.html:146 msgid "View" -msgstr "Geef weer" +msgstr "Toon" #: cps/templates/user_table.html:149 -#, fuzzy msgid "Edit Public Shelves" -msgstr "Openbare boekenplank" +msgstr "Openbare boekenplank bewerken" #: cps/templates/user_table.html:151 msgid "Sync selected Shelves with Kobo" -msgstr "" +msgstr "Synchroniseer geselecteerde boekenplanken met Kobo" #: cps/templates/user_table.html:155 -#, fuzzy msgid "Show read/unread selection" -msgstr "Boekenreeksenkeuze tonen" +msgstr "Toon gelezen/niet gelezen selectie" #: cps/templates/user_table.html:158 msgid "Show random books" diff --git a/cps/translations/pl/LC_MESSAGES/messages.mo b/cps/translations/pl/LC_MESSAGES/messages.mo index b12c6f38..9f57b221 100644 Binary files a/cps/translations/pl/LC_MESSAGES/messages.mo and b/cps/translations/pl/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/pl/LC_MESSAGES/messages.po b/cps/translations/pl/LC_MESSAGES/messages.po index 24227e7a..297eabeb 100644 --- a/cps/translations/pl/LC_MESSAGES/messages.po +++ b/cps/translations/pl/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre Web - polski (POT: 2021-06-12 08:52)\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2021-06-12 15:35+0200\n" "Last-Translator: Radosław Kierznowski \n" "Language: pl\n" @@ -441,24 +441,29 @@ msgstr "Błąd ogólny" msgid "Update File Could Not be Saved in Temp Dir" msgstr "Plik aktualizacji nie mógł zostać zapisany w katalogu tymczasowym" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +#, fuzzy +msgid "Failed to extract at least One LDAP User" +msgstr "Błąd przy tworzeniu przynajmniej jednego użytkownika LDAP" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "Błąd przy tworzeniu przynajmniej jednego użytkownika LDAP" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "Błąd: %(ldaperror)s" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "Błąd. LDAP nie zwrócił żadnego użytkownika" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "Przynajmniej jeden użytkownik LDAP nie został znaleziony w bazie danych" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "{} Użytkownik pomyślnie zaimportowany" diff --git a/cps/translations/pt_BR/LC_MESSAGES/messages.mo b/cps/translations/pt_BR/LC_MESSAGES/messages.mo index bdff3d16..5e77031e 100644 Binary files a/cps/translations/pt_BR/LC_MESSAGES/messages.mo and b/cps/translations/pt_BR/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/pt_BR/LC_MESSAGES/messages.po b/cps/translations/pt_BR/LC_MESSAGES/messages.po index 43b0e270..422e534d 100644 --- a/cps/translations/pt_BR/LC_MESSAGES/messages.po +++ b/cps/translations/pt_BR/LC_MESSAGES/messages.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: br\n" @@ -438,24 +438,29 @@ msgstr "Erro geral" msgid "Update File Could Not be Saved in Temp Dir" msgstr "Arquivo de atualização não pôde ser salvo no diretório temporário" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +#, fuzzy +msgid "Failed to extract at least One LDAP User" +msgstr "Falha na criação no mínimo de um usuário LDAP" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "Falha na criação no mínimo de um usuário LDAP" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "Erro: %(ldaperror)s" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "Erro: Nenhum usuário retornado em resposta do servidor LDAP" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "No mínimo um usuário LDAP não encontrado no banco de dados" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "{} Usuário Importado com Sucesso" @@ -2268,7 +2273,7 @@ msgstr "Marcar como lido" #: cps/templates/detail.html:219 msgid "Read" -msgstr "Leia" +msgstr "Lido" #: cps/templates/detail.html:227 msgid "Restore from archive" diff --git a/cps/translations/ru/LC_MESSAGES/messages.mo b/cps/translations/ru/LC_MESSAGES/messages.mo index 8e0c244c..d307faf3 100644 Binary files a/cps/translations/ru/LC_MESSAGES/messages.mo and b/cps/translations/ru/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/ru/LC_MESSAGES/messages.po b/cps/translations/ru/LC_MESSAGES/messages.po index 54bc7247..4caf9e49 100644 --- a/cps/translations/ru/LC_MESSAGES/messages.po +++ b/cps/translations/ru/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-Web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2020-04-29 01:20+0400\n" "Last-Translator: ZIZA\n" "Language: ru\n" @@ -441,24 +441,29 @@ msgstr "Общая ошибка" msgid "Update File Could Not be Saved in Temp Dir" msgstr "Не удалось сохранить файл обновления во временной папке." -#: cps/admin.py:1746 +#: cps/admin.py:1709 +#, fuzzy +msgid "Failed to extract at least One LDAP User" +msgstr "Не удалось создать хотя бы одного пользователя LDAP" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "Не удалось создать хотя бы одного пользователя LDAP" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "Ошибка: %(ldaperror)s" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "Ошибка: ни одного пользователя не найдено в ответ на запрос сервер LDAP" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "По крайней мере, один пользователь LDAP не найден в базе данных" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "" diff --git a/cps/translations/sv/LC_MESSAGES/messages.mo b/cps/translations/sv/LC_MESSAGES/messages.mo index aa6a10ae..5692088b 100644 Binary files a/cps/translations/sv/LC_MESSAGES/messages.mo and b/cps/translations/sv/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/sv/LC_MESSAGES/messages.po b/cps/translations/sv/LC_MESSAGES/messages.po index 5fd41d96..2abe1ca9 100644 --- a/cps/translations/sv/LC_MESSAGES/messages.po +++ b/cps/translations/sv/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-Web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/Calibre-Web\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2021-05-13 11:00+0000\n" "Last-Translator: Jonatan Nyberg \n" "Language: sv\n" @@ -438,24 +438,29 @@ msgstr "Allmänt fel" msgid "Update File Could Not be Saved in Temp Dir" msgstr "Uppdateringsfilen kunde inte sparas i Temp Dir" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +#, fuzzy +msgid "Failed to extract at least One LDAP User" +msgstr "Det gick inte att skapa minst en LDAP-användare" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "Det gick inte att skapa minst en LDAP-användare" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "Fel: %(ldaperror)s" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "Fel: Ingen användare återges som svar på LDAP-servern" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "Minst en LDAP-användare hittades inte i databasen" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "{} användare har importerats" diff --git a/cps/translations/tr/LC_MESSAGES/messages.mo b/cps/translations/tr/LC_MESSAGES/messages.mo index 3f854a80..ae4f5c1b 100644 Binary files a/cps/translations/tr/LC_MESSAGES/messages.mo and b/cps/translations/tr/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/tr/LC_MESSAGES/messages.po b/cps/translations/tr/LC_MESSAGES/messages.po index 51d1e726..3fec5744 100644 --- a/cps/translations/tr/LC_MESSAGES/messages.po +++ b/cps/translations/tr/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-Web\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2020-04-23 22:47+0300\n" "Last-Translator: iz \n" "Language: tr\n" @@ -435,24 +435,28 @@ msgstr "Genel hata" msgid "Update File Could Not be Saved in Temp Dir" msgstr "" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +msgid "Failed to extract at least One LDAP User" +msgstr "" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "" diff --git a/cps/translations/uk/LC_MESSAGES/messages.mo b/cps/translations/uk/LC_MESSAGES/messages.mo index cd2291b1..2c568c78 100644 Binary files a/cps/translations/uk/LC_MESSAGES/messages.mo and b/cps/translations/uk/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/uk/LC_MESSAGES/messages.po b/cps/translations/uk/LC_MESSAGES/messages.po index 6d99ca6d..6d2b7d82 100644 --- a/cps/translations/uk/LC_MESSAGES/messages.po +++ b/cps/translations/uk/LC_MESSAGES/messages.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/calibre-web\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2017-04-30 00:47+0300\n" "Last-Translator: ABIS Team \n" "Language: uk\n" @@ -437,24 +437,28 @@ msgstr "" msgid "Update File Could Not be Saved in Temp Dir" msgstr "" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +msgid "Failed to extract at least One LDAP User" +msgstr "" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "" diff --git a/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo b/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo index 915dbd4c..78eda562 100644 Binary files a/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo and b/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po b/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po index 7275fd52..a41b1c55 100644 --- a/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po +++ b/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-Web\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: 2020-09-27 22:18+0800\n" "Last-Translator: dalin \n" "Language: zh_CN\n" @@ -439,24 +439,29 @@ msgstr "一般错误" msgid "Update File Could Not be Saved in Temp Dir" msgstr "更新文件无法保存在临时目录中" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +#, fuzzy +msgid "Failed to extract at least One LDAP User" +msgstr "创建至少一个LDAP用户失败" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "创建至少一个LDAP用户失败" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "错误:%(ldaperror)s" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "错误:在LDAP服务器的响应中没有返回用户" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "数据库中没有找到至少一个LDAP用户" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "{} 用户被成功导入" diff --git a/cps/ub.py b/cps/ub.py index f1a33d75..69ea55f6 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import os import sys import datetime @@ -28,7 +27,6 @@ from binascii import hexlify from flask_login import AnonymousUserMixin, current_user from flask_login import user_logged_in -from contextlib import contextmanager try: from flask_dance.consumer.backend.sqla import OAuthConsumerMixin @@ -419,6 +417,12 @@ class ArchivedBook(Base): last_modified = Column(DateTime, default=datetime.datetime.utcnow) +class KoboSyncedBooks(Base): + __tablename__ = 'kobo_synced_books' + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, ForeignKey('user.id')) + book_id = Column(Integer) + # The Kobo ReadingState API keeps track of 4 timestamped entities: # ReadingState, StatusInfo, Statistics, CurrentBookmark # Which we map to the following 4 tables: diff --git a/cps/updater.py b/cps/updater.py index 63ea23bf..9090263f 100644 --- a/cps/updater.py +++ b/cps/updater.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import sys import os import datetime diff --git a/cps/uploader.py b/cps/uploader.py index 0d59fd01..92a73381 100644 --- a/cps/uploader.py +++ b/cps/uploader.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import os import hashlib from tempfile import gettempdir diff --git a/cps/usermanagement.py b/cps/usermanagement.py index b18cc673..71da7701 100644 --- a/cps/usermanagement.py +++ b/cps/usermanagement.py @@ -23,7 +23,6 @@ from sqlalchemy.sql.expression import func from werkzeug.security import check_password_hash from flask_login import login_required, login_user - from . import lm, ub, config, constants, services try: diff --git a/cps/web.py b/cps/web.py index ebf97b30..066169eb 100644 --- a/cps/web.py +++ b/cps/web.py @@ -20,7 +20,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from __future__ import division, print_function, unicode_literals import os from datetime import datetime import json @@ -30,7 +29,6 @@ import copy from babel.dates import format_date from babel import Locale as LC -from babel.core import UnknownLocaleError from flask import Blueprint, jsonify from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for from flask import session as flask_session @@ -48,7 +46,7 @@ from werkzeug.security import generate_password_hash, check_password_hash from . import constants, logger, isoLanguages, services from . import babel, db, ub, config, get_locale, app -from . import calibre_db +from . import calibre_db, kobo_sync_status from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download from .helper import check_valid_domain, render_task_status, check_email, check_username, \ get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \ @@ -84,12 +82,13 @@ except ImportError: @app.after_request def add_security_headers(resp): - resp.headers['Content-Security-Policy']= "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:" + resp.headers['Content-Security-Policy'] = "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:" + if request.endpoint == "editbook.edit_book" or config.config_use_google_drive: + resp.headers['Content-Security-Policy'] += " *" resp.headers['X-Content-Type-Options'] = 'nosniff' resp.headers['X-Frame-Options'] = 'SAMEORIGIN' resp.headers['X-XSS-Protection'] = '1; mode=block' resp.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' - # log.debug(request.full_path) return resp web = Blueprint('web', __name__) @@ -211,7 +210,7 @@ def toggle_archived(book_id): @web.route("/ajax/view", methods=["POST"]) @login_required_if_no_ano def update_view(): - to_save = request.get_json() + to_save = request.form.to_dict() # request.get_json() try: for element in to_save: for param in to_save[element]: @@ -260,10 +259,7 @@ def get_comic_book(book_id, book_format, page): log.error('unsupported comic format') return "", 204 - if sys.version_info.major >= 3: - b64 = codecs.encode(extract(page), 'base64').decode() - else: - b64 = extract(page).encode('base64') + b64 = codecs.encode(extract(page), 'base64').decode() ext = names[page].rpartition('.')[-1] if ext not in ('png', 'gif', 'jpg', 'jpeg', 'webp'): ext = 'png' @@ -273,7 +269,6 @@ def get_comic_book(book_id, book_format, page): return "", 204 ''' - # ################################### Typeahead ################################################################## @@ -606,13 +601,18 @@ def render_category_books(page, book_id, order): def render_language_books(page, name, order): try: - cur_l = LC.parse(name) - lang_name = cur_l.get_language_name(get_locale()) - except UnknownLocaleError: - try: - lang_name = _(isoLanguages.get(part3=name).name) - except KeyError: - abort(404) + lang_name = isoLanguages.get_language_name(get_locale(), name) + except KeyError: + abort(404) + + #try: + # cur_l = LC.parse(name) + # lang_name = cur_l.get_language_name(get_locale()) + #except UnknownLocaleError: + # try: + # lang_name = _(isoLanguages.get(part3=name).name) + # except KeyError: + # abort(404) entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, db.Books.languages.any(db.Languages.lang_code == name), @@ -781,6 +781,7 @@ def list_books(): if sort == "state": state = json.loads(request.args.get("state", "[]")) + # order = [db.Books.timestamp.asc()] if order == "asc" else [db.Books.timestamp.desc()] # ToDo wrong: sort ticked elif sort == "tags": order = [db.Tags.name.asc()] if order == "asc" else [db.Tags.name.desc()] join = db.books_tags_link,db.Books.id == db.books_tags_link.c.book, db.Tags @@ -805,7 +806,7 @@ def list_books(): total_count = filtered_count = calibre_db.session.query(db.Books).count() - if state: + if state is not None: if search: books = calibre_db.search_query(search).all() filtered_count = len(books) @@ -819,12 +820,14 @@ def list_books(): for entry in entries: for index in range(0, len(entry.languages)): - try: - entry.languages[index].language_name = LC.parse(entry.languages[index].lang_code)\ - .get_language_name(get_locale()) - except UnknownLocaleError: - entry.languages[index].language_name = _( - isoLanguages.get(part3=entry.languages[index].lang_code).name) + entry.languages[index].language_name = isoLanguages.get_language_name(get_locale(), entry.languages[ + index].lang_code) + #try: + # entry.languages[index].language_name = LC.parse(entry.languages[index].lang_code)\ + # .get_language_name(get_locale()) + #except UnknownLocaleError: + # entry.languages[index].language_name = _( + # isoLanguages.get(part3=entry.languages[index].lang_code).name) table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": entries} js_list = json.dumps(table_entries, cls=db.AlchemyEncoder) @@ -1003,16 +1006,18 @@ def language_overview(): languages = calibre_db.speaking_language() # ToDo: generate first character list for languages else: - try: - cur_l = LC.parse(current_user.filter_language()) - except UnknownLocaleError: - cur_l = None + #try: + # cur_l = LC.parse(current_user.filter_language()) + #except UnknownLocaleError: + # cur_l = None + languages = calibre_db.session.query(db.Languages).filter( db.Languages.lang_code == current_user.filter_language()).all() - if cur_l: - languages[0].name = cur_l.get_language_name(get_locale()) - else: - languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name) + languages[0].name = isoLanguages.get_language_name(get_locale(), languages[0].name.lang_code) + #if cur_l: + # languages[0].name = cur_l.get_language_name(get_locale()) + #else: + # languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name) lang_counter = calibre_db.session.query(db.books_languages_link, func.count('books_languages_link.book').label('bookcount')).group_by( text('books_languages_link.lang_code')).all() @@ -1194,7 +1199,7 @@ def adv_search_serie(q, include_series_inputs, exclude_series_inputs): def adv_search_shelf(q, include_shelf_inputs, exclude_shelf_inputs): q = q.outerjoin(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)\ - .filter(or_(ub.BookShelf.shelf == None, ub.BookShelf.shelf.notin_(exclude_shelf_inputs))) + .filter(or_(ub.BookShelf.shelf is None, ub.BookShelf.shelf.notin_(exclude_shelf_inputs))) if len(include_shelf_inputs) > 0: q = q.filter(ub.BookShelf.shelf.in_(include_shelf_inputs)) return q @@ -1357,7 +1362,7 @@ def render_adv_search_results(term, offset=None, order=None, limit=None): flask_session['query'] = json.dumps(term) ub.store_ids(q) result_count = len(q) - if offset != None and limit != None: + if offset is not None and limit is not None: offset = int(offset) limit_all = offset + int(limit) pagination = Pagination((offset / (int(limit)) + 1), limit, result_count) @@ -1420,7 +1425,7 @@ def serve_book(book_id, book_format, anyname): "rb").read() result = chardet.detect(rawdata) return make_response( - rawdata.decode(result['encoding']).encode('utf-8')) + rawdata.decode(result['encoding'], 'surrogatepass').encode('utf-8', 'surrogatepass')) except FileNotFoundError: log.error("File Not Found") return "File Not Found" @@ -1557,7 +1562,7 @@ def login(): else: ip_Address = request.headers.get('X-Forwarded-For', request.remote_addr) if 'forgot' in form and form['forgot'] == 'forgot': - if user != None and user.name != "Guest": + if user is not None and user.name != "Guest": ret, __ = reset_password(user.id) if ret == 1: flash(_(u"New Password was send to your email address"), category="info") @@ -1624,7 +1629,13 @@ def change_profile(kobo_support, local_oauth_check, oauth_status, translations, current_user.default_language = to_save["default_language"] if to_save.get("locale"): current_user.locale = to_save["locale"] + old_state = current_user.kobo_only_shelves_sync + # 1 -> 0: nothing has to be done + # 0 -> 1: all synced books have to be added to archived books, + currently synced shelfs which + # don't have to be synced have to be removed (added to Shelf archive) current_user.kobo_only_shelves_sync = int(to_save.get("kobo_only_shelves_sync") == "on") or 0 + if old_state == 0 and current_user.kobo_only_shelves_sync == 1: + kobo_sync_status.update_on_sync_shelfs(current_user.id) except Exception as ex: flash(str(ex), category="error") @@ -1748,12 +1759,8 @@ def show_book(book_id): entries = calibre_db.get_filtered_book(book_id, allow_show_archived=True) if entries: for index in range(0, len(entries.languages)): - try: - entries.languages[index].language_name = LC.parse(entries.languages[index].lang_code)\ - .get_language_name(get_locale()) - except UnknownLocaleError: - entries.languages[index].language_name = _( - isoLanguages.get(part3=entries.languages[index].lang_code).name) + entries.languages[index].language_name = isoLanguages.get_language_name(get_locale(), entries.languages[ + index].lang_code) cc = get_cc_columns(filter_config_custom_read=True) book_in_shelfs = [] shelfs = ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).all() diff --git a/messages.pot b/messages.pot index 5c982a24..e9f455ac 100644 --- a/messages.pot +++ b/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-08-29 13:30+0200\n" +"POT-Creation-Date: 2021-09-25 08:24+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -431,24 +431,28 @@ msgstr "" msgid "Update File Could Not be Saved in Temp Dir" msgstr "" -#: cps/admin.py:1746 +#: cps/admin.py:1709 +msgid "Failed to extract at least One LDAP User" +msgstr "" + +#: cps/admin.py:1752 msgid "Failed to Create at Least One LDAP User" msgstr "" -#: cps/admin.py:1759 +#: cps/admin.py:1765 #, python-format msgid "Error: %(ldaperror)s" msgstr "" -#: cps/admin.py:1763 +#: cps/admin.py:1769 msgid "Error: No user returned in response of LDAP server" msgstr "" -#: cps/admin.py:1796 +#: cps/admin.py:1802 msgid "At Least One LDAP User Not Found in Database" msgstr "" -#: cps/admin.py:1798 +#: cps/admin.py:1804 msgid "{} User Successfully Imported" msgstr "" diff --git a/requirements.txt b/requirements.txt index e7f67593..0332185d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ tornado>=4.1,<6.2 Wand>=0.4.4,<0.7.0 unidecode>=0.04.19,<1.3.0 lxml>=3.8.0,<4.7.0 +flask-wtf>=0.15.0,<0.16.0 diff --git a/setup.cfg b/setup.cfg index 23c4e60b..cea3f5a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,9 +18,11 @@ classifiers = Development Status :: 5 - Production/Stable License :: OSI Approved :: GNU Affero General Public License v3 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Operating System :: OS Independent keywords = calibre @@ -48,6 +50,8 @@ install_requires = tornado>=4.1,<6.2 Wand>=0.4.4,<0.7.0 unidecode>=0.04.19,<1.3.0 + lxml>=3.8.0,<4.7.0 + flask-wtf>=0.15.0,<0.16.0 [options.extras_require] gdrive = @@ -76,8 +80,8 @@ oauth = Flask-Dance>=2.0.0,<5.1.0 SQLAlchemy-Utils>=0.33.5,<0.38.0 metadata = - lxml>=3.8.0,<4.7.0 rarfile>=2.7 + scholarly>=1.2.0, <1.3 comics = natsort>=2.2.0,<7.2.0 comicapi>= 2.2.0,<2.3.0 diff --git a/test/Calibre-Web TestSummary_Linux.html b/test/Calibre-Web TestSummary_Linux.html index e96ea781..069b8146 100644 --- a/test/Calibre-Web TestSummary_Linux.html +++ b/test/Calibre-Web TestSummary_Linux.html @@ -37,20 +37,20 @@
    -

    Start Time: 2021-08-30 20:29:58

    +

    Start Time: 2021-10-12 06:15:04

    -

    Stop Time: 2021-08-30 23:51:39

    +

    Stop Time: 2021-10-12 09:37:09

    -

    Duration: 2h 41 min

    +

    Duration: 2h 43 min

    @@ -404,13 +404,13 @@ TestEbookConvertCalibre - 13 - 13 + 14 + 14 0 0 0 - Detail + Detail @@ -454,7 +454,7 @@ -
    TestEbookConvertCalibre - test_convert_parameter
    +
    TestEbookConvertCalibre - test_convert_options
    PASS @@ -463,7 +463,7 @@ -
    TestEbookConvertCalibre - test_convert_wrong_excecutable
    +
    TestEbookConvertCalibre - test_convert_parameter
    PASS @@ -472,7 +472,7 @@ -
    TestEbookConvertCalibre - test_convert_xss
    +
    TestEbookConvertCalibre - test_convert_wrong_excecutable
    PASS @@ -481,7 +481,7 @@ -
    TestEbookConvertCalibre - test_email_failed
    +
    TestEbookConvertCalibre - test_convert_xss
    PASS @@ -490,7 +490,7 @@ -
    TestEbookConvertCalibre - test_email_only
    +
    TestEbookConvertCalibre - test_email_failed
    PASS @@ -499,7 +499,7 @@ -
    TestEbookConvertCalibre - test_kindle_send_not_configured
    +
    TestEbookConvertCalibre - test_email_only
    PASS @@ -508,7 +508,7 @@ -
    TestEbookConvertCalibre - test_ssl_smtp_setup_error
    +
    TestEbookConvertCalibre - test_kindle_send_not_configured
    PASS @@ -517,7 +517,7 @@ -
    TestEbookConvertCalibre - test_starttls_smtp_setup_error
    +
    TestEbookConvertCalibre - test_ssl_smtp_setup_error
    PASS @@ -525,6 +525,15 @@ + +
    TestEbookConvertCalibre - test_starttls_smtp_setup_error
    + + PASS + + + + +
    TestEbookConvertCalibre - test_user_convert_xss
    @@ -687,15 +696,15 @@ - + TestEditAdditionalBooks - 15 + 16 14 - 0 + 1 0 1 - Detail + Detail @@ -811,8 +820,8 @@