Merge with master

pull/1590/head
OzzieIsaacs 4 years ago
commit ad144922fb

@ -1,30 +1,30 @@
## How to contribute to Calibre-Web ## How to contribute to Calibre-Web
First of all, we would like to thank you for reading this text. we are happy you are willing to contribute to Calibre-Web First of all, we would like to thank you for reading this text. We are happy you are willing to contribute to Calibre-Web.
### **General** ### **General**
**Communication language** is english. Google translated texts are not as bad as you might think, they are usually understandable, so don't worry if you generate your post that way. **Communication language** is English. Google translated texts are not as bad as you might think, they are usually understandable, so don't worry if you generate your post that way.
**Calibre-Web** is not **Calibre**. If you are having a question regarding Calibre please post this at their [repository](https://github.com/kovidgoyal/calibre). **Calibre-Web** is not **Calibre**. If you are having a question regarding Calibre please post this at their [repository](https://github.com/kovidgoyal/calibre).
**Docker-Containers** of Calibre-Web are maintained by different persons than the people who drive Calibre-Web. If we come to the conclusion during our analysis that the problem is related to Docker, we might ask you to open a new issue at the reprository of the Docker Container. **Docker-Containers** of Calibre-Web are maintained by different persons than the people who drive Calibre-Web. If we come to the conclusion during our analysis that the problem is related to Docker, we might ask you to open a new issue at the repository of the Docker Container.
If you are having **Basic Installation Problems** with python or it's dependencies, please consider using your favorite search engine to find a solution. In case you can't find a solution, we are happy to help you. If you are having **Basic Installation Problems** with python or its dependencies, please consider using your favorite search engine to find a solution. In case you can't find a solution, we are happy to help you.
We can offer only very limited support regarding configuration of **Reverse-Proxy Installations**, **OPDS-Reader** or other programs in combination with Calibre-Web. We can offer only very limited support regarding configuration of **Reverse-Proxy Installations**, **OPDS-Reader** or other programs in combination with Calibre-Web.
### **Translation** ### **Translation**
Some of the user languages in Calibre-Web having missing translations. We are happy to add the missing texts if you translate them. Create a Pull Request, create an issue with the .po file attached, or write an email to "ozzie.fernandez.isaacs@googlemail.com" with attached translation file. To display all book languages in your native language an additional file is used (iso_language_names.py). The content of this file is autogenerated with the corresponding translations of Calibre, please do not edit this file on your own. Some of the user languages in Calibre-Web having missing translations. We are happy to add the missing texts if you translate them. Create a Pull Request, create an issue with the .po file attached, or write an email to "ozzie.fernandez.isaacs@googlemail.com" with attached translation file. To display all book languages in your native language an additional file is used (iso_language_names.py). The content of this file is auto-generated with the corresponding translations of Calibre, please do not edit this file on your own.
### **Documentation** ### **Documentation**
The Calibre-Web documentation is hosted in the Github [Wiki](https://github.com/janeczku/calibre-web/wiki). The Wiki is open to everybody, if you find a problem, feel free to correct it. If information is missing, you are welcome to add it. The content will be reviewed time by time. Please try to be consitent with the form with the other Wiki pages (e.g. the project name is Calibre-Web with 2 capital letters and a dash in between). The Calibre-Web documentation is hosted in the Github [Wiki](https://github.com/janeczku/calibre-web/wiki). The Wiki is open to everybody, if you find a problem, feel free to correct it. If information is missing, you are welcome to add it. The content will be reviewed time by time. Please try to be consistent with the form with the other Wiki pages (e.g. the project name is Calibre-Web with 2 capital letters and a dash in between).
### **Reporting a bug** ### **Reporting a bug**
Do not open up a GitHub issue if the bug is a **security vulnerability** in Calibre-Web. Please write intead an email to "ozzie.fernandez.isaacs@googlemail.com". Do not open up a GitHub issue if the bug is a **security vulnerability** in Calibre-Web. Instead, please write an email to "ozzie.fernandez.isaacs@googlemail.com".
Ensure the ***bug was not already reported** by searching on GitHub under [Issues](https://github.com/janeczku/calibre-web/issues). Please also check if a solution for your problem can be found in the [wiki](https://github.com/janeczku/calibre-web/wiki). Ensure the ***bug was not already reported** by searching on GitHub under [Issues](https://github.com/janeczku/calibre-web/issues). Please also check if a solution for your problem can be found in the [wiki](https://github.com/janeczku/calibre-web/wiki).
@ -33,17 +33,14 @@ If you're unable to find an **open issue** addressing the problem, open a [new o
### **Feature Request** ### **Feature Request**
If there is a feature missing in Calibre-Web and you can't find a feature request in the [Issues](https://github.com/janeczku/calibre-web/issues) section, you could create a [feature request](https://github.com/janeczku/calibre-web/issues/new?assignees=&labels=&template=feature_request.md&title=). If there is a feature missing in Calibre-Web and you can't find a feature request in the [Issues](https://github.com/janeczku/calibre-web/issues) section, you could create a [feature request](https://github.com/janeczku/calibre-web/issues/new?assignees=&labels=&template=feature_request.md&title=).
We will not extend Calibre-Web with any more login abilitys or add further files storages, or file syncing ability. Furthermore Calibre-Web is made for home usage for company inhouse usage, so requests regarding any sorts of social interaction capability, payment routines, search engine or web site analytics integration will not be implemeted. We will not extend Calibre-Web with any more login abilities or add further files storages, or file syncing ability. Furthermore Calibre-Web is made for home usage for company in-house usage, so requests regarding any sorts of social interaction capability, payment routines, search engine or web site analytics integration will not be implemented.
### **Contributing code to Calibre-Web** ### **Contributing code to Calibre-Web**
Open a new GitHub pull request with the patch. Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. Open a new GitHub pull request with the patch. Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
In case your code enhances features of Calibre-Web: Create your pull request for the development branch if your enhancement consits 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 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.
Calibre-Web is automatically tested on Linux in combination with python 3.7. The code for testing is in a [seperate repo](https://github.com/OzzieIsaacs/calibre-web-test) on Github. It uses unittest and performs real system tests with selenium, would be great if you could consider also writing some tests. 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.
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.

@ -99,7 +99,7 @@ def shutdown():
if task == 2: if task == 2:
log.warning("reconnecting to calibre database") log.warning("reconnecting to calibre database")
calibre_db.setup_db(config, ub.app_DB_path) calibre_db.reconnect_db(config, ub.app_DB_path)
showtext['text'] = _(u'Reconnect successful') showtext['text'] = _(u'Reconnect successful')
return json.dumps(showtext) return json.dumps(showtext)

@ -22,7 +22,7 @@ import os
import json import json
import sys import sys
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from . import constants, cli, logger, ub from . import constants, cli, logger, ub
@ -92,7 +92,7 @@ class _Settings(_Base):
config_use_google_drive = Column(Boolean, default=False) config_use_google_drive = Column(Boolean, default=False)
config_google_drive_folder = Column(String) config_google_drive_folder = Column(String)
config_google_drive_watch_changes_response = Column(String) config_google_drive_watch_changes_response = Column(JSON, default={})
config_use_goodreads = Column(Boolean, default=False) config_use_goodreads = Column(Boolean, default=False)
config_goodreads_api_key = Column(String) config_goodreads_api_key = Column(String)
@ -102,7 +102,6 @@ class _Settings(_Base):
config_kobo_proxy = Column(Boolean, default=False) config_kobo_proxy = Column(Boolean, default=False)
config_ldap_provider_url = Column(String, default='example.org') config_ldap_provider_url = Column(String, default='example.org')
config_ldap_port = Column(SmallInteger, default=389) config_ldap_port = Column(SmallInteger, default=389)
config_ldap_authentication = Column(SmallInteger, default=constants.LDAP_AUTH_SIMPLE) config_ldap_authentication = Column(SmallInteger, default=constants.LDAP_AUTH_SIMPLE)
@ -215,20 +214,20 @@ class _ConfigSQL(object):
return self.show_element_new_user(constants.DETAIL_RANDOM) return self.show_element_new_user(constants.DETAIL_RANDOM)
def list_denied_tags(self): def list_denied_tags(self):
mct = self.config_denied_tags.split(",") mct = self.config_denied_tags or ""
return [t.strip() for t in mct] return [t.strip() for t in mct.split(",")]
def list_allowed_tags(self): def list_allowed_tags(self):
mct = self.config_allowed_tags.split(",") mct = self.config_allowed_tags or ""
return [t.strip() for t in mct] return [t.strip() for t in mct.split(",")]
def list_denied_column_values(self): def list_denied_column_values(self):
mct = self.config_denied_column_value.split(",") mct = self.config_denied_column_value or ""
return [t.strip() for t in mct] return [t.strip() for t in mct.split(",")]
def list_allowed_column_values(self): def list_allowed_column_values(self):
mct = self.config_allowed_column_value.split(",") mct = self.config_allowed_column_value or ""
return [t.strip() for t in mct] return [t.strip() for t in mct.split(",")]
def get_log_level(self): def get_log_level(self):
return logger.get_level_name(self.config_log_level) return logger.get_level_name(self.config_log_level)
@ -281,10 +280,6 @@ class _ConfigSQL(object):
v = column.default.arg v = column.default.arg
setattr(self, k, v) setattr(self, k, v)
if self.config_google_drive_watch_changes_response:
self.config_google_drive_watch_changes_response = \
json.loads(self.config_google_drive_watch_changes_response)
have_metadata_db = bool(self.config_calibre_dir) have_metadata_db = bool(self.config_calibre_dir)
if have_metadata_db: if have_metadata_db:
if not self.config_use_google_drive: if not self.config_use_google_drive:
@ -303,10 +298,6 @@ class _ConfigSQL(object):
'''Apply all configuration values to the underlying storage.''' '''Apply all configuration values to the underlying storage.'''
s = self._read_from_storage() # type: _Settings s = self._read_from_storage() # type: _Settings
if self.config_google_drive_watch_changes_response:
self.config_google_drive_watch_changes_response = json.dumps(
self.config_google_drive_watch_changes_response)
for k, v in self.__dict__.items(): for k, v in self.__dict__.items():
if k[0] == '_': if k[0] == '_':
continue continue
@ -361,10 +352,10 @@ def _migrate_table(session, orm_class):
def autodetect_calibre_binary(): def autodetect_calibre_binary():
if sys.platform == "win32": if sys.platform == "win32":
calibre_path = ["C:\\program files\calibre\ebook-convert.exe", calibre_path = ["C:\\program files\\calibre\\ebook-convert.exe",
"C:\\program files(x86)\calibre\ebook-convert.exe", "C:\\program files(x86)\\calibre\\ebook-convert.exe",
"C:\\program files(x86)\calibre2\ebook-convert.exe", "C:\\program files(x86)\\calibre2\\ebook-convert.exe",
"C:\\program files\calibre2\ebook-convert.exe"] "C:\\program files\\calibre2\\ebook-convert.exe"]
else: else:
calibre_path = ["/opt/calibre/ebook-convert"] calibre_path = ["/opt/calibre/ebook-convert"]
for element in calibre_path: for element in calibre_path:

@ -33,8 +33,10 @@ from sqlalchemy.orm import relationship, sessionmaker, scoped_session
from sqlalchemy.orm.collections import InstrumentedList from sqlalchemy.orm.collections import InstrumentedList
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
from sqlalchemy.exc import OperationalError from sqlalchemy.exc import OperationalError
from sqlalchemy.pool import StaticPool
from flask_login import current_user from flask_login import current_user
from sqlalchemy.sql.expression import and_, true, false, text, func, or_ from sqlalchemy.sql.expression import and_, true, false, text, func, or_
from sqlalchemy.ext.associationproxy import association_proxy
from babel import Locale as LC from babel import Locale as LC
from babel.core import UnknownLocaleError from babel.core import UnknownLocaleError
from flask_babel import gettext as _ from flask_babel import gettext as _
@ -312,7 +314,7 @@ class Books(Base):
flags = Column(Integer, nullable=False, default=1) flags = Column(Integer, nullable=False, default=1)
authors = relationship('Authors', secondary=books_authors_link, backref='books') authors = relationship('Authors', secondary=books_authors_link, backref='books')
tags = relationship('Tags', secondary=books_tags_link, backref='books',order_by="Tags.name") tags = relationship('Tags', secondary=books_tags_link, backref='books', order_by="Tags.name")
comments = relationship('Comments', backref='books') comments = relationship('Comments', backref='books')
data = relationship('Data', backref='books') data = relationship('Data', backref='books')
series = relationship('Series', secondary=books_series_link, backref='books') series = relationship('Series', secondary=books_series_link, backref='books')
@ -438,7 +440,6 @@ class CalibreDB(threading.Thread):
def setup_db(self, config, app_db_path): def setup_db(self, config, app_db_path):
self.config = config self.config = config
self.dispose() self.dispose()
# global engine
if not config.config_calibre_dir: if not config.config_calibre_dir:
config.invalidate() config.invalidate()
@ -450,11 +451,11 @@ class CalibreDB(threading.Thread):
return False return False
try: try:
#engine = create_engine('sqlite:///{0}'.format(dbpath),
self.engine = create_engine('sqlite://', self.engine = create_engine('sqlite://',
echo=False, echo=False,
isolation_level="SERIALIZABLE", isolation_level="SERIALIZABLE",
connect_args={'check_same_thread': False}) connect_args={'check_same_thread': False},
poolclass=StaticPool)
self.engine.execute("attach database '{}' as calibre;".format(dbpath)) self.engine.execute("attach database '{}' as calibre;".format(dbpath))
self.engine.execute("attach database '{}' as app_settings;".format(app_db_path)) self.engine.execute("attach database '{}' as app_settings;".format(app_db_path))
@ -474,34 +475,46 @@ class CalibreDB(threading.Thread):
books_custom_column_links = {} books_custom_column_links = {}
for row in cc: for row in cc:
if row.datatype not in cc_exceptions: if row.datatype not in cc_exceptions:
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', Base.metadata, if row.datatype == 'series':
dicttable = {'__tablename__': 'books_custom_column_' + str(row.id) + '_link',
'id': Column(Integer, primary_key=True),
'book': Column(Integer, ForeignKey('books.id'),
primary_key=True),
'map_value': Column('value', Integer,
ForeignKey('custom_column_' +
str(row.id) + '.id'),
primary_key=True),
'extra': Column(Float),
'asoc' : relationship('custom_column_' + str(row.id), uselist=False),
'value' : association_proxy('asoc', 'value')
}
books_custom_column_links[row.id] = type(str('books_custom_column_' + str(row.id) + '_link'),
(Base,), dicttable)
else:
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link',
Base.metadata,
Column('book', Integer, ForeignKey('books.id'), Column('book', Integer, ForeignKey('books.id'),
primary_key=True), primary_key=True),
Column('value', Integer, Column('value', Integer,
ForeignKey('custom_column_' + str(row.id) + '.id'), ForeignKey('custom_column_' +
str(row.id) + '.id'),
primary_key=True) primary_key=True)
) )
cc_ids.append([row.id, row.datatype]) cc_ids.append([row.id, row.datatype])
if row.datatype == 'bool':
ccdict = {'__tablename__': 'custom_column_' + str(row.id), ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True), 'id': Column(Integer, primary_key=True)}
'book': Column(Integer, ForeignKey('books.id')), if row.datatype == 'float':
'value': Column(Boolean)} ccdict['value'] = Column(Float)
elif row.datatype == 'int': elif row.datatype == 'int':
ccdict = {'__tablename__': 'custom_column_' + str(row.id), ccdict['value'] = Column(Integer)
'id': Column(Integer, primary_key=True), elif row.datatype == 'bool':
'book': Column(Integer, ForeignKey('books.id')), ccdict['value'] = Column(Boolean)
'value': Column(Integer)}
elif row.datatype == 'float':
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True),
'book': Column(Integer, ForeignKey('books.id')),
'value': Column(Float)}
else: else:
ccdict = {'__tablename__': 'custom_column_' + str(row.id), ccdict['value'] = Column(String)
'id': Column(Integer, primary_key=True), if row.datatype in ['float', 'int', 'bool']:
'value': Column(String)} ccdict['book'] = Column(Integer, ForeignKey('books.id'))
cc_classes[row.id] = type(str('Custom_Column_' + str(row.id)), (Base,), ccdict) cc_classes[row.id] = type(str('custom_column_' + str(row.id)), (Base,), ccdict)
for cc_id in cc_ids: for cc_id in cc_ids:
if (cc_id[1] == 'bool') or (cc_id[1] == 'int') or (cc_id[1] == 'float'): if (cc_id[1] == 'bool') or (cc_id[1] == 'int') or (cc_id[1] == 'float'):
@ -511,6 +524,11 @@ class CalibreDB(threading.Thread):
primaryjoin=( primaryjoin=(
Books.id == cc_classes[cc_id[0]].book), Books.id == cc_classes[cc_id[0]].book),
backref='books')) backref='books'))
elif (cc_id[1] == 'series'):
setattr(Books,
'custom_column_' + str(cc_id[0]),
relationship(books_custom_column_links[cc_id[0]],
backref='books'))
else: else:
setattr(Books, setattr(Books,
'custom_column_' + str(cc_id[0]), 'custom_column_' + str(cc_id[0]),

@ -867,8 +867,8 @@ def upload():
# move cover to final directory, including book id # move cover to final directory, including book id
if has_cover: if has_cover:
try:
new_coverpath = os.path.join(config.config_calibre_dir, db_book.path, "cover.jpg") new_coverpath = os.path.join(config.config_calibre_dir, db_book.path, "cover.jpg")
try:
copyfile(meta.cover, new_coverpath) copyfile(meta.cover, new_coverpath)
os.unlink(meta.cover) os.unlink(meta.cover)
except OSError as e: except OSError as e:

@ -34,18 +34,17 @@ from flask import Blueprint, flash, request, redirect, url_for, abort
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import login_required from flask_login import login_required
try:
from googleapiclient.errors import HttpError
except ImportError:
pass
from . import logger, gdriveutils, config, ub, calibre_db from . import logger, gdriveutils, config, ub, calibre_db
from .web import admin_required from .web import admin_required
gdrive = Blueprint('gdrive', __name__) gdrive = Blueprint('gdrive', __name__)
log = logger.create() log = logger.create()
try:
from googleapiclient.errors import HttpError
except ImportError as err:
log.debug(("Cannot import googleapiclient, using gdrive will not work: %s", err))
current_milli_time = lambda: int(round(time() * 1000)) current_milli_time = lambda: int(round(time() * 1000))
gdrive_watch_callback_token = 'target=calibreweb-watch_files' gdrive_watch_callback_token = 'target=calibreweb-watch_files'
@ -73,7 +72,7 @@ def google_drive_callback():
credentials = gdriveutils.Gauth.Instance().auth.flow.step2_exchange(auth_code) credentials = gdriveutils.Gauth.Instance().auth.flow.step2_exchange(auth_code)
with open(gdriveutils.CREDENTIALS, 'w') as f: with open(gdriveutils.CREDENTIALS, 'w') as f:
f.write(credentials.to_json()) f.write(credentials.to_json())
except ValueError as error: except (ValueError, AttributeError) as error:
log.error(error) log.error(error)
return redirect(url_for('admin.configuration')) return redirect(url_for('admin.configuration'))
@ -94,8 +93,7 @@ def watch_gdrive():
try: try:
result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id, result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id,
'web_hook', address, gdrive_watch_callback_token, current_milli_time() + 604800*1000) 'web_hook', address, gdrive_watch_callback_token, current_milli_time() + 604800*1000)
config.config_google_drive_watch_changes_response = json.dumps(result) config.config_google_drive_watch_changes_response = result
# after save(), config_google_drive_watch_changes_response will be a json object, not string
config.save() config.save()
except HttpError as e: except HttpError as e:
reason=json.loads(e.content)['error']['errors'][0] reason=json.loads(e.content)['error']['errors'][0]
@ -118,7 +116,7 @@ def revoke_watch_gdrive():
last_watch_response['resourceId']) last_watch_response['resourceId'])
except HttpError: except HttpError:
pass pass
config.config_google_drive_watch_changes_response = None config.config_google_drive_watch_changes_response = {}
config.save() config.save()
return redirect(url_for('admin.configuration')) return redirect(url_for('admin.configuration'))
@ -155,7 +153,7 @@ def on_received_watch_confirmation():
log.info('Setting up new DB') log.info('Setting up new DB')
# prevent error on windows, as os.rename does on exisiting files # prevent error on windows, as os.rename does on exisiting files
move(os.path.join(tmpDir, "tmp_metadata.db"), dbpath) move(os.path.join(tmpDir, "tmp_metadata.db"), dbpath)
calibre_db.setup_db(config, ub.app_DB_path) calibre_db.reconnect_db(config, ub.app_DB_path)
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
updateMetaData() updateMetaData()

