Merge branch 'master' into Develop

# Conflicts:
#	cps/admin.py
#	cps/converter.py
#	cps/subproc_wrapper.py
#	test/Calibre-Web TestSummary_Linux.html
This commit is contained in:
Ozzie Isaacs 2021-07-30 16:33:06 +02:00
commit 302679719d
38 changed files with 701 additions and 493 deletions

View File

@ -41,6 +41,6 @@ Open a new GitHub pull request with the patch. Ensure the PR description clearly
In case your code enhances features of Calibre-Web: Create your pull request for the development branch if your enhancement consists of more than some lines of code in a local section of Calibre-Webs code. This makes it easier to test it and check all implication before it's made public. In case your code enhances features of Calibre-Web: Create your pull request for the development branch if your enhancement consists of more than some lines of code in a local section of Calibre-Webs code. This makes it easier to test it and check all implication before it's made public.
Please check if your code runs on Python 2.7 (still necessary in 2020) and mainly on python 3. If possible and the feature is related to operating system functions, try to check it on Windows and Linux. Please check if your code runs with python 3, python 2 is no longer supported. If possible and the feature is related to operating system functions, try to check it on Windows and Linux.
Calibre-Web is automatically tested on Linux in combination with python 3.7. The code for testing is in a [separate repo](https://github.com/OzzieIsaacs/calibre-web-test) on Github. It uses unit tests and performs real system tests with selenium; it would be great if you could consider also writing some tests. Calibre-Web is automatically tested on Linux in combination with python 3.8. The code for testing is in a [separate repo](https://github.com/OzzieIsaacs/calibre-web-test) on Github. It uses unit tests and performs real system tests with selenium; it would be great if you could consider also writing some tests.
A static code analysis is done by Codacy, but it's partly broken and doesn't run automatically. You could check your code with ESLint before contributing, a configuration file can be found in the projects root folder. A static code analysis is done by Codacy, but it's partly broken and doesn't run automatically. You could check your code with ESLint before contributing, a configuration file can be found in the projects root folder.

5
SECURITY.md Normal file
View File

@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Please report security issues to ozzie.fernandez.isaacs@googlemail.com

View File

@ -102,8 +102,9 @@ def create_app():
log.info('Starting Calibre Web...') log.info('Starting Calibre Web...')
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
log.info('Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2 please consider upgrading to Python3') log.info('*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
print('Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2 please consider upgrading to Python3') print('*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
sys.exit(5)
Principal(app) Principal(app)
lm.init_app(app) lm.init_app(app)
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session)) app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))

View File

@ -99,10 +99,11 @@ def admin_required(f):
@admi.before_app_request @admi.before_app_request
def before_request(): def before_request():
if not ub.check_user_session(current_user.id, flask_session.get('_id')): # make remember me function work
if current_user.is_authenticated:
confirm_login()
if not ub.check_user_session(current_user.id, flask_session.get('_id')) and 'opds' not in request.path:
logout_user() logout_user()
# if current_user.is_authenticated:
# confirm_login()
g.constants = constants g.constants = constants
g.user = current_user g.user = current_user
g.allow_registration = config.config_public_reg g.allow_registration = config.config_public_reg
@ -1375,11 +1376,11 @@ def _delete_user(content):
if content.name != "Guest": if content.name != "Guest":
# Delete all books in shelfs belonging to user, all shelfs of user, downloadstat of user, read status # Delete all books in shelfs belonging to user, all shelfs of user, downloadstat of user, read status
# and user itself # and user itself
ub.session.query(ub.ReadBook).filter(ub.User.id == ub.ReadBook.user_id).delete() ub.session.query(ub.ReadBook).filter(content.id == ub.ReadBook.user_id).delete()
ub.session.query(ub.Downloads).filter(ub.User.id == ub.Downloads.user_id).delete() ub.session.query(ub.Downloads).filter(content.id == ub.Downloads.user_id).delete()
for us in ub.session.query(ub.Shelf).filter(ub.User.id == ub.Shelf.user_id): for us in ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id):
ub.session.query(ub.BookShelf).filter(us.id == ub.BookShelf.shelf).delete() ub.session.query(ub.BookShelf).filter(us.id == ub.BookShelf.shelf).delete()
ub.session.query(ub.Shelf).filter(ub.User.id == ub.Shelf.user_id).delete() ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id).delete()
ub.session.query(ub.User).filter(ub.User.id == content.id).delete() ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
ub.session_commit() ub.session_commit()
log.info(u"User {} deleted".format(content.name)) log.info(u"User {} deleted".format(content.name))

View File

@ -20,6 +20,9 @@ from __future__ import division, print_function, unicode_literals
import sys import sys
import os import os
from collections import namedtuple from collections import namedtuple
from sqlalchemy import __version__ as sql_version
sqlalchemy_version2 = ([int(x) for x in sql_version.split('.')] >= [2,0,0])
# if installed via pip this variable is set to true (empty file with name .HOMEDIR present) # if installed via pip this variable is set to true (empty file with name .HOMEDIR present)
HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file__)), '.HOMEDIR')) HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file__)), '.HOMEDIR'))

View File

@ -39,7 +39,9 @@ def _get_command_version(path, pattern, argument=None):
if argument: if argument:
command.append(argument) command.append(argument)
try: try:
return process_wait(command, pattern=pattern).string match = process_wait(command, pattern=pattern)
if isinstance(match, re.Match):
return match.string
except Exception as ex: except Exception as ex:
log.warning("%s: %s", path, ex) log.warning("%s: %s", path, ex)
return _EXECUTION_ERROR return _EXECUTION_ERROR

View File

@ -690,6 +690,8 @@ class CalibreDB():
randm = false() randm = false()
off = int(int(pagesize) * (page - 1)) off = int(int(pagesize) * (page - 1))
query = self.session.query(database) query = self.session.query(database)
if len(join) == 6:
query = query.outerjoin(join[0], join[1]).outerjoin(join[2]).outerjoin(join[3], join[4]).outerjoin(join[5])
if len(join) == 3: if len(join) == 3:
query = query.outerjoin(join[0], join[1]).outerjoin(join[2]) query = query.outerjoin(join[0], join[1]).outerjoin(join[2])
elif len(join) == 2: elif len(join) == 2:
@ -755,6 +757,8 @@ class CalibreDB():
for authorterm in authorterms: for authorterm in authorterms:
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%"))) q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%")))
query = self.session.query(Books) query = self.session.query(Books)
if len(join) == 6:
query = query.outerjoin(join[0], join[1]).outerjoin(join[2]).outerjoin(join[3], join[4]).outerjoin(join[5])
if len(join) == 3: if len(join) == 3:
query = query.outerjoin(join[0], join[1]).outerjoin(join[2]) query = query.outerjoin(join[0], join[1]).outerjoin(join[2])
elif len(join) == 2: elif len(join) == 2:

View File

@ -22,10 +22,6 @@ import glob
import zipfile import zipfile
import json import json
from io import BytesIO from io import BytesIO
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
import os import os
@ -38,9 +34,9 @@ log = logger.create()
def assemble_logfiles(file_name): def assemble_logfiles(file_name):
log_list = sorted(glob.glob(file_name + '*'), reverse=True) log_list = sorted(glob.glob(file_name + '*'), reverse=True)
wfd = StringIO() wfd = BytesIO()
for f in log_list: for f in log_list:
with open(f, 'r') as fd: with open(f, 'rb') as fd:
shutil.copyfileobj(fd, wfd) shutil.copyfileobj(fd, wfd)
wfd.seek(0) wfd.seek(0)
if int(__version__.split('.')[0]) < 2: if int(__version__.split('.')[0]) < 2:

View File

@ -113,10 +113,8 @@ def yesno(value, yes, no):
@jinjia.app_template_filter('formatfloat') @jinjia.app_template_filter('formatfloat')
def formatfloat(value, decimals=1): def formatfloat(value, decimals=1):
formatedstring = '%d' % value value = 0 if not value else value
if (value % 1) != 0: return ('{0:.' + str(decimals) + 'f}').format(value).rstrip('0').rstrip('.')
formatedstring = ('%s.%d' % (formatedstring, (value % 1) * 10**decimals)).rstrip('0')
return formatedstring
@jinjia.app_template_filter('formatseriesindex') @jinjia.app_template_filter('formatseriesindex')

View File

@ -44,11 +44,11 @@ from werkzeug.datastructures import Headers
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.sql.expression import and_, or_ from sqlalchemy.sql.expression import and_, or_
from sqlalchemy.exc import StatementError from sqlalchemy.exc import StatementError
from sqlalchemy import __version__ as sql_version
from sqlalchemy.sql import select from sqlalchemy.sql import select
import requests 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
from .constants import sqlalchemy_version2
from .helper import get_download_link from .helper import get_download_link
from .services import SyncToken as SyncToken from .services import SyncToken as SyncToken
from .web import download_required from .web import download_required
@ -66,7 +66,6 @@ kobo_auth.register_url_value_preprocessor(kobo)
log = logger.create() log = logger.create()
sql2 = ([int(x) for x in sql_version.split('.')] >= [2,0,0])
def get_store_url_for_current_request(): def get_store_url_for_current_request():
# Programmatically modify the current url to point to the official Kobo store # Programmatically modify the current url to point to the official Kobo store
@ -139,6 +138,7 @@ def convert_to_kobo_timestamp_string(timestamp):
def HandleSyncRequest(): def HandleSyncRequest():
sync_token = SyncToken.SyncToken.from_headers(request.headers) sync_token = SyncToken.SyncToken.from_headers(request.headers)
log.info("Kobo library sync request received.") log.info("Kobo library sync request received.")
log.debug("SyncToken: {}".format(sync_token))
if not current_app.wsgi_app.is_proxied: if not current_app.wsgi_app.is_proxied:
log.debug('Kobo: Received unproxied request, changed request port to external server port') log.debug('Kobo: Received unproxied request, changed request port to external server port')
@ -158,7 +158,7 @@ def HandleSyncRequest():
only_kobo_shelves = current_user.kobo_only_shelves_sync only_kobo_shelves = current_user.kobo_only_shelves_sync
if only_kobo_shelves: if only_kobo_shelves:
if sql2: if sqlalchemy_version2:
changed_entries = select(db.Books, changed_entries = select(db.Books,
ub.ArchivedBook.last_modified, ub.ArchivedBook.last_modified,
ub.BookShelf.date_added, ub.BookShelf.date_added,
@ -182,7 +182,7 @@ def HandleSyncRequest():
.distinct() .distinct()
) )
else: else:
if sql2: if sqlalchemy_version2:
changed_entries = select(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived) changed_entries = select(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived)
else: else:
changed_entries = calibre_db.session.query(db.Books, changed_entries = calibre_db.session.query(db.Books,
@ -201,7 +201,7 @@ def HandleSyncRequest():
changed_entries = changed_entries.filter(db.Books.id > sync_token.books_last_id) changed_entries = changed_entries.filter(db.Books.id > sync_token.books_last_id)
reading_states_in_new_entitlements = [] reading_states_in_new_entitlements = []
if sql2: if sqlalchemy_version2:
books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT)) books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT))
else: else:
books = changed_entries.limit(SYNC_ITEM_LIMIT) books = changed_entries.limit(SYNC_ITEM_LIMIT)
@ -245,7 +245,7 @@ def HandleSyncRequest():
new_books_last_created = max(ts_created, new_books_last_created) new_books_last_created = max(ts_created, new_books_last_created)
if sql2: if sqlalchemy_version2:
max_change = calibre_db.session.execute(changed_entries max_change = calibre_db.session.execute(changed_entries
.filter(ub.ArchivedBook.is_archived) .filter(ub.ArchivedBook.is_archived)
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\ .order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\
@ -259,7 +259,7 @@ def HandleSyncRequest():
new_archived_last_modified = max(new_archived_last_modified, max_change) new_archived_last_modified = max(new_archived_last_modified, max_change)
# no. of books returned # no. of books returned
if sql2: if sqlalchemy_version2:
entries = calibre_db.session.execute(changed_entries).all() entries = calibre_db.session.execute(changed_entries).all()
book_count = len(entries) book_count = len(entries)
else: else:
@ -330,6 +330,7 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
extra_headers["x-kobo-sync"] = "continue" extra_headers["x-kobo-sync"] = "continue"
sync_token.to_headers(extra_headers) sync_token.to_headers(extra_headers)
log.debug("Kobo Sync Content: {}".format(sync_results))
response = make_response(jsonify(sync_results), extra_headers) response = make_response(jsonify(sync_results), extra_headers)
return response return response
@ -695,7 +696,7 @@ def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
}) })
extra_filters.append(ub.Shelf.kobo_sync) extra_filters.append(ub.Shelf.kobo_sync)
if sql2: if sqlalchemy_version2:
shelflist = ub.session.execute(select(ub.Shelf).outerjoin(ub.BookShelf).filter( shelflist = ub.session.execute(select(ub.Shelf).outerjoin(ub.BookShelf).filter(
or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified, or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified), func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified),

View File

@ -183,3 +183,12 @@ class SyncToken:
}, },
} }
return b64encode_json(token) return b64encode_json(token)
def __str__(self):
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)

View File

