Merge branch 'master' into development

# Conflicts:
#	cps/static/css/style.css
This commit is contained in:
Ozzie Isaacs 2021-03-14 14:06:33 +01:00
commit f77d72fd86
24 changed files with 547 additions and 470 deletions

View File

@ -74,6 +74,41 @@ def _cover_processing(tmp_file_name, img, extension):
return tmp_cover_name return tmp_cover_name
def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable):
cover_data = None
if original_file_extension.upper() == '.CBZ':
cf = zipfile.ZipFile(tmp_file_name)
for name in cf.namelist():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
cover_data = cf.read(name)
break
elif original_file_extension.upper() == '.CBT':
cf = tarfile.TarFile(tmp_file_name)
for name in cf.getnames():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
cover_data = cf.extractfile(name).read()
break
elif original_file_extension.upper() == '.CBR' and use_rarfile:
try:
rarfile.UNRAR_TOOL = rarExecutable
cf = rarfile.RarFile(tmp_file_name)
for name in cf.getnames():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
cover_data = cf.read(name)
break
except Exception as e:
log.debug('Rarfile failed with error: %s', e)
return cover_data
def _extractCover(tmp_file_name, original_file_extension, rarExecutable): def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
cover_data = extension = None cover_data = extension = None
@ -87,37 +122,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
cover_data = archive.getPage(index) cover_data = archive.getPage(index)
break break
else: else:
if original_file_extension.upper() == '.CBZ': cover_data = _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable)
cf = zipfile.ZipFile(tmp_file_name)
for name in cf.namelist():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
cover_data = cf.read(name)
break
elif original_file_extension.upper() == '.CBT':
cf = tarfile.TarFile(tmp_file_name)
for name in cf.getnames():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
cover_data = cf.extractfile(name).read()
break
elif original_file_extension.upper() == '.CBR' and use_rarfile:
try:
rarfile.UNRAR_TOOL = rarExecutable
cf = rarfile.RarFile(tmp_file_name)
for name in cf.getnames():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
cover_data = cf.read(name)
break
except Exception as e:
log.debug('Rarfile failed with error: %s', e)
return _cover_processing(tmp_file_name, cover_data, extension) return _cover_processing(tmp_file_name, cover_data, extension)
@ -142,7 +147,8 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r
file_path=tmp_file_path, file_path=tmp_file_path,
extension=original_file_extension, extension=original_file_extension,
title=loadedMetadata.title or original_file_name, title=loadedMetadata.title or original_file_name,
author=" & ".join([credit["person"] for credit in loadedMetadata.credits if credit["role"] == "Writer"]) or u'Unknown', author=" & ".join([credit["person"]
for credit in loadedMetadata.credits if credit["role"] == "Writer"]) or u'Unknown',
cover=_extractCover(tmp_file_path, original_file_extension, rarExecutable), cover=_extractCover(tmp_file_path, original_file_extension, rarExecutable),
description=loadedMetadata.comments or "", description=loadedMetadata.comments or "",
tags="", tags="",

View File

@ -146,15 +146,16 @@ class _ConfigSQL(object):
self.load() self.load()
change = False change = False
if self.config_converterpath == None: if self.config_converterpath == None: # pylint: disable=access-member-before-definition
change = True change = True
self.config_converterpath = autodetect_calibre_binary() self.config_converterpath = autodetect_calibre_binary()
if self.config_kepubifypath == None: if self.config_kepubifypath == None: # pylint: disable=access-member-before-definition
change = True change = True
self.config_kepubifypath = autodetect_kepubify_binary() self.config_kepubifypath = autodetect_kepubify_binary()
if self.config_rarfile_location == None: if self.config_rarfile_location == None: # pylint: disable=access-member-before-definition
change = True change = True
self.config_rarfile_location = autodetect_unrar_binary() self.config_rarfile_location = autodetect_unrar_binary()
if change: if change:
@ -181,7 +182,8 @@ class _ConfigSQL(object):
return None return None
return self.config_keyfile return self.config_keyfile
def get_config_ipaddress(self): @staticmethod
def get_config_ipaddress():
return cli.ipadress or "" return cli.ipadress or ""
def _has_role(self, role_flag): def _has_role(self, role_flag):
@ -299,6 +301,7 @@ class _ConfigSQL(object):
have_metadata_db = os.path.isfile(db_file) have_metadata_db = os.path.isfile(db_file)
self.db_configured = have_metadata_db self.db_configured = have_metadata_db
constants.EXTENSIONS_UPLOAD = [x.lstrip().rstrip().lower() for x in self.config_upload_formats.split(',')] constants.EXTENSIONS_UPLOAD = [x.lstrip().rstrip().lower() for x in self.config_upload_formats.split(',')]
# pylint: disable=access-member-before-definition
logfile = logger.setup(self.config_logfile, self.config_log_level) logfile = logger.setup(self.config_logfile, self.config_log_level)
if logfile != self.config_logfile: if logfile != self.config_logfile:
log.warning("Log path %s not valid, falling back to default", self.config_logfile) log.warning("Log path %s not valid, falling back to default", self.config_logfile)

View File

@ -126,7 +126,7 @@ LDAP_AUTH_SIMPLE = 0
DEFAULT_MAIL_SERVER = "mail.example.org" DEFAULT_MAIL_SERVER = "mail.example.org"
DEFAULT_PASSWORD = "admin123" DEFAULT_PASSWORD = "admin123" # nosec # noqa
DEFAULT_PORT = 8083 DEFAULT_PORT = 8083
env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT) env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT)
try: try:

View File

@ -156,10 +156,8 @@ class Identifiers(Base):
return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val) return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val)
elif format_type == "isfdb": elif format_type == "isfdb":
return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val) return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
elif format_type == "url":
return u"{0}".format(self.val)
else: else:
return u"" return u"{0}".format(self.val)
class Comments(Base): class Comments(Base):

View File