@ -36,7 +36,9 @@ try:
from apiclient import errors from apiclient import errors
from httplib2 import ServerNotFoundError from httplib2 import ServerNotFoundError
gdrive_support = True gdrive_support = True
except ImportError: importError = None
except ImportError as err:
importError = err
gdrive_support = False gdrive_support = False
from . import logger, cli, config from . import logger, cli, config
@ -52,6 +54,8 @@ if gdrive_support:
logger.get('googleapiclient.discovery_cache').setLevel(logger.logging.ERROR) logger.get('googleapiclient.discovery_cache').setLevel(logger.logging.ERROR)
if not logger.is_debug_enabled(): if not logger.is_debug_enabled():
logger.get('googleapiclient.discovery').setLevel(logger.logging.ERROR) logger.get('googleapiclient.discovery').setLevel(logger.logging.ERROR)
else:
log.debug("Cannot import pydrive,httplib2, using gdrive will not work: %s", importError)
class Singleton: class Singleton:
@ -99,7 +103,11 @@ class Singleton:
@Singleton @Singleton
class Gauth: class Gauth:
def __init__(self): def __init__(self):
try:
self.auth = GoogleAuth(settings_file=SETTINGS_YAML) self.auth = GoogleAuth(settings_file=SETTINGS_YAML)
except NameError as error:
log.error(error)
self.auth = None
@Singleton @Singleton
@ -594,8 +602,12 @@ def get_error_text(client_secrets=None):
if not os.path.isfile(CLIENT_SECRETS): if not os.path.isfile(CLIENT_SECRETS):
return 'client_secrets.json is missing or not readable' return 'client_secrets.json is missing or not readable'
try:
with open(CLIENT_SECRETS, 'r') as settings: with open(CLIENT_SECRETS, 'r') as settings:
filedata = json.load(settings) filedata = json.load(settings)
except PermissionError:
return 'client_secrets.json is missing or not readable'
if 'web' not in filedata: if 'web' not in filedata:
return 'client_secrets.json is not configured for web application' return 'client_secrets.json is not configured for web application'
if 'redirect_uris' not in filedata['web']: if 'redirect_uris' not in filedata['web']:

@ -295,15 +295,16 @@ def delete_book_file(book, calibrepath, book_format=None):
return True, None return True, None
else: else:
if os.path.isdir(path): if os.path.isdir(path):
if len(next(os.walk(path))[1]):
log.error("Deleting book %s failed, path has subfolders: %s", book.id, book.path)
return False , _("Deleting book %(id)s failed, path has subfolders: %(path)s",
id=book.id,
path=book.path)
try: try:
for root, __, files in os.walk(path): for root, folders, files in os.walk(path):
for f in files: for f in files:
os.unlink(os.path.join(root, f)) os.unlink(os.path.join(root, f))
if len(folders):
log.warning("Deleting book {} failed, path {} has subfolders: {}".format(book.id,
book.path, folders))
return True, _("Deleting bookfolder for book %(id)s failed, path has subfolders: %(path)s",
id=book.id,
path=book.path)
shutil.rmtree(path) shutil.rmtree(path)
except (IOError, OSError) as e: except (IOError, OSError) as e:
log.error("Deleting book %s failed: %s", book.id, e) log.error("Deleting book %s failed: %s", book.id, e)
@ -339,13 +340,13 @@ def update_dir_structure_file(book_id, calibrepath, first_author):
new_title_path = os.path.join(os.path.dirname(path), new_titledir) new_title_path = os.path.join(os.path.dirname(path), new_titledir)
try: try:
if not os.path.exists(new_title_path): if not os.path.exists(new_title_path):
os.renames(path, new_title_path) os.renames(os.path.normcase(path), os.path.normcase(new_title_path))
else: else:
log.info("Copying title: %s into existing: %s", path, new_title_path) log.info("Copying title: %s into existing: %s", path, new_title_path)
for dir_name, __, file_list in os.walk(path): for dir_name, __, file_list in os.walk(path):
for file in file_list: for file in file_list:
os.renames(os.path.join(dir_name, file), os.renames(os.path.normcase(os.path.join(dir_name, file)),
os.path.join(new_title_path + dir_name[len(path):], file)) os.path.normcase(os.path.join(new_title_path + dir_name[len(path):], file)))
path = new_title_path path = new_title_path
localbook.path = localbook.path.split('/')[0] + '/' + new_titledir localbook.path = localbook.path.split('/')[0] + '/' + new_titledir
except OSError as ex: except OSError as ex:
@ -356,7 +357,7 @@ def update_dir_structure_file(book_id, calibrepath, first_author):
if authordir != new_authordir: if authordir != new_authordir:
new_author_path = os.path.join(calibrepath, new_authordir, os.path.basename(path)) new_author_path = os.path.join(calibrepath, new_authordir, os.path.basename(path))
try: try:
os.renames(path, new_author_path) os.renames(os.path.normcase(path), os.path.normcase(new_author_path))
localbook.path = new_authordir + '/' + localbook.path.split('/')[1] localbook.path = new_authordir + '/' + localbook.path.split('/')[1]
except OSError as ex: except OSError as ex:
log.error("Rename author from: %s to %s: %s", path, new_author_path, ex) log.error("Rename author from: %s to %s: %s", path, new_author_path, ex)
@ -369,8 +370,9 @@ def update_dir_structure_file(book_id, calibrepath, first_author):
new_name = get_valid_filename(localbook.title) + ' - ' + get_valid_filename(new_authordir) new_name = get_valid_filename(localbook.title) + ' - ' + get_valid_filename(new_authordir)
path_name = os.path.join(calibrepath, new_authordir, os.path.basename(path)) path_name = os.path.join(calibrepath, new_authordir, os.path.basename(path))
for file_format in localbook.data: for file_format in localbook.data:
os.renames(os.path.join(path_name, file_format.name + '.' + file_format.format.lower()), os.renames(os.path.normcase(
os.path.join(path_name, new_name + '.' + file_format.format.lower())) os.path.join(path_name, file_format.name + '.' + file_format.format.lower())),
os.path.normcase(os.path.join(path_name, new_name + '.' + file_format.format.lower())))
file_format.name = new_name file_format.name = new_name
except OSError as ex: except OSError as ex:
log.error("Rename file in path %s to %s: %s", path, new_name, ex) log.error("Rename file in path %s to %s: %s", path, new_name, ex)