@ -72,10 +72,9 @@ def add_to_shelf(shelf_id, book_id):
if not check_shelf_edit_permissions(shelf): if not check_shelf_edit_permissions(shelf):
if not xhr: if not xhr:
flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name), flash(_(u"Sorry you are not allowed to add a book to that shelf"), category="error")
category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
return "Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name, 403 return "Sorry you are not allowed to add a book to the that shelf", 403
book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id, book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
ub.BookShelf.book_id == book_id).first() ub.BookShelf.book_id == book_id).first()
@ -228,18 +227,21 @@ def remove_from_shelf(shelf_id, book_id):
@login_required @login_required
def create_shelf(): def create_shelf():
shelf = ub.Shelf() shelf = ub.Shelf()
return create_edit_shelf(shelf, title=_(u"Create a Shelf"), page="shelfcreate") return create_edit_shelf(shelf, page_title=_(u"Create a Shelf"), page="shelfcreate")
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"]) @shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
@login_required @login_required
def edit_shelf(shelf_id): def edit_shelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
return create_edit_shelf(shelf, title=_(u"Edit a shelf"), page="shelfedit", shelf_id=shelf_id) if not check_shelf_edit_permissions(shelf):
flash(_(u"Sorry you are not allowed to edit this shelf"), category="error")
return redirect(url_for('web.index'))
return create_edit_shelf(shelf, page_title=_(u"Edit a shelf"), page="shelfedit", shelf_id=shelf_id)
# if shelf ID is set, we are editing a shelf # if shelf ID is set, we are editing a shelf
def create_edit_shelf(shelf, title, page, shelf_id=False): def create_edit_shelf(shelf, page_title, page, shelf_id=False):
sync_only_selected_shelves = current_user.kobo_only_shelves_sync sync_only_selected_shelves = current_user.kobo_only_shelves_sync
# calibre_db.session.query(ub.Shelf).filter(ub.Shelf.user_id == current_user.id).filter(ub.Shelf.kobo_sync).count() # calibre_db.session.query(ub.Shelf).filter(ub.Shelf.user_id == current_user.id).filter(ub.Shelf.kobo_sync).count()
if request.method == "POST": if request.method == "POST":
@ -247,20 +249,20 @@ def create_edit_shelf(shelf, title, page, shelf_id=False):
shelf.is_public = 1 if to_save.get("is_public") else 0 shelf.is_public = 1 if to_save.get("is_public") else 0
if config.config_kobo_sync: if config.config_kobo_sync:
shelf.kobo_sync = True if to_save.get("kobo_sync") else False shelf.kobo_sync = True if to_save.get("kobo_sync") else False
shelf_title = to_save.get("title", "")
if check_shelf_is_unique(shelf, to_save, shelf_id): if check_shelf_is_unique(shelf, shelf_title, shelf_id):
shelf.name = to_save["title"] shelf.name = shelf_title
if not shelf_id: if not shelf_id:
shelf.user_id = int(current_user.id) shelf.user_id = int(current_user.id)
ub.session.add(shelf) ub.session.add(shelf)
shelf_action = "created" shelf_action = "created"
flash_text = _(u"Shelf %(title)s created", title=to_save["title"]) flash_text = _(u"Shelf %(title)s created", title=shelf_title)
else: else:
shelf_action = "changed" shelf_action = "changed"
flash_text = _(u"Shelf %(title)s changed", title=to_save["title"]) flash_text = _(u"Shelf %(title)s changed", title=shelf_title)
try: try:
ub.session.commit() ub.session.commit()
log.info(u"Shelf {} {}".format(to_save["title"], shelf_action)) log.info(u"Shelf {} {}".format(shelf_title, shelf_action))
flash(flash_text, category="success") flash(flash_text, category="success")
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id)) return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
except (OperationalError, InvalidRequestError) as ex: except (OperationalError, InvalidRequestError) as ex:
@ -274,37 +276,37 @@ def create_edit_shelf(shelf, title, page, shelf_id=False):
flash(_(u"There was an error"), category="error") flash(_(u"There was an error"), category="error")
return render_title_template('shelf_edit.html', return render_title_template('shelf_edit.html',
shelf=shelf, shelf=shelf,
title=title, title=page_title,
page=page, page=page,
kobo_sync_enabled=config.config_kobo_sync, kobo_sync_enabled=config.config_kobo_sync,
sync_only_selected_shelves=sync_only_selected_shelves) sync_only_selected_shelves=sync_only_selected_shelves)
def check_shelf_is_unique(shelf, to_save, shelf_id=False): def check_shelf_is_unique(shelf, title, shelf_id=False):
if shelf_id: if shelf_id:
ident = ub.Shelf.id != shelf_id ident = ub.Shelf.id != shelf_id
else: else:
ident = true() ident = true()
if shelf.is_public == 1: if shelf.is_public == 1:
is_shelf_name_unique = ub.session.query(ub.Shelf) \ is_shelf_name_unique = ub.session.query(ub.Shelf) \
.filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1)) \ .filter((ub.Shelf.name == title) & (ub.Shelf.is_public == 1)) \
.filter(ident) \ .filter(ident) \
.first() is None .first() is None
if not is_shelf_name_unique: if not is_shelf_name_unique:
log.error("A public shelf with the name '{}' already exists.".format(to_save["title"])) log.error("A public shelf with the name '{}' already exists.".format(title))
flash(_(u"A public shelf with the name '%(title)s' already exists.", title=to_save["title"]), flash(_(u"A public shelf with the name '%(title)s' already exists.", title=title),
category="error") category="error")
else: else:
is_shelf_name_unique = ub.session.query(ub.Shelf) \ is_shelf_name_unique = ub.session.query(ub.Shelf) \
.filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 0) & .filter((ub.Shelf.name == title) & (ub.Shelf.is_public == 0) &
(ub.Shelf.user_id == int(current_user.id))) \ (ub.Shelf.user_id == int(current_user.id))) \
.filter(ident) \ .filter(ident) \
.first() is None .first() is None
if not is_shelf_name_unique: if not is_shelf_name_unique:
log.error("A private shelf with the name '{}' already exists.".format(to_save["title"])) log.error("A private shelf with the name '{}' already exists.".format(title))
flash(_(u"A private shelf with the name '%(title)s' already exists.", title=to_save["title"]), flash(_(u"A private shelf with the name '%(title)s' already exists.", title=title),
category="error") category="error")
return is_shelf_name_unique return is_shelf_name_unique
@ -378,7 +380,9 @@ def order_shelf(shelf_id):
def change_shelf_order(shelf_id, order): def change_shelf_order(shelf_id, order):
result = calibre_db.session.query(db.Books).join(ub.BookShelf, ub.BookShelf.book_id == db.Books.id) \ result = calibre_db.session.query(db.Books).outerjoin(db.books_series_link,
db.Books.id == db.books_series_link.c.book)\
.outerjoin(db.Series).join(ub.BookShelf, ub.BookShelf.book_id == db.Books.id) \
.filter(ub.BookShelf.shelf == shelf_id).order_by(*order).all() .filter(ub.BookShelf.shelf == shelf_id).order_by(*order).all()
for index, entry in enumerate(result): for index, entry in enumerate(result):
book = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \ book = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
@ -408,9 +412,11 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
if sort_param == 'old': if sort_param == 'old':
change_shelf_order(shelf_id, [db.Books.timestamp]) change_shelf_order(shelf_id, [db.Books.timestamp])
if sort_param == 'authaz': if sort_param == 'authaz':
change_shelf_order(shelf_id, [db.Books.author_sort.asc()]) change_shelf_order(shelf_id, [db.Books.author_sort.asc(), db.Series.name, db.Books.series_index])
if sort_param == 'authza': if sort_param == 'authza':
change_shelf_order(shelf_id, [db.Books.author_sort.desc()]) change_shelf_order(shelf_id, [db.Books.author_sort.desc(),
db.Series.name.desc(),
db.Books.series_index.desc()])
page = "shelf.html" page = "shelf.html"
pagesize = 0 pagesize = 0
else: else:

View File

@ -3291,7 +3291,6 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd
transform-origin: center top; transform-origin: center top;
border: 0; border: 0;
left: 0 !important; left: 0 !important;
max-height: 80%;
overflow-y: auto; overflow-y: auto;
} }

View File

@ -413,7 +413,11 @@ if($("body.advsearch").length > 0) {
}); });
$('#add-to-shelf').height("40px"); $('#add-to-shelf').height("40px");
function search_dropdownToggle() { function search_dropdownToggle() {
topPos = $("#add-to-shelf").offset().top-20; if( $("#add-to-shelf").length) {
topPos = $("#add-to-shelf").offset().top - 20;
} else {
topPos = 0
}
if ($('div[aria-label="Add to shelves"]').length > 0) { if ($('div[aria-label="Add to shelves"]').length > 0) {
position = $('div[aria-label="Add to shelves"]').offset().left position = $('div[aria-label="Add to shelves"]').offset().left

View File

@ -609,7 +609,10 @@ $(function() {
if (xhr.status < 400) { if (xhr.status < 400) {
$("#spinning_success").hide(); $("#spinning_success").hide();
clearInterval(rebootInterval); clearInterval(rebootInterval);
handle_response(data.result); if (data.result) {
handle_response(data.result);
data.result = "";
}
} }
}, },
}); });

View File

@ -662,33 +662,34 @@ function move_header_elements() {
} }
}); });
$(".multi_selector").selectpicker(); $(".multi_selector").selectpicker();
if ($(".multi_head").length) {
if (! $._data($(".multi_head").get(0), "events") ) { if (!$._data($(".multi_head").get(0), "events")) {
// Functions have to be here, otherwise the callbacks are not fired if visible columns are changed // Functions have to be here, otherwise the callbacks are not fired if visible columns are changed
$(".multi_head").on("click", function () { $(".multi_head").on("click", function () {
var val = $(this).data("set"); var val = $(this).data("set");
var field = $(this).data("name"); var field = $(this).data("name");
var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id); var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id);
var values = $("#" + field).val(); var values = $("#" + field).val();
confirmDialog( confirmDialog(
"restrictions", "restrictions",
"GeneralChangeModal", "GeneralChangeModal",
0, 0,
function () { function () {
$.ajax({ $.ajax({
method: "post", method: "post",
url: window.location.pathname + "/../../ajax/editlistusers/" + field, url: window.location.pathname + "/../../ajax/editlistusers/" + field,
data: {"pk": result, "value": values, "action": val}, data: {"pk": result, "value": values, "action": val},
success: function (data) { success: function (data) {
handleListServerResponse(data); handleListServerResponse(data);
}, },
error: function (data) { error: function (data) {
handleListServerResponse([{type: "danger", message: data.responseText}]) handleListServerResponse([{type: "danger", message: data.responseText}])
}, },
}); });
} }
); );
}); });
}
} }
$("#user_delete_selection").click(function () { $("#user_delete_selection").click(function () {
@ -700,38 +701,41 @@ function move_header_elements() {
$("#select_default_language").on("change", function () { $("#select_default_language").on("change", function () {
selectHeader(this, "default_language"); selectHeader(this, "default_language");
}); });
if ($(".check_head").length) {
if (! $._data($(".check_head").get(0), "events") ) { if (!$._data($(".check_head").get(0), "events")) {
$(".check_head").on("change", function () { $(".check_head").on("change", function () {
var val = $(this).data("set"); var val = $(this).data("set");
var name = $(this).data("name"); var name = $(this).data("name");
var data = $(this).data("val"); var data = $(this).data("val");
checkboxHeader(val, name, data); checkboxHeader(val, name, data);
}); });
}
} }
if (! $._data($(".button_head").get(0), "events") ) { if ($(".button_head").length) {
$(".button_head").on("click", function () { if (!$._data($(".button_head").get(0), "events")) {
var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id); $(".button_head").on("click", function () {
confirmDialog( var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id);
"btndeluser", confirmDialog(
"GeneralDeleteModal", "btndeluser",
0, "GeneralDeleteModal",
function () { 0,
$.ajax({ function () {
method: "post", $.ajax({
url: window.location.pathname + "/../../ajax/deleteuser", method: "post",
data: {"userid": result}, url: window.location.pathname + "/../../ajax/deleteuser",
success: function (data) { data: {"userid": result},
selections = selections.filter((el) => !result.includes(el)); success: function (data) {
handleListServerResponse(data); selections = selections.filter((el) => !result.includes(el));
}, handleListServerResponse(data);
error: function (data) { },
handleListServerResponse([{type: "danger", message: data.responseText}]) error: function (data) {
}, handleListServerResponse([{type: "danger", message: data.responseText}])
}); },
} });
); }
}); );
});
}
} }
} }

View File

@ -52,10 +52,11 @@ def process_wait(command, serr=subprocess.PIPE, pattern=""):
p.wait() p.wait()
for line in p.stdout.readlines(): for line in p.stdout.readlines():
if isinstance(line, bytes): if isinstance(line, bytes):
line = line.decode('utf-8') line = line.decode('utf-8', errors="ignore")
match = re.search(pattern, line, re.IGNORECASE) match = re.search(pattern, line, re.IGNORECASE)
if match and ret_val == "": if match and ret_val == "":
ret_val = match ret_val = match
break
p.stdout.close() p.stdout.close()
p.stderr.close() p.stderr.close()
return ret_val return ret_val

View File

@ -5,7 +5,7 @@
{% if author is not none %} {% if author is not none %}
<section class="author-bio"> <section class="author-bio">
{%if author.image_url is not none %} {%if author.image_url is not none %}
<img src="{{author.image_url}}" alt="{{author.name|safe}}" class="author-photo pull-left"> <img title="{{author.name|safe}}" src="{{author.image_url}}" alt="{{author.name|safe}}" class="author-photo pull-left">
{% endif %} {% endif %}
{%if author.about is not none %} {%if author.about is not none %}
@ -37,14 +37,14 @@
<div class="cover"> <div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}">
<span class="img"> <span class="img">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" /> <img title="{{author.name|safe}}" src="{{ url_for('web.get_cover', book_id=entry.id) }}" />
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %} {% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span> </span>
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}">
<p class="title">{{entry.title|shortentitle}}</p> <p title="{{ entry.title }}" class="title">{{entry.title|shortentitle}}</p>
</a> </a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
@ -104,11 +104,11 @@
<div class="col-sm-3 col-lg-2 col-xs-6 book"> <div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover"> <div class="cover">
<a href="https://www.goodreads.com/book/show/{{ entry.gid['#text'] }}" target="_blank" rel="noopener"> <a href="https://www.goodreads.com/book/show/{{ entry.gid['#text'] }}" target="_blank" rel="noopener">
<img src="{{ entry.image_url }}" /> <img title="{{entry.title}}" src="{{ entry.image_url }}" />
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">
<p class="title">{{entry.title|shortentitle}}</p> <p title="{{ entry.title }}" class="title">{{entry.title|shortentitle}}</p>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %} {% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}