@ -134,63 +134,71 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
taskMessage=_(u"Registration e-mail for user: %(name)s", name=user_name), taskMessage=_(u"Registration e-mail for user: %(name)s", name=user_name),
text=txt text=txt
)) ))
return return
def check_send_to_kindle_without_converter(entry):
bookformats = list()
# no converter - only for mobi and pdf formats
for ele in iter(entry.data):
if ele.uncompressed_size < config.mail_size:
if 'MOBI' in ele.format:
bookformats.append({'format': 'Mobi',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Mobi')})
if 'PDF' in ele.format:
bookformats.append({'format': 'Pdf',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Pdf')})
if 'AZW' in ele.format:
bookformats.append({'format': 'Azw',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Azw')})
return bookformats
def check_send_to_kindle_with_converter(entry):
bookformats = list()
formats = list()
for ele in iter(entry.data):
if ele.uncompressed_size < config.mail_size:
formats.append(ele.format)
if 'MOBI' in formats:
bookformats.append({'format': 'Mobi',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Mobi')})
if 'AZW' in formats:
bookformats.append({'format': 'Azw',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Azw')})
if 'PDF' in formats:
bookformats.append({'format': 'Pdf',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Pdf')})
if 'EPUB' in formats and 'MOBI' not in formats:
bookformats.append({'format': 'Mobi',
'convert': 1,
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
orig='Epub',
format='Mobi')})
if 'AZW3' in formats and not 'MOBI' in formats:
bookformats.append({'format': 'Mobi',
'convert': 2,
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
orig='Azw3',
format='Mobi')})
return bookformats
def check_send_to_kindle(entry): def check_send_to_kindle(entry):
""" """
returns all available book formats for sending to Kindle returns all available book formats for sending to Kindle
""" """
if len(entry.data): if len(entry.data):
bookformats = list()
if not config.config_converterpath: if not config.config_converterpath:
# no converter - only for mobi and pdf formats book_formats = check_send_to_kindle_with_converter(entry)
for ele in iter(entry.data):
if ele.uncompressed_size < config.mail_size:
if 'MOBI' in ele.format:
bookformats.append({'format': 'Mobi',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Mobi')})
if 'PDF' in ele.format:
bookformats.append({'format': 'Pdf',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Pdf')})
if 'AZW' in ele.format:
bookformats.append({'format': 'Azw',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Azw')})
else: else:
formats = list() book_formats = check_send_to_kindle_with_converter(entry)
for ele in iter(entry.data): return book_formats
if ele.uncompressed_size < config.mail_size:
formats.append(ele.format)
if 'MOBI' in formats:
bookformats.append({'format': 'Mobi',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Mobi')})
if 'AZW' in formats:
bookformats.append({'format': 'Azw',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Azw')})
if 'PDF' in formats:
bookformats.append({'format': 'Pdf',
'convert': 0,
'text': _('Send %(format)s to Kindle', format='Pdf')})
if config.config_converterpath:
if 'EPUB' in formats and 'MOBI' not in formats:
bookformats.append({'format': 'Mobi',
'convert':1,
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
orig='Epub',
format='Mobi')})
if 'AZW3' in formats and not 'MOBI' in formats:
bookformats.append({'format': 'Mobi',
'convert': 2,
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
orig='Azw3',
format='Mobi')})
return bookformats
else: else:
log.error(u'Cannot find book entry %d', entry.id) log.error(u'Cannot find book entry %d', entry.id)
return None return None
@ -742,7 +750,7 @@ def format_runtime(runtime):
# helper function to apply localize status information in tasklist entries # helper function to apply localize status information in tasklist entries
def render_task_status(tasklist): def render_task_status(tasklist):
renderedtasklist = list() renderedtasklist = list()
for num, user, added, task in tasklist: for __, user, added, task in tasklist:
if user == current_user.nickname or current_user.role_admin(): if user == current_user.nickname or current_user.role_admin():
ret = {} ret = {}
if task.start_time: if task.start_time:

View File

@ -72,7 +72,7 @@ def get_valid_language_codes(locale, language_names, remainder=None):
languages = list() languages = list()
if "" in language_names: if "" in language_names:
language_names.remove("") language_names.remove("")
for k, v in get_language_names(locale).items(): for k, __ in get_language_names(locale).items():
if k in language_names: if k in language_names:
languages.append(k) languages.append(k)
language_names.remove(k) language_names.remove(k)

View File

@ -19,7 +19,6 @@
from __future__ import division, print_function, unicode_literals from __future__ import division, print_function, unicode_literals
from flask import session from flask import session
try: try:
from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
@ -34,134 +33,131 @@ except ImportError:
except ImportError: except ImportError:
pass pass
try:
class OAuthBackend(SQLAlchemyBackend):
"""
Stores and retrieves OAuth tokens using a relational database through
the `SQLAlchemy`_ ORM.
.. _SQLAlchemy: https://www.sqlalchemy.org/ class OAuthBackend(SQLAlchemyBackend):
""" """
def __init__(self, model, session, provider_id, Stores and retrieves OAuth tokens using a relational database through
user=None, user_id=None, user_required=None, anon_user=None, the `SQLAlchemy`_ ORM.
cache=None):
self.provider_id = provider_id
super(OAuthBackend, self).__init__(model, session, user, user_id, user_required, anon_user, cache)
def get(self, blueprint, user=None, user_id=None): .. _SQLAlchemy: https://www.sqlalchemy.org/
if self.provider_id + '_oauth_token' in session and session[self.provider_id + '_oauth_token'] != '': """
return session[self.provider_id + '_oauth_token'] def __init__(self, model, session, provider_id,
# check cache user=None, user_id=None, user_required=None, anon_user=None,
cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id) cache=None):
token = self.cache.get(cache_key) self.provider_id = provider_id
if token: super(OAuthBackend, self).__init__(model, session, user, user_id, user_required, anon_user, cache)
return token
# if not cached, make database queries
query = (
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
use_provider_user_id = False
if self.provider_id + '_oauth_user_id' in session and session[self.provider_id + '_oauth_user_id'] != '':
query = query.filter_by(provider_user_id=session[self.provider_id + '_oauth_user_id'])
use_provider_user_id = True
if self.user_required and not u and not uid and not use_provider_user_id:
# raise ValueError("Cannot get OAuth token without an associated user")
return None
# check for user ID
if hasattr(self.model, "user_id") and uid:
query = query.filter_by(user_id=uid)
# check for user (relationship property)
elif hasattr(self.model, "user") and u:
query = query.filter_by(user=u)
# if we have the property, but not value, filter by None
elif hasattr(self.model, "user_id"):
query = query.filter_by(user_id=None)
# run query
try:
token = query.one().token
except NoResultFound:
token = None
# cache the result
self.cache.set(cache_key, token)
def get(self, blueprint, user=None, user_id=None):
if self.provider_id + '_oauth_token' in session and session[self.provider_id + '_oauth_token'] != '':
return session[self.provider_id + '_oauth_token']
# check cache
cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id)
token = self.cache.get(cache_key)
if token:
return token return token
def set(self, blueprint, token, user=None, user_id=None): # if not cached, make database queries
uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) query = (
u = first(_get_real_user(ref, self.anon_user) self.session.query(self.model)
for ref in (user, self.user, blueprint.config.get("user"))) .filter_by(provider=self.provider_id)
)
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
if self.user_required and not u and not uid: use_provider_user_id = False
raise ValueError("Cannot set OAuth token without an associated user") if self.provider_id + '_oauth_user_id' in session and session[self.provider_id + '_oauth_user_id'] != '':
query = query.filter_by(provider_user_id=session[self.provider_id + '_oauth_user_id'])
use_provider_user_id = True
# if there was an existing model, delete it if self.user_required and not u and not uid and not use_provider_user_id:
existing_query = ( # raise ValueError("Cannot get OAuth token without an associated user")
self.session.query(self.model) return None
.filter_by(provider=self.provider_id) # check for user ID
) if hasattr(self.model, "user_id") and uid:
# check for user ID query = query.filter_by(user_id=uid)
has_user_id = hasattr(self.model, "user_id") # check for user (relationship property)
if has_user_id and uid: elif hasattr(self.model, "user") and u:
existing_query = existing_query.filter_by(user_id=uid) query = query.filter_by(user=u)
# check for user (relationship property) # if we have the property, but not value, filter by None
has_user = hasattr(self.model, "user") elif hasattr(self.model, "user_id"):
if has_user and u: query = query.filter_by(user_id=None)
existing_query = existing_query.filter_by(user=u) # run query
# queue up delete query -- won't be run until commit() try:
existing_query.delete() token = query.one().token
# create a new model for this token except NoResultFound:
kwargs = { token = None
"provider": self.provider_id,
"token": token,
}
if has_user_id and uid:
kwargs["user_id"] = uid
if has_user and u:
kwargs["user"] = u
self.session.add(self.model(**kwargs))
# commit to delete and add simultaneously
self.session.commit()
# invalidate cache
self.cache.delete(self.make_cache_key(
blueprint=blueprint, user=user, user_id=user_id
))
def delete(self, blueprint, user=None, user_id=None): # cache the result
query = ( self.cache.set(cache_key, token)
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
if self.user_required and not u and not uid: return token
raise ValueError("Cannot delete OAuth token without an associated user")
# check for user ID def set(self, blueprint, token, user=None, user_id=None):
if hasattr(self.model, "user_id") and uid: uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
query = query.filter_by(user_id=uid) u = first(_get_real_user(ref, self.anon_user)
# check for user (relationship property) for ref in (user, self.user, blueprint.config.get("user")))
elif hasattr(self.model, "user") and u:
query = query.filter_by(user=u)
# if we have the property, but not value, filter by None
elif hasattr(self.model, "user_id"):
query = query.filter_by(user_id=None)
# run query
query.delete()
self.session.commit()
# invalidate cache
self.cache.delete(self.make_cache_key(
blueprint=blueprint, user=user, user_id=user_id,
))
except Exception: if self.user_required and not u and not uid:
pass raise ValueError("Cannot set OAuth token without an associated user")
# if there was an existing model, delete it
existing_query = (
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
# check for user ID
has_user_id = hasattr(self.model, "user_id")
if has_user_id and uid:
existing_query = existing_query.filter_by(user_id=uid)
# check for user (relationship property)
has_user = hasattr(self.model, "user")
if has_user and u:
existing_query = existing_query.filter_by(user=u)
# queue up delete query -- won't be run until commit()
existing_query.delete()
# create a new model for this token
kwargs = {
"provider": self.provider_id,
"token": token,
}
if has_user_id and uid:
kwargs["user_id"] = uid
if has_user and u:
kwargs["user"] = u
self.session.add(self.model(**kwargs))
# commit to delete and add simultaneously
self.session.commit()
# invalidate cache
self.cache.delete(self.make_cache_key(
blueprint=blueprint, user=user, user_id=user_id
))
def delete(self, blueprint, user=None, user_id=None):
query = (
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
if self.user_required and not u and not uid:
raise ValueError("Cannot delete OAuth token without an associated user")
# check for user ID
if hasattr(self.model, "user_id") and uid:
query = query.filter_by(user_id=uid)
# check for user (relationship property)
elif hasattr(self.model, "user") and u:
query = query.filter_by(user=u)
# if we have the property, but not value, filter by None
elif hasattr(self.model, "user_id"):
query = query.filter_by(user_id=None)
# run query
query.delete()
self.session.commit()
# invalidate cache
self.cache.delete(self.make_cache_key(
blueprint=blueprint, user=user, user_id=user_id,
))

View File

@ -35,7 +35,10 @@ from sqlalchemy.orm.exc import NoResultFound
from . import constants, logger, config, app, ub from . import constants, logger, config, app, ub
from .oauth import OAuthBackend, backend_resultcode try:
from .oauth import OAuthBackend, backend_resultcode
except NameError:
pass
oauth_check = {} oauth_check = {}

View File

@ -137,7 +137,7 @@ class WebServer(object):
return sock, _readable_listen_address(*address) return sock, _readable_listen_address(*address)
@staticmethod
def _get_args_for_reloading(self): def _get_args_for_reloading(self):
"""Determine how the script was executed, and return the args needed """Determine how the script was executed, and return the args needed
to execute it again in a new process. to execute it again in a new process.

View File

@ -64,7 +64,7 @@ class SyncToken:
books_last_modified: Datetime representing the last modified book that the device knows about. books_last_modified: Datetime representing the last modified book that the device knows about.
""" """
SYNC_TOKEN_HEADER = "x-kobo-synctoken" SYNC_TOKEN_HEADER = "x-kobo-synctoken" # nosec
VERSION = "1-1-0" VERSION = "1-1-0"
LAST_MODIFIED_ADDED_VERSION = "1-1-0" LAST_MODIFIED_ADDED_VERSION = "1-1-0"
MIN_VERSION = "1-0-0" MIN_VERSION = "1-0-0"
@ -91,7 +91,7 @@ class SyncToken:
def __init__( def __init__(
self, self,
raw_kobo_store_token="", raw_kobo_store_token="", # nosec
books_last_created=datetime.min, books_last_created=datetime.min,
books_last_modified=datetime.min, books_last_modified=datetime.min,
archive_last_modified=datetime.min, archive_last_modified=datetime.min,
@ -110,7 +110,7 @@ class SyncToken:
@staticmethod @staticmethod
def from_headers(headers): def from_headers(headers):
sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "") sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "")
if sync_token_header == "": if sync_token_header == "": # nosec
return SyncToken() return SyncToken()
# On the first sync from a Kobo device, we may receive the SyncToken # On the first sync from a Kobo device, we may receive the SyncToken

View File

@ -1,22 +1,24 @@
body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{ body.serieslist.grid-view div.container-fluid > div > div.col-sm-10:before{
display: none; display: none;
}
.cover .badge{
position: absolute;
top: 0;
left: 0;
color: #fff;
background-color: #cc7b19;
border-radius: 0;
padding: 0 8px;
box-shadow: 0 0 4px rgba(0,0,0,.6);
line-height: 24px;
}
.cover{
box-shadow: 0 0 4px rgba(0,0,0,.6);
} }
.cover .read{ .cover .badge{
padding: 0 0px; position: absolute;
line-height: 15px; top: 0;
left: 0;
color: #fff;
background-color: #cc7b19;
border-radius: 0;
padding: 0 8px;
box-shadow: 0 0 4px rgba(0, 0, 0, .6);
line-height: 24px;
}
.cover {
box-shadow: 0 0 4px rgba(0, 0, 0, .6);
}
.cover .read {
padding: 0px 0px;
line-height: 15px;
} }

View File

@ -33,7 +33,6 @@ body {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
padding: 4px; padding: 4px;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
@ -45,7 +44,7 @@ body {
#sidebar a.active, #sidebar a.active,
#sidebar a.active img + span { #sidebar a.active img + span {
background-color: #45B29D; background-color: #45b29d;
} }
#sidebar li img { #sidebar li img {
@ -99,7 +98,7 @@ body {
background-color: #ccc; background-color: #ccc;
} }
#progress .bar-read { #progress .bar-read {
color: #fff; color: #fff;
background-color: #45b29d; background-color: #45b29d;
} }

View File

@ -35,7 +35,6 @@ body {
height: 8%; height: 8%;
min-height: 20px; min-height: 20px;
padding: 10px; padding: 10px;
/* margin: 0 50px 0 50px; */
position: relative; position: relative;
color: #4f4f4f; color: #4f4f4f;
font-weight: 100; font-weight: 100;
@ -114,7 +113,7 @@ body {
top: 50%; top: 50%;
margin-top: -192px; margin-top: -192px;
font-size: 64px; font-size: 64px;
color: #E2E2E2; color: #e2e2e2;
font-family: arial, sans-serif; font-family: arial, sans-serif;
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
@ -148,12 +147,6 @@ body {
overflow: hidden; overflow: hidden;
} }
#sidebar.open {
/* left: 0; */
/* -webkit-transform: translate(0, 0);
-moz-transform: translate(0, 0); */
}
#main.closed { #main.closed {
/* left: 300px; */ /* left: 300px; */
-webkit-transform: translate(300px, 0); -webkit-transform: translate(300px, 0);
@ -238,7 +231,7 @@ input:-moz-placeholder { color: #454545; }
left: 50%; left: 50%;
margin-left: -1px; margin-left: -1px;
top: 10%; top: 10%;
opacity: .15; opacity: 0.15;
box-shadow: -2px 0 15px rgba(0, 0, 0, 1); box-shadow: -2px 0 15px rgba(0, 0, 0, 1);
display: none; display: none;
} }
@ -291,7 +284,7 @@ input:-moz-placeholder { color: #454545; }
#tocView li, #tocView li,
#bookmarksView li { #bookmarksView li {
margin-bottom:10px; margin-bottom: 10px;
width: 225px; width: 225px;
font-family: Georgia, "Times New Roman", Times, serif; font-family: Georgia, "Times New Roman", Times, serif;
list-style: none; list-style: none;
@ -299,8 +292,7 @@ input:-moz-placeholder { color: #454545; }
} }
#tocView li:active, #tocView li:active,
#tocView li.currentChapter #tocView li.currentChapter {
{
list-style: none; list-style: none;
} }
@ -319,7 +311,7 @@ input:-moz-placeholder { color: #454545; }
.list_item.currentChapter > a, .list_item.currentChapter > a,
.list_item a:hover { .list_item a:hover {
color: #f1f1f1 color: #f1f1f1;
} }
/* #tocView li.openChapter > a, */ /* #tocView li.openChapter > a, */
@ -328,7 +320,7 @@ input:-moz-placeholder { color: #454545; }
} }
.list_item ul { .list_item ul {
padding-left:10px; padding-left: 10px;
margin-top: 8px; margin-top: 8px;
display: none; display: none;
} }
@ -414,7 +406,7 @@ input:-moz-placeholder { color: #454545; }
} }
#notes { #notes {
padding: 0 0 0 34px; padding: 0 0 0 34px;
} }
#notes li { #notes li {
@ -449,8 +441,9 @@ input:-moz-placeholder { color: #454545; }
border-radius: 5px; border-radius: 5px;
} }
#note-text[disabled], #note-text[disabled="disabled"]{ #note-text[disabled],
opacity: 0.5; #note-text[disabled="disabled"]{
opacity: 0.5;
} }
#note-anchor { #note-anchor {
@ -478,26 +471,24 @@ input:-moz-placeholder { color: #454545; }
color: #f1f1f1; color: #f1f1f1;
} }
#settingsPanel .xsmall { font-size: x-small; } #settingsPanel .xsmall { font-size: x-small; }
#settingsPanel .small { font-size: small; } #settingsPanel .small { font-size: small; }
#settingsPanel .medium { font-size: medium; } #settingsPanel .medium { font-size: medium; }
#settingsPanel .large { font-size: large; } #settingsPanel .large { font-size: large; }
#settingsPanel .xlarge { font-size: x-large; } #settingsPanel .xlarge { font-size: x-large; }
.highlight { background-color: yellow } .highlight { background-color: yellow; }
.modal { .modal {
position: fixed; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;
// width: 50%;
width: 630px; width: 630px;
height: auto; height: auto;
z-index: 2000; z-index: 2000;
visibility: hidden; visibility: hidden;
margin-left: -320px; margin-left: -320px;
margin-top: -160px; margin-top: -160px;
} }
.overlay { .overlay {
@ -516,12 +507,12 @@ input:-moz-placeholder { color: #454545; }
} }
.md-show { .md-show {
visibility: visible; visibility: visible;
} }
.md-show ~ .overlay { .md-show ~ .overlay {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
} }
/* Content styles */ /* Content styles */
@ -593,7 +584,6 @@ input:-moz-placeholder { color: #454545; }
} }
.md-content > .closer { .md-content > .closer {
//font-size: 18px;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
@ -602,7 +592,7 @@ input:-moz-placeholder { color: #454545; }
} }
@media only screen and (max-width: 1040px) and (orientation: portrait) { @media only screen and (max-width: 1040px) and (orientation: portrait) {
#viewer{ #viewer {
width: 80%; width: 80%;
margin-left: 10%; margin-left: 10%;
} }
@ -614,7 +604,7 @@ input:-moz-placeholder { color: #454545; }
} }
@media only screen and (max-width: 900px) { @media only screen and (max-width: 900px) {
#viewer{ #viewer {
width: 60%; width: 60%;
margin-left: 20%; margin-left: 20%;
} }
@ -653,9 +643,9 @@ input:-moz-placeholder { color: #454545; }
-webkit-transform: translate(0, 0); -webkit-transform: translate(0, 0);
-moz-transform: translate(0, 0); -moz-transform: translate(0, 0);
-ms-transform: translate(0, 0); -ms-transform: translate(0, 0);
-webkit-transition: -webkit-transform .3s; -webkit-transition: -webkit-transform 0.3s;
-moz-transition: -moz-transform .3s; -moz-transition: -moz-transform 0.3s;
transition: -moz-transform .3s; transition: -moz-transform 0.3s;
} }
#main.closed { #main.closed {
@ -681,12 +671,11 @@ input:-moz-placeholder { color: #454545; }
font-size: 12px; font-size: 12px;
} }
#tocView > ul{ #tocView > ul {
padding-left: 10px; padding-left: 10px;
} }
} }
/* For iPad portrait layouts only */ /* For iPad portrait layouts only */
@media only screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation: portrait) { @media only screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation: portrait) {
#viewer iframe { #viewer iframe {
@ -694,20 +683,13 @@ input:-moz-placeholder { color: #454545; }
height: 740px; height: 740px;
} }
} }
/*For iPad landscape layouts only *//*
@media only screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation: landscape) {
#viewer iframe {
width: 460px;
height: 415px;
}
}*/
@media only screen @media only screen
and (min-device-width : 768px) and (min-device-width : 768px)
and (max-device-width : 1024px) and (max-device-width : 1024px)
and (orientation : landscape) and (orientation : landscape)
/*and (-webkit-min-device-pixel-ratio: 2)*/ { /*and (-webkit-min-device-pixel-ratio: 2)*/ {
#viewer{ #viewer {
width: 80%; width: 80%;
margin-left: 10%; margin-left: 10%;
} }
@ -720,8 +702,8 @@ and (orientation : landscape)
/*For iPad landscape layouts only */ /*For iPad landscape layouts only */
@media only screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation: landscape) { @media only screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation: landscape) {
#viewer iframe { #viewer iframe {
width: 960px; width: 960px;
height: 515px; height: 515px;
} }
} }
@ -764,8 +746,8 @@ and (orientation : landscape)
/* For iPhone landscape layouts only */ /* For iPhone landscape layouts only */
@media only screen and (max-device-width: 374px) and (orientation: landscape) { @media only screen and (max-device-width: 374px) and (orientation: landscape) {
#viewer iframe { #viewer iframe {
width: 256px; width: 256px;
height: 124px; height: 124px;
} }
} }