@ -107,3 +107,10 @@ def timestamptodate(date, fmt=None):
@jinjia.app_template_filter('yesno') @jinjia.app_template_filter('yesno')
def yesno(value, yes, no): def yesno(value, yes, no):
return yes if value else no return yes if value else no
@jinjia.app_template_filter('formatfloat')
def formatfloat(value, decimals=1):
formatedstring = '%d' % value
if (value % 1) != 0:
formatedstring = ('%s.%d' % (formatedstring, (value % 1) * 10**decimals)).rstrip('0')
return formatedstring

@ -19,8 +19,6 @@
import base64 import base64
import datetime import datetime
import itertools
import json
import sys import sys
import os import os
import uuid import uuid
@ -267,7 +265,7 @@ def HandleMetadataRequest(book_uuid):
def get_download_url_for_book(book, book_format): def get_download_url_for_book(book, book_format):
if not current_app.wsgi_app.is_proxied: if not current_app.wsgi_app.is_proxied:
if ':' in request.host and not request.host.endswith(']') : if ':' in request.host and not request.host.endswith(']'):
host = "".join(request.host.split(':')[:-1]) host = "".join(request.host.split(':')[:-1])
else: else:
host = request.host host = request.host
@ -317,8 +315,15 @@ def get_description(book):
# TODO handle multiple authors # TODO handle multiple authors
def get_author(book): def get_author(book):
if not book.authors: if not book.authors:
return None return {"Contributors": None}
return book.authors[0].name if len(book.authors) > 1:
author_list = []
autor_roles = []
for author in book.authors:
autor_roles.append({"Name":author.name, "Role":"Author"})
author_list.append(author.name)
return {"ContributorRoles": autor_roles, "Contributors":author_list}
return {"ContributorRoles": [{"Name":book.authors[0].name, "Role":"Author"}], "Contributors": book.authors[0].name}
def get_publisher(book): def get_publisher(book):
@ -357,7 +362,7 @@ def get_metadata(book):
book_uuid = book.uuid book_uuid = book.uuid
metadata = { metadata = {
"Categories": ["00000000-0000-0000-0000-000000000001",], "Categories": ["00000000-0000-0000-0000-000000000001",],
"Contributors": get_author(book), # "Contributors": get_author(book),
"CoverImageId": book_uuid, "CoverImageId": book_uuid,
"CrossRevisionId": book_uuid, "CrossRevisionId": book_uuid,
"CurrentDisplayPrice": {"CurrencyCode": "USD", "TotalAmount": 0}, "CurrentDisplayPrice": {"CurrencyCode": "USD", "TotalAmount": 0},
@ -381,6 +386,7 @@ def get_metadata(book):
"Title": book.title, "Title": book.title,
"WorkId": book_uuid, "WorkId": book_uuid,
} }
metadata.update(get_author(book))
if get_series(book): if get_series(book):
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
@ -399,7 +405,7 @@ def get_metadata(book):
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"]) @kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
@login_required @requires_kobo_auth
# Creates a Shelf with the given items, and returns the shelf's uuid. # Creates a Shelf with the given items, and returns the shelf's uuid.
def HandleTagCreate(): def HandleTagCreate():
# catch delete requests, otherwise the are handeld in the book delete handler # catch delete requests, otherwise the are handeld in the book delete handler
@ -434,6 +440,7 @@ def HandleTagCreate():
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"]) @kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"])
@requires_kobo_auth
def HandleTagUpdate(tag_id): def HandleTagUpdate(tag_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.uuid == tag_id, shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.uuid == tag_id,
ub.Shelf.user_id == current_user.id).one_or_none() ub.Shelf.user_id == current_user.id).one_or_none()
@ -488,7 +495,7 @@ def add_items_to_shelf(items, shelf):
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"]) @kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
@login_required @requires_kobo_auth
def HandleTagAddItem(tag_id): def HandleTagAddItem(tag_id):
items = None items = None
try: try:
@ -518,7 +525,7 @@ def HandleTagAddItem(tag_id):
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"]) @kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
@login_required @requires_kobo_auth
def HandleTagRemoveItem(tag_id): def HandleTagRemoveItem(tag_id):
items = None items = None
try: try:
@ -627,7 +634,7 @@ def create_kobo_tag(shelf):
@kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"]) @kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"])
@login_required @requires_kobo_auth
def HandleStateRequest(book_uuid): def HandleStateRequest(book_uuid):
book = calibre_db.get_book_by_uuid(book_uuid) book = calibre_db.get_book_by_uuid(book_uuid)
if not book or not book.data: if not book or not book.data:
@ -801,7 +808,7 @@ def TopLevelEndpoint():
@kobo.route("/v1/library/<book_uuid>", methods=["DELETE"]) @kobo.route("/v1/library/<book_uuid>", methods=["DELETE"])
@login_required @requires_kobo_auth
def HandleBookDeletionRequest(book_uuid): def HandleBookDeletionRequest(book_uuid):
log.info("Kobo book deletion request received for book %s" % book_uuid) log.info("Kobo book deletion request received for book %s" % book_uuid)
book = calibre_db.get_book_by_uuid(book_uuid) book = calibre_db.get_book_by_uuid(book_uuid)

@ -287,7 +287,7 @@ if ub.oauth_support:
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error") flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
except NoResultFound: except NoResultFound:
log.warning("oauth %s for user %d not found", provider, current_user.id) log.warning("oauth %s for user %d not found", provider, current_user.id)
flash(_(u"Not Linked to %(oauth)s.", oauth=oauth_check[provider]), category="error") flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error")
return redirect(url_for('web.profile')) return redirect(url_for('web.profile'))
@ -355,4 +355,4 @@ if ub.oauth_support:
@oauth.route('/unlink/google', methods=["GET"]) @oauth.route('/unlink/google', methods=["GET"])
@login_required @login_required
def google_login_unlink(): def google_login_unlink():
return unlink_oauth(oauthblueprints[1]['blueprint'].name) return unlink_oauth(oauthblueprints[1]['id'])

@ -77,6 +77,7 @@ class ReverseProxied(object):
servr = environ.get('HTTP_X_FORWARDED_HOST', '') servr = environ.get('HTTP_X_FORWARDED_HOST', '')
if servr: if servr:
environ['HTTP_HOST'] = servr environ['HTTP_HOST'] = servr
self.proxied = True
return self.app(environ, start_response) return self.app(environ, start_response)
@property @property

@ -27,7 +27,10 @@ except ImportError:
from urllib.parse import unquote from urllib.parse import unquote
from flask import json from flask import json
from .. import logger as log from .. import logger
log = logger.create()
def b64encode_json(json_data): def b64encode_json(json_data):
@ -45,7 +48,8 @@ def to_epoch_timestamp(datetime_object):
def get_datetime_from_json(json_object, field_name): def get_datetime_from_json(json_object, field_name):
try: try:
return datetime.utcfromtimestamp(json_object[field_name]) return datetime.utcfromtimestamp(json_object[field_name])
except KeyError: except (KeyError, OSError, OverflowError):
# OSError is thrown on Windows if timestamp is <1970 or >2038
return datetime.min return datetime.min

@ -65,7 +65,10 @@ def init_app(app, config):
app.config['LDAP_GROUP_OBJECT_FILTER'] = config.config_ldap_group_object_filter app.config['LDAP_GROUP_OBJECT_FILTER'] = config.config_ldap_group_object_filter
app.config['LDAP_GROUP_MEMBERS_FIELD'] = config.config_ldap_group_members_field app.config['LDAP_GROUP_MEMBERS_FIELD'] = config.config_ldap_group_members_field
try:
_ldap.init_app(app) _ldap.init_app(app)
except RuntimeError as e:
log.error(e)
def get_object_details(user=None, group=None, query_filter=None, dn_only=False): def get_object_details(user=None, group=None, query_filter=None, dn_only=False):