View File

@ -3,7 +3,7 @@
{% if book %} {% if book %}
<div class="col-sm-3 col-lg-3 col-xs-12"> <div class="col-sm-3 col-lg-3 col-xs-12">
<div class="cover"> <div class="cover">
<img id="detailcover" src="{{ url_for('web.get_cover', book_id=book.id, edit=1|uuidfilter) }}" alt="{{ book.title }}"/> <img id="detailcover" title="{{book.title}}" src="{{ url_for('web.get_cover', book_id=book.id, edit=1|uuidfilter) }}" alt="{{ book.title }}"/>
</div> </div>
{% if g.user.role_delete_books() %} {% if g.user.role_delete_books() %}
<div class="text-center"> <div class="text-center">

View File

@ -20,7 +20,7 @@
<input type="checkbox" id="config_use_google_drive" name="config_use_google_drive" data-control="gdrive_settings" {% if config.config_use_google_drive %}checked{% endif %} > <input type="checkbox" id="config_use_google_drive" name="config_use_google_drive" data-control="gdrive_settings" {% if config.config_use_google_drive %}checked{% endif %} >
<label for="config_use_google_drive">{{_('Use Google Drive?')}}</label> <label for="config_use_google_drive">{{_('Use Google Drive?')}}</label>
</div> </div>
{% if not gdriveError %} {% if not gdriveError and config.config_use_google_drive %}
{% if show_authenticate_google_drive and config.config_use_google_drive %} {% if show_authenticate_google_drive and config.config_use_google_drive %}
<div class="form-group required"> <div class="form-group required">
<a href="{{ url_for('gdrive.authenticate_google_drive') }}" id="gdrive_auth" class="btn btn-primary">{{_('Authenticate Google Drive')}}</a> <a href="{{ url_for('gdrive.authenticate_google_drive') }}" id="gdrive_auth" class="btn btn-primary">{{_('Authenticate Google Drive')}}</a>

View File

@ -4,7 +4,7 @@
<div class="row"> <div class="row">
<div class="col-sm-3 col-lg-3 col-xs-5"> <div class="col-sm-3 col-lg-3 col-xs-5">
<div class="cover"> <div class="cover">
<img id="detailcover" src="{{ url_for('web.get_cover', book_id=entry.id, edit=1|uuidfilter) }}" alt="{{ entry.title }}" /> <img id="detailcover" title="{{entry.title}}" src="{{ url_for('web.get_cover', book_id=entry.id, edit=1|uuidfilter) }}" alt="{{ entry.title }}" />
</div> </div>
</div> </div>
<div class="col-sm-9 col-lg-9 book-meta"> <div class="col-sm-9 col-lg-9 book-meta">
@ -122,7 +122,7 @@
{% endif %} {% endif %}
{% if entry.series|length > 0 %} {% if entry.series|length > 0 %}
<p>{{_('Book')}} {{entry.series_index}} {{_('of')}} <a href="{{url_for('web.books_list', data='series', sort_param='stored', book_id=entry.series[0].id)}}">{{entry.series[0].name}}</a></p> <p>{{_('Book')}} {{entry.series_index|formatfloat(2)}} {{_('of')}} <a href="{{url_for('web.books_list', data='series', sort_param='stored', book_id=entry.series[0].id)}}">{{entry.series[0].name}}</a></p>
{% endif %} {% endif %}
{% if entry.languages.__len__() > 0 %} {% if entry.languages.__len__() > 0 %}

View File

@ -9,7 +9,7 @@
{% if entry.has_cover is defined %} {% if entry.has_cover is defined %}
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<span class="img"> <span class="img">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" /> <img title="{{entry.title}}" src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %} {% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span> </span>
</a> </a>
@ -17,7 +17,7 @@
</div> </div>
<div class="meta"> <div class="meta">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p> <p title="{{ entry.title }}" class="title">{{entry.title|shortentitle}}</p>
</a> </a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}

View File

@ -29,14 +29,14 @@
<div class="cover"> <div class="cover">
<a href="{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].series[0].id )}}"> <a href="{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].series[0].id )}}">
<span class="img"> <span class="img">
<img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/> <img title="{{entry.title}}" src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/>
<span class="badge">{{entry.count}}</span> <span class="badge">{{entry.count}}</span>
</span> </span>
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">
<a href="{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].series[0].id )}}"> <a href="{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].series[0].id )}}">
<p class="title">{{entry[0].series[0].name|shortentitle}}</p> <p title="{{entry[0].series[0].name|shortentitle}}" class="title">{{entry[0].series[0].name|shortentitle}}</p>
</a> </a>
</div> </div>
</div> </div>

View File