View File

@ -1,7 +1,7 @@
.tooltip.bottom .tooltip-inner { .tooltip.bottom .tooltip-inner {
font-size: 13px; font-size: 13px;
font-family: Open Sans Semibold,Helvetica Neue,Helvetica,Arial,sans-serif; font-family: Open Sans Semibold, Helvetica Neue, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
padding: 3px 10px; padding: 3px 10px;
@ -48,10 +48,14 @@ body {
body h2 { body h2 {
font-weight: normal; font-weight: normal;
color:#444; color: #444;
} }
a, .danger, .book-remove, .user-remove, .editable-empty, .editable-empty:hover { color: #45b29d; } a,
.danger,
.book-remove,
.editable-empty,
.editable-empty:hover { color: #45b29d; }
.book-remove:hover { color: #23527c; } .book-remove:hover { color: #23527c; }
@ -60,15 +64,17 @@ a, .danger, .book-remove, .user-remove, .editable-empty, .editable-empty:hover {
.btn-default a { color: #444; } .btn-default a { color: #444; }
.btn-default a:hover { .btn-default a:hover {
color: #45b29d; color: #45b29d;
text-decoration: None; text-decoration: None;
} }
.btn-default:hover { .btn-default:hover {
color: #45b29d; color: #45b29d;
} }
.editable-click, a.editable-click, a.editable-click:hover { border-bottom: None; } .editable-click,
a.editable-click,
a.editable-click:hover { border-bottom: None; }
.navigation .nav-head { .navigation .nav-head {
text-transform: uppercase; text-transform: uppercase;
@ -121,9 +127,10 @@ a, .danger, .book-remove, .user-remove, .editable-empty, .editable-empty:hover {
max-height: 100%; max-height: 100%;
} }
.container-fluid .discover{ margin-bottom: 50px; } .container-fluid .discover { margin-bottom: 50px; }
.container-fluid .new-books { border-top: 1px solid #ccc; } .container-fluid .new-books { border-top: 1px solid #ccc; }
.container-fluid .new-books h2 { margin: 50px 0 0 0; } .container-fluid .new-books h2 { margin: 50px 0 0 0; }
.container-fluid .book { .container-fluid .book {
margin-top: 20px; margin-top: 20px;
display: flex; display: flex;
@ -176,9 +183,10 @@ a, .danger, .book-remove, .user-remove, .editable-empty, .editable-empty:hover {
.container-fluid .book .meta .rating { margin-top: 5px; } .container-fluid .book .meta .rating { margin-top: 5px; }
.rating .glyphicon-star-empty { color: #444; } .rating .glyphicon-star-empty { color: #444; }
.rating .glyphicon-star.good { color: #444; } .rating .glyphicon-star.good { color: #444; }
.rating-clear .glyphicon-remove { color: #333 } .rating-clear .glyphicon-remove { color: #333; }
.container-fluid .author .author-hidden, .container-fluid .author .author-hidden-divider { display: none; } .container-fluid .author .author-hidden,
.container-fluid .author .author-hidden-divider { display: none; }
.navbar-brand { .navbar-brand {
font-family: 'Grand Hotel', cursive; font-family: 'Grand Hotel', cursive;
@ -192,7 +200,7 @@ a, .danger, .book-remove, .user-remove, .editable-empty, .editable-empty:hover {
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
} }
.more-stuff>li { margin-bottom: 10px; } .more-stuff > li { margin-bottom: 10px; }
.navbar-collapse.in .navbar-nav { margin: 0; } .navbar-collapse.in .navbar-nav { margin: 0; }
span.glyphicon.glyphicon-tags { span.glyphicon.glyphicon-tags {
@ -213,19 +221,20 @@ span.glyphicon.glyphicon-tags {
box-shadow: 0 5px 8px -6px #777; box-shadow: 0 5px 8px -6px #777;
} }
.navbar-default .navbar-toggle .icon-bar {background-color: #000; } .navbar-default .navbar-toggle .icon-bar { background-color: #000; }
.navbar-default .navbar-toggle {border-color: #000; } .navbar-default .navbar-toggle { border-color: #000; }
.cover { margin-bottom: 10px; } .cover { margin-bottom: 10px; }
.cover .badge{ .cover .badge {
position: absolute; position: absolute;
top: 2px; top: 2px;
left: 2px; left: 2px;
color: #000; color: #000;
border-radius: 10px; border-radius: 10px;
background-color: #fff; background-color: #fff;
} }
.cover .read{
.cover .read {
left: auto; left: auto;
right: 2px; right: 2px;
width: 17px; width: 17px;
@ -233,14 +242,17 @@ span.glyphicon.glyphicon-tags {
display: inline-block; display: inline-block;
padding: 2px; padding: 2px;
} }
.cover-height { max-height: 100px;} .cover-height { max-height: 100px; }
.col-sm-2 a .cover-small { .col-sm-2 a .cover-small {
margin: 5px; margin: 5px;
max-height: 200px; max-height: 200px;
} }
.btn-file {position: relative; overflow: hidden;} .btn-file {
position: relative;
overflow: hidden;
}
.btn-file input[type=file] { .btn-file input[type=file] {
position: absolute; position: absolute;
@ -258,24 +270,62 @@ span.glyphicon.glyphicon-tags {
display: block; display: block;
} }
.btn-toolbar .btn,.discover .btn { margin-bottom: 5px; } .btn-toolbar .btn,
.button-link {color: #fff; } .discover .btn { margin-bottom: 5px; }
.btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open .dropdown-toggle.btn-primary{ background-color: #1C5484; } .button-link { color: #fff; }
.btn-primary.disabled, .btn-primary[disabled], fieldset[disabled] .btn-primary, .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled:active, .btn-primary[disabled]:active, fieldset[disabled] .btn-primary:active, .btn-primary.disabled.active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary.active { background-color: #89B9E2; }
.btn-toolbar>.btn+.btn, .btn-toolbar>.btn-group+.btn, .btn-toolbar>.btn+.btn-group, .btn-toolbar>.btn-group+.btn-group { margin-left: 0; }
.panel-body {background-color: #f5f5f5; }
.spinner {margin: 0 41%; }
.spinner2 {margin: 0 41%; }
.intend-form { margin-left:20px; }
table .bg-dark-danger {background-color: #d9534f; color: #fff; }
table .bg-dark-danger a {color: #fff; }
table .bg-dark-danger:hover {background-color: #c9302c; }
table .bg-primary:hover {background-color: #1C5484; }
table .bg-primary a {color: #fff; }
.block-label {display: block;}
.fake-input {position: absolute; pointer-events: none; top: 0; }
input.pill { position: absolute; opacity: 0; } .btn-primary:hover,
.btn-primary:focus,
.btn-primary:active,
.btn-primary.active,
.open .dropdown-toggle.btn-primary { background-color: #1c5484; }
.btn-primary.disabled,
.btn-primary[disabled],
fieldset[disabled] .btn-primary,
.btn-primary.disabled:hover,
.btn-primary[disabled]:hover,
fieldset[disabled] .btn-primary:hover,
.btn-primary.disabled:focus,
.btn-primary[disabled]:focus,
fieldset[disabled] .btn-primary:focus,
.btn-primary.disabled:active,
.btn-primary[disabled]:active,
fieldset[disabled] .btn-primary:active,
.btn-primary.disabled.active,
.btn-primary[disabled].active,
fieldset[disabled] .btn-primary.active { background-color: #89b9e2; }
.btn-toolbar > .btn + .btn,
.btn-toolbar > .btn-group + .btn,
.btn-toolbar > .btn + .btn-group,
.btn-toolbar > .btn-group + .btn-group { margin-left: 0; }
.panel-body { background-color: #f5f5f5; }
.spinner { margin: 0 41%; }
.spinner2 { margin: 0 41%; }
.intend-form { margin-left: 20px; }
table .bg-dark-danger {
background-color: #d9534f;
color: #fff;
}
table .bg-dark-danger a { color: #fff; }
table .bg-dark-danger:hover { background-color: #c9302c; }
table .bg-primary:hover { background-color: #1c5484; }
table .bg-primary a { color: #fff; }
.block-label { display: block; }
.fake-input {
position: absolute;
pointer-events: none;
top: 0;
}
input.pill {
position: absolute;
opacity: 0;
}
input.pill + label { input.pill + label {
border: 2px solid #45b29d; border: 2px solid #45b29d;
@ -298,11 +348,24 @@ input.pill:checked + label {
input.pill:not(:checked) + label .glyphicon { display: none; } input.pill:not(:checked) + label .glyphicon { display: none; }
.author-bio img { margin: 0 1em 1em 0; } .author-bio img { margin: 0 1em 1em 0; }
.author-link { display: inline-block; margin-top: 10px; width: 100px; }
.author-link img { display: block; height: 100%; }
#remove-from-shelves .btn, #shelf-action-errors { margin-left: 5px; }
.tags_click, .serie_click, .language_click { margin-right: 5px; } .author-link {
display: inline-block;
margin-top: 10px;
width: 100px;
}
.author-link img {
display: block;
height: 100%;
}
#remove-from-shelves .btn,
#shelf-action-errors { margin-left: 5px; }
.tags_click,
.serie_click,
.language_click { margin-right: 5px; }
#meta-info { #meta-info {
height: 600px; height: 600px;
@ -325,11 +388,11 @@ input.pill:not(:checked) + label .glyphicon { display: none; }
#btn-upload-cover { display: none; } #btn-upload-cover { display: none; }
.panel-title > a { text-decoration: none; } .panel-title > a { text-decoration: none; }
.editable-buttons { .editable-buttons {
display:inline-block; display: inline-block;
margin-left: 7px; margin-left: 7px;
} }
.editable-input { display:inline-block; } .editable-input { display: inline-block; }
.editable-cancel { .editable-cancel {
margin-bottom: 0 !important; margin-bottom: 0 !important;

View File

@ -677,7 +677,7 @@ $(".navbar-collapse.collapse.in").before('<div class="sidebar-backdrop"></div>')
// Get rid of leading white space // Get rid of leading white space
recentlyAdded = $("#nav_new a:contains('Recently')").text().trim(); recentlyAdded = $("#nav_new a:contains('Recently')").text().trim();
$("#nav_new a:contains('Recently')").contents().filter(function () { $("#nav_new a:contains('Recently')").contents().filter(function () {
return this.nodeType == 3 return this.nodeType === 3
}).each(function () { }).each(function () {
this.textContent = this.textContent.replace(" Recently Added", recentlyAdded); this.textContent = this.textContent.replace(" Recently Added", recentlyAdded);
}); });

View File

@ -1,7 +1,7 @@
/** /**
* Created by SpeedProg on 05.04.2015. * Created by SpeedProg on 05.04.2015.
*/ */
/* global Bloodhound, language, Modernizr, tinymce */ /* global Bloodhound, language, Modernizr, tinymce, getPath */
if ($("#description").length) { if ($("#description").length) {
tinymce.init({ tinymce.init({
@ -250,14 +250,14 @@ promisePublishers.done(function() {
}); });
$("#search").on("change input.typeahead:selected", function(event) { $("#search").on("change input.typeahead:selected", function(event) {
if (event.target.type == "search" && event.target.tagName == "INPUT") { if (event.target.type === "search" && event.target.tagName === "INPUT") {
return; return;
} }
var form = $("form").serialize(); var form = $("form").serialize();
$.getJSON( getPath() + "/get_matching_tags", form, function( data ) { $.getJSON( getPath() + "/get_matching_tags", form, function( data ) {
$(".tags_click").each(function() { $(".tags_click").each(function() {
if ($.inArray(parseInt($(this).val(), 10), data.tags) === -1) { if ($.inArray(parseInt($(this).val(), 10), data.tags) === -1) {
if(!$(this).prop("selected")) { if (!$(this).prop("selected")) {
$(this).prop("disabled", true); $(this).prop("disabled", true);
} }
} else { } else {
@ -265,10 +265,10 @@ $("#search").on("change input.typeahead:selected", function(event) {
} }
}); });
$("#include_tag option:selected").each(function () { $("#include_tag option:selected").each(function () {
$("#exclude_tag").find("[value="+$(this).val()+"]").prop("disabled", true); $("#exclude_tag").find("[value="+$(this).val() + "]").prop("disabled", true);
}); });
$('#include_tag').selectpicker("refresh"); $("#include_tag").selectpicker("refresh");
$('#exclude_tag').selectpicker("refresh"); $("#exclude_tag").selectpicker("refresh");
}); });
}); });

View File

@ -88,7 +88,7 @@ $("#desc").click(function() {
// Find count of middle element // Find count of middle element
var count = $(".row:visible").length; var count = $(".row:visible").length;
if (count > 20) { if (count > 20) {
middle = parseInt(count / 2) + (count % 2); middle = parseInt(count / 2, 10) + (count % 2);
//var middle = parseInt(count / 2) + (count % 2); //var middle = parseInt(count / 2) + (count % 2);
// search for the middle of all visible elements // search for the middle of all visible elements
@ -135,7 +135,7 @@ $("#asc").click(function() {
// Find count of middle element // Find count of middle element
var count = $(".row:visible").length; var count = $(".row:visible").length;
if (count > 20) { if (count > 20) {
var middle = parseInt(count / 2) + (count % 2); var middle = parseInt(count / 2, 10) + (count % 2);
//var middle = parseInt(count / 2) + (count % 2); //var middle = parseInt(count / 2) + (count % 2);
// search for the middle of all visible elements // search for the middle of all visible elements

View File

@ -146,6 +146,9 @@ kthoom.ImageFile = function(file) {
case "jpeg": case "jpeg":
this.mimeType = "image/jpeg"; this.mimeType = "image/jpeg";
break; break;
case "png":
this.mimeType = "image/png";
break;
case "gif": case "gif":
this.mimeType = "image/gif"; this.mimeType = "image/gif";
break; break;

View File

@ -38,10 +38,10 @@ $(document).on("change", "input[type=\"checkbox\"][data-control]", function () {
$(document).on("change", "select[data-control]", function() { $(document).on("change", "select[data-control]", function() {
var $this = $(this); var $this = $(this);
var name = $this.data("control"); var name = $this.data("control");
var showOrHide = parseInt($this.val()); var showOrHide = parseInt($this.val(), 10);
// var showOrHideLast = $("#" + name + " option:last").val() // var showOrHideLast = $("#" + name + " option:last").val()
for (var i = 0; i < $(this)[0].length; i++) { for (var i = 0; i < $(this)[0].length; i++) {
var element = parseInt($(this)[0][i].value); var element = parseInt($(this)[0][i].value, 10);
if (element === showOrHide) { if (element === showOrHide) {
$("[data-related^=" + name + "][data-related*=-" + element + "]").show(); $("[data-related^=" + name + "][data-related*=-" + element + "]").show();
} else { } else {

View File

@ -16,6 +16,7 @@
*/ */
/* exported TableActions, RestrictionActions, EbookActions, responseHandler */ /* exported TableActions, RestrictionActions, EbookActions, responseHandler */
/* global getPath, ConfirmDialog */
var selections = []; var selections = [];

View File

@ -12,7 +12,6 @@ class TaskUpload(CalibreTask):
def run(self, worker_thread): def run(self, worker_thread):
"""Upload task doesn't have anything to do, it's simply a way to add information to the task list""" """Upload task doesn't have anything to do, it's simply a way to add information to the task list"""
pass
@property @property
def name(self): def name(self):

117
cps/ub.py
View File

@ -138,15 +138,15 @@ class UserBase:
mct = self.allowed_column_value or "" mct = self.allowed_column_value or ""
return [t.strip() for t in mct.split(",")] return [t.strip() for t in mct.split(",")]
def get_view_property(self, page, property): def get_view_property(self, page, prop):
if not self.view_settings.get(page): if not self.view_settings.get(page):
return None return None
return self.view_settings[page].get(property) return self.view_settings[page].get(prop)
def set_view_property(self, page, property, value): def set_view_property(self, page, prop, value):
if not self.view_settings.get(page): if not self.view_settings.get(page):
self.view_settings[page] = dict() self.view_settings[page] = dict()
self.view_settings[page][property] = value self.view_settings[page][prop] = value
try: try:
flag_modified(self, "view_settings") flag_modified(self, "view_settings")
except AttributeError: except AttributeError:
@ -437,11 +437,8 @@ class RemoteAuthToken(Base):
return '<Token %r>' % self.id return '<Token %r>' % self.id
# Migrate database to current version, has to be updated after every database change. Currently migration from # Add missing tables during migration of database
# everywhere to current should work. Migration is done by checking if relevant columns are existing, and than adding def add_missing_tables(engine, session):
# rows with SQL commands
def migrate_Database(session):
engine = session.bind
if not engine.dialect.has_table(engine.connect(), "book_read_link"): if not engine.dialect.has_table(engine.connect(), "book_read_link"):
ReadBook.__table__.create(bind=engine) ReadBook.__table__.create(bind=engine)
if not engine.dialect.has_table(engine.connect(), "bookmark"): if not engine.dialect.has_table(engine.connect(), "bookmark"):
@ -459,6 +456,10 @@ def migrate_Database(session):
with engine.connect() as conn: with engine.connect() as conn:
conn.execute("insert into registration (domain, allow) values('%.%',1)") conn.execute("insert into registration (domain, allow) values('%.%',1)")
session.commit() session.commit()
# migrate all settings missing in registration table
def migrate_registration_table(engine, session):
try: try:
session.query(exists().where(Registration.allow)).scalar() session.query(exists().where(Registration.allow)).scalar()
session.commit() session.commit()
@ -468,27 +469,29 @@ def migrate_Database(session):
conn.execute("update registration set 'allow' = 1") conn.execute("update registration set 'allow' = 1")
session.commit() session.commit()
try: try:
session.query(exists().where(RemoteAuthToken.token_type)).scalar() # Handle table exists, but no content
session.commit() cnt = session.query(Registration).count()
except exc.OperationalError: # Database is not compatible, some columns are missing if not cnt:
with engine.connect() as conn: with engine.connect() as conn:
conn.execute("ALTER TABLE remote_auth_token ADD column 'token_type' INTEGER DEFAULT 0") conn.execute("insert into registration (domain, allow) values('%.%',1)")
conn.execute("update remote_auth_token set 'token_type' = 0") session.commit()
session.commit() except exc.OperationalError: # Database is not writeable
print('Settings database is not writeable. Exiting...')
sys.exit(2)
# Remove login capability of user Guest
def migrate_guest_password(engine, session):
try: try:
session.query(exists().where(ReadBook.read_status)).scalar()
except exc.OperationalError:
with engine.connect() as conn: with engine.connect() as conn:
conn.execute("ALTER TABLE book_read_link ADD column 'read_status' INTEGER DEFAULT 0") conn.execute("UPDATE user SET password='' where nickname = 'Guest' and password !=''")
conn.execute("UPDATE book_read_link SET 'read_status' = 1 WHERE is_read")
conn.execute("ALTER TABLE book_read_link ADD column 'last_modified' DATETIME")
conn.execute("ALTER TABLE book_read_link ADD column 'last_time_started_reading' DATETIME")
conn.execute("ALTER TABLE book_read_link ADD column 'times_started_reading' INTEGER DEFAULT 0")
session.commit() session.commit()
test = session.query(ReadBook).filter(ReadBook.last_modified == None).all() except exc.OperationalError:
for book in test: print('Settings database is not writeable. Exiting...')
book.last_modified = datetime.datetime.utcnow() sys.exit(2)
session.commit()
def migrate_shelfs(engine, session):
try: try:
session.query(exists().where(Shelf.uuid)).scalar() session.query(exists().where(Shelf.uuid)).scalar()
except exc.OperationalError: except exc.OperationalError:
@ -504,22 +507,51 @@ def migrate_Database(session):
for book_shelf in session.query(BookShelf).all(): for book_shelf in session.query(BookShelf).all():
book_shelf.date_added = datetime.datetime.now() book_shelf.date_added = datetime.datetime.now()
session.commit() session.commit()
try:
# Handle table exists, but no content
cnt = session.query(Registration).count()
if not cnt:
with engine.connect() as conn:
conn.execute("insert into registration (domain, allow) values('%.%',1)")
session.commit()
except exc.OperationalError: # Database is not writeable
print('Settings database is not writeable. Exiting...')
sys.exit(2)
try: try:
session.query(exists().where(BookShelf.order)).scalar() session.query(exists().where(BookShelf.order)).scalar()
except exc.OperationalError: # Database is not compatible, some columns are missing except exc.OperationalError: # Database is not compatible, some columns are missing
with engine.connect() as conn: with engine.connect() as conn:
conn.execute("ALTER TABLE book_shelf_link ADD column 'order' INTEGER DEFAULT 1") conn.execute("ALTER TABLE book_shelf_link ADD column 'order' INTEGER DEFAULT 1")
session.commit() session.commit()
def migrate_readBook(engine, session):
try:
session.query(exists().where(ReadBook.read_status)).scalar()
except exc.OperationalError:
with engine.connect() as conn:
conn.execute("ALTER TABLE book_read_link ADD column 'read_status' INTEGER DEFAULT 0")
conn.execute("UPDATE book_read_link SET 'read_status' = 1 WHERE is_read")
conn.execute("ALTER TABLE book_read_link ADD column 'last_modified' DATETIME")
conn.execute("ALTER TABLE book_read_link ADD column 'last_time_started_reading' DATETIME")
conn.execute("ALTER TABLE book_read_link ADD column 'times_started_reading' INTEGER DEFAULT 0")
session.commit()
test = session.query(ReadBook).filter(ReadBook.last_modified == None).all()
for book in test:
book.last_modified = datetime.datetime.utcnow()
session.commit()
def migrate_remoteAuthToken(engine, session):
try:
session.query(exists().where(RemoteAuthToken.token_type)).scalar()
session.commit()
except exc.OperationalError: # Database is not compatible, some columns are missing
with engine.connect() as conn:
conn.execute("ALTER TABLE remote_auth_token ADD column 'token_type' INTEGER DEFAULT 0")
conn.execute("update remote_auth_token set 'token_type' = 0")
session.commit()
# Migrate database to current version, has to be updated after every database change. Currently migration from
# everywhere to current should work. Migration is done by checking if relevant columns are existing, and than adding
# rows with SQL commands
def migrate_Database(session):
engine = session.bind
add_missing_tables(engine, session)
migrate_registration_table(engine, session)
migrate_readBook(engine, session)
migrate_remoteAuthToken(engine, session)
migrate_shelfs(engine, session)
try: try:
create = False create = False
session.query(exists().where(User.sidebar_view)).scalar() session.query(exists().where(User.sidebar_view)).scalar()
@ -578,7 +610,6 @@ def migrate_Database(session):
"locale VARCHAR(2)," "locale VARCHAR(2),"
"sidebar_view INTEGER," "sidebar_view INTEGER,"
"default_language VARCHAR(3)," "default_language VARCHAR(3),"
# "series_view VARCHAR(10),"
"view_settings VARCHAR," "view_settings VARCHAR,"
"UNIQUE (nickname)," "UNIQUE (nickname),"
"UNIQUE (email))") "UNIQUE (email))")
@ -590,15 +621,7 @@ def migrate_Database(session):
conn.execute("DROP TABLE user") conn.execute("DROP TABLE user")
conn.execute("ALTER TABLE user_id RENAME TO user") conn.execute("ALTER TABLE user_id RENAME TO user")
session.commit() session.commit()
migrate_guest_password(engine, session)
# Remove login capability of user Guest
try:
with engine.connect() as conn:
conn.execute("UPDATE user SET password='' where nickname = 'Guest' and password !=''")
session.commit()
except exc.OperationalError:
print('Settings database is not writeable. Exiting...')
sys.exit(2)
def clean_database(session): def clean_database(session):

View File

@ -72,7 +72,7 @@ def load_user_from_request(request):
def load_user_from_auth_header(header_val): def load_user_from_auth_header(header_val):
if header_val.startswith('Basic '): if header_val.startswith('Basic '):
header_val = header_val.replace('Basic ', '', 1) header_val = header_val.replace('Basic ', '', 1)
basic_username = basic_password = '' basic_username = basic_password = '' # nosec
try: try:
header_val = base64.b64decode(header_val).decode('utf-8') header_val = base64.b64decode(header_val).decode('utf-8')
basic_username = header_val.split(':')[0] basic_username = header_val.split(':')[0]

View File

@ -216,7 +216,7 @@ def update_view():
for param in to_save[element]: for param in to_save[element]:
current_user.set_view_property(element, param, to_save[element][param]) current_user.set_view_property(element, param, to_save[element][param])
except Exception as e: except Exception as e:
log.error("Could not save view_settings: %r %r: e", request, to_save, e) log.error("Could not save view_settings: %r %r: %e", request, to_save, e)
return "Invalid request", 400 return "Invalid request", 400
return "1", 200 return "1", 200
@ -340,7 +340,7 @@ def get_matching_tags():
return json_dumps return json_dumps
def render_books_list(data, sort, book_id, page): def get_sort_function(sort, data):
order = [db.Books.timestamp.desc()] order = [db.Books.timestamp.desc()]
if sort == 'stored': if sort == 'stored':
sort = current_user.get_view_property(data, 'stored') sort = current_user.get_view_property(data, 'stored')
@ -366,6 +366,11 @@ def render_books_list(data, sort, book_id, page):
order = [db.Books.series_index.asc()] order = [db.Books.series_index.asc()]
if sort == 'seriesdesc': if sort == 'seriesdesc':
order = [db.Books.series_index.desc()] order = [db.Books.series_index.desc()]
return order
def render_books_list(data, sort, book_id, page):
order = get_sort_function(sort, data)
if data == "rated": if data == "rated":
if current_user.check_visibility(constants.SIDEBAR_BEST_RATED): if current_user.check_visibility(constants.SIDEBAR_BEST_RATED):
@ -453,18 +458,11 @@ def render_hot_books(page):
def render_downloaded_books(page, order): def render_downloaded_books(page, order):
if current_user.check_visibility(constants.SIDEBAR_DOWNLOAD): if current_user.check_visibility(constants.SIDEBAR_DOWNLOAD):
# order = order or []
if current_user.show_detail_random(): if current_user.show_detail_random():
random = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \ random = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \
.order_by(func.random()).limit(config.config_random_books) .order_by(func.random()).limit(config.config_random_books)
else: else:
random = false() random = false()
# off = int(int(config.config_books_per_page) * (page - 1))
'''entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db_filter,
order,
ub.ReadBook, db.Books.id==ub.ReadBook.book_id)'''
entries, __, pagination = calibre_db.fill_indexpage(page, entries, __, pagination = calibre_db.fill_indexpage(page,
0, 0,
@ -748,7 +746,7 @@ def list_books():
search = request.args.get("search") search = request.args.get("search")
total_count = calibre_db.session.query(db.Books).count() total_count = calibre_db.session.query(db.Books).count()
if search: if search:
entries, filtered_count, pagination = calibre_db.get_search_results(search, off, order, limit) entries, filtered_count, __ = calibre_db.get_search_results(search, off, order, limit)
else: else:
entries, __, __ = calibre_db.fill_indexpage((int(off) / (int(limit)) + 1), limit, db.Books, True, order) entries, __, __ = calibre_db.fill_indexpage((int(off) / (int(limit)) + 1), limit, db.Books, True, order)
filtered_count = total_count filtered_count = total_count
@ -1411,12 +1409,71 @@ def logout():
# ################################### Users own configuration ######################################################### # ################################### Users own configuration #########################################################
def change_profile(kobo_support, local_oauth_check, oauth_status, translations, languages):
to_save = request.form.to_dict()
current_user.random_books = 0
if current_user.role_passwd() or current_user.role_admin():
if "password" in to_save and to_save["password"]:
current_user.password = generate_password_hash(to_save["password"])
if "kindle_mail" in to_save and to_save["kindle_mail"] != current_user.kindle_mail:
current_user.kindle_mail = to_save["kindle_mail"]
if "allowed_tags" in to_save and to_save["allowed_tags"] != current_user.allowed_tags:
current_user.allowed_tags = to_save["allowed_tags"].strip()
if "email" in to_save and to_save["email"] != current_user.email:
if config.config_public_reg and not check_valid_domain(to_save["email"]):
flash(_(u"E-mail is not from valid domain"), category="error")
return render_title_template("user_edit.html", content=current_user,
title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
kobo_support=kobo_support,
registered_oauth=local_oauth_check, oauth_status=oauth_status)
current_user.email = to_save["email"]
if "nickname" in to_save and to_save["nickname"] != current_user.nickname:
# Query User nickname, if not existing, change
if not ub.session.query(ub.User).filter(ub.User.nickname == to_save["nickname"]).scalar():
current_user.nickname = to_save["nickname"]
else:
flash(_(u"This username is already taken"), category="error")
return render_title_template("user_edit.html",
translations=translations,
languages=languages,
kobo_support=kobo_support,
new_user=0, content=current_user,
registered_oauth=local_oauth_check,
title=_(u"Edit User %(nick)s",
nick=current_user.nickname),
page="edituser")
if "show_random" in to_save and to_save["show_random"] == "on":
current_user.random_books = 1
if "default_language" in to_save:
current_user.default_language = to_save["default_language"]
if "locale" in to_save:
current_user.locale = to_save["locale"]
val = 0
for key, __ in to_save.items():
if key.startswith('show'):
val += int(key[5:])
current_user.sidebar_view = val
if "Show_detail_random" in to_save:
current_user.sidebar_view += constants.DETAIL_RANDOM
try:
ub.session.commit()
flash(_(u"Profile updated"), category="success")
log.debug(u"Profile updated")
except IntegrityError:
ub.session.rollback()
flash(_(u"Found an existing account for this e-mail address."), category="error")
log.debug(u"Found an existing account for this e-mail address.")
except OperationalError as e:
ub.session.rollback()
log.error("Database error: %s", e)
flash(_(u"Database error: %(error)s.", error=e), category="error")
@web.route("/me", methods=["GET", "POST"]) @web.route("/me", methods=["GET", "POST"])
@login_required @login_required
def profile(): def profile():
# downloads = list()
languages = calibre_db.speaking_language() languages = calibre_db.speaking_language()
translations = babel.list_translations() + [LC('en')] translations = babel.list_translations() + [LC('en')]
kobo_support = feature_support['kobo'] and config.config_kobo_sync kobo_support = feature_support['kobo'] and config.config_kobo_sync
@ -1427,74 +1484,8 @@ def profile():
oauth_status = None oauth_status = None
local_oauth_check = {} local_oauth_check = {}
'''entries, __, pagination = calibre_db.fill_indexpage(page,
0,
db.Books,
ub.Downloads.user_id == int(current_user.id), # True,
[],
ub.Downloads, db.Books.id == ub.Downloads.book_id)'''
if request.method == "POST": if request.method == "POST":
to_save = request.form.to_dict() change_profile(kobo_support, local_oauth_check, oauth_status, translations, languages)
current_user.random_books = 0
if current_user.role_passwd() or current_user.role_admin():
if "password" in to_save and to_save["password"]:
current_user.password = generate_password_hash(to_save["password"])
if "kindle_mail" in to_save and to_save["kindle_mail"] != current_user.kindle_mail:
current_user.kindle_mail = to_save["kindle_mail"]
if "allowed_tags" in to_save and to_save["allowed_tags"] != current_user.allowed_tags:
current_user.allowed_tags = to_save["allowed_tags"].strip()
if "email" in to_save and to_save["email"] != current_user.email:
if config.config_public_reg and not check_valid_domain(to_save["email"]):
flash(_(u"E-mail is not from valid domain"), category="error")
return render_title_template("user_edit.html", content=current_user,
title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
kobo_support=kobo_support,
registered_oauth=local_oauth_check, oauth_status=oauth_status)
current_user.email = to_save["email"]
if "nickname" in to_save and to_save["nickname"] != current_user.nickname:
# Query User nickname, if not existing, change
if not ub.session.query(ub.User).filter(ub.User.nickname == to_save["nickname"]).scalar():
current_user.nickname = to_save["nickname"]
else:
flash(_(u"This username is already taken"), category="error")
return render_title_template("user_edit.html",
translations=translations,
languages=languages,
kobo_support=kobo_support,
new_user=0, content=current_user,
registered_oauth=local_oauth_check,
title=_(u"Edit User %(nick)s",
nick=current_user.nickname),
page="edituser")
if "show_random" in to_save and to_save["show_random"] == "on":
current_user.random_books = 1
if "default_language" in to_save:
current_user.default_language = to_save["default_language"]
if "locale" in to_save:
current_user.locale = to_save["locale"]
val = 0
for key, __ in to_save.items():
if key.startswith('show'):
val += int(key[5:])
current_user.sidebar_view = val
if "Show_detail_random" in to_save:
current_user.sidebar_view += constants.DETAIL_RANDOM
try:
ub.session.commit()
flash(_(u"Profile updated"), category="success")
log.debug(u"Profile updated")
except IntegrityError:
ub.session.rollback()
flash(_(u"Found an existing account for this e-mail address."), category="error")
log.debug(u"Found an existing account for this e-mail address.")
except OperationalError as e:
ub.session.rollback()
log.error("Database error: %s", e)
flash(_(u"Database error: %(error)s.", error=e), category="error")
return render_title_template("user_edit.html", return render_title_template("user_edit.html",
translations=translations, translations=translations,
profile=1, profile=1,