@ -130,19 +130,20 @@
<input type="number" step="{% if c.datatype == 'float' %}0.01{% else %}1{% endif %}" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" value="{% if book['custom_column_' ~ c.id]|length > 0 %}{{ book['custom_column_' ~ c.id][0].value }}{% endif %}"> <input type="number" step="{% if c.datatype == 'float' %}0.01{% else %}1{% endif %}" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" value="{% if book['custom_column_' ~ c.id]|length > 0 %}{{ book['custom_column_' ~ c.id][0].value }}{% endif %}">
{% endif %} {% endif %}
{% if c.datatype in ['text', 'series'] and not c.is_multiple %} {% if c.datatype == 'text' %}
<input type="text" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" <input type="text" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}"
{% if book['custom_column_' ~ c.id]|length > 0 %} {% if book['custom_column_' ~ c.id]|length > 0 %}
value="{{ book['custom_column_' ~ c.id][0].value }}" value="{% for column in book['custom_column_' ~ c.id] %}{{ column.value.strip() }}{% if not loop.last %}, {% endif %}{% endfor %}"{% endif %}>
{% endif %}>
{% endif %} {% endif %}
{% if c.datatype in ['text', 'series'] and c.is_multiple %} {% if c.datatype == 'series' %}
<input type="text" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" <input type="text" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}"
{% if book['custom_column_' ~ c.id]|length > 0 %} {% if book['custom_column_' ~ c.id]|length > 0 %}
value="{% for column in book['custom_column_' ~ c.id] %}{{ column.value.strip() }}{% if not loop.last %}, {% endif %}{% endfor %}"{% endif %}> value="{% for column in book['custom_column_' ~ c.id] %} {{ '%s [%s]' % (book['custom_column_' ~ c.id][0].value, book['custom_column_' ~ c.id][0].extra|formatfloat(2)) }}{% if not loop.last %}, {% endif %}{% endfor %}"
{% endif %}>
{% endif %} {% endif %}
{% if c.datatype == 'enumeration' %} {% if c.datatype == 'enumeration' %}
<select class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}"> <select class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}">
<option></option> <option></option>
@ -157,9 +158,9 @@
{% endif %} {% endif %}
{% if c.datatype == 'rating' %} {% if c.datatype == 'rating' %}
<input type="number" min="1" max="5" step="1" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" <input type="number" min="1" max="5" step="0.5" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}"
{% if book['custom_column_' ~ c.id]|length > 0 %} {% if book['custom_column_' ~ c.id]|length > 0 %}
value="{{ '%d' % (book['custom_column_' ~ c.id][0].value / 2) }}" value="{{ '%.1f' % (book['custom_column_' ~ c.id][0].value / 2) }}"
{% endif %}> {% endif %}>
{% endif %} {% endif %}
</div> </div>

@ -30,20 +30,20 @@
<div data-related="gdrive_settings"> <div data-related="gdrive_settings">
{% if gdriveError %} {% if gdriveError %}
<div class="form-group"> <div class="form-group">
<label> <label id="gdrive_error">
{{_('Google Drive config problem')}}: {{ gdriveError }} {{_('Google Drive config problem')}}: {{ gdriveError }}
</label> </label>
</div> </div>
{% else %} {% else %}
{% if show_authenticate_google_drive and g.user.is_authenticated and config.config_use_google_drive %} {% if show_authenticate_google_drive and g.user.is_authenticated and config.config_use_google_drive %}
<div class="form-group required"> <div class="form-group required">
<a href="{{ url_for('gdrive.authenticate_google_drive') }}" 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>
</div> </div>
{% else %} {% else %}
{% if show_authenticate_google_drive and g.user.is_authenticated and not config.config_use_google_drive %} {% if show_authenticate_google_drive and g.user.is_authenticated and not config.config_use_google_drive %}
<div >{{_('Please hit submit to continue with setup')}}</div> <div >{{_('Please hit save to continue with setup')}}</div>
{% endif %} {% endif %}
{% if not g.user.is_authenticated %} {% if not g.user.is_authenticated and show_login_button %}
<div >{{_('Please finish Google Drive setup after login')}}</div> <div >{{_('Please finish Google Drive setup after login')}}</div>
{% endif %} {% endif %}
{% if g.user.is_authenticated %} {% if g.user.is_authenticated %}

@ -174,7 +174,7 @@
{{ c.name }}: {{ c.name }}:
{% for column in entry['custom_column_' ~ c.id] %} {% for column in entry['custom_column_' ~ c.id] %}
{% if c.datatype == 'rating' %} {% if c.datatype == 'rating' %}
{{ '%d' % (column.value / 2) }} {{ (column.value / 2)|formatfloat }}
{% else %} {% else %}
{% if c.datatype == 'bool' %} {% if c.datatype == 'bool' %}
{% if column.value == true %} {% if column.value == true %}
@ -182,10 +182,18 @@
{% else %} {% else %}
<span class="glyphicon glyphicon-remove"></span> <span class="glyphicon glyphicon-remove"></span>
{% endif %} {% endif %}
{% else %}
{% if c.datatype == 'float' %}
{{ column.value|formatfloat(2) }}
{% else %}
{% if c.datatype == 'series' %}
{{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }}
{% else %} {% else %}
{{ column.value }} {{ column.value }}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %}
{% endif %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div> </div>

@ -165,7 +165,7 @@
{% endif %} {% endif %}
{% if c.datatype == 'rating' %} {% if c.datatype == 'rating' %}
<input type="number" min="1" max="5" step="1" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}"> <input type="number" min="1" max="5" step="0.5" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}">
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}

@ -25,6 +25,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
{% if g.user.role_admin() %}
<h3>{{_('Linked Libraries')}}</h3> <h3>{{_('Linked Libraries')}}</h3>
<table id="libs" class="table"> <table id="libs" class="table">
<thead> <thead>
@ -44,4 +45,5 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% endif %}
{% endblock %} {% endblock %}