@ -9,14 +9,14 @@
<div class="cover"> <div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<span class="img"> <span class="img">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" /> <img title="{{ entry.title }}" src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %} {% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span> </span>
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p> <p title="{{entry.title}}" class="title">{{entry.title|shortentitle}}</p>
</a> </a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
@ -86,14 +86,14 @@
<div class="cover"> <div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<span class="img"> <span class="img">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}"/> <img title="{{ entry.title }}" src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}"/>
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %} {% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span> </span>
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p> <p title="{{ entry.title }}" class="title">{{entry.title|shortentitle}}</p>
</a> </a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>ePub Reader</title> <title>{{_('epub Reader')}} | {{title}}</title>
<meta name="description" content=""> <meta name="description" content="">
<meta name="viewport" content="width=device-width, user-scalable=no"> <meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">

View File

@ -1,10 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Comic Reader</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="description" content=""> <meta name="description" content="">
<title>{{_('Comic Reader')}} | {{title}}</title>
<meta name="viewport" content="width=device-width, user-scalable=no"> <meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">

View File

@ -7,7 +7,7 @@
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='js/libs/djvu_html5/Djvu_html5.css') }}"> <link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='js/libs/djvu_html5/Djvu_html5.css') }}">
<title>Djvu HTML5 browser demo</title> <title>{{_('DJVU Reader')}} | {{title}}</title>
<script type="text/javascript" language="javascript" <script type="text/javascript" language="javascript"
src="{{ url_for('static', filename='js/libs/djvu_html5/djvu_html5/djvu_html5.nocache.js') }}"></script> src="{{ url_for('static', filename='js/libs/djvu_html5/djvu_html5/djvu_html5.nocache.js') }}"></script>

View File

@ -26,7 +26,7 @@ See https://github.com/adobe-type-tools/cmap-resources
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="google" content="notranslate"> <meta name="google" content="notranslate">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{_('PDF reader')}}</title> <title>{{_('PDF Reader')}} | {{title}}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/libs/viewer.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/libs/viewer.css') }}">

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>{{_('Basic txt Reader')}}</title> <title>{{_('txt Reader')}} | {{title}}</title>
<meta name="description" content=""> <meta name="description" content="">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">

View File

@ -44,7 +44,7 @@
{% if entry.has_cover is defined %} {% if entry.has_cover is defined %}
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<span class="img"> <span class="img">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" /> <img title="{{entry.title}}" src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %} {% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span> </span>
</a> </a>
@ -52,7 +52,7 @@
</div> </div>
<div class="meta"> <div class="meta">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p> <p title="{{entry.title}}" class="title">{{entry.title|shortentitle}}</p>
</a> </a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}

View File

@ -31,14 +31,14 @@
<div class="cover"> <div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<span class="img"> <span class="img">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" /> <img title="{{entry.title}}" src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %} {% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span> </span>
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p> <p title="{{entry.title}}" class="title">{{entry.title|shortentitle}}</p>
</a> </a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}

View File

@ -9,9 +9,9 @@
<div class="row"> <div class="row">
<div class="col-lg-2 col-sm-4 hidden-xs"> <div class="col-lg-2 col-sm-4 hidden-xs">
{% if entry['visible'] %} {% if entry['visible'] %}
<img class="cover-height" src="{{ url_for('web.get_cover', book_id=entry['Books']['id']) }}"> <img title="{{entry.title}}" class="cover-height" src="{{ url_for('web.get_cover', book_id=entry['Books']['id']) }}">
{% else %} {% else %}
<img class="cover-height" src="{{ url_for('static', filename='generic_cover.jpg') }}"> <img title="{{entry.title}}" class="cover-height" src="{{ url_for('static', filename='generic_cover.jpg') }}">
{% endif %} {% endif %}
</div> </div>
<div class="col-lg-10 col-sm-8 col-xs-12"> <div class="col-lg-10 col-sm-8 col-xs-12">

View File

@ -35,7 +35,7 @@
<div class="col-sm-3 col-lg-2 col-xs-6 book"> <div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="meta"> <div class="meta">
<p class="title">{{entry.title|shortentitle}}</p> <p title="{{entry.title}}" class="title">{{entry.title|shortentitle}}</p>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')}}</a> <a href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')}}</a>

View File

@ -67,15 +67,14 @@
</div> </div>
{% endif %} {% endif %}
<div class="col-sm-6"> <div class="col-sm-6">
{% for element in sidebar %} {% for element in sidebar %}
{% if element['config_show'] %} {% if element['config_show'] %}
<div class="form-group"> <div class="form-group">
<input type="checkbox" name="show_{{element['visibility']}}" id="show_{{element['visibility']}}" {% if content.check_visibility(element['visibility']) %}checked{% endif %}> <input type="checkbox" name="show_{{element['visibility']}}" id="show_{{element['visibility']}}" {% if content.check_visibility(element['visibility']) %}checked{% endif %}>
<label for="show_{{element['visibility']}}">{{element['show_text']}}</label> <label for="show_{{element['visibility']}}">{{element['show_text']}}</label>
</div> </div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<div class="form-group"> <div class="form-group">
<input type="checkbox" name="Show_detail_random" id="Show_detail_random" {% if content.show_detail_random() %}checked{% endif %}> <input type="checkbox" name="Show_detail_random" id="Show_detail_random" {% if content.show_detail_random() %}checked{% endif %}>
<label for="Show_detail_random">{{_('Show Random Books in Detail View')}}</label> <label for="Show_detail_random">{{_('Show Random Books in Detail View')}}</label>
@ -131,32 +130,33 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="col-sm-12"> <div class="col-sm-12">
<div id="user_submit" class="btn btn-default">{{_('Save')}}</div> <div id="user_submit" class="btn btn-default">{{_('Save')}}</div>
{% if not profile %} {% if not profile %}
<div class="btn btn-default" data-back="{{ url_for('admin.admin') }}" id="back">{{_('Cancel')}}</div> <div class="btn btn-default" data-back="{{ url_for('admin.admin') }}" id="back">{{_('Cancel')}}</div>
{% endif %} {% endif %}
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %} {% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
<div class="btn btn-danger" id="btndeluser" data-value="{{ content.id }}" data-remote="false" >{{_('Delete User')}}</div> <div class="btn btn-danger" id="btndeluser" data-value="{{ content.id }}" data-remote="false" >{{_('Delete User')}}</div>
{% endif %} {% endif %}
</div>
</div> </div>
</form> </form>
</div> </div>
<div class="modal fade" id="modal_kobo_token" tabindex="-1" role="dialog" aria-labelledby="kobo_tokenModalLabel"> <div class="modal fade" id="modal_kobo_token" tabindex="-1" role="dialog" aria-labelledby="kobo_tokenModalLabel">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="kobo_tokenModalLabel">{{_('Generate Kobo Auth URL')}}</h4> <h4 class="modal-title" id="kobo_tokenModalLabel">{{_('Generate Kobo Auth URL')}}</h4>
</div> </div>
<div class="modal-body">...</div> <div class="modal-body">...</div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" id="kobo_close" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button> <button type="button" id="kobo_close" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
</div>
</div>
</div> </div>
</div> </div>
</div>
</div>
{% endblock %} {% endblock %}
{% block modal %} {% block modal %}

View File

@ -27,6 +27,8 @@ from flask import session as flask_session
from binascii import hexlify from binascii import hexlify
from flask_login import AnonymousUserMixin, current_user from flask_login import AnonymousUserMixin, current_user
from flask_login import user_logged_in
from contextlib import contextmanager
try: try:
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
@ -79,6 +81,36 @@ def delete_user_session(user_id, session_key):
def check_user_session(user_id, session_key): def check_user_session(user_id, session_key):
return session_key in logged_in.get(str(user_id), []) return session_key in logged_in.get(str(user_id), [])
def signal_store_user_session(object, user):
store_user_session()
def store_user_session():
if flask_session.get('_user_id', ""):
try:
if not check_user_session(flask_session.get('_user_id', ""), flask_session.get('_id', "")):
user_session = User_Sessions(flask_session.get('_user_id', ""), flask_session.get('_id', ""))
session.add(user_session)
session.commit()
except (exc.OperationalError, exc.InvalidRequestError):
session.rollback()
# log.debug(flask_session.get('_id', ""))
def delete_user_session(user_id, session_key):
try:
# log.debug(session_key)
session.query(User_Sessions).filter(User_Sessions.user_id==user_id,
User_Sessions.session_key==session_key).delete()
session.commit()
except (exc.OperationalError, exc.InvalidRequestError):
session.rollback()
def check_user_session(user_id, session_key):
return bool(session.query(User_Sessions).filter(User_Sessions.user_id==user_id,
User_Sessions.session_key==session_key).one_or_none())
user_logged_in.connect(signal_store_user_session)
def store_ids(result): def store_ids(result):
ids = list() ids = list()
for element in result: for element in result:
@ -279,6 +311,17 @@ class Anonymous(AnonymousUserMixin, UserBase):
flask_session['view'][page][prop] = value flask_session['view'][page][prop] = value
return None return None
class User_Sessions(Base):
__tablename__ = 'user_session'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('user.id'))
session_key = Column(String, default="")
def __init__(self, user_id, session_key):
self.user_id = user_id
self.session_key = session_key
# Baseclass representing Shelfs in calibre-web in app.db # Baseclass representing Shelfs in calibre-web in app.db
class Shelf(Base): class Shelf(Base):

View File

@ -21,7 +21,8 @@ import binascii
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
from flask_login import login_required from flask_login import login_required, login_user
from . import lm, ub, config, constants, services from . import lm, ub, config, constants, services
@ -58,6 +59,7 @@ def load_user_from_request(request):
if rp_header_username: if rp_header_username:
user = _fetch_user_by_name(rp_header_username) user = _fetch_user_by_name(rp_header_username)
if user: if user:
login_user(user)
return user return user
auth_header = request.headers.get("Authorization") auth_header = request.headers.get("Authorization")

View File

@ -360,9 +360,9 @@ def get_sort_function(sort, data):
if sort == 'old': if sort == 'old':
order = [db.Books.timestamp] order = [db.Books.timestamp]
if sort == 'authaz': if sort == 'authaz':
order = [db.Books.author_sort.asc()] order = [db.Books.author_sort.asc(), db.Series.name, db.Books.series_index]
if sort == 'authza': if sort == 'authza':
order = [db.Books.author_sort.desc()] order = [db.Books.author_sort.desc(), db.Series.name.desc(), db.Books.series_index.desc()]
if sort == 'seriesasc': if sort == 'seriesasc':
order = [db.Books.series_index.asc()] order = [db.Books.series_index.asc()]
if sort == 'seriesdesc': if sort == 'seriesdesc':
@ -410,7 +410,10 @@ def render_books_list(data, sort, book_id, page):
return render_adv_search_results(term, offset, order, config.config_books_per_page) return render_adv_search_results(term, offset, order, config.config_books_per_page)
else: else:
website = data or "newest" website = data or "newest"
entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order) entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=_(u"Books"), page=website) title=_(u"Books"), page=website)
@ -509,8 +512,10 @@ def render_author_books(page, author_id, order):
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
category="error") category="error")
return redirect(url_for("web.index")) return redirect(url_for("web.index"))
if constants.sqlalchemy_version2:
author = calibre_db.session.query(db.Authors).get(author_id) author = calibre_db.session.get(db.Authors, author_id)
else:
author = calibre_db.session.query(db.Authors).get(author_id)
author_name = author.name.replace('|', ',') author_name = author.name.replace('|', ',')
author_info = None author_info = None
@ -713,7 +718,8 @@ def render_prepare_search_form(cc):
def render_search_results(term, offset=None, order=None, limit=None): def render_search_results(term, offset=None, order=None, limit=None):
entries, result_count, pagination = calibre_db.get_search_results(term, offset, order, limit) join = db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series
entries, result_count, pagination = calibre_db.get_search_results(term, offset, order, limit, *join)
return render_title_template('search.html', return render_title_template('search.html',
searchterm=term, searchterm=term,
pagination=pagination, pagination=pagination,
@ -775,8 +781,10 @@ def list_books():
order = [db.Publishers.name.asc()] if order == "asc" else [db.Publishers.name.desc()] order = [db.Publishers.name.asc()] if order == "asc" else [db.Publishers.name.desc()]
join = db.books_publishers_link,db.Books.id == db.books_publishers_link.c.book, db.Publishers join = db.books_publishers_link,db.Books.id == db.books_publishers_link.c.book, db.Publishers
elif sort == "authors": elif sort == "authors":
order = [db.Authors.name.asc()] if order == "asc" else [db.Authors.name.desc()] order = [db.Authors.name.asc(), db.Series.name, db.Books.series_index] if order == "asc" \
join = db.books_authors_link,db.Books.id == db.books_authors_link.c.book, db.Authors else [db.Authors.name.desc(), db.Series.name.desc(), db.Books.series_index.desc()]
join = db.books_authors_link, db.Books.id == db.books_authors_link.c.book, db.Authors, \
db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series
elif sort == "languages": elif sort == "languages":
order = [db.Languages.lang_code.asc()] if order == "asc" else [db.Languages.lang_code.desc()] order = [db.Languages.lang_code.asc()] if order == "asc" else [db.Languages.lang_code.desc()]
join = db.books_languages_link,db.Books.id == db.books_languages_link.c.book, db.Languages join = db.books_languages_link,db.Books.id == db.books_languages_link.c.book, db.Languages
@ -793,7 +801,7 @@ def list_books():
filtered_count = len(books) filtered_count = len(books)
else: else:
books = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).all() books = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).all()
entries = calibre_db.get_checkbox_sorted(books, state, off, limit,order) entries = calibre_db.get_checkbox_sorted(books, state, off, limit, order)
elif search: elif search:
entries, filtered_count, __ = calibre_db.get_search_results(search, off, order, limit, *join) entries, filtered_count, __ = calibre_db.get_search_results(search, off, order, limit, *join)
else: else:
@ -1242,7 +1250,9 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
cc = get_cc_columns(filter_config_custom_read=True) cc = get_cc_columns(filter_config_custom_read=True)
calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase) calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase)
q = calibre_db.session.query(db.Books).filter(calibre_db.common_filters(True)) q = calibre_db.session.query(db.Books).outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book)\
.outerjoin(db.Series)\
.filter(calibre_db.common_filters(True))
# parse multiselects to a complete dict # parse multiselects to a complete dict
tags = dict() tags = dict()
@ -1591,15 +1601,14 @@ def change_profile(kobo_support, local_oauth_check, oauth_status, translations,
if to_save.get("password"): if to_save.get("password"):
current_user.password = generate_password_hash(to_save["password"]) current_user.password = generate_password_hash(to_save["password"])
try: try:
if to_save.get("allowed_tags", current_user.allowed_tags) != current_user.allowed_tags:
current_user.allowed_tags = to_save["allowed_tags"].strip()
if to_save.get("kindle_mail", current_user.kindle_mail) != current_user.kindle_mail: if to_save.get("kindle_mail", current_user.kindle_mail) != current_user.kindle_mail:
current_user.kindle_mail = valid_email(to_save["kindle_mail"]) current_user.kindle_mail = valid_email(to_save["kindle_mail"])
if to_save.get("email", current_user.email) != current_user.email: if to_save.get("email", current_user.email) != current_user.email:
current_user.email = check_email(to_save["email"]) current_user.email = check_email(to_save["email"])
if to_save.get("name", current_user.name) != current_user.name: if current_user.role_admin():
# Query User name, if not existing, change if to_save.get("name", current_user.name) != current_user.name:
current_user.name = check_username(to_save["name"]) # Query User name, if not existing, change
current_user.name = check_username(to_save["name"])
current_user.random_books = 1 if to_save.get("show_random") == "on" else 0 current_user.random_books = 1 if to_save.get("show_random") == "on" else 0
if to_save.get("default_language"): if to_save.get("default_language"):
current_user.default_language = to_save["default_language"] current_user.default_language = to_save["default_language"]
@ -1609,10 +1618,16 @@ def change_profile(kobo_support, local_oauth_check, oauth_status, translations,
except Exception as ex: except Exception as ex:
flash(str(ex), category="error") flash(str(ex), category="error")
return render_title_template("user_edit.html", content=current_user, return render_title_template("user_edit.html",
title=_(u"%(name)s's profile", name=current_user.name), page="me", content=current_user,
translations=translations,
profile=1,
languages=languages,
title=_(u"%(name)s's profile", name=current_user.name),
page="me",
kobo_support=kobo_support, kobo_support=kobo_support,
registered_oauth=local_oauth_check, oauth_status=oauth_status) registered_oauth=local_oauth_check,
oauth_status=oauth_status)
val = 0 val = 0
for key, __ in to_save.items(): for key, __ in to_save.items():
@ -1684,28 +1699,33 @@ def read_book(book_id, book_format):
ub.Bookmark.format == book_format.upper())).first() ub.Bookmark.format == book_format.upper())).first()
if book_format.lower() == "epub": if book_format.lower() == "epub":
log.debug(u"Start epub reader for %d", book_id) log.debug(u"Start epub reader for %d", book_id)
return render_title_template('read.html', bookid=book_id, title=_(u"Read a Book"), bookmark=bookmark) return render_title_template('read.html', bookid=book_id, title=book.title, bookmark=bookmark)
elif book_format.lower() == "pdf": elif book_format.lower() == "pdf":
log.debug(u"Start pdf reader for %d", book_id) log.debug(u"Start pdf reader for %d", book_id)
return render_title_template('readpdf.html', pdffile=book_id, title=_(u"Read a Book")) return render_title_template('readpdf.html', pdffile=book_id, title=book.title)
elif book_format.lower() == "txt": elif book_format.lower() == "txt":
log.debug(u"Start txt reader for %d", book_id) log.debug(u"Start txt reader for %d", book_id)
return render_title_template('readtxt.html', txtfile=book_id, title=_(u"Read a Book")) return render_title_template('readtxt.html', txtfile=book_id, title=book.title)
elif book_format.lower() == "djvu": elif book_format.lower() == "djvu":
log.debug(u"Start djvu reader for %d", book_id) log.debug(u"Start djvu reader for %d", book_id)
return render_title_template('readdjvu.html', djvufile=book_id, title=_(u"Read a Book")) return render_title_template('readdjvu.html', djvufile=book_id, title=book.title)
else: else:
for fileExt in constants.EXTENSIONS_AUDIO: for fileExt in constants.EXTENSIONS_AUDIO:
if book_format.lower() == fileExt: if book_format.lower() == fileExt:
entries = calibre_db.get_filtered_book(book_id) entries = calibre_db.get_filtered_book(book_id)
log.debug(u"Start mp3 listening for %d", book_id) log.debug(u"Start mp3 listening for %d", book_id)
return render_title_template('listenmp3.html', mp3file=book_id, audioformat=book_format.lower(), return render_title_template('listenmp3.html', mp3file=book_id, audioformat=book_format.lower(),
title=_(u"Read a Book"), entry=entries, bookmark=bookmark) entry=entries, bookmark=bookmark)
for fileExt in ["cbr", "cbt", "cbz"]: for fileExt in ["cbr", "cbt", "cbz"]:
if book_format.lower() == fileExt: if book_format.lower() == fileExt:
all_name = str(book_id) all_name = str(book_id)
title = book.title
if len(book.series):
title = title + " - " + book.series[0].name
if book.series_index:
title = title + " #" + '{0:.2f}'.format(book.series_index).rstrip('0').rstrip('.')
log.debug(u"Start comic reader for %d", book_id) log.debug(u"Start comic reader for %d", book_id)
return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book"), return render_title_template('readcbr.html', comicfile=all_name, title=title,
extension=fileExt) extension=fileExt)
log.debug(u"Oops! Selected book title is unavailable. File does not exist or is not accessible") log.debug(u"Oops! Selected book title is unavailable. File does not exist or is not accessible")
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), category="error") flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), category="error")

File diff suppressed because it is too large Load Diff