@ -1556,7 +1556,7 @@ msgid "Authenticate Google Drive"
msgstr "Ověřit Google Drive" msgstr "Ověřit Google Drive"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "Klikněte na odeslat pro pokračování v nastavení" msgstr "Klikněte na odeslat pro pokračování v nastavení"
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -1557,7 +1557,7 @@ msgid "Authenticate Google Drive"
msgstr "Google Drive authentifizieren" msgstr "Google Drive authentifizieren"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "Bitte auf Abschicken drücken, um mit dem Setup fortzufahren" msgstr "Bitte auf Abschicken drücken, um mit dem Setup fortzufahren"
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -1560,7 +1560,7 @@ msgid "Authenticate Google Drive"
msgstr "Autentificar Google Drive" msgstr "Autentificar Google Drive"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "Por favor, pulsa enviar para continuar con la configuración" msgstr "Por favor, pulsa enviar para continuar con la configuración"
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -1557,7 +1557,7 @@ msgid "Authenticate Google Drive"
msgstr "Autentikoi Google Drive" msgstr "Autentikoi Google Drive"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "Ole hyvä ja paina lähetä jatkaaksesi asennusta" msgstr "Ole hyvä ja paina lähetä jatkaaksesi asennusta"
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -1571,7 +1571,7 @@ msgid "Authenticate Google Drive"
msgstr "Authentification Google Drive" msgstr "Authentification Google Drive"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "Veuillez cliquer sur soumettre pour continuer linitialisation" msgstr "Veuillez cliquer sur soumettre pour continuer linitialisation"
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -1557,7 +1557,7 @@ msgid "Authenticate Google Drive"
msgstr "Google Drive hitelesítés" msgstr "Google Drive hitelesítés"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "A beállítás folytatásához kattints a Küldés gombra" msgstr "A beállítás folytatásához kattints a Küldés gombra"
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -1556,7 +1556,7 @@ msgid "Authenticate Google Drive"
msgstr "Autenticazione Google Drive" msgstr "Autenticazione Google Drive"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "Per favore premi invio per proseguire con la configurazione" msgstr "Per favore premi invio per proseguire con la configurazione"
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -1557,7 +1557,7 @@ msgid "Authenticate Google Drive"
msgstr "Googleドライブを認証" msgstr "Googleドライブを認証"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "決定を押して設定を続けてください" msgstr "決定を押して設定を続けてください"
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -1558,7 +1558,7 @@ msgid "Authenticate Google Drive"
msgstr "វាយបញ្ចូលគណនី Google Drive" msgstr "វាយបញ្ចូលគណនី Google Drive"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "" msgstr ""
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -1558,7 +1558,7 @@ msgid "Authenticate Google Drive"
msgstr "Google Drive goedkeuren" msgstr "Google Drive goedkeuren"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "Druk op 'Opslaan' om door te gaan met instellen" msgstr "Druk op 'Opslaan' om door te gaan met instellen"
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -1570,7 +1570,7 @@ msgid "Authenticate Google Drive"
msgstr "Uwierzytelnij Dysk Google" msgstr "Uwierzytelnij Dysk Google"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "Kliknij przycisk, aby kontynuować instalację" msgstr "Kliknij przycisk, aby kontynuować instalację"
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -1558,7 +1558,7 @@ msgid "Authenticate Google Drive"
msgstr "Аутентификация Google Drive" msgstr "Аутентификация Google Drive"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "Пожалуйста, нажмите «Отправить», чтобы продолжить настройку" msgstr "Пожалуйста, нажмите «Отправить», чтобы продолжить настройку"
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -1557,7 +1557,7 @@ msgid "Authenticate Google Drive"
msgstr "Autentisera Google Drive" msgstr "Autentisera Google Drive"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "Klicka på skicka för att fortsätta med installationen" msgstr "Klicka på skicka för att fortsätta med installationen"
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -1557,7 +1557,7 @@ msgid "Authenticate Google Drive"
msgstr "Google Drive Doğrula" msgstr "Google Drive Doğrula"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "Kuruluma devam etmek için Gönder'e tıklayın" msgstr "Kuruluma devam etmek için Gönder'e tıklayın"
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -1556,7 +1556,7 @@ msgid "Authenticate Google Drive"
msgstr "Автентифікація Google Drive" msgstr "Автентифікація Google Drive"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "" msgstr ""
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -1557,7 +1557,7 @@ msgid "Authenticate Google Drive"
msgstr "认证 Google Drive" msgstr "认证 Google Drive"
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "请点击提交以继续设置" msgstr "请点击提交以继续设置"
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -177,20 +177,20 @@ class UserBase:
return self.check_visibility(constants.DETAIL_RANDOM) return self.check_visibility(constants.DETAIL_RANDOM)
def list_denied_tags(self): def list_denied_tags(self):
mct = self.denied_tags.split(",") mct = self.denied_tags or ""
return [t.strip() for t in mct] return [t.strip() for t in mct.split(",")]
def list_allowed_tags(self): def list_allowed_tags(self):
mct = self.allowed_tags.split(",") mct = self.allowed_tags or ""
return [t.strip() for t in mct] return [t.strip() for t in mct.split(",")]
def list_denied_column_values(self): def list_denied_column_values(self):
mct = self.denied_column_value.split(",") mct = self.denied_column_value or ""
return [t.strip() for t in mct] return [t.strip() for t in mct.split(",")]
def list_allowed_column_values(self): def list_allowed_column_values(self):
mct = self.allowed_column_value.split(",") mct = self.allowed_column_value or ""
return [t.strip() for t in mct] return [t.strip() for t in mct.split(",")]
def __repr__(self): def __repr__(self):
return '<User %r>' % self.nickname return '<User %r>' % self.nickname
@ -478,14 +478,14 @@ def migrate_Database(session):
ArchivedBook.__table__.create(bind=engine) ArchivedBook.__table__.create(bind=engine)
if not engine.dialect.has_table(engine.connect(), "registration"): if not engine.dialect.has_table(engine.connect(), "registration"):
ReadBook.__table__.create(bind=engine) ReadBook.__table__.create(bind=engine)
conn = engine.connect() 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()
try: try:
session.query(exists().where(Registration.allow)).scalar() session.query(exists().where(Registration.allow)).scalar()
session.commit() session.commit()
except exc.OperationalError: # Database is not compatible, some columns are missing except exc.OperationalError: # Database is not compatible, some columns are missing
conn = engine.connect() with engine.connect() as conn:
conn.execute("ALTER TABLE registration ADD column 'allow' INTEGER") conn.execute("ALTER TABLE registration ADD column 'allow' INTEGER")
conn.execute("update registration set 'allow' = 1") conn.execute("update registration set 'allow' = 1")
session.commit() session.commit()
@ -493,14 +493,14 @@ def migrate_Database(session):
session.query(exists().where(RemoteAuthToken.token_type)).scalar() session.query(exists().where(RemoteAuthToken.token_type)).scalar()
session.commit() session.commit()
except exc.OperationalError: # Database is not compatible, some columns are missing except exc.OperationalError: # Database is not compatible, some columns are missing
conn = engine.connect() with engine.connect() as conn:
conn.execute("ALTER TABLE remote_auth_token ADD column 'token_type' INTEGER DEFAULT 0") conn.execute("ALTER TABLE remote_auth_token ADD column 'token_type' INTEGER DEFAULT 0")
conn.execute("update remote_auth_token set 'token_type' = 0") conn.execute("update remote_auth_token set 'token_type' = 0")
session.commit() session.commit()
try: try:
session.query(exists().where(ReadBook.read_status)).scalar() session.query(exists().where(ReadBook.read_status)).scalar()
except exc.OperationalError: except exc.OperationalError:
conn = engine.connect() with engine.connect() as conn:
conn.execute("ALTER TABLE book_read_link ADD column 'read_status' INTEGER DEFAULT 0") 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("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_modified' DATETIME")
@ -514,7 +514,7 @@ def migrate_Database(session):
try: try:
session.query(exists().where(Shelf.uuid)).scalar() session.query(exists().where(Shelf.uuid)).scalar()
except exc.OperationalError: except exc.OperationalError:
conn = engine.connect() with engine.connect() as conn:
conn.execute("ALTER TABLE shelf ADD column 'uuid' STRING") conn.execute("ALTER TABLE shelf ADD column 'uuid' STRING")
conn.execute("ALTER TABLE shelf ADD column 'created' DATETIME") conn.execute("ALTER TABLE shelf ADD column 'created' DATETIME")
conn.execute("ALTER TABLE shelf ADD column 'last_modified' DATETIME") conn.execute("ALTER TABLE shelf ADD column 'last_modified' DATETIME")
@ -529,30 +529,30 @@ def migrate_Database(session):
# Handle table exists, but no content # Handle table exists, but no content
cnt = session.query(Registration).count() cnt = session.query(Registration).count()
if not cnt: if not cnt:
conn = engine.connect() 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()
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
conn = engine.connect() 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()
try: try:
create = False create = False
session.query(exists().where(User.sidebar_view)).scalar() session.query(exists().where(User.sidebar_view)).scalar()
except exc.OperationalError: # Database is not compatible, some columns are missing except exc.OperationalError: # Database is not compatible, some columns are missing
conn = engine.connect() with engine.connect() as conn:
conn.execute("ALTER TABLE user ADD column `sidebar_view` Integer DEFAULT 1") conn.execute("ALTER TABLE user ADD column `sidebar_view` Integer DEFAULT 1")
session.commit() session.commit()
create = True create = True
try: try:
if create: if create:
conn = engine.connect() with engine.connect() as conn:
conn.execute("SELECT language_books FROM user") conn.execute("SELECT language_books FROM user")
session.commit() session.commit()
except exc.OperationalError: except exc.OperationalError:
conn = engine.connect() with engine.connect() as conn:
conn.execute("UPDATE user SET 'sidebar_view' = (random_books* :side_random + language_books * :side_lang " conn.execute("UPDATE user SET 'sidebar_view' = (random_books* :side_random + language_books * :side_lang "
"+ series_books * :side_series + category_books * :side_category + hot_books * " "+ series_books * :side_series + category_books * :side_category + hot_books * "
":side_hot + :side_autor + :detail_random)", ":side_hot + :side_autor + :detail_random)",
@ -564,34 +564,28 @@ def migrate_Database(session):
try: try:
session.query(exists().where(User.denied_tags)).scalar() session.query(exists().where(User.denied_tags)).scalar()
except exc.OperationalError: # Database is not compatible, some columns are missing except exc.OperationalError: # Database is not compatible, some columns are missing
conn = engine.connect() with engine.connect() as conn:
conn.execute("ALTER TABLE user ADD column `denied_tags` String DEFAULT ''") conn.execute("ALTER TABLE user ADD column `denied_tags` String DEFAULT ''")
conn.execute("ALTER TABLE user ADD column `allowed_tags` String DEFAULT ''") conn.execute("ALTER TABLE user ADD column `allowed_tags` String DEFAULT ''")
conn.execute("ALTER TABLE user ADD column `denied_column_value` DEFAULT ''") conn.execute("ALTER TABLE user ADD column `denied_column_value` String DEFAULT ''")
conn.execute("ALTER TABLE user ADD column `allowed_column_value` DEFAULT ''") conn.execute("ALTER TABLE user ADD column `allowed_column_value` String DEFAULT ''")
session.commit() session.commit()
#try:
# session.query(exists().where(User.series_view)).scalar()
#except exc.OperationalError:
# conn = engine.connect()
# conn.execute("ALTER TABLE user ADD column `series_view` VARCHAR(10) DEFAULT 'list'")
try: try:
session.query(exists().where(User.view_settings)).scalar() session.query(exists().where(User.view_settings)).scalar()
except exc.OperationalError: except exc.OperationalError:
conn = engine.connect() with engine.connect() as conn:
conn.execute("ALTER TABLE user ADD column `view_settings` JSON default '{}'") conn.execute("ALTER TABLE user ADD column `series_view` VARCHAR(10) DEFAULT 'list'")
session.commit()
if session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() \ if session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() \
is None: is None:
create_anonymous_user(session) create_anonymous_user(session)
try: try:
# check if one table with autoincrement is existing (should be user table) # check if one table with autoincrement is existing (should be user table)
conn = engine.connect() with engine.connect() as conn:
conn.execute("SELECT COUNT(*) FROM sqlite_sequence WHERE name='user'") conn.execute("SELECT COUNT(*) FROM sqlite_sequence WHERE name='user'")
except exc.OperationalError: except exc.OperationalError:
# Create new table user_id and copy contents of table user into it # Create new table user_id and copy contents of table user into it
conn = engine.connect() with engine.connect() as conn:
conn.execute("CREATE TABLE user_id (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," conn.execute("CREATE TABLE user_id (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
"nickname VARCHAR(64)," "nickname VARCHAR(64),"
"email VARCHAR(120)," "email VARCHAR(120),"
@ -606,8 +600,7 @@ def migrate_Database(session):
"UNIQUE (nickname)," "UNIQUE (nickname),"
"UNIQUE (email))") "UNIQUE (email))")
conn.execute("INSERT INTO user_id(id, nickname, email, role, password, kindle_mail,locale," conn.execute("INSERT INTO user_id(id, nickname, email, role, password, kindle_mail,locale,"
# "sidebar_view, default_language, series_view) " "sidebar_view, default_language, series_view) "
"sidebar_view, default_language) "
"SELECT id, nickname, email, role, password, kindle_mail, locale," "SELECT id, nickname, email, role, password, kindle_mail, locale,"
"sidebar_view, default_language FROM user") "sidebar_view, default_language FROM user")
# delete old user table and rename new user_id table to user: # delete old user table and rename new user_id table to user:
@ -617,7 +610,7 @@ def migrate_Database(session):
# Remove login capability of user Guest # Remove login capability of user Guest
try: try:
conn = engine.connect() with engine.connect() as conn:
conn.execute("UPDATE user SET password='' where nickname = 'Guest' and password !=''") conn.execute("UPDATE user SET password='' where nickname = 'Guest' and password !=''")
session.commit() session.commit()
except exc.OperationalError: except exc.OperationalError:
@ -686,8 +679,6 @@ def init_db(app_db_path):
app_DB_path = app_db_path app_DB_path = app_db_path
engine = create_engine(u'sqlite:///{0}'.format(app_db_path), echo=False) engine = create_engine(u'sqlite:///{0}'.format(app_db_path), echo=False)
# engine.execute("attach database '{0}' as app_settings;".format(app_db_path))
Session = sessionmaker() Session = sessionmaker()
Session.configure(bind=engine) Session.configure(bind=engine)

@ -35,7 +35,7 @@ except ImportError:
lxmlversion = None lxmlversion = None
try: try:
from wand.image import Image from wand.image import Image, Color
from wand import version as ImageVersion from wand import version as ImageVersion
from wand.exceptions import PolicyError from wand.exceptions import PolicyError
use_generic_pdf_cover = False use_generic_pdf_cover = False
@ -116,8 +116,8 @@ def default_meta(tmp_file_path, original_file_name, original_file_extension):
def pdf_meta(tmp_file_path, original_file_name, original_file_extension): def pdf_meta(tmp_file_path, original_file_name, original_file_extension):
doc_info = None doc_info = None
if use_pdf_meta: if use_pdf_meta:
doc_info = PdfFileReader(open(tmp_file_path, 'rb')).getDocumentInfo() with open(tmp_file_path, 'rb') as f:
doc_info = PdfFileReader(f).getDocumentInfo()
if doc_info: if doc_info:
author = doc_info.author if doc_info.author else u'Unknown' author = doc_info.author if doc_info.author else u'Unknown'
title = doc_info.title if doc_info.title else original_file_name title = doc_info.title if doc_info.title else original_file_name
@ -149,6 +149,9 @@ def pdf_preview(tmp_file_path, tmp_dir):
img.options["pdf:use-cropbox"] = "true" img.options["pdf:use-cropbox"] = "true"
img.read(filename=tmp_file_path + '[0]', resolution=150) img.read(filename=tmp_file_path + '[0]', resolution=150)
img.compression_quality = 88 img.compression_quality = 88
if img.alpha_channel:
img.alpha_channel = 'remove'
img.background_color = Color('white')
img.save(filename=os.path.join(tmp_dir, cover_file_name)) img.save(filename=os.path.join(tmp_dir, cover_file_name))
return cover_file_name return cover_file_name
except PolicyError as ex: except PolicyError as ex:
@ -156,6 +159,7 @@ def pdf_preview(tmp_file_path, tmp_dir):
return None return None
except Exception as ex: except Exception as ex:
log.warning('Cannot extract cover image, using default: %s', ex) log.warning('Cannot extract cover image, using default: %s', ex)
log.warning('On Windows this error could be caused by missing ghostscript')
return None return None

@ -1316,7 +1316,7 @@ def advanced_search():
db.cc_classes[c.id].value == custom_query)) db.cc_classes[c.id].value == custom_query))
elif c.datatype == 'rating': elif c.datatype == 'rating':
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any( q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
db.cc_classes[c.id].value == int(custom_query) * 2)) db.cc_classes[c.id].value == int(float(custom_query) * 2)))
else: else:
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any( q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
func.lower(db.cc_classes[c.id].value).ilike("%" + custom_query + "%"))) func.lower(db.cc_classes[c.id].value).ilike("%" + custom_query + "%")))
@ -1349,6 +1349,9 @@ def advanced_search_form():
def get_cover(book_id): def get_cover(book_id):
return get_book_cover(book_id) return get_book_cover(book_id)
@web.route("/robots.txt")
def get_robots():
return send_from_directory(constants.STATIC_DIR, "robots.txt")
@web.route("/show/<int:book_id>/<book_format>", defaults={'anyname': 'None'}) @web.route("/show/<int:book_id>/<book_format>", defaults={'anyname': 'None'})
@web.route("/show/<int:book_id>/<book_format>/<anyname>") @web.route("/show/<int:book_id>/<book_format>/<anyname>")

@ -1556,7 +1556,7 @@ msgid "Authenticate Google Drive"
msgstr "" msgstr ""
#: cps/templates/config_edit.html:44 #: cps/templates/config_edit.html:44
msgid "Please hit submit to continue with setup" msgid "Please hit save to continue with setup"
msgstr "" msgstr ""
#: cps/templates/config_edit.html:47 #: cps/templates/config_edit.html:47

@ -36,17 +36,17 @@
<div class="col-xs-12 col-sm-6"> <div class="col-xs-12 col-sm-6">
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;"> <div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;">
<p class='text-justify attribute'><strong>Start Time: </strong>2020-06-28 20:44:31</p> <p class='text-justify attribute'><strong>Start Time: </strong>2020-08-14 19:46:42</p>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3"> <div class="col-xs-6 col-md-6 col-sm-offset-3">
<p class='text-justify attribute'><strong>Stop Time: </strong>2020-06-28 21:48:11</p> <p class='text-justify attribute'><strong>Stop Time: </strong>2020-08-14 21:02:08</p>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3"> <div class="col-xs-6 col-md-6 col-sm-offset-3">
<p class='text-justify attribute'><strong>Duration: </strong>53:53 min</p> <p class='text-justify attribute'><strong>Duration: </strong>1h 243 min</p>
</div> </div>
</div> </div>
</div> </div>
@ -570,8 +570,8 @@ AssertionError: False is not true</pre>
<tr class="result['header']['style']"> <tr class="result['header']['style']">
<td>test_edit_books.TestEditBooks</td> <td>test_edit_books.TestEditBooks</td>
<td class="text-center">33</td> <td class="text-center">33</td>
<td class="text-center">30</td> <td class="text-center">29</td>
<td class="text-center">1</td> <td class="text-center">2</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">2</td> <td class="text-center">2</td>
<td class="text-center"> <td class="text-center">
@ -894,11 +894,33 @@ AssertionError: False is not true</pre>
<tr id='pt7.32' class='hiddenRow bg-success'> <tr id='ft7.32' class='none bg-danger'>
<td> <td>
<div class='testcase'>test_upload_book_pdf</div> <div class='testcase'>test_upload_book_pdf</div>
</td> </td>
<td colspan='6' align='center'>PASS</td> <td colspan='6'>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft7.32')">FAIL</a>
</div>
<!--css div popup start-->
<div id='div_ft7.32' class="popup_window test_output" style="display:none;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus='this.blur();'
onclick="document.getElementById('div_ft7.32').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File "/home/matthias/Entwicklung/calibre-web-test/test/test_edit_books.py", line 751, in test_upload_book_pdf
self.assertEqual('23390', resp.headers['Content-Length'])
AssertionError: '23390' != '23427'
- 23390
+ 23427</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr> </tr>
@ -1070,13 +1092,13 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<tr class="result['header']['style']"> <tr class="result['header']['style']">
<td>test_helper.CalibreHelper</td> <td>test_helper.CalibreHelper</td>
<td class="text-center">13</td> <td class="text-center">15</td>
<td class="text-center">13</td> <td class="text-center">15</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center"> <td class="text-center">
<a onclick="showClassDetail('c11', 13)">Detail</a> <a onclick="showClassDetail('c11', 15)">Detail</a>
</td> </td>
</tr> </tr>
@ -1199,6 +1221,24 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<tr id='pt11.14' class='hiddenRow bg-success'>
<td>
<div class='testcase'>test_random_password</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
<tr id='pt11.15' class='hiddenRow bg-success'>
<td>
<div class='testcase'>test_whitespaces</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
<tr class="result['header']['style']"> <tr class="result['header']['style']">
<td>test_kobo_sync.TestKoboSync</td> <td>test_kobo_sync.TestKoboSync</td>
@ -1289,13 +1329,13 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<tr class="result['header']['style']"> <tr class="result['header']['style']">
<td>test_ldap.test_ldap_login</td> <td>test_ldap.test_ldap_login</td>
<td class="text-center">9</td> <td class="text-center">10</td>
<td class="text-center">9</td> <td class="text-center">10</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center"> <td class="text-center">
<a onclick="showClassDetail('c13', 9)">Detail</a> <a onclick="showClassDetail('c13', 10)">Detail</a>
</td> </td>
</tr> </tr>
@ -1374,6 +1414,15 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<tr id='pt13.9' class='hiddenRow bg-success'> <tr id='pt13.9' class='hiddenRow bg-success'>
<td>
<div class='testcase'>test_ldap_kobo_sync</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
<tr id='pt13.10' class='hiddenRow bg-success'>
<td> <td>
<div class='testcase'>test_ldap_opds_download_book</div> <div class='testcase'>test_ldap_opds_download_book</div>
</td> </td>
@ -1480,13 +1529,13 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<tr class="result['header']['style']"> <tr class="result['header']['style']">
<td>test_login.test_Login</td> <td>test_login.test_Login</td>
<td class="text-center">10</td> <td class="text-center">11</td>
<td class="text-center">10</td> <td class="text-center">11</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center"> <td class="text-center">
<a onclick="showClassDetail('c15', 10)">Detail</a> <a onclick="showClassDetail('c15', 11)">Detail</a>
</td> </td>
</tr> </tr>
@ -1582,6 +1631,15 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<tr id='pt15.11' class='hiddenRow bg-success'>
<td>
<div class='testcase'>test_robots</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
<tr class="result['header']['style']"> <tr class="result['header']['style']">
<td>test_oauth.test_OAuth_login</td> <td>test_oauth.test_OAuth_login</td>
@ -1813,13 +1871,13 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<tr class="result['header']['style']"> <tr class="result['header']['style']">
<td>test_register.test_register</td> <td>test_register.test_register</td>
<td class="text-center">6</td> <td class="text-center">7</td>
<td class="text-center">6</td> <td class="text-center">7</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center"> <td class="text-center">
<a onclick="showClassDetail('c18', 6)">Detail</a> <a onclick="showClassDetail('c18', 7)">Detail</a>
</td> </td>
</tr> </tr>
@ -1854,7 +1912,7 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<tr id='pt18.4' class='hiddenRow bg-success'> <tr id='pt18.4' class='hiddenRow bg-success'>
<td> <td>
<div class='testcase'>test_registering_user</div> <div class='testcase'>test_registering_only_email</div>
</td> </td>
<td colspan='6' align='center'>PASS</td> <td colspan='6' align='center'>PASS</td>
</tr> </tr>
@ -1863,7 +1921,7 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<tr id='pt18.5' class='hiddenRow bg-success'> <tr id='pt18.5' class='hiddenRow bg-success'>
<td> <td>
<div class='testcase'>test_registering_user_fail</div> <div class='testcase'>test_registering_user</div>
</td> </td>
<td colspan='6' align='center'>PASS</td> <td colspan='6' align='center'>PASS</td>
</tr> </tr>
@ -1871,6 +1929,15 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<tr id='pt18.6' class='hiddenRow bg-success'> <tr id='pt18.6' class='hiddenRow bg-success'>
<td>
<div class='testcase'>test_registering_user_fail</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
<tr id='pt18.7' class='hiddenRow bg-success'>
<td> <td>
<div class='testcase'>test_user_change_password</div> <div class='testcase'>test_user_change_password</div>
</td> </td>
@ -2578,9 +2645,9 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<tr id='total_row' class="text-center bg-grey"> <tr id='total_row' class="text-center bg-grey">
<td>Total</td> <td>Total</td>
<td>223</td> <td>228</td>
<td>215</td> <td>219</td>
<td>2</td> <td>3</td>
<td>0</td> <td>0</td>
<td>6</td> <td>6</td>
<td>&nbsp;</td> <td>&nbsp;</td>
@ -2610,13 +2677,13 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<tr> <tr>
<th>Platform</th> <th>Platform</th>
<td>Linux 5.3.0-59-generic #53~18.04.1-Ubuntu SMP Thu Jun 4 14:58:26 UTC 2020 x86_64 x86_64</td> <td>Linux 5.4.0-42-generic #46-Ubuntu SMP Fri Jul 10 00:24:02 UTC 2020 x86_64 x86_64</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
<tr> <tr>
<th>Python</th> <th>Python</th>
<td>3.7.5</td> <td>3.8.2</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
@ -2730,7 +2797,7 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<tr> <tr>
<th>Pillow</th> <th>Pillow</th>
<td>7.1.2</td> <td>7.2.0</td>
<td>testCoverEditBooks</td> <td>testCoverEditBooks</td>
</tr> </tr>
@ -2742,31 +2809,31 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<tr> <tr>
<th>lxml</th> <th>lxml</th>
<td>4.5.1</td> <td>4.5.2</td>
<td>TestEditAdditionalBooks</td> <td>TestEditAdditionalBooks</td>
</tr> </tr>
<tr> <tr>
<th>Pillow</th> <th>Pillow</th>
<td>7.1.2</td> <td>7.2.0</td>
<td>TestEditAdditionalBooks</td> <td>TestEditAdditionalBooks</td>
</tr> </tr>
<tr> <tr>
<th>rarfile</th> <th>rarfile</th>
<td>3.1</td> <td>4.0</td>
<td>TestEditAdditionalBooks</td> <td>TestEditAdditionalBooks</td>
</tr> </tr>
<tr> <tr>
<th>lxml</th> <th>lxml</th>
<td>4.5.1</td> <td>4.5.2</td>
<td>TestEditBooks</td> <td>TestEditBooks</td>
</tr> </tr>
<tr> <tr>
<th>Pillow</th> <th>Pillow</th>
<td>7.1.2</td> <td>7.2.0</td>
<td>TestEditBooks</td> <td>TestEditBooks</td>
</tr> </tr>
@ -2788,9 +2855,15 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<td>test_ldap_login</td> <td>test_ldap_login</td>
</tr> </tr>
<tr>
<th>jsonschema</th>
<td>3.2.0</td>
<td>test_ldap_login</td>
</tr>
<tr> <tr>
<th>python-ldap</th> <th>python-ldap</th>
<td>3.3.0</td> <td>3.3.1</td>
<td>test_ldap_login</td> <td>test_ldap_login</td>
</tr> </tr>
@ -2802,7 +2875,7 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
<tr> <tr>
<th>SQLAlchemy-Utils</th> <th>SQLAlchemy-Utils</th>
<td>0.36.6</td> <td>0.36.8</td>
<td>test_OAuth_login</td> <td>test_OAuth_login</td>
</tr> </tr>
@ -2814,7 +2887,7 @@ AssertionError: False is not true : Browser-Cache Problem: Old Cover is displaye
</div> </div>
<script> <script>
drawCircle(215, 2, 0, 6); drawCircle(219, 3, 0, 6);
</script> </script>
</div> </div>

Loading…
Cancel
Save